From 37a20936465e6b352ed26a94567337617179cd45 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Fri, 2 Oct 2026 00:58:17 +0300 Subject: [PATCH 1/2] fix(workers): distribute only tests matching grep run-workers split every loaded test across workers and let each worker apply --grep afterwards. A narrow pattern could put all matching tests on one worker while the others started only to report an empty run. Tests are now filtered by grep (and --invert, config grep, .only) before they are assigned, and workers that would get no tests are not started. When nothing matches, distribution is unchanged so workers still print the empty-run hint. Co-Authored-By: Claude Opus 5.5 --- docs/parallel.md | 2 +- lib/workers.js | 40 ++++++++++++++++++++++----------- test/runner/run_workers_test.js | 32 ++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 14 deletions(-) diff --git a/docs/parallel.md b/docs/parallel.md index d6ed1e080..ebefbe27c 100644 --- a/docs/parallel.md +++ b/docs/parallel.md @@ -20,7 +20,7 @@ For anything more specific — your own grouping, a config per group, several br npx codeceptjs run-workers 4 ``` -Steps are not streamed to the console in this mode — output from separate threads can't be interleaved cleanly. While workers run, CodeceptJS sets `process.env.RUNS_WITH_WORKERS=true`, so plugins and helpers can branch on it. All `run` options work here too: `--grep "@smoke"`, `-c codecept.conf.js`, `--debug`, and the rest. +Steps are not streamed to the console in this mode — output from separate threads can't be interleaved cleanly. While workers run, CodeceptJS sets `process.env.RUNS_WITH_WORKERS=true`, so plugins and helpers can branch on it. All `run` options work here too: `--grep "@smoke"`, `-c codecept.conf.js`, `--debug`, and the rest. With `--grep`, only matching tests are spread across workers, so a narrow pattern may start fewer workers than requested. By default, workers are created with a staggered delay of 200ms to prevent CPU spikes and stagger browser initializations. You can adjust this via `workerInitializationDelay` in your configuration. diff --git a/lib/workers.js b/lib/workers.js index 84a96b02d..1957bb8a9 100644 --- a/lib/workers.js +++ b/lib/workers.js @@ -57,6 +57,17 @@ const populateGroups = numberOfWorkers => { return groups } +const matchesGrep = (mocha, test) => { + const { grep, invert } = mocha.options + if (!grep) return true + return grep.test(test.fullTitle()) !== Boolean(invert) +} + +const withoutEmptyGroups = groups => { + const nonEmpty = groups.filter(group => group.length) + return nonEmpty.length ? nonEmpty : groups.slice(0, 1) +} + const createWorker = (workerObject, isPoolMode = false) => { const worker = new Worker(pathToWorker, { workerData: { @@ -345,7 +356,8 @@ class Workers extends EventEmitter { if (config.by === 'pool') { this.createTestPool(numberOfWorkers) } else { - this.testGroups = config.by === 'suite' ? await this.createGroupsOfSuites(numberOfWorkers) : await this.createGroupsOfTests(numberOfWorkers) + const groups = config.by === 'suite' ? await this.createGroupsOfSuites(numberOfWorkers) : await this.createGroupsOfTests(numberOfWorkers) + this.testGroups = withoutEmptyGroups(groups) } } } @@ -379,13 +391,16 @@ class Workers extends EventEmitter { const groups = populateGroups(numberOfWorkers) let groupCounter = 0 + const tests = [] mocha.suite.eachTest(test => { - const i = groupCounter % groups.length - if (test) { - groups[i].push(test.uid) - groupCounter++ - } + if (test) tests.push(test) }) + const selected = tests.filter(test => matchesGrep(mocha, test)) + + for (const test of selected.length ? selected : tests) { + groups[groupCounter % groups.length].push(test.uid) + groupCounter++ + } // Clean up after collecting test UIDs mocha.unloadFiles() @@ -464,14 +479,13 @@ class Workers extends EventEmitter { mocha.files = files await loadTests(mocha) - mocha.suite.suites.forEach(suite => { + const suites = mocha.suite.suites.map(suite => suite.tests.filter(test => test)) + const selected = suites.map(tests => tests.filter(test => matchesGrep(mocha, test))).filter(tests => tests.length) + + for (const tests of selected.length ? selected : suites) { const i = indexOfSmallestElement(groups) - suite.tests.forEach(test => { - if (test) { - groups[i].push(test.uid) - } - }) - }) + tests.forEach(test => groups[i].push(test.uid)) + } // Clean up after collecting test UIDs mocha.unloadFiles() diff --git a/test/runner/run_workers_test.js b/test/runner/run_workers_test.js index fe805602a..9781e9446 100644 --- a/test/runner/run_workers_test.js +++ b/test/runner/run_workers_test.js @@ -95,6 +95,38 @@ describe('CodeceptJS Workers Runner', function () { }) }) + it('should distribute only tests matching grep', function (done) { + exec(`${codecept_run} 3 --grep "grep"`, (err, stdout) => { + expect(stdout).toContain('Worker 1: 1 test') + expect(stdout).toContain('Worker 2: 1 test') + expect(stdout).not.toContain('Worker 3:') + expect(stdout).not.toContain('No tests found by pattern') + expect(stdout).toContain('OK | 2 passed') + expect(err).toEqual(null) + done() + }) + }) + + it('should distribute only tests not matching inverted grep', function (done) { + exec(`${codecept_run} 2 --grep "Workers" --invert`, (err, stdout) => { + expect(stdout).toContain('Worker 1: 1 test') + expect(stdout).toContain('Worker 2: 1 test') + expect(stdout).toContain('From worker @1_grep print message 1') + expect(stdout).toContain('From worker @2_grep print message 2') + expect(stdout).toContain('OK | 2 passed') + expect(err).toEqual(null) + done() + }) + }) + + it('should suggest tests when grep matches nothing', function (done) { + exec(`${codecept_run} 2 --grep "nonexistent"`, (err, stdout) => { + expect(stdout).toContain('No tests found by pattern: /nonexistent/') + expect(stdout).toContain('OK | 0 passed') + done() + }) + }) + it('should use suites', function (done) { if (!semver.satisfies(process.version, '>=11.7.0')) this.skip('not for node version') exec(`${codecept_run} 2 --suites`, (err, stdout) => { From 0a2ecc9ca35f9fa612eff5659dd75717f401c0fa Mon Sep 17 00:00:00 2001 From: Michael Bodnarchuk Date: Thu, 8 Oct 2026 01:33:13 +0300 Subject: [PATCH 2/2] feat(workers): run serial tests after parallel workers (#5728) * feat(workers): run serial tests after parallel workers Tests matching the new `serial` config pattern are kept out of the parallel workers. Once those exit, one more worker starts and runs the serial tests one by one, inside the same run: bootstrapAll, teardownAll and workers.result fire once and the exit code covers both parts. Works with every --by strategy and combines with --grep. Co-Authored-By: Claude Opus 5.5 * feat(workers): add --serial option to run-workers Sets or overrides the serial config pattern for a single run. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: DavertMik Co-authored-by: Claude Opus 5.5 --- bin/codecept.js | 1 + docs/commands.md | 5 ++ docs/configuration.md | 1 + docs/parallel.md | 34 +++++++++ lib/command/run-workers.js | 2 +- lib/command/workers/runTests.js | 19 +++-- lib/workers.js | 66 +++++++++++++--- .../sandbox/codecept.workers-serial.conf.js | 12 +++ .../sandbox/workers-serial/a_serial_test.js | 11 +++ .../sandbox/workers-serial/b_parallel_test.js | 19 +++++ test/data/sandbox/workers-serial/timeline.js | 10 +++ test/runner/run_workers_test.js | 75 +++++++++++++++++++ typings/index.d.ts | 9 +++ 13 files changed, 244 insertions(+), 20 deletions(-) create mode 100644 test/data/sandbox/codecept.workers-serial.conf.js create mode 100644 test/data/sandbox/workers-serial/a_serial_test.js create mode 100644 test/data/sandbox/workers-serial/b_parallel_test.js create mode 100644 test/data/sandbox/workers-serial/timeline.js diff --git a/bin/codecept.js b/bin/codecept.js index e270e2920..65112a1e3 100755 --- a/bin/codecept.js +++ b/bin/codecept.js @@ -209,6 +209,7 @@ program .option('-i, --invert', 'inverts --grep matches') .option('-o, --override [value]', 'override current config options') .option('--suites', 'parallel execution of suites not single tests') + .option('--serial ', 'run tests matching one by one after parallel workers finish') .option('--by ', 'test distribution strategy: "test" (pre-assign individual tests), "suite" (pre-assign test suites), or "pool" (dynamic distribution for optimal load balancing, recommended)') .option(commandFlags.debug.flag, commandFlags.debug.description) .option(commandFlags.verbose.flag, commandFlags.verbose.description) diff --git a/docs/commands.md b/docs/commands.md index dff6a23b6..fdc90efc2 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -157,6 +157,9 @@ npx codeceptjs run-workers 3 --by suite # Pool mode with filtering npx codeceptjs run-workers 4 --by pool --grep "@smoke" + +# Run tests tagged @serial one by one after parallel workers finish +npx codeceptjs run-workers 3 --serial "@serial" ``` **Test Distribution Strategies:** @@ -165,6 +168,8 @@ npx codeceptjs run-workers 4 --by pool --grep "@smoke" - `--by suite`: Pre-assigns entire test suites to workers - `--by pool`: Dynamic distribution for optimal load balancing (recommended for best performance) +`--serial ` keeps tests matching the pattern out of parallel workers and runs them one by one in an extra worker once the others finish, within the same run. It overrides the `serial` config option. See [Serial tests](/parallel#serial-tests). + The pool mode provides the best load balancing by maintaining tests in a shared pool and distributing them dynamically as workers become available. This prevents workers from sitting idle and ensures optimal CPU utilization, especially when tests have varying execution times. See [Parallel Execution](/parallel) documentation for more details. diff --git a/docs/configuration.md b/docs/configuration.md index 51d8ff097..2dbc4d367 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -44,6 +44,7 @@ export const config = { - `timeout` — default per-test timeout in seconds; a test is killed if it stops responding. - `mocha` — [Mocha options](https://mochajs.org/#configuring-mocha-nodejs), including extra reporters. See [Reporters](/reports). +- `serial` — pattern for tests that must not run in parallel, e.g. `serial: '@serial'`. With `run-workers` they run one by one after all parallel workers finish. `--serial` overrides it. See [Serial tests](/parallel#serial-tests). - `workerInitializationDelay` — delay in milliseconds between spinning up parallel workers to prevent CPU spikes and stagger browser startup. Defaults to `200`. Set to `0` to disable. - `workerInitializationMaxDelay` — maximum total delay (in milliseconds) for worker initialization staggering. Defaults to `10000` (10 s). Set to `0` to disable capping. diff --git a/docs/parallel.md b/docs/parallel.md index ebefbe27c..ed1187f10 100644 --- a/docs/parallel.md +++ b/docs/parallel.md @@ -40,6 +40,40 @@ npx codeceptjs run-workers 4 --by pool `--suites` is shorthand for `--by suite`. +### Serial tests + +Some tests can't share the stage: they change global settings, reset data, or toggle a feature flag that other tests read. Mark them with a tag and set the `serial` pattern in config or pass it with `--serial`: + +```js +// codecept.conf.js +export const config = { + serial: '@serial', +} +``` + +```js +Scenario('change account timezone @serial', ({ I }) => { + // ... +}) +``` + +`run-workers` keeps matching tests out of the parallel workers. When those workers finish, one more worker starts and runs the serial tests one by one. It is still a single run: `bootstrapAll`, `teardownAll` and `event.workers.result` fire once, and the exit code covers both parts. + +``` +Test distribution: + Worker 1: 14 tests + Worker 2: 13 tests + Worker 3: 4 tests (serial) +``` + +Pass `--serial` to set or override the pattern for a single run: + +```sh +npx codeceptjs run-workers 3 --serial "@serial" +``` + +`serial` is matched against the full test title the same way `--grep` is, so a tag on a `Feature` makes all its scenarios serial. It works with every `--by` strategy and combines with `--grep`: only serial tests that also match grep are run. With [multiple browsers](#multiple-browsers) each profile gets its own serial worker, started one after another. + ### Multiple browsers Define browser profiles under `multiple` in `codecept.conf.js`: diff --git a/lib/command/run-workers.js b/lib/command/run-workers.js index 00c343532..76ecea0a0 100644 --- a/lib/command/run-workers.js +++ b/lib/command/run-workers.js @@ -55,7 +55,7 @@ export default async function (workerCount, selectedRuns, options) { output.print('Test distribution:') workerObjects.forEach((worker, index) => { const testCount = worker.tests.length - output.print(` Worker ${index + 1}: ${testCount} test${testCount !== 1 ? 's' : ''}`) + output.print(` Worker ${index + 1}: ${testCount} test${testCount !== 1 ? 's' : ''}${worker.serial ? ' (serial)' : ''}`) }) output.print() diff --git a/lib/command/workers/runTests.js b/lib/command/workers/runTests.js index 9d6519117..a0e12ca90 100644 --- a/lib/command/workers/runTests.js +++ b/lib/command/workers/runTests.js @@ -17,7 +17,7 @@ let stdout = '' const stderr = '' -const { options, tests, testRoot, workerIndex, poolMode } = workerData +const { options, tests, skipTests = [], testRoot, workerIndex, poolMode } = workerData // Global error handlers to catch critical errors but not test failures process.on('uncaughtException', (err) => { @@ -287,6 +287,7 @@ async function runPoolTests() { // Load only the assigned test file mocha.files = [testIdentifier] await loadTests(mocha) + filterSuites(mocha.suite.suites, test => !skipTests.includes(test.uid)) if (mocha.suite.total() > 0) { // Run only the tests in the current mocha suite @@ -423,16 +424,14 @@ async function filterTests() { mocha.files = files await loadTests(mocha) - // Recursively filter tests in all suites (including nested ones) - const filterSuiteTests = (suite) => { - suite.tests = suite.tests.filter(test => tests.indexOf(test.uid) >= 0) - for (const childSuite of suite.suites) { - filterSuiteTests(childSuite) - } - } + filterSuites(mocha.suite.suites, test => tests.indexOf(test.uid) >= 0) +} - for (const suite of mocha.suite.suites) { - filterSuiteTests(suite) +// Recursively filter tests in all suites (including nested ones) +function filterSuites(suites, keep) { + for (const suite of suites) { + suite.tests = suite.tests.filter(keep) + filterSuites(suite.suites, keep) } } diff --git a/lib/workers.js b/lib/workers.js index 1957bb8a9..af3e39b1d 100644 --- a/lib/workers.js +++ b/lib/workers.js @@ -68,11 +68,12 @@ const withoutEmptyGroups = groups => { return nonEmpty.length ? nonEmpty : groups.slice(0, 1) } -const createWorker = (workerObject, isPoolMode = false) => { +const createWorker = (workerObject, isPoolMode = false, skipTests = []) => { const worker = new Worker(pathToWorker, { workerData: { options: simplifyObject(workerObject.options), tests: workerObject.tests, + skipTests, testRoot: workerObject.testRoot, workerIndex: workerObject.workerIndex + 1, poolMode: isPoolMode, @@ -224,6 +225,7 @@ class WorkerObject { */ constructor(workerIndex) { this.workerIndex = workerIndex + this.serial = false this.options = {} this.tests = [] this.testRoot = getTestRoot() @@ -299,6 +301,7 @@ class Workers extends EventEmitter { this.testGroups = [] this.testPool = [] this.testPoolInitialized = false + this.serialTests = [] this.isPoolMode = config.by === 'pool' this.activeWorkers = new Map() this.maxWorkers = numberOfWorkers // Track original worker count for pool mode @@ -323,9 +326,21 @@ class Workers extends EventEmitter { async _initWorkers(numberOfWorkers, config) { await this.splitTestsByGroups(numberOfWorkers, config) + this.serialTests = await this.createGroupOfSerialTests() + if (this.serialTests.length && !this.isPoolMode) { + this.testGroups = this.testGroups.map(group => group.filter(uid => !this.serialTests.includes(uid))).filter(group => group.length) + } // For function-based grouping, use the actual number of test groups created const actualNumberOfWorkers = isFunction(config.by) ? this.testGroups.length : numberOfWorkers this.workers = createWorkerObjects(this.testGroups, this.codecept.config, getTestRoot(config.testConfig), config.options, config.selectedRuns) + if (this.serialTests.length) { + const serialWorkers = createWorkerObjects([this.serialTests], this.codecept.config, getTestRoot(config.testConfig), config.options, config.selectedRuns) + for (const worker of serialWorkers) { + worker.workerIndex = this.workers.length + worker.serial = true + this.workers.push(worker) + } + } this.numberOfWorkers = this.workers.length } @@ -408,6 +423,32 @@ class Workers extends EventEmitter { return groups } + /** + * Collects tests matching `--serial` option or `serial` config pattern. + * These tests are excluded from parallel workers and run one by one after them. + * + * @returns {Promise} + */ + async createGroupOfSerialTests() { + const pattern = this.options.serial || this.codecept?.config.serial + if (!pattern) return [] + const serial = new RegExp(pattern) + + Container.createMocha(this.codecept.config.mocha || {}, this.options) + const mocha = Container.mocha() + mocha.files = this.codecept.testFiles + await loadTests(mocha, { reload: true }) + + const group = [] + mocha.suite.eachTest(test => { + if (test && serial.test(test.fullTitle()) && matchesGrep(mocha, test)) group.push(test.uid) + }) + + mocha.unloadFiles() + + return group + } + /** * @param {Number} numberOfWorkers */ @@ -534,7 +575,8 @@ class Workers extends EventEmitter { : 0 for (const worker of this.workers) { - const workerThread = createWorker(worker, this.isPoolMode) + if (worker.serial) continue + const workerThread = createWorker(worker, this.isPoolMode, this.serialTests) this._listenWorkerEvents(workerThread) workerThreads.push(workerThread) @@ -545,6 +587,8 @@ class Workers extends EventEmitter { } } + if (!workerThreads.length) this._runSerialWorker() + recorder.add('workers started', () => { // Workers are already running, this is just a placeholder step }) @@ -556,6 +600,14 @@ class Workers extends EventEmitter { }) } + _runSerialWorker() { + const parallelWorkers = this.workers.filter(worker => !worker.serial) + if (this.closedWorkers < parallelWorkers.length) return + const worker = this.workers[this.closedWorkers] + if (!worker) return + this._listenWorkerEvents(createWorker(worker)) + } + /** * @returns {Array} */ @@ -787,14 +839,10 @@ class Workers extends EventEmitter { process.exitCode = 1 } - if (this.isPoolMode) { - // Pool mode: finish when all workers have exited and no more tests - if (this.closedWorkers === this.numberOfWorkers) { - this._finishRun() - } - } else if (this.closedWorkers === this.numberOfWorkers) { - // Regular mode: finish when all original workers have exited + if (this.closedWorkers === this.numberOfWorkers) { this._finishRun() + } else { + this._runSerialWorker() } }) } diff --git a/test/data/sandbox/codecept.workers-serial.conf.js b/test/data/sandbox/codecept.workers-serial.conf.js new file mode 100644 index 000000000..d827a7c90 --- /dev/null +++ b/test/data/sandbox/codecept.workers-serial.conf.js @@ -0,0 +1,12 @@ +export const config = { + tests: './workers-serial/*_test.js', + timeout: 10000, + output: './output', + helpers: { + FileSystem: {}, + }, + include: {}, + serial: '@serial', + mocha: {}, + name: 'sandbox', +}; diff --git a/test/data/sandbox/workers-serial/a_serial_test.js b/test/data/sandbox/workers-serial/a_serial_test.js new file mode 100644 index 000000000..b383661ef --- /dev/null +++ b/test/data/sandbox/workers-serial/a_serial_test.js @@ -0,0 +1,11 @@ +import { track } from './timeline.js' + +Feature('Serial lane @serial') + +Scenario('serial one', async () => { + await track('serial one') +}) + +Scenario('serial two', async () => { + await track('serial two') +}) diff --git a/test/data/sandbox/workers-serial/b_parallel_test.js b/test/data/sandbox/workers-serial/b_parallel_test.js new file mode 100644 index 000000000..f23d39de7 --- /dev/null +++ b/test/data/sandbox/workers-serial/b_parallel_test.js @@ -0,0 +1,19 @@ +import { track } from './timeline.js' + +Feature('Parallel lane') + +Scenario('parallel one', async () => { + await track('parallel one') +}) + +Scenario('parallel two', async () => { + await track('parallel two') +}) + +Scenario('serial three @serial', async () => { + await track('serial three') +}) + +Scenario('parallel three', async () => { + await track('parallel three') +}) diff --git a/test/data/sandbox/workers-serial/timeline.js b/test/data/sandbox/workers-serial/timeline.js new file mode 100644 index 000000000..d407a2b78 --- /dev/null +++ b/test/data/sandbox/workers-serial/timeline.js @@ -0,0 +1,10 @@ +import fs from 'fs' +import { fileURLToPath } from 'url' + +export const timelineFile = fileURLToPath(new URL('../output/serial_timeline.log', import.meta.url)) + +export async function track(name, duration = 300) { + fs.appendFileSync(timelineFile, `start ${name}\n`) + await new Promise(resolve => setTimeout(resolve, duration)) + fs.appendFileSync(timelineFile, `end ${name}\n`) +} diff --git a/test/runner/run_workers_test.js b/test/runner/run_workers_test.js index 9781e9446..45930d8ad 100644 --- a/test/runner/run_workers_test.js +++ b/test/runner/run_workers_test.js @@ -591,4 +591,79 @@ describe('CodeceptJS Workers Runner', function () { done() }) }) + + describe('serial tests', () => { + const timelineFile = path.join(codecept_dir, 'output', 'serial_timeline.log') + const serial_run = `${codecept_run_glob('codecept.workers-serial.conf.js')} 2` + + const timeline = () => fs.readFileSync(timelineFile, 'utf8').trim().split('\n') + + const expectSerialAfterParallel = (events, serialTests) => { + const serialEvents = serialTests.flatMap(name => [`start ${name}`, `end ${name}`]) + expect(events.slice(-serialEvents.length)).toEqual(serialEvents) + expect(events.slice(0, -serialEvents.length).filter(e => e.includes('serial'))).toEqual([]) + } + + beforeEach(() => { + fs.mkdirSync(path.dirname(timelineFile), { recursive: true }) + fs.rmSync(timelineFile, { force: true }) + }) + + after(() => fs.rmSync(timelineFile, { force: true })) + + for (const by of ['test', 'suite', 'pool']) { + it(`should run serial tests one by one after parallel ones with --by ${by}`, function (done) { + exec(`${serial_run} --by ${by}`, (err, stdout) => { + expect(stdout).toContain('3 tests (serial)') + expect(stdout).toContain('OK | 6 passed') + const events = timeline() + expect(events).toHaveLength(12) + expectSerialAfterParallel(events, ['serial three', 'serial one', 'serial two']) + expect(err).toEqual(null) + done() + }) + }) + } + + it('should apply grep to serial tests', function (done) { + exec(`${serial_run} --grep "serial one|parallel"`, (err, stdout) => { + expect(stdout).toContain('1 test (serial)') + expect(stdout).toContain('OK | 4 passed') + expectSerialAfterParallel(timeline(), ['serial one']) + expect(err).toEqual(null) + done() + }) + }) + + it('should run only serial tests when no parallel tests match grep', function (done) { + exec(`${serial_run} --grep "@serial"`, (err, stdout) => { + expect(stdout).toContain('Worker 1: 3 tests (serial)') + expect(stdout).not.toContain('Worker 2:') + expect(stdout).toContain('OK | 3 passed') + expect(err).toEqual(null) + done() + }) + }) + + it('should take serial pattern from --serial option', function (done) { + exec(`${serial_run} --serial "parallel one"`, (err, stdout) => { + expect(stdout).toContain('1 test (serial)') + expect(stdout).toContain('OK | 6 passed') + const events = timeline() + expect(events).toHaveLength(12) + expect(events.slice(-2)).toEqual(['start parallel one', 'end parallel one']) + expect(err).toEqual(null) + done() + }) + }) + + it('should not start a serial worker when no serial tests match grep', function (done) { + exec(`${serial_run} --grep "parallel one"`, (err, stdout) => { + expect(stdout).not.toContain('(serial)') + expect(stdout).toContain('OK | 1 passed') + expect(err).toEqual(null) + done() + }) + }) + }) }) diff --git a/typings/index.d.ts b/typings/index.d.ts index 3d57f98ba..8686a5c3b 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -132,6 +132,15 @@ declare namespace CodeceptJS { * ``` */ grep?: string + /** + * Pattern for tests that must not run in parallel. + * In `run-workers` matching tests run one by one after all parallel workers finish. + * + * ```js + * serial: '@serial' + * ``` + */ + serial?: string | RegExp /** * Enable and configure helpers: *