-
Notifications
You must be signed in to change notification settings - Fork 675
fix(testing): top-level hook outside describe no longer inflates step count
#7138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -69,12 +69,27 @@ export class TestSuiteInternal<T> implements TestSuite<T> { | |
| protected describe: DescribeDefinition<T>; | ||
| protected steps: (TestSuiteInternal<T> | ItDefinition<T>)[]; | ||
| protected hasOnlyStep: boolean; | ||
| /** | ||
| * Whether this is the synthetic "global" suite created when a top-level | ||
| * `beforeAll`/`afterAll`/`beforeEach`/`afterEach` is called outside any | ||
| * `describe`. Synthetic suites are only registered with `Deno.test` if a | ||
| * top-level `it()` is also added; otherwise their hooks are inherited by | ||
| * child describes promoted to top-level `Deno.test`s. | ||
| */ | ||
| protected isSynthetic: boolean; | ||
| /** | ||
| * For a child of a synthetic global suite, this points back to the synthetic | ||
| * suite so its hooks can be invoked around the child's tests at run time. | ||
| */ | ||
| protected syntheticParent: TestSuiteInternal<T> | null; | ||
| #registeredOptions: Deno.TestDefinition | undefined; | ||
|
|
||
| constructor(describe: DescribeDefinition<T>) { | ||
| constructor(describe: DescribeDefinition<T>, isSynthetic = false) { | ||
| this.describe = describe; | ||
| this.steps = []; | ||
| this.hasOnlyStep = false; | ||
| this.isSynthetic = isSynthetic; | ||
| this.syntheticParent = null; | ||
|
|
||
| const { suite } = describe; | ||
| if (suite && !TestSuiteInternal.suites.has(suite.symbol)) { | ||
|
|
@@ -138,41 +153,82 @@ export class TestSuiteInternal<T> implements TestSuite<T> { | |
| } | ||
| } | ||
|
|
||
| if (testSuite) { | ||
| if (testSuite && testSuite.isSynthetic) { | ||
| // Promote: a child describe of the synthetic global is registered as its | ||
| // own top-level Deno.test rather than as a step of the global suite. The | ||
| // child inherits the global's hooks at run time via syntheticParent. | ||
| this.syntheticParent = testSuite; | ||
| this.registerAsDenoTest(); | ||
|
Comment on lines
+156
to
+161
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If that's an accepted behavior change, please add a regression test that pins it down. If it's a regression, the fix is probably to propagate |
||
| } else if (testSuite) { | ||
| TestSuiteInternal.addStep(testSuite, this); | ||
| } else { | ||
| const { | ||
| name, | ||
| ignore, | ||
| permissions, | ||
| sanitizeExit = globalSanitizersState.sanitizeExit, | ||
| sanitizeOps = globalSanitizersState.sanitizeOps, | ||
| sanitizeResources = globalSanitizersState.sanitizeResources, | ||
| } = describe; | ||
| let { only } = describe; | ||
| if (!ignore && this.hasOnlyStep) { | ||
| only = true; | ||
| } | ||
| const options: Deno.TestDefinition = { | ||
| name, | ||
| fn: async (t) => { | ||
| TestSuiteInternal.runningCount++; | ||
| try { | ||
| const context = {} as T; | ||
| const { beforeAll } = this.describe; | ||
| } else if (!this.isSynthetic) { | ||
| this.registerAsDenoTest(); | ||
| } | ||
| // Synthetic suites without a parent are not registered eagerly. They are | ||
| // registered lazily by `addStep` when a top-level `it()` is added. | ||
| } | ||
|
|
||
| /** Builds the Deno.test options for this suite and registers them. */ | ||
| protected registerAsDenoTest() { | ||
| if (this.#registeredOptions) return; | ||
| const { | ||
| name, | ||
| ignore, | ||
| permissions, | ||
| sanitizeExit = globalSanitizersState.sanitizeExit, | ||
| sanitizeOps = globalSanitizersState.sanitizeOps, | ||
| sanitizeResources = globalSanitizersState.sanitizeResources, | ||
| } = this.describe; | ||
| let { only } = this.describe; | ||
| if (!ignore && this.hasOnlyStep) { | ||
| only = true; | ||
| } | ||
| const options: Deno.TestDefinition = { | ||
| name, | ||
| fn: async (t) => { | ||
| TestSuiteInternal.runningCount++; | ||
| try { | ||
| const context = {} as T; | ||
| const parent = this.syntheticParent; | ||
| if (parent) { | ||
| const { beforeAll } = parent.describe; | ||
| if (typeof beforeAll === "function") { | ||
| await beforeAll.call(context); | ||
| } else if (beforeAll) { | ||
| for (const hook of beforeAll) { | ||
| await hook.call(context); | ||
| } | ||
| } | ||
| try { | ||
| TestSuiteInternal.active.push(this.symbol); | ||
| await TestSuiteInternal.run(this, context, t); | ||
| } finally { | ||
| } | ||
| const { beforeAll } = this.describe; | ||
| if (typeof beforeAll === "function") { | ||
| await beforeAll.call(context); | ||
| } else if (beforeAll) { | ||
| for (const hook of beforeAll) { | ||
| await hook.call(context); | ||
| } | ||
| } | ||
| try { | ||
| if (parent) { | ||
| TestSuiteInternal.active.push(parent.symbol); | ||
| } | ||
| TestSuiteInternal.active.push(this.symbol); | ||
| await TestSuiteInternal.run(this, context, t); | ||
| } finally { | ||
| TestSuiteInternal.active.pop(); | ||
| if (parent) { | ||
| TestSuiteInternal.active.pop(); | ||
| const { afterAll } = this.describe; | ||
| } | ||
| const { afterAll } = this.describe; | ||
| if (typeof afterAll === "function") { | ||
| await afterAll.call(context); | ||
| } else if (afterAll) { | ||
| for (const hook of afterAll) { | ||
| await hook.call(context); | ||
| } | ||
|
Comment on lines
+192
to
+228
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: the |
||
| } | ||
| if (parent) { | ||
| const { afterAll } = parent.describe; | ||
| if (typeof afterAll === "function") { | ||
| await afterAll.call(context); | ||
| } else if (afterAll) { | ||
|
|
@@ -181,31 +237,31 @@ export class TestSuiteInternal<T> implements TestSuite<T> { | |
| } | ||
| } | ||
| } | ||
| } finally { | ||
| TestSuiteInternal.runningCount--; | ||
| } | ||
| }, | ||
| }; | ||
| if (ignore !== undefined) { | ||
| options.ignore = ignore; | ||
| } | ||
| if (only !== undefined) { | ||
| options.only = only; | ||
| } | ||
| if (permissions !== undefined) { | ||
| options.permissions = permissions; | ||
| } | ||
| if (sanitizeExit !== undefined) { | ||
| options.sanitizeExit = sanitizeExit; | ||
| } | ||
| if (sanitizeOps !== undefined) { | ||
| options.sanitizeOps = sanitizeOps; | ||
| } | ||
| if (sanitizeResources !== undefined) { | ||
| options.sanitizeResources = sanitizeResources; | ||
| } | ||
| this.#registeredOptions = TestSuiteInternal.registerTest(options); | ||
| } finally { | ||
| TestSuiteInternal.runningCount--; | ||
| } | ||
| }, | ||
| }; | ||
| if (ignore !== undefined) { | ||
| options.ignore = ignore; | ||
| } | ||
| if (only !== undefined) { | ||
| options.only = only; | ||
| } | ||
| if (permissions !== undefined) { | ||
| options.permissions = permissions; | ||
| } | ||
| if (sanitizeExit !== undefined) { | ||
| options.sanitizeExit = sanitizeExit; | ||
| } | ||
| if (sanitizeOps !== undefined) { | ||
| options.sanitizeOps = sanitizeOps; | ||
| } | ||
| if (sanitizeResources !== undefined) { | ||
| options.sanitizeResources = sanitizeResources; | ||
| } | ||
| this.#registeredOptions = TestSuiteInternal.registerTest(options); | ||
| } | ||
|
|
||
| /** Stores how many test suites are executing. */ | ||
|
|
@@ -289,6 +345,17 @@ export class TestSuiteInternal<T> implements TestSuite<T> { | |
| suite: TestSuiteInternal<T>, | ||
| step: TestSuiteInternal<T> | ItDefinition<T>, | ||
| ) { | ||
| // When adding a top-level `it()` to the synthetic global suite, the global | ||
| // needs to become a real `Deno.test` so the test has a place to run. | ||
| // Child `describe`s are promoted at construction time and never reach | ||
| // `addStep` with the synthetic suite as their parent. | ||
| if ( | ||
| suite.isSynthetic && !suite.#registeredOptions && | ||
| !(step instanceof TestSuiteInternal) | ||
| ) { | ||
| suite.registerAsDenoTest(); | ||
|
Comment on lines
+348
to
+356
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is where the mixed case (top-level hook + top-level
|
||
| } | ||
|
|
||
| if (!suite.hasOnlyStep) { | ||
| if (step instanceof TestSuiteInternal) { | ||
| if (step.hasOnlyStep || step.describe.only) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -834,7 +834,7 @@ function addHook<T>( | |
| TestSuiteInternal.current = new TestSuiteInternal({ | ||
| name: "global", | ||
| [name]: fn, | ||
| }); | ||
| }, true); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Worth a one-line comment here explaining why |
||
| } else { | ||
| TestSuiteInternal.setHook(TestSuiteInternal.current!, name, fn); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two small things on these fields:
readonly(or use a#private prefix to match#registeredOptionsjust below). It signals immutability and prevents accidental mutation.syntheticParentreads slightly off because the parent is what's synthetic, not the field itself. Something likeparentSyntheticorglobalHookSuitewould be clearer. Taste call.Also worth noting in the
syntheticParentJSDoc that this is an asymmetric linkage — normal parents are reached viadescribe.suite, only synthetic globals use this back-reference. Future readers will wonder why two pointers exist.