diff --git a/packages/core/src/runtime/runner/task.ts b/packages/core/src/runtime/runner/task.ts index 98309ae06..9a0a6c29b 100644 --- a/packages/core/src/runtime/runner/task.ts +++ b/packages/core/src/runtime/runner/task.ts @@ -27,97 +27,152 @@ export const getTestStatus = ( : 'pass'; }; -function hasOnlyTest(test: Test[]): boolean { - return test.some((t) => { - return t.runMode === 'only' || (t.type === 'suite' && hasOnlyTest(t.tests)); - }); -} +type TestModeContext = { + shouldSkipByName?: (test: TestCase) => boolean; + suiteHasOnlyDescendants: WeakMap; +}; + +const collectOnlyTests = ( + tests: Test[], + suiteHasOnlyDescendants: WeakMap, +): boolean => { + let hasOnly = false; + + for (const test of tests) { + const childrenHaveOnly = + test.type === 'suite' + ? collectOnlyTests(test.tests, suiteHasOnlyDescendants) + : false; + + if (test.type === 'suite') { + suiteHasOnlyDescendants.set(test, childrenHaveOnly); + } + + if (test.runMode === 'only' || childrenHaveOnly) { + hasOnly = true; + } + } + + return hasOnly; +}; + +const createShouldSkipByName = ( + testNamePattern?: RegExp | string, +): ((test: TestCase) => boolean) | undefined => { + if (!testNamePattern) { + return undefined; + } + + const regex = + typeof testNamePattern === 'string' + ? new RegExp(testNamePattern) + : testNamePattern; + const delimiter = regex.toString().includes(TEST_DELIMITER) + ? TEST_DELIMITER + : ''; + + return (test: TestCase) => { + if (regex.global || regex.sticky) { + regex.lastIndex = 0; + } + + return !regex.test(getTaskNameWithPrefix(test, delimiter)); + }; +}; const shouldTestSkip = ( test: TestCase, runOnly: boolean, - testNamePattern?: RegExp | string, + shouldSkipByName?: (test: TestCase) => boolean, ) => { if (runOnly && test.runMode !== 'only') { return true; } - const delimiter = testNamePattern?.toString().includes(TEST_DELIMITER) - ? TEST_DELIMITER - : ''; - - if ( - testNamePattern && - !getTaskNameWithPrefix(test, delimiter).match(testNamePattern) - ) { + if (shouldSkipByName?.(test)) { return true; } return false; }; -export const traverseUpdateTestRunMode = ( +const traverseUpdateTestRunModeWithContext = ( testSuite: TestSuite, parentRunMode: TestRunMode, runOnly: boolean, - testNamePattern?: RegExp | string, + context: TestModeContext, ): void => { if (testSuite.tests.length === 0) { return; } - if ( - runOnly && - testSuite.runMode !== 'only' && - !hasOnlyTest(testSuite.tests) - ) { + const childrenHaveOnly = + context.suiteHasOnlyDescendants.get(testSuite) ?? false; + + if (runOnly && testSuite.runMode !== 'only' && !childrenHaveOnly) { testSuite.runMode = 'skip'; } else if (['skip', 'todo'].includes(parentRunMode)) { testSuite.runMode = parentRunMode; } - const tests = testSuite.tests.map((test) => { - const runSubOnly = - runOnly && testSuite.runMode !== 'only' - ? runOnly - : hasOnlyTest(testSuite.tests); + const runSubOnly = + runOnly && testSuite.runMode !== 'only' ? runOnly : childrenHaveOnly; + let hasRunTest = false; + let allTodoTest = true; + for (const test of testSuite.tests) { if (test.type === 'case') { if (['skip', 'todo'].includes(testSuite.runMode)) { test.runMode = testSuite.runMode; } - if (shouldTestSkip(test, runSubOnly, testNamePattern)) { + if (shouldTestSkip(test, runSubOnly, context.shouldSkipByName)) { test.runMode = 'skip'; } - return test; + } else { + traverseUpdateTestRunModeWithContext( + test, + testSuite.runMode, + runSubOnly, + context, + ); } - traverseUpdateTestRunMode( - test, - testSuite.runMode, - runSubOnly, - testNamePattern, - ); - return test; - }); + + if (test.runMode === 'run' || test.runMode === 'only') { + hasRunTest = true; + } + + if (test.runMode !== 'todo') { + allTodoTest = false; + } + } if (testSuite.runMode !== 'run') { return; } - const hasRunTest = tests.some( - (test) => test.runMode === 'run' || test.runMode === 'only', - ); - if (hasRunTest) { testSuite.runMode = 'run'; return; } - const allTodoTest = tests.every((test) => test.runMode === 'todo'); - testSuite.runMode = allTodoTest ? 'todo' : 'skip'; }; +export const traverseUpdateTestRunMode = ( + testSuite: TestSuite, + parentRunMode: TestRunMode, + runOnly: boolean, + testNamePattern?: RegExp | string, +): void => { + const suiteHasOnlyDescendants = new WeakMap(); + collectOnlyTests([testSuite], suiteHasOnlyDescendants); + + traverseUpdateTestRunModeWithContext(testSuite, parentRunMode, runOnly, { + shouldSkipByName: createShouldSkipByName(testNamePattern), + suiteHasOnlyDescendants, + }); +}; + /** * sets the runMode of the test based on the runMode of its parent suite * - if the parent suite is 'todo', set the test to 'todo' @@ -136,12 +191,17 @@ export const updateTestModes = ( tests: Test[], testNamePattern?: RegExp | string, ): void => { - const hasOnly = hasOnlyTest(tests); + const suiteHasOnlyDescendants = new WeakMap(); + const hasOnly = collectOnlyTests(tests, suiteHasOnlyDescendants); + const shouldSkipByName = createShouldSkipByName(testNamePattern); for (const test of tests) { if (test.type === 'suite') { - traverseUpdateTestRunMode(test, 'run', hasOnly, testNamePattern); - } else if (shouldTestSkip(test, hasOnly, testNamePattern)) { + traverseUpdateTestRunModeWithContext(test, 'run', hasOnly, { + shouldSkipByName, + suiteHasOnlyDescendants, + }); + } else if (shouldTestSkip(test, hasOnly, shouldSkipByName)) { test.runMode = 'skip'; } } diff --git a/packages/core/tests/runner/runner.test.ts b/packages/core/tests/runner/runner.test.ts index 3fac40744..db7f09d27 100644 --- a/packages/core/tests/runner/runner.test.ts +++ b/packages/core/tests/runner/runner.test.ts @@ -339,4 +339,66 @@ describe('traverseUpdateTest', () => { ] `); }); + + it('updateTestMode with string and global regex patterns', () => { + const createTests = (): [TestSuite, TestCase] => [ + { + name: 'testA', + runMode: 'run', + type: 'suite', + tests: [ + { + name: 'test-0', + type: 'case', + runMode: 'run', + }, + { + name: 'test-1', + type: 'case', + runMode: 'run', + }, + { + name: 'test-2', + type: 'suite', + runMode: 'run', + tests: [ + { + name: 'test-2-1', + type: 'case', + runMode: 'run', + }, + { + name: 'test-2-2', + type: 'case', + runMode: 'run', + }, + ], + }, + ], + } as TestSuite, + { + name: 'testB', + runMode: 'run', + type: 'case', + } as TestCase, + ]; + + const stringPatternTests = createTests(); + traverseUpdateTest(stringPatternTests, '2-1'); + + expect(stringPatternTests[0].tests[0]?.runMode).toBe('skip'); + expect(stringPatternTests[0].tests[1]?.runMode).toBe('skip'); + expect( + (stringPatternTests[0].tests[2] as TestSuite).tests[0]?.runMode, + ).toBe('run'); + expect( + (stringPatternTests[0].tests[2] as TestSuite).tests[1]?.runMode, + ).toBe('skip'); + expect(stringPatternTests[1].runMode).toBe('skip'); + + const globalRegexTests = createTests(); + traverseUpdateTest(globalRegexTests, /2-1/g); + + expect(globalRegexTests).toEqual(stringPatternTests); + }); });