diff --git a/packages/playwright/src/common/test.ts b/packages/playwright/src/common/test.ts index a1afee5b08..578a1574f4 100644 --- a/packages/playwright/src/common/test.ts +++ b/packages/playwright/src/common/test.ts @@ -54,10 +54,12 @@ export class Suite extends Base implements SuitePrivate { _fullProject: FullProjectInternal | undefined; _fileId: string | undefined; readonly _type: 'root' | 'project' | 'file' | 'describe'; + readonly _testTypeImpl: TestTypeImpl | undefined; - constructor(title: string, type: 'root' | 'project' | 'file' | 'describe') { + constructor(title: string, type: 'root' | 'project' | 'file' | 'describe', testTypeImpl?: TestTypeImpl) { super(title); this._type = type; + this._testTypeImpl = testTypeImpl; } get suites(): Suite[] { diff --git a/packages/playwright/src/common/testType.ts b/packages/playwright/src/common/testType.ts index 01d936f4d9..aba682f6dc 100644 --- a/packages/playwright/src/common/testType.ts +++ b/packages/playwright/src/common/testType.ts @@ -38,7 +38,7 @@ export class TestTypeImpl { test.only = wrapFunctionWithLocation(this._createTest.bind(this, 'only')); test.describe = wrapFunctionWithLocation(this._describe.bind(this, 'default')); test.describe.only = wrapFunctionWithLocation(this._describe.bind(this, 'only')); - test.describe.configure = wrapFunctionWithLocation(this._configure.bind(this)); + test.describe.configure = this._configure.bind(this); test.describe.fixme = wrapFunctionWithLocation(this._describe.bind(this, 'fixme')); test.describe.parallel = wrapFunctionWithLocation(this._describe.bind(this, 'parallel')); test.describe.parallel.only = wrapFunctionWithLocation(this._describe.bind(this, 'parallel.only')); @@ -53,7 +53,7 @@ export class TestTypeImpl { test.fixme = wrapFunctionWithLocation(this._modifier.bind(this, 'fixme')); test.fail = wrapFunctionWithLocation(this._modifier.bind(this, 'fail')); test.slow = wrapFunctionWithLocation(this._modifier.bind(this, 'slow')); - test.setTimeout = wrapFunctionWithLocation(this._setTimeout.bind(this)); + test.setTimeout = this._setTimeout.bind(this); test.step = this._step.bind(this); test.use = wrapFunctionWithLocation(this._use.bind(this)); test.extend = wrapFunctionWithLocation(this._extend.bind(this)); @@ -66,7 +66,7 @@ export class TestTypeImpl { this.test = test; } - private _currentSuite(location: Location, title: string): Suite | undefined { + private _currentSuite(title: string): Suite | undefined { const suite = currentlyLoadingFileSuite(); if (!suite) { throw new Error([ @@ -78,12 +78,18 @@ export class TestTypeImpl { ` when one of the dependencies in your package.json depends on @playwright/test.`, ].join('\n')); } + if (suite._testTypeImpl && suite._testTypeImpl !== this) { + throw new Error([ + `Can't call ${title} inside a describe() suite of a different test type.`, + `Make sure to use the same "test" function (created by the test.extend() call) for all declarations inside a suite.`, + ].join('\n')); + } return suite; } private _createTest(type: 'default' | 'only' | 'skip' | 'fixme' | 'fail', location: Location, title: string, fn: Function) { throwIfRunningInsideJest(); - const suite = this._currentSuite(location, 'test()'); + const suite = this._currentSuite('test()'); if (!suite) return; const test = new TestCase(title, fn, this, location); @@ -98,7 +104,7 @@ export class TestTypeImpl { private _describe(type: 'default' | 'only' | 'serial' | 'serial.only' | 'parallel' | 'parallel.only' | 'skip' | 'fixme', location: Location, title: string | Function, fn?: Function) { throwIfRunningInsideJest(); - const suite = this._currentSuite(location, 'test.describe()'); + const suite = this._currentSuite('test.describe()'); if (!suite) return; @@ -107,7 +113,7 @@ export class TestTypeImpl { title = ''; } - const child = new Suite(title, 'describe'); + const child = new Suite(title, 'describe', this); child._requireFile = suite._requireFile; child.location = location; suite._addSuite(child); @@ -134,7 +140,7 @@ export class TestTypeImpl { } private _hook(name: 'beforeEach' | 'afterEach' | 'beforeAll' | 'afterAll', location: Location, title: string | Function, fn?: Function) { - const suite = this._currentSuite(location, `test.${name}()`); + const suite = this._currentSuite(`test.${name}()`); if (!suite) return; if (typeof title === 'function') { @@ -145,9 +151,9 @@ export class TestTypeImpl { suite._hooks.push({ type: name, fn: fn!, title, location }); } - private _configure(location: Location, options: { mode?: 'default' | 'parallel' | 'serial', retries?: number, timeout?: number }) { + private _configure(options: { mode?: 'default' | 'parallel' | 'serial', retries?: number, timeout?: number }) { throwIfRunningInsideJest(); - const suite = this._currentSuite(location, `test.describe.configure()`); + const suite = this._currentSuite(`test.describe.configure()`); if (!suite) return; @@ -198,7 +204,7 @@ export class TestTypeImpl { testInfo[type](...modifierArgs as [any, any]); } - private _setTimeout(location: Location, timeout: number) { + private _setTimeout(timeout: number) { const suite = currentlyLoadingFileSuite(); if (suite) { suite._timeout = timeout; @@ -212,7 +218,7 @@ export class TestTypeImpl { } private _use(location: Location, fixtures: Fixtures) { - const suite = this._currentSuite(location, `test.use()`); + const suite = this._currentSuite(`test.use()`); if (!suite) return; suite._use.push({ fixtures, location }); diff --git a/tests/playwright-test/basic.spec.ts b/tests/playwright-test/basic.spec.ts index 3b47603c25..4c4b28ceeb 100644 --- a/tests/playwright-test/basic.spec.ts +++ b/tests/playwright-test/basic.spec.ts @@ -550,3 +550,22 @@ test('should support describe.fixme', async ({ runInlineTest }) => { expect(result.skipped).toBe(3); expect(result.output).toContain('heytest4'); }); + +test('should not allow mixing test types', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'mixed.spec.ts': ` + import { test } from '@playwright/test'; + + export const test2 = test.extend({ + value: 42, + }); + + test.describe("test1 suite", () => { + test2("test 2", async () => {}); + }); + ` + }); + expect(result.exitCode).toBe(1); + expect(result.output).toContain(`Can't call test() inside a describe() suite of a different test type.`); + expect(result.output).toContain('> 9 | test2('); +});