2021-06-06 17:09:53 -07:00
/ * *
* Copyright Microsoft Corporation . All rights reserved .
*
* Licensed under the Apache License , Version 2.0 ( the "License" ) ;
* you may not use this file except in compliance with the License .
* You may obtain a copy of the License at
*
* http : //www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing , software
* distributed under the License is distributed on an "AS IS" BASIS ,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
* See the License for the specific language governing permissions and
* limitations under the License .
* /
2022-01-31 17:09:04 -08:00
import { formatLocation , debugTest } from './util' ;
2021-06-06 17:09:53 -07:00
import * as crypto from 'crypto' ;
2022-04-06 13:57:14 -08:00
import type { FixturesWithLocation , Location , WorkerInfo } from './types' ;
2022-04-07 19:18:22 -08:00
import { ManualPromise } from 'playwright-core/lib/utils/manualPromise' ;
2022-04-06 13:57:14 -08:00
import type { TestInfoImpl } from './testInfo' ;
import type { FixtureDescription , TimeoutManager } from './timeoutManager' ;
2021-06-06 17:09:53 -07:00
2021-08-09 12:33:16 -07:00
type FixtureScope = 'test' | 'worker' ;
2022-05-13 11:17:20 +01:00
type FixtureAuto = boolean | 'all-hooks-included' ;
2022-05-03 15:19:27 +01:00
const kScopeOrder : FixtureScope [ ] = [ 'test' , 'worker' ] ;
2022-05-13 11:17:20 +01:00
type FixtureOptions = { auto? : FixtureAuto , scope? : FixtureScope , option? : boolean , timeout? : number | undefined } ;
2021-12-22 09:59:58 -08:00
type FixtureTuple = [ value : any , options : FixtureOptions ] ;
2021-06-06 17:09:53 -07:00
type FixtureRegistration = {
2022-05-03 15:19:27 +01:00
// Fixture registration location.
location : Location ;
// Fixture name comes from test.extend() call.
2021-06-06 17:09:53 -07:00
name : string ;
scope : FixtureScope ;
2022-05-03 15:19:27 +01:00
// Either a fixture function, or a fixture value.
fn : Function | any ;
// Auto fixtures always run without user explicitly mentioning them.
2022-05-13 11:17:20 +01:00
auto : FixtureAuto ;
2022-05-03 15:19:27 +01:00
// An "option" fixture can have a value set in the config.
2021-12-22 09:59:58 -08:00
option : boolean ;
2022-05-03 15:19:27 +01:00
// Custom title to be used instead of the name, internal-only.
2022-04-13 15:13:31 -07:00
customTitle? : string ;
2022-05-03 15:19:27 +01:00
// Fixture with a separate timeout does not count towards the test time.
2022-03-17 09:36:03 -07:00
timeout? : number ;
2022-05-03 15:19:27 +01:00
// Names of the dependencies, comes from the declaration "({ foo, bar }) => {...}"
deps : string [ ] ;
// Unique id, to differentiate between fixtures with the same name.
id : string ;
// A fixture override can use the previous version of the fixture.
super ? : FixtureRegistration ;
2021-06-06 17:09:53 -07:00
} ;
class Fixture {
runner : FixtureRunner ;
registration : FixtureRegistration ;
value : any ;
2022-07-05 17:15:28 -07:00
failed = false ;
2021-12-20 16:19:21 -08:00
_useFuncFinished : ManualPromise < void > | undefined ;
_selfTeardownComplete : Promise < void > | undefined ;
_teardownWithDepsComplete : Promise < void > | undefined ;
2022-03-17 09:36:03 -07:00
_runnableDescription : FixtureDescription ;
2022-05-23 16:54:56 -07:00
_deps = new Set < Fixture > ( ) ;
_usages = new Set < Fixture > ( ) ;
2021-06-06 17:09:53 -07:00
constructor ( runner : FixtureRunner , registration : FixtureRegistration ) {
this . runner = runner ;
this . registration = registration ;
this . value = null ;
2022-06-30 17:05:08 -07:00
const title = this . registration . customTitle || this . registration . name ;
2022-03-17 09:36:03 -07:00
this . _runnableDescription = {
2022-06-30 17:05:08 -07:00
title : this.registration.timeout !== undefined ? ` Fixture " ${ title } " ` : ` setting up " ${ title } " ` ,
2022-03-17 09:36:03 -07:00
location : registration.location ,
slot : this.registration.timeout === undefined ? undefined : {
timeout : this.registration.timeout ,
elapsed : 0 ,
}
} ;
2021-06-06 17:09:53 -07:00
}
2022-03-17 09:36:03 -07:00
async setup ( testInfo : TestInfoImpl ) {
2021-06-06 17:09:53 -07:00
if ( typeof this . registration . fn !== 'function' ) {
this . value = this . registration . fn ;
return ;
}
const params : { [ key : string ] : any } = { } ;
for ( const name of this . registration . deps ) {
2021-06-23 10:30:54 -07:00
const registration = this . runner . pool ! . resolveDependency ( this . registration , name ) ! ;
2022-03-08 16:35:14 -08:00
const dep = await this . runner . setupFixtureForRegistration ( registration , testInfo ) ;
2022-05-23 16:54:56 -07:00
// Fixture teardown is root => leafs, when we need to teardown a fixture,
// it recursively tears down its usages first.
dep . _usages . add ( this ) ;
// Don't forget to decrement all usages when fixture goes.
// Otherwise worker-scope fixtures will retain test-scope fixtures forever.
this . _deps . add ( dep ) ;
2021-06-06 17:09:53 -07:00
params [ name ] = dep . value ;
2022-07-05 17:15:28 -07:00
if ( dep . failed ) {
this . failed = true ;
return ;
}
2021-06-06 17:09:53 -07:00
}
let called = false ;
2021-12-20 16:19:21 -08:00
const useFuncStarted = new ManualPromise < void > ( ) ;
2021-09-23 11:56:39 -04:00
debugTest ( ` setup ${ this . registration . name } ` ) ;
2021-12-20 16:19:21 -08:00
const useFunc = async ( value : any ) = > {
2021-06-06 17:09:53 -07:00
if ( called )
2021-06-23 10:30:54 -07:00
throw new Error ( ` Cannot provide fixture value for the second time ` ) ;
2021-06-06 17:09:53 -07:00
called = true ;
this . value = value ;
2021-12-20 16:19:21 -08:00
this . _useFuncFinished = new ManualPromise < void > ( ) ;
useFuncStarted . resolve ( ) ;
await this . _useFuncFinished ;
} ;
2022-03-08 16:35:14 -08:00
const workerInfo : WorkerInfo = { config : testInfo.config , parallelIndex : testInfo.parallelIndex , workerIndex : testInfo.workerIndex , project : testInfo.project } ;
2021-12-20 16:19:21 -08:00
const info = this . registration . scope === 'worker' ? workerInfo : testInfo ;
2022-03-17 09:36:03 -07:00
testInfo . _timeoutManager . setCurrentFixture ( this . _runnableDescription ) ;
2022-01-31 17:09:04 -08:00
this . _selfTeardownComplete = Promise . resolve ( ) . then ( ( ) = > this . registration . fn ( params , useFunc , info ) ) . catch ( ( e : any ) = > {
2022-07-05 17:15:28 -07:00
this . failed = true ;
2021-12-20 16:19:21 -08:00
if ( ! useFuncStarted . isDone ( ) )
useFuncStarted . reject ( e ) ;
2021-06-06 17:09:53 -07:00
else
throw e ;
} ) ;
2021-12-20 16:19:21 -08:00
await useFuncStarted ;
2022-03-17 09:36:03 -07:00
testInfo . _timeoutManager . setCurrentFixture ( undefined ) ;
2021-06-06 17:09:53 -07:00
}
2022-03-17 09:36:03 -07:00
async teardown ( timeoutManager : TimeoutManager ) {
2022-07-15 13:05:48 -07:00
if ( this . _teardownWithDepsComplete ) {
// When we are waiting for the teardown for the second time,
// most likely after the first time did timeout, annotate current fixture
// for better error messages.
this . _setTeardownDescription ( timeoutManager ) ;
await this . _teardownWithDepsComplete ;
timeoutManager . setCurrentFixture ( undefined ) ;
return ;
}
this . _teardownWithDepsComplete = this . _teardownInternal ( timeoutManager ) ;
2021-12-20 16:19:21 -08:00
await this . _teardownWithDepsComplete ;
}
2022-07-15 13:05:48 -07:00
private _setTeardownDescription ( timeoutManager : TimeoutManager ) {
const title = this . registration . customTitle || this . registration . name ;
this . _runnableDescription . title = this . registration . timeout !== undefined ? ` Fixture " ${ title } " ` : ` tearing down " ${ title } " ` ;
timeoutManager . setCurrentFixture ( this . _runnableDescription ) ;
}
2022-03-17 09:36:03 -07:00
private async _teardownInternal ( timeoutManager : TimeoutManager ) {
2021-06-06 17:09:53 -07:00
if ( typeof this . registration . fn !== 'function' )
return ;
2022-01-13 10:38:47 -08:00
try {
2022-05-23 16:54:56 -07:00
for ( const fixture of this . _usages )
2022-03-17 09:36:03 -07:00
await fixture . teardown ( timeoutManager ) ;
2022-05-23 16:54:56 -07:00
if ( this . _usages . size !== 0 ) {
// TODO: replace with assert.
console . error ( 'Internal error: fixture integrity at' , this . _runnableDescription . title ) ; // eslint-disable-line no-console
this . _usages . clear ( ) ;
}
2022-01-13 10:38:47 -08:00
if ( this . _useFuncFinished ) {
debugTest ( ` teardown ${ this . registration . name } ` ) ;
2022-07-15 13:05:48 -07:00
this . _setTeardownDescription ( timeoutManager ) ;
2022-01-13 10:38:47 -08:00
this . _useFuncFinished . resolve ( ) ;
await this . _selfTeardownComplete ;
2022-03-17 09:36:03 -07:00
timeoutManager . setCurrentFixture ( undefined ) ;
2022-01-13 10:38:47 -08:00
}
} finally {
2022-05-23 16:54:56 -07:00
for ( const dep of this . _deps )
dep . _usages . delete ( this ) ;
2022-01-13 10:38:47 -08:00
this . runner . instanceForId . delete ( this . registration . id ) ;
2021-06-06 17:09:53 -07:00
}
}
}
2021-12-22 09:59:58 -08:00
function isFixtureTuple ( value : any ) : value is FixtureTuple {
2022-03-17 09:36:03 -07:00
return Array . isArray ( value ) && typeof value [ 1 ] === 'object' && ( 'scope' in value [ 1 ] || 'auto' in value [ 1 ] || 'option' in value [ 1 ] || 'timeout' in value [ 1 ] ) ;
feat(test runner): replace declare/define with "options" (#10293)
1. Fixtures defined in test.extend() can now have `{ option: true }` configuration that makes them overridable in the config. Options support all other properties of fixtures - value/function, scope, auto.
```
const test = base.extend<MyOptions>({
foo: ['default', { option: true }],
});
```
2. test.declare() and project.define are removed.
3. project.use applies overrides to default option values and nothing else. Any test.extend() and test.use() calls take priority over config options.
Required user changes: if someone used to define fixture options with test.extend(), overriding them in config will stop working. The solution is to add `{ option: true }`.
```
// Old code
export const test = base.extend<{ myOption: number, myFixture: number }>({
myOption: 123,
myFixture: ({ myOption }, use) => use(2 * myOption),
});
// New code
export const test = base.extend<{ myOption: number, myFixture: number }>({
myOption: [123, { option: true }],
myFixture: ({ myOption }, use) => use(2 * myOption),
});
```
2021-11-18 15:45:52 -08:00
}
2021-12-22 09:59:58 -08:00
export function isFixtureOption ( value : any ) : value is FixtureTuple {
feat(test runner): replace declare/define with "options" (#10293)
1. Fixtures defined in test.extend() can now have `{ option: true }` configuration that makes them overridable in the config. Options support all other properties of fixtures - value/function, scope, auto.
```
const test = base.extend<MyOptions>({
foo: ['default', { option: true }],
});
```
2. test.declare() and project.define are removed.
3. project.use applies overrides to default option values and nothing else. Any test.extend() and test.use() calls take priority over config options.
Required user changes: if someone used to define fixture options with test.extend(), overriding them in config will stop working. The solution is to add `{ option: true }`.
```
// Old code
export const test = base.extend<{ myOption: number, myFixture: number }>({
myOption: 123,
myFixture: ({ myOption }, use) => use(2 * myOption),
});
// New code
export const test = base.extend<{ myOption: number, myFixture: number }>({
myOption: [123, { option: true }],
myFixture: ({ myOption }, use) => use(2 * myOption),
});
```
2021-11-18 15:45:52 -08:00
return isFixtureTuple ( value ) && ! ! value [ 1 ] . option ;
}
2021-06-06 17:09:53 -07:00
export class FixturePool {
2021-08-09 12:33:16 -07:00
readonly digest : string ;
2021-06-06 17:09:53 -07:00
readonly registrations : Map < string , FixtureRegistration > ;
2021-08-10 16:32:32 -07:00
constructor ( fixturesList : FixturesWithLocation [ ] , parentPool? : FixturePool , disallowWorkerFixtures? : boolean ) {
2021-06-06 17:09:53 -07:00
this . registrations = new Map ( parentPool ? parentPool . registrations : [ ] ) ;
for ( const { fixtures , location } of fixturesList ) {
2021-06-23 10:30:54 -07:00
for ( const entry of Object . entries ( fixtures ) ) {
const name = entry [ 0 ] ;
let value = entry [ 1 ] ;
2022-05-13 11:17:20 +01:00
let options : { auto : FixtureAuto , scope : FixtureScope , option : boolean , timeout : number | undefined , customTitle : string | undefined } | undefined ;
feat(test runner): replace declare/define with "options" (#10293)
1. Fixtures defined in test.extend() can now have `{ option: true }` configuration that makes them overridable in the config. Options support all other properties of fixtures - value/function, scope, auto.
```
const test = base.extend<MyOptions>({
foo: ['default', { option: true }],
});
```
2. test.declare() and project.define are removed.
3. project.use applies overrides to default option values and nothing else. Any test.extend() and test.use() calls take priority over config options.
Required user changes: if someone used to define fixture options with test.extend(), overriding them in config will stop working. The solution is to add `{ option: true }`.
```
// Old code
export const test = base.extend<{ myOption: number, myFixture: number }>({
myOption: 123,
myFixture: ({ myOption }, use) => use(2 * myOption),
});
// New code
export const test = base.extend<{ myOption: number, myFixture: number }>({
myOption: [123, { option: true }],
myFixture: ({ myOption }, use) => use(2 * myOption),
});
```
2021-11-18 15:45:52 -08:00
if ( isFixtureTuple ( value ) ) {
2021-06-23 10:30:54 -07:00
options = {
2022-05-13 11:17:20 +01:00
auto : value [ 1 ] . auto ? ? false ,
2021-12-22 09:59:58 -08:00
scope : value [ 1 ] . scope || 'test' ,
option : ! ! value [ 1 ] . option ,
2022-03-17 09:36:03 -07:00
timeout : value [ 1 ] . timeout ,
2022-04-13 15:13:31 -07:00
customTitle : ( value [ 1 ] as any ) . _title ,
2021-06-23 10:30:54 -07:00
} ;
value = value [ 0 ] ;
2021-06-06 17:09:53 -07:00
}
2022-07-28 12:57:05 -07:00
let fn = value as ( Function | any ) ;
2021-06-23 10:30:54 -07:00
const previous = this . registrations . get ( name ) ;
if ( previous && options ) {
if ( previous . scope !== options . scope )
throw errorWithLocations ( ` Fixture " ${ name } " has already been registered as a { scope: ' ${ previous . scope } ' } fixture. ` , { location , name } , previous ) ;
if ( previous . auto !== options . auto )
throw errorWithLocations ( ` Fixture " ${ name } " has already been registered as a { auto: ' ${ previous . scope } ' } fixture. ` , { location , name } , previous ) ;
} else if ( previous ) {
2022-04-13 15:13:31 -07:00
options = { auto : previous.auto , scope : previous.scope , option : previous.option , timeout : previous.timeout , customTitle : previous.customTitle } ;
2021-06-23 10:30:54 -07:00
} else if ( ! options ) {
2022-04-13 15:13:31 -07:00
options = { auto : false , scope : 'test' , option : false , timeout : undefined , customTitle : undefined } ;
2021-06-23 10:30:54 -07:00
}
2022-05-03 15:19:27 +01:00
if ( ! kScopeOrder . includes ( options . scope ) )
2021-08-04 21:11:02 -07:00
throw errorWithLocations ( ` Fixture " ${ name } " has unknown { scope: ' ${ options . scope } ' }. ` , { location , name } ) ;
2021-08-10 16:32:32 -07:00
if ( options . scope === 'worker' && disallowWorkerFixtures )
throw errorWithLocations ( ` Cannot use({ ${ name } }) in a describe group, because it forces a new worker. \ nMake it top-level in the test file or put in the configuration file. ` , { location , name } ) ;
2021-08-04 21:11:02 -07:00
2022-07-28 12:57:05 -07:00
// Overriding option with "undefined" value means setting it to the default value
// from the original declaration of the option.
if ( fn === undefined && options . option && previous ) {
let original = previous ;
while ( original . super )
original = original . super ;
fn = original . fn ;
}
2021-06-23 10:30:54 -07:00
const deps = fixtureParameterNames ( fn , location ) ;
2022-04-13 15:13:31 -07:00
const registration : FixtureRegistration = { id : '' , name , location , scope : options.scope , fn , auto : options.auto , option : options.option , timeout : options.timeout , customTitle : options.customTitle , deps , super : previous } ;
2021-06-23 10:30:54 -07:00
registrationId ( registration ) ;
this . registrations . set ( name , registration ) ;
2021-06-06 17:09:53 -07:00
}
}
this . digest = this . validate ( ) ;
}
private validate() {
const markers = new Map < FixtureRegistration , 'visiting' | 'visited' > ( ) ;
const stack : FixtureRegistration [ ] = [ ] ;
const visit = ( registration : FixtureRegistration ) = > {
markers . set ( registration , 'visiting' ) ;
stack . push ( registration ) ;
for ( const name of registration . deps ) {
const dep = this . resolveDependency ( registration , name ) ;
if ( ! dep ) {
if ( name === registration . name )
throw errorWithLocations ( ` Fixture " ${ registration . name } " references itself, but does not have a base implementation. ` , registration ) ;
else
throw errorWithLocations ( ` Fixture " ${ registration . name } " has unknown parameter " ${ name } ". ` , registration ) ;
}
2022-05-03 15:19:27 +01:00
if ( kScopeOrder . indexOf ( registration . scope ) > kScopeOrder . indexOf ( dep . scope ) )
throw errorWithLocations ( ` ${ registration . scope } fixture " ${ registration . name } " cannot depend on a ${ dep . scope } fixture " ${ name } ". ` , registration , dep ) ;
2021-06-06 17:09:53 -07:00
if ( ! markers . has ( dep ) ) {
visit ( dep ) ;
} else if ( markers . get ( dep ) === 'visiting' ) {
const index = stack . indexOf ( dep ) ;
const regs = stack . slice ( index , stack . length ) ;
const names = regs . map ( r = > ` " ${ r . name } " ` ) ;
throw errorWithLocations ( ` Fixtures ${ names . join ( ' -> ' ) } -> " ${ dep . name } " form a dependency cycle. ` , . . . regs ) ;
}
}
markers . set ( registration , 'visited' ) ;
stack . pop ( ) ;
} ;
2021-08-09 12:33:16 -07:00
const hash = crypto . createHash ( 'sha1' ) ;
2021-06-06 17:09:53 -07:00
const names = Array . from ( this . registrations . keys ( ) ) . sort ( ) ;
for ( const name of names ) {
const registration = this . registrations . get ( name ) ! ;
visit ( registration ) ;
if ( registration . scope === 'worker' )
2021-08-09 12:33:16 -07:00
hash . update ( registration . id + ';' ) ;
2021-06-06 17:09:53 -07:00
}
2021-08-09 12:33:16 -07:00
return hash . digest ( 'hex' ) ;
2021-06-06 17:09:53 -07:00
}
2021-08-09 13:26:33 -07:00
validateFunction ( fn : Function , prefix : string , location : Location ) {
2021-06-06 17:09:53 -07:00
for ( const name of fixtureParameterNames ( fn , location ) ) {
const registration = this . registrations . get ( name ) ;
if ( ! registration )
throw errorWithLocations ( ` ${ prefix } has unknown parameter " ${ name } ". ` , { location , name : prefix , quoted : false } ) ;
}
}
resolveDependency ( registration : FixtureRegistration , name : string ) : FixtureRegistration | undefined {
if ( name === registration . name )
return registration . super ;
return this . registrations . get ( name ) ;
}
}
export class FixtureRunner {
private testScopeClean = true ;
pool : FixturePool | undefined ;
instanceForId = new Map < string , Fixture > ( ) ;
setPool ( pool : FixturePool ) {
if ( ! this . testScopeClean )
throw new Error ( 'Did not teardown test scope' ) ;
2022-07-26 08:53:32 -07:00
if ( this . pool && pool . digest !== this . pool . digest ) {
throw new Error ( [
` Playwright detected inconsistent test.use() options. ` ,
` Most common mistakes that lead to this issue: ` ,
` - Calling test.use() outside of the test file, for example in a common helper. ` ,
` - One test file imports from another test file. ` ,
] . join ( '\n' ) ) ;
}
2021-06-06 17:09:53 -07:00
this . pool = pool ;
}
2022-03-17 09:36:03 -07:00
async teardownScope ( scope : FixtureScope , timeoutManager : TimeoutManager ) {
2021-08-12 09:08:56 -07:00
let error : Error | undefined ;
2021-08-09 13:26:33 -07:00
// Teardown fixtures in the reverse order.
const fixtures = Array . from ( this . instanceForId . values ( ) ) . reverse ( ) ;
for ( const fixture of fixtures ) {
2021-08-12 09:08:56 -07:00
if ( fixture . registration . scope === scope ) {
try {
2022-03-17 09:36:03 -07:00
await fixture . teardown ( timeoutManager ) ;
2021-08-12 09:08:56 -07:00
} catch ( e ) {
if ( error === undefined )
error = e ;
}
}
2021-06-06 17:09:53 -07:00
}
if ( scope === 'test' )
this . testScopeClean = true ;
2021-08-12 09:08:56 -07:00
if ( error !== undefined )
throw error ;
2021-06-06 17:09:53 -07:00
}
2022-07-05 17:15:28 -07:00
async resolveParametersForFunction ( fn : Function , testInfo : TestInfoImpl , autoFixtures : 'worker' | 'test' | 'all-hooks-only' ) : Promise < object | null > {
2022-05-13 11:17:20 +01:00
// Install automatic fixtures.
2021-06-06 17:09:53 -07:00
for ( const registration of this . pool ! . registrations . values ( ) ) {
2022-05-13 11:17:20 +01:00
if ( registration . auto === false )
continue ;
let shouldRun = true ;
if ( autoFixtures === 'all-hooks-only' )
shouldRun = registration . scope === 'worker' || registration . auto === 'all-hooks-included' ;
else if ( autoFixtures === 'worker' )
shouldRun = registration . scope === 'worker' ;
2022-07-05 17:15:28 -07:00
if ( shouldRun ) {
const fixture = await this . setupFixtureForRegistration ( registration , testInfo ) ;
if ( fixture . failed )
return null ;
}
2021-06-06 17:09:53 -07:00
}
// Install used fixtures.
const names = fixtureParameterNames ( fn , { file : '<unused>' , line : 1 , column : 1 } ) ;
const params : { [ key : string ] : any } = { } ;
for ( const name of names ) {
2021-06-23 10:30:54 -07:00
const registration = this . pool ! . registrations . get ( name ) ! ;
2022-03-08 16:35:14 -08:00
const fixture = await this . setupFixtureForRegistration ( registration , testInfo ) ;
2022-07-05 17:15:28 -07:00
if ( fixture . failed )
return null ;
2021-06-06 17:09:53 -07:00
params [ name ] = fixture . value ;
}
2022-01-10 20:25:56 -08:00
return params ;
}
2021-06-06 17:09:53 -07:00
2022-05-13 11:17:20 +01:00
async resolveParametersAndRunFunction ( fn : Function , testInfo : TestInfoImpl , autoFixtures : 'worker' | 'test' | 'all-hooks-only' ) {
const params = await this . resolveParametersForFunction ( fn , testInfo , autoFixtures ) ;
2022-07-05 17:15:28 -07:00
if ( params === null ) {
// Do not run the function when fixture setup has already failed.
return null ;
}
2022-03-08 16:35:14 -08:00
return fn ( params , testInfo ) ;
2021-06-06 17:09:53 -07:00
}
2022-03-17 09:36:03 -07:00
async setupFixtureForRegistration ( registration : FixtureRegistration , testInfo : TestInfoImpl ) : Promise < Fixture > {
2021-06-06 17:09:53 -07:00
if ( registration . scope === 'test' )
this . testScopeClean = false ;
let fixture = this . instanceForId . get ( registration . id ) ;
if ( fixture )
return fixture ;
fixture = new Fixture ( this , registration ) ;
this . instanceForId . set ( registration . id , fixture ) ;
2022-03-08 16:35:14 -08:00
await fixture . setup ( testInfo ) ;
2021-06-06 17:09:53 -07:00
return fixture ;
}
2021-07-02 15:49:05 -07:00
dependsOnWorkerFixturesOnly ( fn : Function , location : Location ) : boolean {
const names = fixtureParameterNames ( fn , location ) ;
for ( const name of names ) {
const registration = this . pool ! . registrations . get ( name ) ! ;
if ( registration . scope !== 'worker' )
return false ;
}
return true ;
}
2021-06-06 17:09:53 -07:00
}
const signatureSymbol = Symbol ( 'signature' ) ;
function fixtureParameterNames ( fn : Function | any , location : Location ) : string [ ] {
if ( typeof fn !== 'function' )
return [ ] ;
if ( ! fn [ signatureSymbol ] )
fn [ signatureSymbol ] = innerFixtureParameterNames ( fn , location ) ;
return fn [ signatureSymbol ] ;
}
function innerFixtureParameterNames ( fn : Function , location : Location ) : string [ ] {
const text = fn . toString ( ) ;
const match = text . match ( /(?:async)?(?:\s+function)?[^(]*\(([^)]*)/ ) ;
if ( ! match )
return [ ] ;
const trimmedParams = match [ 1 ] . trim ( ) ;
if ( ! trimmedParams )
return [ ] ;
const [ firstParam ] = splitByComma ( trimmedParams ) ;
if ( firstParam [ 0 ] !== '{' || firstParam [ firstParam . length - 1 ] !== '}' )
throw errorWithLocations ( 'First argument must use the object destructuring pattern: ' + firstParam , { location } ) ;
const props = splitByComma ( firstParam . substring ( 1 , firstParam . length - 1 ) ) . map ( prop = > {
const colon = prop . indexOf ( ':' ) ;
return colon === - 1 ? prop : prop.substring ( 0 , colon ) . trim ( ) ;
} ) ;
return props ;
}
function splitByComma ( s : string ) {
const result : string [ ] = [ ] ;
const stack : string [ ] = [ ] ;
let start = 0 ;
for ( let i = 0 ; i < s . length ; i ++ ) {
if ( s [ i ] === '{' || s [ i ] === '[' ) {
stack . push ( s [ i ] === '{' ? '}' : ']' ) ;
} else if ( s [ i ] === stack [ stack . length - 1 ] ) {
stack . pop ( ) ;
} else if ( ! stack . length && s [ i ] === ',' ) {
const token = s . substring ( start , i ) . trim ( ) ;
if ( token )
result . push ( token ) ;
start = i + 1 ;
}
}
const lastToken = s . substring ( start ) . trim ( ) ;
if ( lastToken )
result . push ( lastToken ) ;
return result ;
}
// name + superId, fn -> id
const registrationIdMap = new Map < string , Map < Function | any , string > > ( ) ;
let lastId = 0 ;
function registrationId ( registration : FixtureRegistration ) : string {
if ( registration . id )
return registration . id ;
const key = registration . name + '@@@' + ( registration . super ? registrationId ( registration . super ) : '' ) ;
let map = registrationIdMap . get ( key ) ;
if ( ! map ) {
map = new Map ( ) ;
registrationIdMap . set ( key , map ) ;
}
if ( ! map . has ( registration . fn ) )
map . set ( registration . fn , String ( lastId ++ ) ) ;
registration . id = map . get ( registration . fn ) ! ;
return registration . id ;
}
function errorWithLocations ( message : string , . . . defined : { location : Location , name? : string , quoted? : boolean } [ ] ) : Error {
for ( const { name , location , quoted } of defined ) {
let prefix = '' ;
if ( name && quoted === false )
prefix = name + ' ' ;
else if ( name )
prefix = ` " ${ name } " ` ;
message += ` \ n ${ prefix } defined at ${ formatLocation ( location ) } ` ;
}
2021-06-23 10:30:54 -07:00
return new Error ( message ) ;
}