├── .editorconfig
├── .github
└── workflows
│ ├── checks.yml
│ ├── labels.yml
│ ├── release.yml
│ └── stale.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── LICENSE.md
├── README.md
├── eslint.config.js
├── examples
├── dot.ts
├── ndjson.ts
└── spec.ts
├── factories
├── create_dummy_tests.ts
├── main.ts
└── runner.ts
├── index.ts
├── modules
└── core
│ ├── main.ts
│ ├── reporters
│ └── base.ts
│ └── types.ts
├── package.json
├── src
├── cli_parser.ts
├── config_manager.ts
├── create_test.ts
├── debug.ts
├── exceptions_manager.ts
├── files_manager.ts
├── helpers.ts
├── hooks.ts
├── planner.ts
├── plugins
│ └── retry.ts
├── reporters
│ ├── dot.ts
│ ├── github.ts
│ ├── main.ts
│ ├── ndjson.ts
│ └── spec.ts
├── types.ts
└── validator.ts
├── tests
├── base_reporter.spec.ts
├── cli_parser.spec.ts
├── config_manager.spec.ts
├── core.spec.ts
├── files_manager.spec.ts
├── github_reporter.spec.ts
├── helpers.ts
├── planner.spec.ts
└── runner.spec.ts
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.json]
12 | insert_final_newline = ignore
13 |
14 | [**.min.js]
15 | indent_style = ignore
16 | insert_final_newline = ignore
17 |
18 | [MakeFile]
19 | indent_style = space
20 |
21 | [*.md]
22 | trim_trailing_whitespace = false
23 |
--------------------------------------------------------------------------------
/.github/workflows/checks.yml:
--------------------------------------------------------------------------------
1 | name: test
2 | on:
3 | - push
4 | - pull_request
5 | - workflow_call
6 |
7 | jobs:
8 | test:
9 | uses: japa/.github/.github/workflows/test.yml@main
10 | lint:
11 | uses: japa/.github/.github/workflows/lint.yml@main
12 | typecheck:
13 | uses: japa/.github/.github/workflows/typecheck.yml@main
14 |
--------------------------------------------------------------------------------
/.github/workflows/labels.yml:
--------------------------------------------------------------------------------
1 | name: Sync labels
2 | on:
3 | workflow_dispatch:
4 | permissions:
5 | issues: write
6 | jobs:
7 | labels:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v4
11 | - uses: EndBug/label-sync@v2
12 | with:
13 | config-file: 'https://raw.githubusercontent.com/thetutlage/static/main/labels.yml'
14 | delete-other-labels: true
15 | token: ${{ secrets.GITHUB_TOKEN }}
16 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 | on: workflow_dispatch
3 | permissions:
4 | contents: write
5 | id-token: write
6 | jobs:
7 | checks:
8 | uses: ./.github/workflows/checks.yml
9 | release:
10 | needs: checks
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | with:
15 | fetch-depth: 0
16 | - uses: actions/setup-node@v4
17 | with:
18 | node-version: 20
19 | - name: git config
20 | run: |
21 | git config user.name "${GITHUB_ACTOR}"
22 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
23 | - name: Init npm config
24 | run: npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN
25 | env:
26 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
27 | - run: npm install
28 | - run: npm run release -- --ci
29 | env:
30 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
32 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
33 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | name: 'Close stale issues and PRs'
2 | on:
3 | schedule:
4 | - cron: '30 0 * * *'
5 |
6 | jobs:
7 | stale:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/stale@v9
11 | with:
12 | stale-issue-message: 'This issue has been marked as stale because it has been inactive for more than 21 days. Please reopen if you still need help on this issue'
13 | stale-pr-message: 'This pull request has been marked as stale because it has been inactive for more than 21 days. Please reopen if you still intend to submit this pull request'
14 | close-issue-message: 'This issue has been automatically closed because it has been inactive for more than 4 weeks. Please reopen if you still need help on this issue'
15 | close-pr-message: 'This pull request has been automatically closed because it has been inactive for more than 4 weeks. Please reopen if you still intend to submit this pull request'
16 | days-before-stale: 21
17 | days-before-close: 5
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | test/__app
4 | .DS_STORE
5 | .nyc_output
6 | .idea
7 | .vscode/
8 | *.sublime-project
9 | *.sublime-workspace
10 | *.log
11 | build
12 | dist
13 | shrinkwrap.yaml
14 | .env
15 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | build
2 | docs
3 | *.md
4 | config.json
5 | .eslintrc.json
6 | package.json
7 | *.html
8 | *.txt
9 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # The MIT License
2 |
3 | Copyright (c) 2023 Harminder Virk
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # @japa/runner
2 | > A simple yet powerful testing framework for Node.js
3 |
4 | [![github-actions-image]][github-actions-url] [![npm-image]][npm-url] [![license-image]][license-url] [![typescript-image]][typescript-url]
5 |
6 | Japa comes with everything you need to test your backend applications. Be it writing JSON API tests using an Open API schema or writing browser tests using Playwright.
7 |
8 | Unlike other testing frameworks born out of the frontend ecosystem, Japa focuses only on testing backend applications and libraries. Therefore, Japa is **simpler**, **faster**, and **bloatware free**.
9 |
10 | Japa
11 |
12 | #### 💁 Please visit https://japa.dev for documentation
13 |
14 |
15 |
16 |
17 | 
18 |
19 | [github-actions-image]: https://img.shields.io/github/actions/workflow/status/japa/runner/checks.yml?style=for-the-badge "github-actions"
20 |
21 | [github-actions-url]: https://github.com/japa/runner/actions/workflows/checks.yml
22 |
23 | [npm-image]: https://img.shields.io/npm/v/@japa/runner.svg?style=for-the-badge&logo=npm
24 | [npm-url]: https://npmjs.org/package/@japa/runner "npm"
25 |
26 | [license-image]: https://img.shields.io/npm/l/@japa/runner?color=blueviolet&style=for-the-badge
27 | [license-url]: LICENSE.md "license"
28 |
29 | [typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
30 | [typescript-url]: "typescript"
31 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import { configPkg } from '@adonisjs/eslint-config'
2 | export default configPkg({
3 | ignores: ['coverage'],
4 | })
5 |
--------------------------------------------------------------------------------
/examples/dot.ts:
--------------------------------------------------------------------------------
1 | import { dot } from '../src/reporters/main.js'
2 | import { createDummyTests, runner } from '../factories/main.js'
3 |
4 | await runner()
5 | .configure({
6 | files: [],
7 | reporters: {
8 | list: [dot()],
9 | activated: ['dot'],
10 | },
11 | })
12 | .runSuites(createDummyTests)
13 |
--------------------------------------------------------------------------------
/examples/ndjson.ts:
--------------------------------------------------------------------------------
1 | import { ndjson } from '../src/reporters/main.js'
2 | import { createDummyTests, runner } from '../factories/main.js'
3 |
4 | await runner()
5 | .configure({
6 | files: [],
7 | reporters: {
8 | list: [ndjson()],
9 | activated: ['ndjson'],
10 | },
11 | })
12 | .runSuites(createDummyTests)
13 |
--------------------------------------------------------------------------------
/examples/spec.ts:
--------------------------------------------------------------------------------
1 | import { spec } from '../src/reporters/main.js'
2 | import { createDummyTests, runner } from '../factories/main.js'
3 |
4 | await runner()
5 | .configure({
6 | files: [],
7 | reporters: {
8 | list: [spec()],
9 | activated: ['spec'],
10 | },
11 | })
12 | .runSuites(createDummyTests)
13 |
--------------------------------------------------------------------------------
/factories/create_dummy_tests.ts:
--------------------------------------------------------------------------------
1 | import assert from 'node:assert'
2 | import { Suite, Emitter, Refiner } from '../modules/core/main.js'
3 | import { createTest, createTestGroup } from '../src/create_test.js'
4 |
5 | /**
6 | * Creates a unit tests suite with bunch of dummy tests
7 | * reproducing different tests behavior
8 | */
9 | function createUnitTestsSuite(emitter: Emitter, refiner: Refiner, file?: string) {
10 | const suite = new Suite('unit', emitter, refiner)
11 | const group = createTestGroup('Maths#add', emitter, refiner, {
12 | suite,
13 | file,
14 | })
15 |
16 | createTest('A top level test inside a suite', emitter, refiner, {
17 | suite,
18 | file,
19 | }).run(() => {})
20 |
21 | createTest('add two numbers', emitter, refiner, { group, file }).run(() => {
22 | assert.equal(2 + 2, 4)
23 | })
24 | createTest('add three numbers', emitter, refiner, {
25 | group,
26 | file,
27 | }).run(() => {
28 | assert.equal(2 + 2 + 2, 6)
29 | })
30 |
31 | createTest('add group of numbers', emitter, refiner, { group, file })
32 | createTest('use math.js lib', emitter, refiner, { group, file }).skip(
33 | true,
34 | 'Library work pending'
35 | )
36 | createTest('add multiple numbers', emitter, refiner, {
37 | file,
38 | group,
39 | }).run(() => {
40 | assert.equal(2 + 2 + 2 + 2, 6)
41 | })
42 | createTest('add floating numbers', emitter, refiner, { group, file })
43 | .run(() => {
44 | assert.equal(2 + 2.2 + 2.1, 6)
45 | })
46 | .fails('Have to add support for floating numbers')
47 |
48 | createTest('regression test that is passing', emitter, refiner, { group, file })
49 | .run(() => {
50 | assert.equal(2 + 2.2 + 2.1, 2 + 2.2 + 2.1)
51 | })
52 | .fails('Have to add support for floating numbers')
53 |
54 | createTest('A test with an error that is not an AssertionError', emitter, refiner, {
55 | group,
56 | file,
57 | }).run(() => {
58 | throw new Error('This is an error')
59 | })
60 |
61 | return suite
62 | }
63 |
64 | /**
65 | * Creates a unit functional suite with bunch of dummy tests
66 | * reproducing different tests behavior
67 | */
68 | function createFunctionalTestsSuite(emitter: Emitter, refiner: Refiner, file?: string) {
69 | const suite = new Suite('functional', emitter, refiner)
70 |
71 | const group = createTestGroup('Users/store', emitter, refiner, {
72 | suite,
73 | file: file,
74 | })
75 | createTest('Validate user data', emitter, refiner, {
76 | group,
77 | file: file,
78 | }).run(() => {})
79 | createTest('Disallow duplicate emails', emitter, refiner, {
80 | group,
81 | file: file,
82 | }).run(() => {})
83 | createTest('Disallow duplicate emails across tenants', emitter, refiner, {
84 | group,
85 | file: file,
86 | }).run(() => {
87 | const users = ['', '']
88 | assert.equal(users.length, 1)
89 | })
90 | createTest('Normalize email before persisting it', emitter, refiner, {
91 | group,
92 | file: file,
93 | }).skip(true, 'Have to build a normalizer')
94 | createTest('Send email verification mail', emitter, refiner, {
95 | group,
96 | file: file,
97 | })
98 |
99 | createTest('Test that times out', emitter, refiner, {
100 | group,
101 | file: file,
102 | }).run(() => {
103 | return new Promise((resolve) => {
104 | setTimeout(resolve, 2100)
105 | })
106 | })
107 |
108 | const usersListGroup = createTestGroup('Users/list', emitter, refiner, {
109 | suite,
110 | file: file,
111 | })
112 | usersListGroup.setup(() => {
113 | throw new Error('Unable to cleanup database')
114 | })
115 | createTest('A test that will never run because the group hooks fails', emitter, refiner, {
116 | group: usersListGroup,
117 | })
118 |
119 | createTest('A top level test inside functional suite', emitter, refiner, {
120 | suite,
121 | file: file,
122 | }).run(() => {})
123 |
124 | return suite
125 | }
126 |
127 | /**
128 | * Returns an array of suites with dummy tests reproducting
129 | * different test behavior
130 | */
131 | export function createDummyTests(emitter: Emitter, refiner: Refiner, file?: string): Suite[] {
132 | return [
133 | createUnitTestsSuite(emitter, refiner, file),
134 | createFunctionalTestsSuite(emitter, refiner, file),
135 | ]
136 | }
137 |
--------------------------------------------------------------------------------
/factories/main.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import { ReporterContract } from '../src/types.js'
11 | import { RunnerFactory } from './runner.js'
12 |
13 | /**
14 | * Create an instance of the runner factory
15 | */
16 | export const runner = () => new RunnerFactory()
17 | export { createDummyTests } from './create_dummy_tests.js'
18 | export const syncReporter: ReporterContract = {
19 | name: 'sync',
20 | handler(r, emitter) {
21 | emitter.on('runner:end', function () {
22 | const summary = r.getSummary()
23 | if (summary.hasError) {
24 | if (summary.failureTree[0].errors.length) {
25 | throw summary.failureTree[0].errors[0].error
26 | }
27 | if (summary.failureTree[0].children[0].errors.length) {
28 | throw summary.failureTree[0].children[0].errors[0].error
29 | }
30 | }
31 | })
32 | },
33 | }
34 |
--------------------------------------------------------------------------------
/factories/runner.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import { fileURLToPath } from 'node:url'
11 |
12 | import { Planner } from '../src/planner.js'
13 | import { GlobalHooks } from '../src/hooks.js'
14 | import { CliParser } from '../src/cli_parser.js'
15 | import { createTest } from '../src/create_test.js'
16 | import { ConfigManager } from '../src/config_manager.js'
17 | import { Suite, Runner, Emitter, TestContext, Refiner } from '../modules/core/main.js'
18 | import type {
19 | Config,
20 | CLIArgs,
21 | TestExecutor,
22 | RunnerSummary,
23 | NormalizedConfig,
24 | } from '../src/types.js'
25 |
26 | /**
27 | * Runner factory exposes the API to run dummy suites, groups and tests.
28 | * You might want to use the factory for testing reporters and
29 | * plugins usage
30 | */
31 | export class RunnerFactory {
32 | #emitter = new Emitter()
33 | #config?: NormalizedConfig
34 | #cliArgs?: CLIArgs
35 | #file = fileURLToPath(import.meta.url)
36 | #bail: boolean = false
37 |
38 | get #refiner() {
39 | return this.#config!.refiner
40 | }
41 |
42 | /**
43 | * Registers plugins
44 | */
45 | async #registerPlugins(runner: Runner) {
46 | for (let plugin of this.#config!.plugins) {
47 | await plugin({
48 | config: this.#config!,
49 | runner,
50 | emitter: this.#emitter,
51 | cliArgs: this.#cliArgs!,
52 | })
53 | }
54 | }
55 |
56 | /**
57 | * Configure runner
58 | */
59 | configure(config: Config, argv?: string[]) {
60 | this.#cliArgs = new CliParser().parse(argv || [])
61 | this.#config = new ConfigManager(config, this.#cliArgs).hydrate()
62 | return this
63 | }
64 |
65 | /**
66 | * Define a custom emitter instance to use
67 | */
68 | useEmitter(emitter: Emitter) {
69 | this.#emitter = emitter
70 | return this
71 | }
72 |
73 | /**
74 | * Run a test using the runner
75 | */
76 | async runTest(
77 | title: string,
78 | callback: TestExecutor
79 | ): Promise {
80 | return this.runSuites((emitter, refiner, file) => {
81 | const defaultSuite = new Suite('default', emitter, refiner)
82 |
83 | createTest(title, emitter, refiner, {
84 | suite: defaultSuite,
85 | file: file,
86 | }).run(callback)
87 |
88 | return [defaultSuite]
89 | })
90 | }
91 |
92 | /**
93 | * Enable/disable the bail mode
94 | */
95 | bail(toggle: boolean = true) {
96 | this.#bail = toggle
97 | return this
98 | }
99 |
100 | /**
101 | * Run dummy tests. You might use
102 | */
103 | async runSuites(
104 | suites: (emitter: Emitter, refiner: Refiner, file?: string) => Suite[]
105 | ): Promise {
106 | const runner = new Runner(this.#emitter)
107 | runner.bail(this.#bail)
108 |
109 | await this.#registerPlugins(runner)
110 |
111 | const { config, reporters, refinerFilters } = await new Planner(this.#config!).plan()
112 | const globalHooks = new GlobalHooks()
113 | globalHooks.apply(config)
114 |
115 | reporters.forEach((reporter) => {
116 | runner.registerReporter(reporter)
117 | })
118 |
119 | refinerFilters.forEach((filter) => {
120 | config.refiner.add(filter.layer, filter.filters)
121 | })
122 |
123 | suites(this.#emitter, this.#refiner, this.#file).forEach((suite) => runner.add(suite))
124 |
125 | await globalHooks.setup(runner)
126 | await runner.start()
127 | await runner.exec()
128 | await runner.end()
129 | await globalHooks.teardown(null, runner)
130 |
131 | return runner.getSummary()
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import { fileURLToPath } from 'node:url'
11 | import { ErrorsPrinter } from '@japa/errors-printer'
12 | import type { TestExecutor } from '@japa/core/types'
13 |
14 | import debug from './src/debug.js'
15 | import validator from './src/validator.js'
16 | import { Planner } from './src/planner.js'
17 | import { GlobalHooks } from './src/hooks.js'
18 | import { CliParser } from './src/cli_parser.js'
19 | import { retryPlugin } from './src/plugins/retry.js'
20 | import { ConfigManager } from './src/config_manager.js'
21 | import { ExceptionsManager } from './src/exceptions_manager.js'
22 | import { createTest, createTestGroup } from './src/create_test.js'
23 | import type { CLIArgs, Config, NormalizedConfig } from './src/types.js'
24 | import { Emitter, Group, Runner, Suite, Test, TestContext } from './modules/core/main.js'
25 |
26 | type OmitFirstArg = F extends [_: any, ...args: infer R] ? R : never
27 |
28 | /**
29 | * Global emitter instance used by the test
30 | */
31 | const emitter = new Emitter()
32 |
33 | /**
34 | * The current active test
35 | */
36 | let activeTest: Test | undefined
37 |
38 | /**
39 | * Parsed commandline arguments
40 | */
41 | let cliArgs: CLIArgs = {}
42 |
43 | /**
44 | * Hydrated config
45 | */
46 | let runnerConfig: NormalizedConfig | undefined
47 |
48 | /**
49 | * The state refers to the phase where we configure suites and import
50 | * test files. We stick this metadata to the test instance one can
51 | * later reference within the test.
52 | */
53 | const executionPlanState: {
54 | phase: 'idle' | 'planning' | 'executing'
55 | file?: string
56 | suite?: Suite
57 | group?: Group
58 | timeout?: number
59 | retries?: number
60 | } = {
61 | phase: 'idle',
62 | }
63 |
64 | /**
65 | * Create a Japa test. Defining a test without the callback
66 | * will create a todo test.
67 | */
68 | export function test(title: string, callback?: TestExecutor) {
69 | validator.ensureIsInPlanningPhase(executionPlanState.phase)
70 |
71 | const testInstance = createTest(title, emitter, runnerConfig!.refiner, executionPlanState)
72 | testInstance.setup((t) => {
73 | activeTest = t
74 | return () => {
75 | activeTest = undefined
76 | }
77 | })
78 |
79 | if (callback) {
80 | testInstance.run(callback, new Error())
81 | }
82 |
83 | return testInstance
84 | }
85 |
86 | /**
87 | * Create a Japa test group
88 | */
89 | test.group = function (title: string, callback: (group: Group) => void) {
90 | validator.ensureIsInPlanningPhase(executionPlanState.phase)
91 |
92 | executionPlanState.group = createTestGroup(
93 | title,
94 | emitter,
95 | runnerConfig!.refiner,
96 | executionPlanState
97 | )
98 |
99 | /**
100 | * Enable bail on the group an when bailLayer is set to "group"
101 | */
102 | if (cliArgs.bail && cliArgs.bailLayer === 'group') {
103 | executionPlanState.group.bail(true)
104 | }
105 |
106 | callback(executionPlanState.group)
107 | executionPlanState.group = undefined
108 | }
109 |
110 | /**
111 | * Create a test bound macro. Within the macro, you can access the
112 | * currently executed test to read its context values or define
113 | * cleanup hooks
114 | */
115 | test.macro = function any>(
116 | callback: T
117 | ): (...args: OmitFirstArg>) => ReturnType {
118 | return (...args) => {
119 | if (!activeTest) {
120 | throw new Error('Cannot invoke macro outside of the test callback')
121 | }
122 | return callback(activeTest, ...args)
123 | }
124 | }
125 |
126 | /**
127 | * Get the test of currently running test
128 | */
129 | export function getActiveTest() {
130 | return activeTest
131 | }
132 |
133 | /**
134 | * Make Japa process command line arguments. Later the parsed output
135 | * will be used by Japa to compute the configuration
136 | */
137 | export function processCLIArgs(argv: string[]) {
138 | cliArgs = new CliParser().parse(argv)
139 | }
140 |
141 | /**
142 | * Configure the tests runner with inline configuration. You must
143 | * call configure method before the run method.
144 | *
145 | * Do note: The CLI flags will overwrite the options provided
146 | * to the configure method.
147 | */
148 | export function configure(options: Config) {
149 | runnerConfig = new ConfigManager(options, cliArgs).hydrate()
150 | }
151 |
152 | /**
153 | * Execute Japa tests. Calling this function will import the test
154 | * files behind the scenes
155 | */
156 | export async function run() {
157 | /**
158 | * Display help when help flag is used
159 | */
160 | if (cliArgs.help) {
161 | console.log(new CliParser().getHelp())
162 | return
163 | }
164 |
165 | validator.ensureIsConfigured(runnerConfig)
166 |
167 | executionPlanState.phase = 'planning'
168 | const runner = new Runner(emitter)
169 |
170 | /**
171 | * Enable bail on the runner and all the layers after the
172 | * runner when no specific bailLayer is specified
173 | */
174 | if (cliArgs.bail && cliArgs.bailLayer === '') {
175 | runner.bail(true)
176 | }
177 |
178 | const globalHooks = new GlobalHooks()
179 | const exceptionsManager = new ExceptionsManager()
180 |
181 | try {
182 | /**
183 | * Executing the retry plugin as the first thing
184 | */
185 | await retryPlugin({ config: runnerConfig!, runner, emitter, cliArgs })
186 |
187 | /**
188 | * Step 1: Executing plugins before creating a plan, so that it can mutate
189 | * the config
190 | */
191 | for (let plugin of runnerConfig!.plugins) {
192 | debug('executing "%s" plugin', plugin.name || 'anonymous')
193 | await plugin({ runner, emitter, cliArgs, config: runnerConfig! })
194 | }
195 |
196 | /**
197 | * Step 2: Creating an execution plan. The output is the result of
198 | * applying all the filters and validations.
199 | */
200 | const { config, reporters, suites, refinerFilters } = await new Planner(runnerConfig!).plan()
201 |
202 | /**
203 | * Step 3: Registering reporters and filters with the runner
204 | */
205 | reporters.forEach((reporter) => {
206 | debug('registering "%s" reporter', reporter.name)
207 | runner.registerReporter(reporter)
208 | })
209 | refinerFilters.forEach((filter) => {
210 | debug('apply %s filters "%O" ', filter.layer, filter.filters)
211 | config.refiner.add(filter.layer, filter.filters)
212 | })
213 | config.refiner.matchAllTags(cliArgs.matchAll ?? false)
214 | runner.onSuite(config.configureSuite)
215 |
216 | /**
217 | * Step 4: Running the setup hooks
218 | */
219 | debug('executing global hooks')
220 | globalHooks.apply(config)
221 | await globalHooks.setup(runner)
222 |
223 | /**
224 | * Step 5: Register suites and import test files
225 | */
226 | for (let suite of suites) {
227 | /**
228 | * Creating and configuring the suite
229 | */
230 | debug('initiating suite %s', suite.name)
231 | executionPlanState.suite = new Suite(suite.name, emitter, config.refiner)
232 | executionPlanState.retries = suite.retries
233 | executionPlanState.timeout = suite.timeout
234 | if (typeof suite.configure === 'function') {
235 | suite.configure(executionPlanState.suite)
236 | }
237 |
238 | /**
239 | * Enable bail on the suite and all the layers after the
240 | * suite when bailLayer is set to "suite"
241 | */
242 | if (cliArgs.bail && cliArgs.bailLayer === 'suite') {
243 | debug('enabling bail mode for the suite %s', suite.name)
244 | executionPlanState.suite.bail(true)
245 | }
246 | runner.add(executionPlanState.suite)
247 |
248 | /**
249 | * Importing suite files
250 | */
251 | for (let fileURL of suite.filesURLs) {
252 | executionPlanState.file = fileURLToPath(fileURL)
253 | debug('importing test file %s', executionPlanState.file)
254 | await config.importer(fileURL)
255 | }
256 |
257 | /**
258 | * Resetting global state
259 | */
260 | executionPlanState.suite = undefined
261 | }
262 |
263 | /**
264 | * Onto execution phase
265 | */
266 | executionPlanState.phase = 'executing'
267 |
268 | /**
269 | * Monitor for unhandled erorrs and rejections
270 | */
271 | exceptionsManager.monitor()
272 |
273 | await runner.start()
274 | await runner.exec()
275 |
276 | await globalHooks.teardown(null, runner)
277 | await runner.end()
278 |
279 | /**
280 | * Print unhandled errors
281 | */
282 | await exceptionsManager.report()
283 |
284 | const summary = runner.getSummary()
285 | if (summary.hasError || exceptionsManager.hasErrors) {
286 | debug(
287 | 'updating exit code to 1. summary.hasError %s, process.hasError',
288 | summary.hasError,
289 | exceptionsManager.hasErrors
290 | )
291 | process.exitCode = 1
292 | }
293 | if (config.forceExit) {
294 | debug('force exiting process')
295 | process.exit()
296 | }
297 | } catch (error) {
298 | debug('error running tests %O', error)
299 | await globalHooks.teardown(error, runner)
300 | const printer = new ErrorsPrinter()
301 | await printer.printError(error)
302 |
303 | /**
304 | * Print unhandled errors in case the code inside
305 | * the try block never got triggered
306 | */
307 | await exceptionsManager.report()
308 |
309 | process.exitCode = 1
310 | if (runnerConfig!.forceExit) {
311 | debug('force exiting process')
312 | process.exit()
313 | }
314 | }
315 | }
316 |
--------------------------------------------------------------------------------
/modules/core/main.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import {
11 | Emitter,
12 | Refiner,
13 | Test as BaseTest,
14 | Suite as BaseSuite,
15 | Group as BaseGroup,
16 | Runner as BaseRunner,
17 | TestContext as BaseTestContext,
18 | } from '@japa/core'
19 | import { inspect } from 'node:util'
20 | import { AssertionError } from 'node:assert'
21 | import { BaseReporter } from './reporters/base.js'
22 | import type { DataSetNode, TestHooksCleanupHandler } from './types.js'
23 |
24 | declare module '@japa/core' {
25 | interface Test, TestData extends DataSetNode = undefined> {
26 | /**
27 | * Assert the test throws an exception with a certain error message
28 | * and optionally is an instance of a given Error class.
29 | */
30 | throws(message: string | RegExp, errorConstructor?: any): this
31 | }
32 | interface TestContext {
33 | /**
34 | * Register a cleanup function that runs after the test finishes
35 | * successfully or with an error.
36 | */
37 | cleanup: (cleanupCallback: TestHooksCleanupHandler) => void
38 | }
39 | }
40 |
41 | export { Emitter, Refiner, BaseReporter }
42 |
43 | /**
44 | * Test context carries context data for a given test.
45 | */
46 | export class TestContext extends BaseTestContext {
47 | /**
48 | * Register a cleanup function that runs after the test finishes
49 | * successfully or with an error.
50 | */
51 | declare cleanup: (cleanupCallback: TestHooksCleanupHandler) => void
52 |
53 | constructor(public test: Test) {
54 | super()
55 | this.cleanup = (cleanupCallback: TestHooksCleanupHandler) => {
56 | test.cleanup(cleanupCallback)
57 | }
58 | }
59 | }
60 |
61 | /**
62 | * Test class represents an individual test and exposes API to tweak
63 | * its runtime behavior.
64 | */
65 | export class Test extends BaseTest<
66 | TestContext,
67 | TestData
68 | > {
69 | /**
70 | * @inheritdoc
71 | */
72 | static executedCallbacks = []
73 |
74 | /**
75 | * @inheritdoc
76 | */
77 | static executingCallbacks = []
78 |
79 | /**
80 | * Assert the test throws an exception with a certain error message
81 | * and optionally is an instance of a given Error class.
82 | */
83 | throws(message: string | RegExp, errorConstructor?: any) {
84 | const errorInPoint = new AssertionError({})
85 | const existingExecutor = this.options.executor
86 | if (!existingExecutor) {
87 | throw new Error('Cannot use "test.throws" method without a test callback')
88 | }
89 |
90 | /**
91 | * Overwriting existing callback
92 | */
93 | this.options.executor = async (...args: [any, any, any]) => {
94 | let raisedException: any
95 | try {
96 | await existingExecutor(...args)
97 | } catch (error) {
98 | raisedException = error
99 | }
100 |
101 | /**
102 | * Notify no exception has been raised
103 | */
104 | if (!raisedException) {
105 | errorInPoint.message = 'Expected test to throw an exception'
106 | throw errorInPoint
107 | }
108 |
109 | /**
110 | * Constructor mis-match
111 | */
112 | if (errorConstructor && !(raisedException instanceof errorConstructor)) {
113 | errorInPoint.message = `Expected test to throw "${inspect(errorConstructor)}"`
114 | throw errorInPoint
115 | }
116 |
117 | /**
118 | * Error does not have a message property
119 | */
120 | const exceptionMessage: unknown = raisedException.message
121 | if (!exceptionMessage || typeof exceptionMessage !== 'string') {
122 | errorInPoint.message = 'Expected test to throw an exception with message property'
123 | throw errorInPoint
124 | }
125 |
126 | /**
127 | * Message does not match
128 | */
129 | if (typeof message === 'string') {
130 | if (exceptionMessage !== message) {
131 | errorInPoint.message = `Expected test to throw "${message}". Instead received "${raisedException.message}"`
132 | errorInPoint.actual = raisedException.message
133 | errorInPoint.expected = message
134 | throw errorInPoint
135 | }
136 | return
137 | }
138 |
139 | if (!message.test(exceptionMessage)) {
140 | errorInPoint.message = `Expected test error to match "${message}" regular expression`
141 | throw errorInPoint
142 | }
143 | }
144 |
145 | return this
146 | }
147 | }
148 |
149 | /**
150 | * TestGroup is used to bulk configure a collection of tests and
151 | * define lifecycle hooks for them
152 | */
153 | export class Group extends BaseGroup {}
154 |
155 | /**
156 | * A suite is a collection of tests created around a given
157 | * testing type. For example: A suite for unit tests, a
158 | * suite for functional tests and so on.
159 | */
160 | export class Suite extends BaseSuite {}
161 |
162 | /**
163 | * Runner class is used to execute the tests
164 | */
165 | export class Runner extends BaseRunner {}
166 |
--------------------------------------------------------------------------------
/modules/core/reporters/base.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import ms from 'ms'
11 | import { ErrorsPrinter } from '@japa/errors-printer'
12 |
13 | import { Emitter, Runner } from '../main.js'
14 | import { colors } from '../../../src/helpers.js'
15 | import type {
16 | TestEndNode,
17 | SuiteEndNode,
18 | GroupEndNode,
19 | TestStartNode,
20 | RunnerSummary,
21 | RunnerEndNode,
22 | GroupStartNode,
23 | SuiteStartNode,
24 | RunnerStartNode,
25 | BaseReporterOptions,
26 | } from '../types.js'
27 |
28 | /**
29 | * Base reporter to build custom reporters on top of
30 | */
31 | export abstract class BaseReporter {
32 | runner?: Runner
33 |
34 | /**
35 | * Path to the file for which the tests are getting executed
36 | */
37 | currentFileName?: string
38 |
39 | /**
40 | * Suite for which the tests are getting executed
41 | */
42 | currentSuiteName?: string
43 |
44 | /**
45 | * Group for which the tests are getting executed
46 | */
47 | currentGroupName?: string
48 |
49 | protected options: BaseReporterOptions
50 |
51 | constructor(options: BaseReporterOptions = {}) {
52 | this.options = Object.assign({ stackLinesCount: 2 }, options)
53 | }
54 |
55 | /**
56 | * Pretty prints the aggregates
57 | */
58 | protected printAggregates(summary: RunnerSummary) {
59 | const tests: string[] = []
60 |
61 | /**
62 | * Set value for tests row
63 | */
64 | if (summary.aggregates.passed) {
65 | tests.push(colors.green(`${summary.aggregates.passed} passed`))
66 | }
67 | if (summary.aggregates.failed) {
68 | tests.push(colors.red(`${summary.aggregates.failed} failed`))
69 | }
70 | if (summary.aggregates.todo) {
71 | tests.push(colors.cyan(`${summary.aggregates.todo} todo`))
72 | }
73 | if (summary.aggregates.skipped) {
74 | tests.push(colors.yellow(`${summary.aggregates.skipped} skipped`))
75 | }
76 | if (summary.aggregates.regression) {
77 | tests.push(colors.magenta(`${summary.aggregates.regression} regression`))
78 | }
79 |
80 | this.runner!.summaryBuilder.use(() => {
81 | return [
82 | {
83 | key: colors.dim('Tests'),
84 | value: `${tests.join(', ')} ${colors.dim(`(${summary.aggregates.total})`)}`,
85 | },
86 | {
87 | key: colors.dim('Time'),
88 | value: colors.dim(ms(summary.duration)),
89 | },
90 | ]
91 | })
92 |
93 | console.log(this.runner!.summaryBuilder.build().join('\n'))
94 | }
95 |
96 | /**
97 | * Aggregates errors tree to a flat array
98 | */
99 | protected aggregateErrors(summary: RunnerSummary) {
100 | const errorsList: { phase: string; title: string; error: Error }[] = []
101 |
102 | summary.failureTree.forEach((suite) => {
103 | suite.errors.forEach((error) => errorsList.push({ title: suite.name, ...error }))
104 |
105 | suite.children.forEach((testOrGroup) => {
106 | /**
107 | * Suite child is a test
108 | */
109 | if (testOrGroup.type === 'test') {
110 | testOrGroup.errors.forEach((error) => {
111 | errorsList.push({ title: `${suite.name} / ${testOrGroup.title}`, ...error })
112 | })
113 | return
114 | }
115 |
116 | /**
117 | * Suite child is a group
118 | */
119 | testOrGroup.errors.forEach((error) => {
120 | errorsList.push({ title: testOrGroup.name, ...error })
121 | })
122 | testOrGroup.children.forEach((test) => {
123 | test.errors.forEach((error) => {
124 | errorsList.push({ title: `${testOrGroup.name} / ${test.title}`, ...error })
125 | })
126 | })
127 | })
128 | })
129 |
130 | return errorsList
131 | }
132 |
133 | /**
134 | * Pretty print errors
135 | */
136 | protected async printErrors(summary: RunnerSummary) {
137 | if (!summary.failureTree.length) {
138 | return
139 | }
140 |
141 | const errorPrinter = new ErrorsPrinter({
142 | framesMaxLimit: this.options.framesMaxLimit,
143 | })
144 |
145 | errorPrinter.printSectionHeader('ERRORS')
146 | await errorPrinter.printErrors(this.aggregateErrors(summary))
147 | }
148 |
149 | /**
150 | * Handlers to capture events
151 | */
152 | protected onTestStart(_: TestStartNode): void {}
153 | protected onTestEnd(_: TestEndNode) {}
154 |
155 | protected onGroupStart(_: GroupStartNode) {}
156 | protected onGroupEnd(_: GroupEndNode) {}
157 |
158 | protected onSuiteStart(_: SuiteStartNode) {}
159 | protected onSuiteEnd(_: SuiteEndNode) {}
160 |
161 | protected async start(_: RunnerStartNode) {}
162 | protected async end(_: RunnerEndNode) {}
163 |
164 | /**
165 | * Print tests summary
166 | */
167 | protected async printSummary(summary: RunnerSummary) {
168 | await this.printErrors(summary)
169 |
170 | console.log('')
171 | if (summary.aggregates.total === 0 && !summary.hasError) {
172 | console.log(colors.bgYellow().black(' NO TESTS EXECUTED '))
173 | return
174 | }
175 |
176 | if (summary.hasError) {
177 | console.log(colors.bgRed().black(' FAILED '))
178 | } else {
179 | console.log(colors.bgGreen().black(' PASSED '))
180 | }
181 | console.log('')
182 | this.printAggregates(summary)
183 | }
184 |
185 | /**
186 | * Invoked by the tests runner when tests are about to start
187 | */
188 | boot(runner: Runner, emitter: Emitter) {
189 | this.runner = runner
190 |
191 | emitter.on('test:start', (payload) => {
192 | this.currentFileName = payload.meta.fileName
193 | this.onTestStart(payload)
194 | })
195 |
196 | emitter.on('test:end', (payload) => {
197 | this.onTestEnd(payload)
198 | })
199 |
200 | emitter.on('group:start', (payload) => {
201 | this.currentGroupName = payload.title
202 | this.currentFileName = payload.meta.fileName
203 | this.onGroupStart(payload)
204 | })
205 |
206 | emitter.on('group:end', (payload) => {
207 | this.currentGroupName = undefined
208 | this.onGroupEnd(payload)
209 | })
210 |
211 | emitter.on('suite:start', (payload) => {
212 | this.currentSuiteName = payload.name
213 | this.onSuiteStart(payload)
214 | })
215 |
216 | emitter.on('suite:end', (payload) => {
217 | this.currentSuiteName = undefined
218 | this.onSuiteEnd(payload)
219 | })
220 |
221 | emitter.on('runner:start', async (payload) => {
222 | await this.start(payload)
223 | })
224 |
225 | emitter.on('runner:end', async (payload) => {
226 | await this.end(payload)
227 | })
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/modules/core/types.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | export * from '@japa/core/types'
11 |
12 | export type BaseReporterOptions = {
13 | framesMaxLimit?: number
14 | }
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@japa/runner",
3 | "description": "A simple yet powerful testing framework for Node.js",
4 | "version": "4.2.0",
5 | "engines": {
6 | "node": ">=18.16.0"
7 | },
8 | "main": "build/index.js",
9 | "type": "module",
10 | "files": [
11 | "build",
12 | "!build/examples",
13 | "!build/tests"
14 | ],
15 | "exports": {
16 | ".": "./build/index.js",
17 | "./types": "./build/src/types.js",
18 | "./reporters": "./build/src/reporters/main.js",
19 | "./factories": "./build/factories/main.js",
20 | "./core": "./build/modules/core/main.js"
21 | },
22 | "scripts": {
23 | "pretest": "npm run lint",
24 | "test": "cross-env NODE_DEBUG=japa:runner c8 npm run quick:test",
25 | "lint": "eslint .",
26 | "format": "prettier --write .",
27 | "typecheck": "tsc --noEmit",
28 | "clean": "del-cli build",
29 | "precompile": "npm run lint && npm run clean",
30 | "compile": "tsup-node && tsc --emitDeclarationOnly --declaration",
31 | "build": "npm run compile",
32 | "version": "npm run build",
33 | "prepublishOnly": "npm run build",
34 | "release": "release-it",
35 | "quick:test": "glob -c \"node --import=ts-node-maintained/register/esm --enable-source-maps --test-reporter=spec --test\" \"tests/*.spec.ts\""
36 | },
37 | "devDependencies": {
38 | "@adonisjs/eslint-config": "^2.0.0-beta.7",
39 | "@adonisjs/prettier-config": "^1.4.0",
40 | "@adonisjs/tsconfig": "^1.4.0",
41 | "@release-it/conventional-changelog": "^10.0.0",
42 | "@swc/core": "1.10.7",
43 | "@types/chai": "^5.0.1",
44 | "@types/chai-subset": "^1.3.5",
45 | "@types/find-cache-dir": "^5.0.2",
46 | "@types/ms": "^2.1.0",
47 | "@types/node": "^22.12.0",
48 | "c8": "^10.1.3",
49 | "chai": "^5.1.2",
50 | "chai-subset": "^1.6.0",
51 | "cross-env": "^7.0.3",
52 | "del-cli": "^6.0.0",
53 | "eslint": "^9.19.0",
54 | "glob": "^11.0.1",
55 | "prettier": "^3.4.2",
56 | "release-it": "^18.1.2",
57 | "ts-node-maintained": "^10.9.5",
58 | "tsup": "^8.3.6",
59 | "typescript": "^5.7.3"
60 | },
61 | "dependencies": {
62 | "@japa/core": "^10.3.0",
63 | "@japa/errors-printer": "^4.1.2",
64 | "@poppinss/colors": "^4.1.4",
65 | "@poppinss/hooks": "^7.2.5",
66 | "fast-glob": "^3.3.3",
67 | "find-cache-dir": "^5.0.0",
68 | "getopts": "^2.3.0",
69 | "ms": "^2.1.3",
70 | "serialize-error": "^12.0.0",
71 | "slash": "^5.1.0",
72 | "supports-color": "^10.0.0"
73 | },
74 | "homepage": "https://github.com/japa/runner#readme",
75 | "repository": {
76 | "type": "git",
77 | "url": "git+https://github.com/japa/runner.git"
78 | },
79 | "bugs": {
80 | "url": "https://github.com/japa/runner/issues"
81 | },
82 | "keywords": [
83 | "japa",
84 | "tests",
85 | "test-runner"
86 | ],
87 | "author": "Harminder Virk ",
88 | "license": "MIT",
89 | "publishConfig": {
90 | "access": "public",
91 | "provenance": true
92 | },
93 | "tsup": {
94 | "entry": [
95 | "./index.ts",
96 | "./src/types.ts",
97 | "./src/reporters/main.ts",
98 | "./factories/main.ts",
99 | "./modules/core/main.ts"
100 | ],
101 | "outDir": "./build",
102 | "clean": true,
103 | "format": "esm",
104 | "dts": false,
105 | "sourcemap": false,
106 | "target": "esnext"
107 | },
108 | "release-it": {
109 | "git": {
110 | "requireCleanWorkingDir": true,
111 | "requireUpstream": true,
112 | "commitMessage": "chore(release): ${version}",
113 | "tagAnnotation": "v${version}",
114 | "push": true,
115 | "tagName": "v${version}"
116 | },
117 | "github": {
118 | "release": true
119 | },
120 | "npm": {
121 | "publish": true,
122 | "skipChecks": true
123 | },
124 | "plugins": {
125 | "@release-it/conventional-changelog": {
126 | "preset": {
127 | "name": "angular"
128 | }
129 | }
130 | }
131 | },
132 | "c8": {
133 | "reporter": [
134 | "text",
135 | "html"
136 | ],
137 | "exclude": [
138 | "tests/**",
139 | "tests_helpers/**",
140 | "build/**",
141 | "factories/**",
142 | "modules/core/**",
143 | "src/reporters/**"
144 | ]
145 | },
146 | "prettier": "@adonisjs/prettier-config"
147 | }
148 |
--------------------------------------------------------------------------------
/src/cli_parser.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | // @ts-ignore-error
11 | import getopts from 'getopts'
12 | import { colors } from './helpers.js'
13 | import type { CLIArgs } from './types.js'
14 |
15 | /**
16 | * Known commandline options. The user can still define additional flags and they
17 | * will be parsed aswell, but without any normalization
18 | */
19 | const OPTIONS = {
20 | string: ['tests', 'groups', 'tags', 'files', 'timeout', 'retries', 'reporters', 'bailLayer'],
21 | boolean: ['help', 'matchAll', 'failed', 'bail'],
22 | alias: {
23 | forceExit: 'force-exit',
24 | matchAll: 'match-all',
25 | bailLayer: 'bail-layer',
26 | help: 'h',
27 | },
28 | }
29 |
30 | /**
31 | * Help string to display when the `--help flag is used`
32 | */
33 | const GET_HELP = () => `
34 | ${colors.yellow('@japa/runner v2.3.0')}
35 |
36 | ${colors.green('--tests')} ${colors.dim('Filter tests by the test title')}
37 | ${colors.green('--groups')} ${colors.dim('Filter tests by the group title')}
38 | ${colors.green('--tags')} ${colors.dim('Filter tests by tags')}
39 | ${colors.green('--match-all')} ${colors.dim('Run tests that matches all the supplied tags')}
40 | ${colors.green('--files')} ${colors.dim('Filter tests by the file name')}
41 | ${colors.green('--force-exit')} ${colors.dim('Forcefully exit the process')}
42 | ${colors.green('--timeout')} ${colors.dim('Define default timeout for all tests')}
43 | ${colors.green('--retries')} ${colors.dim('Define default retries for all tests')}
44 | ${colors.green('--reporters')} ${colors.dim('Activate one or more test reporters')}
45 | ${colors.green('--failed')} ${colors.dim('Run tests failed during the last run')}
46 | ${colors.green('--bail')} ${colors.dim('Exit early when a test fails')}
47 | ${colors.green('--bail-layer')} ${colors.dim('Specify at which layer to enable the bail mode. Can be "group" or "suite"')}
48 | ${colors.green('-h, --help')} ${colors.dim('View help')}
49 |
50 | ${colors.yellow('Examples:')}
51 | ${colors.dim('node bin/test.js --tags="@github"')}
52 | ${colors.dim('node bin/test.js --tags="~@github"')}
53 | ${colors.dim('node bin/test.js --tags="@github,@slow,@integration" --match-all')}
54 | ${colors.dim('node bin/test.js --force-exit')}
55 | ${colors.dim('node bin/test.js --files="user"')}
56 | ${colors.dim('node bin/test.js --files="functional/user"')}
57 | ${colors.dim('node bin/test.js --files="unit/user"')}
58 | ${colors.dim('node bin/test.js --failed')}
59 | ${colors.dim('node bin/test.js --bail')}
60 | ${colors.dim('node bin/test.js --bail=group')}
61 |
62 | ${colors.yellow('Notes:')}
63 | - When groups and tests filters are applied together. We will first filter the
64 | tests by group title and then apply the tests filter.
65 | - The timeout defined on test object takes precedence over the ${colors.green('--timeout')} flag.
66 | - The retries defined on test object takes precedence over the ${colors.green('--retries')} flag.
67 | - The ${colors.green('--files')} flag checks for the file names ending with the filter substring.
68 | - The ${colors.green('--tags')} filter runs tests that has one or more of the supplied tags.
69 | - You can use the ${colors.green('--match-all')} flag to run tests that has all the supplied tags.
70 | `
71 |
72 | /**
73 | * CLI Parser is used to parse the commandline argument
74 | */
75 | export class CliParser {
76 | /**
77 | * Parses command-line arguments
78 | */
79 | parse(argv: string[]): CLIArgs {
80 | return getopts(argv, OPTIONS)
81 | }
82 |
83 | /**
84 | * Returns the help string
85 | */
86 | getHelp() {
87 | return GET_HELP()
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/config_manager.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import debug from './debug.js'
11 | import { Refiner } from '../modules/core/main.js'
12 | import { dot, github, ndjson, spec } from './reporters/main.js'
13 | import type { CLIArgs, Config, Filters, NormalizedBaseConfig, NormalizedConfig } from './types.js'
14 |
15 | export const NOOP = () => {}
16 |
17 | /**
18 | * Defaults to use for configuration
19 | */
20 | const DEFAULTS = {
21 | files: [],
22 | timeout: 2000,
23 | retries: 0,
24 | forceExit: false,
25 | plugins: [],
26 | reporters: {
27 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
28 | list: [spec(), ndjson(), dot(), github()],
29 | },
30 | importer: (filePath) => import(filePath.href),
31 | configureSuite: () => {},
32 | } satisfies Config
33 |
34 | /**
35 | * Config manager is used to hydrate the configuration by merging
36 | * the defaults with the user defined config and the command line
37 | * flags.
38 | *
39 | * The command line flags have the upmost priority
40 | */
41 | export class ConfigManager {
42 | #config: Config
43 | #cliArgs: CLIArgs
44 |
45 | constructor(config: Config, cliArgs: CLIArgs) {
46 | this.#config = config
47 | this.#cliArgs = cliArgs
48 | }
49 |
50 | /**
51 | * Processes a CLI argument and converts it to an
52 | * array of strings
53 | */
54 | #processAsArray(value: string | string[], splitByComma: boolean): string[] {
55 | return Array.isArray(value)
56 | ? value
57 | : splitByComma
58 | ? value.split(',').map((item: string) => item.trim())
59 | : [value]
60 | }
61 |
62 | /**
63 | * Returns a copy of filters based upon the CLI
64 | * arguments.
65 | */
66 | #getCLIFilters(): Filters {
67 | const filters: Filters = {}
68 |
69 | if (this.#cliArgs.tags) {
70 | filters.tags = this.#processAsArray(this.#cliArgs.tags, true)
71 | }
72 | if (this.#cliArgs.tests) {
73 | filters.tests = this.#processAsArray(this.#cliArgs.tests, false)
74 | }
75 | if (this.#cliArgs.files) {
76 | filters.files = this.#processAsArray(this.#cliArgs.files, true)
77 | }
78 | if (this.#cliArgs.groups) {
79 | filters.groups = this.#processAsArray(this.#cliArgs.groups, false)
80 | }
81 | if (this.#cliArgs._ && this.#cliArgs._.length) {
82 | filters.suites = this.#processAsArray(this.#cliArgs._, true)
83 | }
84 |
85 | return filters
86 | }
87 |
88 | /**
89 | * Returns the timeout from the CLI args
90 | */
91 | #getCLITimeout(): number | undefined {
92 | if (this.#cliArgs.timeout) {
93 | const value = Number(this.#cliArgs.timeout)
94 | if (!Number.isNaN(value)) {
95 | return value
96 | }
97 | }
98 | }
99 |
100 | /**
101 | * Returns the retries from the CLI args
102 | */
103 | #getCLIRetries(): number | undefined {
104 | if (this.#cliArgs.retries) {
105 | const value = Number(this.#cliArgs.retries)
106 | if (!Number.isNaN(value)) {
107 | return value
108 | }
109 | }
110 | }
111 |
112 | /**
113 | * Returns the forceExit property from the CLI args
114 | */
115 | #getCLIForceExit(): boolean | undefined {
116 | if (this.#cliArgs.forceExit) {
117 | return true
118 | }
119 | }
120 |
121 | /**
122 | * Returns reporters selected using the commandline
123 | * --reporter flag
124 | */
125 | #getCLIReporters(): string[] | undefined {
126 | if (this.#cliArgs.reporters) {
127 | return this.#processAsArray(this.#cliArgs.reporters, true)
128 | }
129 | }
130 |
131 | /**
132 | * Hydrates the config with user defined options and the
133 | * command-line flags.
134 | */
135 | hydrate(): NormalizedConfig {
136 | const cliFilters = this.#getCLIFilters()
137 | const cliRetries = this.#getCLIRetries()
138 | const cliTimeout = this.#getCLITimeout()
139 | const cliReporters = this.#getCLIReporters()
140 | const cliForceExit = this.#getCLIForceExit()
141 |
142 | debug('filters applied using CLI flags %O', cliFilters)
143 |
144 | const baseConfig: NormalizedBaseConfig = {
145 | cwd: this.#config.cwd ?? process.cwd(),
146 | exclude: this.#config.exclude || ['node_modules/**', '.git/**', 'coverage/**'],
147 | filters: Object.assign({}, this.#config.filters ?? {}, cliFilters),
148 | importer: this.#config.importer ?? DEFAULTS.importer,
149 | refiner: this.#config.refiner ?? new Refiner(),
150 | retries: cliRetries ?? this.#config.retries ?? DEFAULTS.retries,
151 | timeout: cliTimeout ?? this.#config.timeout ?? DEFAULTS.timeout,
152 | plugins: this.#config.plugins ?? DEFAULTS.plugins,
153 | forceExit: cliForceExit ?? this.#config.forceExit ?? DEFAULTS.forceExit,
154 | reporters: this.#config.reporters
155 | ? {
156 | activated: this.#config.reporters.activated,
157 | list: this.#config.reporters.list || DEFAULTS.reporters.list,
158 | }
159 | : DEFAULTS.reporters,
160 | configureSuite: this.#config.configureSuite ?? DEFAULTS.configureSuite,
161 | setup: this.#config.setup || [],
162 | teardown: this.#config.teardown || [],
163 | }
164 |
165 | /**
166 | * Overwrite activated reporters when defined using CLI
167 | * flag
168 | */
169 | if (cliReporters) {
170 | baseConfig.reporters.activated = cliReporters
171 | }
172 |
173 | if ('files' in this.#config) {
174 | return {
175 | files: this.#config.files,
176 | ...baseConfig,
177 | }
178 | }
179 |
180 | return {
181 | suites: this.#config.suites.map((suite) => {
182 | return {
183 | name: suite.name,
184 | files: suite.files,
185 | timeout: cliTimeout ?? suite.timeout ?? baseConfig.timeout,
186 | retries: cliRetries ?? suite.retries ?? baseConfig.retries,
187 | configure: suite.configure || NOOP,
188 | }
189 | }),
190 | ...baseConfig,
191 | }
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/src/create_test.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import { Emitter, Group, Refiner, Suite, Test, TestContext } from '../modules/core/main.js'
11 |
12 | /**
13 | * Function to create the test context for the test
14 | */
15 | const contextBuilder = (testInstance: Test) => new TestContext(testInstance)
16 |
17 | /**
18 | * Create a new instance of the Test
19 | */
20 | export function createTest(
21 | title: string,
22 | emitter: Emitter,
23 | refiner: Refiner,
24 | options: {
25 | group?: Group
26 | suite?: Suite
27 | file?: string
28 | timeout?: number
29 | retries?: number
30 | }
31 | ) {
32 | const testInstance = new Test(title, contextBuilder, emitter, refiner, options.group)
33 | testInstance.options.meta.suite = options.suite
34 | testInstance.options.meta.group = options.group
35 | testInstance.options.meta.fileName = options.file
36 |
37 | if (options.timeout !== undefined) {
38 | testInstance.timeout(options.timeout)
39 | }
40 | if (options.retries !== undefined) {
41 | testInstance.retry(options.retries)
42 | }
43 |
44 | /**
45 | * Register test as a child either with the group or the suite
46 | */
47 | if (options.group) {
48 | options.group.add(testInstance)
49 | } else if (options.suite) {
50 | options.suite.add(testInstance)
51 | }
52 |
53 | return testInstance
54 | }
55 |
56 | /**
57 | * Create a new instance of the Group
58 | */
59 | export function createTestGroup(
60 | title: string,
61 | emitter: Emitter,
62 | refiner: Refiner,
63 | options: {
64 | group?: Group
65 | suite?: Suite
66 | file?: string
67 | timeout?: number
68 | retries?: number
69 | }
70 | ) {
71 | if (options.group) {
72 | throw new Error('Nested groups are not supported by Japa')
73 | }
74 |
75 | const group = new Group(title, emitter, refiner)
76 | group.options.meta.suite = options.suite
77 | group.options.meta.fileName = options.file
78 |
79 | if (options.suite) {
80 | options.suite.add(group)
81 | }
82 |
83 | return group
84 | }
85 |
--------------------------------------------------------------------------------
/src/debug.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import { debuglog } from 'node:util'
11 | export default debuglog('japa:runner')
12 |
--------------------------------------------------------------------------------
/src/exceptions_manager.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import { ErrorsPrinter } from '@japa/errors-printer'
11 | import debug from './debug.js'
12 |
13 | /**
14 | * Handles uncaught exceptions and prints them to the
15 | * console
16 | */
17 | export class ExceptionsManager {
18 | #exceptionsBuffer: any[] = []
19 | #rejectionsBuffer: any[] = []
20 | #state: 'watching' | 'reporting' = 'watching'
21 | #errorsPrinter = new ErrorsPrinter({ stackLinesCount: 2, framesMaxLimit: 4 })
22 |
23 | hasErrors: boolean = false
24 |
25 | /**
26 | * Monitors unhandled exceptions and rejections. The exceptions
27 | * are stacked in a buffer, so that we do not clutter the
28 | * tests output and once the tests are over, we will
29 | * print them to the console.
30 | *
31 | * In case the tests are completed, we will print errors as they
32 | * happen.
33 | */
34 | monitor() {
35 | process.on('uncaughtException', async (error) => {
36 | debug('received uncaught exception %O', error)
37 | this.hasErrors = true
38 | if (this.#state === 'watching') {
39 | this.#exceptionsBuffer.push(error)
40 | } else {
41 | this.#errorsPrinter.printSectionBorder('[Unhandled Error]')
42 | await this.#errorsPrinter.printError(error)
43 | process.exitCode = 1
44 | }
45 | })
46 |
47 | process.on('unhandledRejection', async (error) => {
48 | debug('received unhandled rejection %O', error)
49 | this.hasErrors = true
50 | if (this.#state === 'watching') {
51 | this.#rejectionsBuffer.push(error)
52 | } else {
53 | this.#errorsPrinter.printSectionBorder('[Unhandled Rejection]')
54 | await this.#errorsPrinter.printError(error)
55 | process.exitCode = 1
56 | }
57 | })
58 | }
59 |
60 | async report() {
61 | if (this.#state === 'reporting') {
62 | return
63 | }
64 |
65 | this.#state = 'reporting'
66 |
67 | /**
68 | * Print exceptions
69 | */
70 | if (this.#exceptionsBuffer.length) {
71 | let exceptionsCount = this.#exceptionsBuffer.length
72 | let exceptionsIndex = this.#exceptionsBuffer.length
73 | this.#errorsPrinter.printSectionHeader('Unhandled Errors')
74 | for (let exception of this.#exceptionsBuffer) {
75 | await this.#errorsPrinter.printError(exception)
76 | this.#errorsPrinter.printSectionBorder(`[${++exceptionsIndex}/${exceptionsCount}]`)
77 | }
78 | this.#exceptionsBuffer = []
79 | }
80 |
81 | /**
82 | * Print rejections
83 | */
84 | if (this.#rejectionsBuffer.length) {
85 | let rejectionsCount = this.#exceptionsBuffer.length
86 | let rejectionsIndex = this.#exceptionsBuffer.length
87 | this.#errorsPrinter.printSectionBorder('Unhandled Rejections')
88 | for (let rejection of this.#rejectionsBuffer) {
89 | await this.#errorsPrinter.printError(rejection)
90 | this.#errorsPrinter.printSectionBorder(`[${++rejectionsIndex}/${rejectionsCount}]`)
91 | }
92 | this.#rejectionsBuffer = []
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/files_manager.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import slash from 'slash'
11 | import fastGlob from 'fast-glob'
12 | import { pathToFileURL } from 'node:url'
13 | import type { TestFiles } from './types.js'
14 |
15 | /**
16 | * The expression to remove file extension and optionally
17 | * .spec|.test from the test file name
18 | */
19 | const FILE_SUFFIX_EXPRESSION = /(\.spec|\.test)?\.[js|ts|jsx|tsx|mjs|mts|cjs|cts]+$/
20 |
21 | /**
22 | * Files manager exposes the API to collect, filter and import test
23 | * files based upon the config
24 | */
25 | export class FilesManager {
26 | /**
27 | * Returns a collection of files from the user defined
28 | * glob or the implementation function
29 | */
30 | async getFiles(cwd: string, files: TestFiles, excludes: string[]): Promise {
31 | if (Array.isArray(files) || typeof files === 'string') {
32 | const testFiles = await fastGlob(files, {
33 | absolute: true,
34 | onlyFiles: true,
35 | cwd: cwd,
36 | ignore: excludes,
37 | })
38 | return testFiles.map((file) => pathToFileURL(file))
39 | }
40 |
41 | return await files()
42 | }
43 |
44 | /**
45 | * Applies file name filter on a collection of file
46 | * URLs
47 | */
48 | grep(files: URL[], filters: string[]): URL[] {
49 | return files.filter((file) => {
50 | const filename = slash(file.pathname)
51 | const filenameWithoutTestSuffix = filename.replace(FILE_SUFFIX_EXPRESSION, '')
52 |
53 | return !!filters.find((filter) => {
54 | if (filename.endsWith(filter)) {
55 | return true
56 | }
57 |
58 | const filterSegments = filter.split('/').reverse()
59 | const fileSegments = filenameWithoutTestSuffix.split('/').reverse()
60 |
61 | return filterSegments.every((segment, index) => {
62 | return fileSegments[index] && (segment === '*' || fileSegments[index].endsWith(segment))
63 | })
64 | })
65 | })
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/helpers.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import useColors from '@poppinss/colors'
11 | import supportsColor from 'supports-color'
12 | import { Colors } from '@poppinss/colors/types'
13 |
14 | export const colors: Colors = supportsColor.stdout ? useColors.ansi() : useColors.silent()
15 |
16 | /**
17 | * A collection of platform specific icons
18 | */
19 | export const icons =
20 | process.platform === 'win32' && !process.env.WT_SESSION
21 | ? {
22 | tick: '√',
23 | cross: '×',
24 | bullet: '*',
25 | nodejs: '♦',
26 | pointer: '>',
27 | info: 'i',
28 | warning: '‼',
29 | branch: ' -',
30 | squareSmallFilled: '[█]',
31 | }
32 | : {
33 | tick: '✔',
34 | cross: '✖',
35 | bullet: '●',
36 | nodejs: '⬢',
37 | pointer: '❯',
38 | info: 'ℹ',
39 | warning: '⚠',
40 | branch: '└──',
41 | squareSmallFilled: '◼',
42 | }
43 |
--------------------------------------------------------------------------------
/src/hooks.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import Hooks from '@poppinss/hooks'
11 | import type { Runner as HooksRunner } from '@poppinss/hooks/types'
12 |
13 | import { Runner } from '../modules/core/main.js'
14 | import type { HooksEvents, SetupHookState, NormalizedConfig, TeardownHookState } from './types.js'
15 |
16 | /**
17 | * Exposes API for working with global hooks
18 | */
19 | export class GlobalHooks {
20 | #hooks = new Hooks()
21 | #setupRunner: HooksRunner | undefined
22 | #teardownRunner: HooksRunner | undefined
23 |
24 | /**
25 | * Apply hooks from the config
26 | */
27 | apply(config: NormalizedConfig) {
28 | config.setup.forEach((hook) => this.#hooks.add('setup', hook))
29 | config.teardown.forEach((hook) => this.#hooks.add('teardown', hook))
30 | }
31 |
32 | /**
33 | * Perform setup
34 | */
35 | async setup(runner: Runner) {
36 | this.#setupRunner = this.#hooks.runner('setup')
37 | this.#teardownRunner = this.#hooks.runner('teardown')
38 | await this.#setupRunner.run(runner)
39 | }
40 |
41 | /**
42 | * Perform cleanup
43 | */
44 | async teardown(error: Error | null, runner: Runner) {
45 | if (this.#setupRunner) {
46 | await this.#setupRunner.cleanup(error, runner)
47 | }
48 | if (this.#teardownRunner) {
49 | if (!error) {
50 | await this.#teardownRunner.run(runner)
51 | }
52 | await this.#teardownRunner.cleanup(error, runner)
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/planner.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import validator from './validator.js'
11 | import { FilesManager } from './files_manager.js'
12 | import type { NamedReporterContract, NormalizedConfig, TestFiles, TestSuite } from './types.js'
13 |
14 | /**
15 | * The tests planner is used to plan the tests by doing all
16 | * the heavy lifting of executing plugins, registering
17 | * reporters, filtering tests and so on.
18 | */
19 | export class Planner {
20 | #config: NormalizedConfig
21 | #fileManager = new FilesManager()
22 |
23 | constructor(config: NormalizedConfig) {
24 | validator.validateActivatedReporters(config!)
25 | validator.validateSuitesFilter(config!)
26 | validator.validateSuitesForUniqueness(config!)
27 | this.#config = config
28 | }
29 |
30 | /**
31 | * Returns a list of reporters based upon the activated
32 | * reporters list.
33 | */
34 | #getActivatedReporters(): NamedReporterContract[] {
35 | return this.#config.reporters.activated.map((activated) => {
36 | return this.#config.reporters.list.find(({ name }) => activated === name)!
37 | })
38 | }
39 |
40 | /**
41 | * A generic method to collect files from the user defined
42 | * files glob and apply the files filter
43 | */
44 | async #collectFiles(files: TestFiles) {
45 | let filesURLs = await this.#fileManager.getFiles(this.#config.cwd, files, this.#config.exclude)
46 | if (this.#config.filters.files && this.#config.filters.files.length) {
47 | filesURLs = this.#fileManager.grep(filesURLs, this.#config.filters.files)
48 | }
49 |
50 | return filesURLs
51 | }
52 |
53 | /**
54 | * Returns a collection of suites and their associated
55 | * test files by applying all the filters
56 | */
57 | async #getSuites(): Promise<(TestSuite & { filesURLs: URL[] })[]> {
58 | let suites: (TestSuite & { filesURLs: URL[] })[] = []
59 | let suitesFilters = this.#config.filters.suites || []
60 |
61 | if ('files' in this.#config) {
62 | suites.push({
63 | name: 'default',
64 | files: this.#config.files,
65 | timeout: this.#config.timeout,
66 | retries: this.#config.retries,
67 | filesURLs: await this.#collectFiles(this.#config.files),
68 | })
69 | }
70 |
71 | if ('suites' in this.#config) {
72 | for (let suite of this.#config.suites) {
73 | if (!suitesFilters.length || suitesFilters.includes(suite.name)) {
74 | suites.push({
75 | ...suite,
76 | filesURLs: await this.#collectFiles(suite.files),
77 | })
78 | }
79 | }
80 | }
81 |
82 | return suites
83 | }
84 |
85 | /**
86 | * Returns a list of filters to the passed to the refiner
87 | */
88 | #getRefinerFilters() {
89 | return Object.keys(this.#config.filters).reduce(
90 | (result, layer) => {
91 | if (layer === 'tests' || layer === 'tags' || layer === 'groups') {
92 | result.push({ layer, filters: this.#config.filters[layer]! })
93 | }
94 | return result
95 | },
96 | [] as { layer: 'tags' | 'tests' | 'groups'; filters: string[] }[]
97 | )
98 | }
99 |
100 | /**
101 | * Creates a plan for running the tests
102 | */
103 | async plan() {
104 | const suites = await this.#getSuites()
105 | const reporters = this.#getActivatedReporters()
106 | const refinerFilters = this.#getRefinerFilters()
107 | return {
108 | reporters,
109 | suites,
110 | refinerFilters,
111 | config: this.#config,
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/plugins/retry.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import { join } from 'node:path'
11 | import findCacheDirectory from 'find-cache-dir'
12 | import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises'
13 |
14 | import { colors } from '../helpers.js'
15 | import type { PluginFn } from '../types.js'
16 |
17 | /**
18 | * Paths to the cache directory and the summary file
19 | */
20 | const CACHE_DIR = findCacheDirectory({ name: '@japa/runner' })
21 | const SUMMARY_FILE = CACHE_DIR ? join(CACHE_DIR, 'summary.json') : undefined
22 |
23 | /**
24 | * Returns an object with the title of the tests failed during
25 | * the last run.
26 | */
27 | export async function getFailedTests(): Promise<{ tests?: string[] }> {
28 | try {
29 | const summary = await readFile(SUMMARY_FILE!, 'utf-8')
30 | return JSON.parse(summary)
31 | } catch (error) {
32 | if (error.code === 'ENOENT') {
33 | return {}
34 | }
35 | throw new Error('Unable to read failed tests cache file', { cause: error })
36 | }
37 | }
38 |
39 | /**
40 | * Writes failing tests to the cache directory
41 | */
42 | export async function cacheFailedTests(tests: string[]) {
43 | await mkdir(CACHE_DIR!, { recursive: true })
44 | await writeFile(SUMMARY_FILE!, JSON.stringify({ tests: tests }))
45 | }
46 |
47 | /**
48 | * Clears the cache dir
49 | */
50 | export async function clearCache() {
51 | await unlink(SUMMARY_FILE!)
52 | }
53 |
54 | /**
55 | * Exposes the API to run failing tests using the "failed" CLI flag.
56 | */
57 | export const retryPlugin: PluginFn = async function retry({ config, cliArgs }) {
58 | if (!SUMMARY_FILE) {
59 | return
60 | }
61 |
62 | config.teardown.push(async (runner) => {
63 | const summary = runner.getSummary()
64 | await cacheFailedTests(summary.failedTestsTitles)
65 | })
66 |
67 | if (cliArgs.failed) {
68 | try {
69 | const { tests } = await getFailedTests()
70 | if (!tests || !tests.length) {
71 | console.log(colors.bgYellow().black(' No failing tests found. Running all the tests '))
72 | return
73 | }
74 | config.filters.tests = tests
75 | } catch (error) {
76 | console.log(colors.bgRed().black(' Unable to read failed tests. Running all the tests '))
77 | console.log(colors.red(error))
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/reporters/dot.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import { colors, icons } from '../helpers.js'
11 | import type { TestEndNode } from '../../modules/core/types.js'
12 | import { BaseReporter } from '../../modules/core/reporters/base.js'
13 |
14 | /**
15 | * Minimal reporter that prints each test as an icon.
16 | */
17 | export class DotReporter extends BaseReporter {
18 | /**
19 | * When a test ended
20 | */
21 | protected onTestEnd(payload: TestEndNode) {
22 | let output = ''
23 | if (payload.isTodo) {
24 | output = colors.cyan(icons.info)
25 | } else if (payload.hasError) {
26 | output = colors.red(icons.cross)
27 | } else if (payload.isSkipped) {
28 | output = colors.yellow(icons.bullet)
29 | } else if (payload.isFailing) {
30 | output = colors.magenta(icons.squareSmallFilled)
31 | } else {
32 | output = colors.green(icons.tick)
33 | }
34 |
35 | process.stdout.write(`${output}`)
36 | }
37 |
38 | /**
39 | * When test runner ended
40 | */
41 | protected async end() {
42 | console.log('')
43 | await this.printSummary(this.runner!.getSummary())
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/reporters/github.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import slash from 'slash'
11 | import { relative } from 'node:path'
12 | import { stripVTControlCharacters } from 'node:util'
13 | import { ErrorsPrinter } from '@japa/errors-printer'
14 | import { BaseReporter } from '../../modules/core/main.js'
15 |
16 | /**
17 | * Prints annotations when executing tests within Github actions
18 | */
19 | export class GithubReporter extends BaseReporter {
20 | /**
21 | * Performs string escape on annotation message as per
22 | * https://github.com/actions/toolkit/blob/f1d9b4b985e6f0f728b4b766db73498403fd5ca3/packages/core/src/command.ts#L80-L85
23 | */
24 | protected escapeMessage(value: string): string {
25 | return value.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A')
26 | }
27 |
28 | /**
29 | * Performs string escape on annotation properties as per
30 | * https://github.com/actions/toolkit/blob/f1d9b4b985e6f0f728b4b766db73498403fd5ca3/packages/core/src/command.ts#L80-L85
31 | */
32 | protected escapeProperty(value: string): string {
33 | return value
34 | .replace(/%/g, '%25')
35 | .replace(/\r/g, '%0D')
36 | .replace(/\n/g, '%0A')
37 | .replace(/:/g, '%3A')
38 | .replace(/,/g, '%2C')
39 | }
40 |
41 | /**
42 | * Formats the message as per the Github annotations spec.
43 | * https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-error-message
44 | */
45 | protected formatMessage({
46 | command,
47 | properties,
48 | message,
49 | }: {
50 | command: string
51 | properties: Record
52 | message: string
53 | }): string {
54 | let result = `::${command}`
55 | Object.entries(properties).forEach(([k, v], i) => {
56 | result += i === 0 ? ' ' : ','
57 | result += `${k}=${this.escapeProperty(v)}`
58 | })
59 | result += `::${this.escapeMessage(message)}`
60 | return result
61 | }
62 |
63 | /**
64 | * Prints Github annotation for a given error
65 | */
66 | async getErrorAnnotation(
67 | printer: ErrorsPrinter,
68 | error: { phase: string; title: string; error: Error }
69 | ) {
70 | const parsedError = await printer.parseError(error.error)
71 | if (!('frames' in parsedError)) {
72 | return
73 | }
74 |
75 | const mainFrame = parsedError.frames.find((frame) => frame.type === 'app')
76 | if (!mainFrame) {
77 | return
78 | }
79 |
80 | return this.formatMessage({
81 | command: 'error',
82 | properties: {
83 | file: slash(relative(process.cwd(), mainFrame.fileName!)),
84 | title: error.title,
85 | line: String(mainFrame.lineNumber!),
86 | column: String(mainFrame.columnNumber!),
87 | },
88 | message: stripVTControlCharacters(parsedError.message),
89 | })
90 | }
91 |
92 | async end() {
93 | const summary = this.runner!.getSummary()
94 | const errorsList = this.aggregateErrors(summary)
95 | const errorPrinter = new ErrorsPrinter(this.options)
96 |
97 | for (let error of errorsList) {
98 | const formatted = await this.getErrorAnnotation(errorPrinter, error)
99 | if (formatted) {
100 | console.log(`\n${formatted}`)
101 | }
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/reporters/main.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import { DotReporter } from './dot.js'
11 | import { SpecReporter } from './spec.js'
12 | import { NdJSONReporter } from './ndjson.js'
13 | import type { BaseReporterOptions, NamedReporterContract } from '../types.js'
14 | import { GithubReporter } from './github.js'
15 |
16 | /**
17 | * Create an instance of the spec reporter
18 | */
19 | export const spec: (options?: BaseReporterOptions) => NamedReporterContract = (options) => {
20 | return {
21 | name: 'spec',
22 | handler: (...args) => new SpecReporter(options).boot(...args),
23 | }
24 | }
25 |
26 | /**
27 | * Create an instance of the dot reporter
28 | */
29 | export const dot: (options?: BaseReporterOptions) => NamedReporterContract = (options) => {
30 | return {
31 | name: 'dot',
32 | handler: (...args) => new DotReporter(options).boot(...args),
33 | }
34 | }
35 |
36 | /**
37 | * Create an instance of the ndjson reporter
38 | */
39 | export const ndjson: (options?: BaseReporterOptions) => NamedReporterContract = (options) => {
40 | return {
41 | name: 'ndjson',
42 | handler: (...args) => new NdJSONReporter(options).boot(...args),
43 | }
44 | }
45 |
46 | /**
47 | * Create an instance of the github reporter
48 | */
49 | export const github: (options?: BaseReporterOptions) => NamedReporterContract = (options) => {
50 | return {
51 | name: 'github',
52 | handler: (...args) => new GithubReporter(options).boot(...args),
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/reporters/ndjson.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import { relative } from 'node:path'
11 | import { serializeError } from 'serialize-error'
12 |
13 | import { BaseReporter } from '../../modules/core/main.js'
14 | import type {
15 | TestEndNode,
16 | SuiteEndNode,
17 | GroupEndNode,
18 | SuiteStartNode,
19 | GroupStartNode,
20 | } from '../../modules/core/types.js'
21 |
22 | /**
23 | * Prints tests progress as JSON. Each event is emitted
24 | * independently
25 | */
26 | export class NdJSONReporter extends BaseReporter {
27 | /**
28 | * Returns the filename relative from the current working dir
29 | */
30 | #getRelativeFilename(fileName: string) {
31 | return relative(process.cwd(), fileName)
32 | }
33 |
34 | /**
35 | * Serialize errors to JSON
36 | */
37 | #serializeErrors(
38 | errors: TestEndNode['errors'] | GroupEndNode['errors'] | SuiteEndNode['errors']
39 | ) {
40 | return errors.map((error) => ({
41 | phase: error.phase,
42 | error: serializeError(error.error),
43 | }))
44 | }
45 |
46 | protected onTestEnd(payload: TestEndNode): void {
47 | console.log(
48 | JSON.stringify({
49 | event: 'test:end',
50 | filePath: this.currentFileName,
51 | relativePath: this.currentFileName
52 | ? this.#getRelativeFilename(this.currentFileName)
53 | : undefined,
54 | title: payload.title,
55 | duration: payload.duration,
56 | failReason: payload.failReason,
57 | isFailing: payload.isFailing,
58 | skipReason: payload.skipReason,
59 | isSkipped: payload.isSkipped,
60 | isTodo: payload.isTodo,
61 | isPinned: payload.isPinned,
62 | retryAttempt: payload.retryAttempt,
63 | retries: payload.retries,
64 | errors: this.#serializeErrors(payload.errors),
65 | })
66 | )
67 | }
68 |
69 | protected onGroupStart(payload: GroupStartNode): void {
70 | console.log(
71 | JSON.stringify({
72 | event: 'group:start',
73 | title: payload.title,
74 | })
75 | )
76 | }
77 |
78 | protected onGroupEnd(payload: GroupEndNode): void {
79 | JSON.stringify({
80 | event: 'group:end',
81 | title: payload.title,
82 | errors: this.#serializeErrors(payload.errors),
83 | })
84 | }
85 |
86 | protected onSuiteStart(payload: SuiteStartNode): void {
87 | console.log(
88 | JSON.stringify({
89 | event: 'suite:start',
90 | ...payload,
91 | })
92 | )
93 | }
94 |
95 | protected onSuiteEnd(payload: SuiteEndNode): void {
96 | console.log(
97 | JSON.stringify({
98 | event: 'suite:end',
99 | name: payload.name,
100 | hasError: payload.hasError,
101 | errors: this.#serializeErrors(payload.errors),
102 | })
103 | )
104 | }
105 |
106 | protected async end() {
107 | const summary = this.runner!.getSummary()
108 | console.log(
109 | JSON.stringify({
110 | aggregates: summary.aggregates,
111 | duration: summary.duration,
112 | failedTestsTitles: summary.failedTestsTitles,
113 | hasError: summary.hasError,
114 | })
115 | )
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/reporters/spec.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import ms from 'ms'
11 | import { relative } from 'node:path'
12 |
13 | import { colors, icons } from '../helpers.js'
14 | import { BaseReporter } from '../../modules/core/main.js'
15 | import { GroupStartNode, TestEndNode } from '../../modules/core/types.js'
16 |
17 | /**
18 | * Pretty prints the tests on the console
19 | */
20 | export class SpecReporter extends BaseReporter {
21 | /**
22 | * Tracking if the first event we get is for a test without any parent group
23 | * We need this to decide the display style for tests without groups.
24 | */
25 | #isFirstLoneTest = true
26 |
27 | /**
28 | * Returns the icon for the test
29 | */
30 | #getTestIcon(payload: TestEndNode) {
31 | if (payload.isTodo) {
32 | return colors.cyan(icons.info)
33 | }
34 |
35 | if (payload.hasError) {
36 | return colors.red(icons.cross)
37 | }
38 |
39 | if (payload.isSkipped) {
40 | return colors.yellow(icons.bullet)
41 | }
42 |
43 | if (payload.isFailing) {
44 | return colors.magenta(icons.squareSmallFilled)
45 | }
46 |
47 | return colors.green(icons.tick)
48 | }
49 |
50 | /**
51 | * Returns the test message
52 | */
53 | #getTestMessage(payload: TestEndNode) {
54 | const message = payload.title.expanded
55 |
56 | if (payload.isTodo) {
57 | return colors.blue(message)
58 | }
59 |
60 | if (payload.hasError) {
61 | return colors.red(message)
62 | }
63 |
64 | if (payload.isSkipped) {
65 | return colors.yellow(message)
66 | }
67 |
68 | if (payload.isFailing) {
69 | return colors.magenta(message)
70 | }
71 |
72 | return colors.grey(message)
73 | }
74 |
75 | /**
76 | * Returns the subtext message for the test
77 | */
78 | #getSubText(payload: TestEndNode): string | undefined {
79 | if (payload.isSkipped && payload.skipReason) {
80 | return colors.dim(`${icons.branch} ${colors.italic(payload.skipReason)}`)
81 | }
82 |
83 | if (!payload.isFailing) {
84 | return
85 | }
86 |
87 | if (payload.hasError) {
88 | const message =
89 | payload.errors.find((error) => error.phase === 'test')?.error.message ??
90 | `Test marked with ".fails()" must finish with an error`
91 |
92 | return colors.dim(`${icons.branch} ${colors.italic(message)}`)
93 | }
94 |
95 | if (payload.failReason) {
96 | return colors.dim(`${icons.branch} ${colors.italic(payload.failReason)}`)
97 | }
98 |
99 | const testErrorMessage = payload.errors.find((error) => error.phase === 'test')
100 | if (testErrorMessage && testErrorMessage.error) {
101 | return colors.dim(`${icons.branch} ${colors.italic(testErrorMessage.error.message)}`)
102 | }
103 | }
104 |
105 | /**
106 | * Returns the filename relative from the current working dir
107 | */
108 | #getRelativeFilename(fileName: string) {
109 | return relative(process.cwd(), fileName)
110 | }
111 |
112 | /**
113 | * Prints the test details
114 | */
115 | #printTest(payload: TestEndNode) {
116 | const icon = this.#getTestIcon(payload)
117 | const message = this.#getTestMessage(payload)
118 | const prefix = payload.isPinned ? colors.yellow('[PINNED] ') : ''
119 | const indentation = this.currentFileName || this.currentGroupName ? ' ' : ''
120 | const duration = colors.dim(`(${ms(Number(payload.duration.toFixed(2)))})`)
121 | const retries =
122 | payload.retryAttempt && payload.retryAttempt > 1
123 | ? colors.dim(`(x${payload.retryAttempt}) `)
124 | : ''
125 |
126 | let subText = this.#getSubText(payload)
127 | subText = subText ? `\n${indentation} ${subText}` : ''
128 |
129 | console.log(`${indentation}${icon} ${prefix}${retries}${message} ${duration}${subText}`)
130 | }
131 |
132 | /**
133 | * Prints the group name
134 | */
135 | #printGroup(payload: GroupStartNode) {
136 | const title =
137 | this.currentSuiteName !== 'default'
138 | ? `${this.currentSuiteName} / ${payload.title}`
139 | : payload.title
140 |
141 | const suffix = this.currentFileName
142 | ? colors.dim(` (${this.#getRelativeFilename(this.currentFileName)})`)
143 | : ''
144 |
145 | console.log(`\n${title}${suffix}`)
146 | }
147 |
148 | protected onTestStart(): void {
149 | /**
150 | * Display the filename when
151 | *
152 | * - The filename exists
153 | * - The test is not under a group
154 | * - Test is first in a sequence
155 | */
156 | if (this.currentFileName && this.#isFirstLoneTest) {
157 | console.log(`\n${colors.dim(this.#getRelativeFilename(this.currentFileName))}`)
158 | }
159 | this.#isFirstLoneTest = false
160 | }
161 |
162 | protected onTestEnd(payload: TestEndNode): void {
163 | this.#printTest(payload)
164 | }
165 |
166 | protected onGroupStart(payload: GroupStartNode): void {
167 | /**
168 | * When a group starts, we mark the upcoming test as NOT a
169 | * lone test
170 | */
171 | this.#isFirstLoneTest = false
172 | this.#printGroup(payload)
173 | }
174 |
175 | protected onGroupEnd(): void {
176 | /**
177 | * When the group ends we assume that the next test can
178 | * be out of the group, hence a lone test.
179 | *
180 | * If this assumption is false, then the `onGroupStart` method
181 | * will toggle the boolean
182 | */
183 | this.#isFirstLoneTest = true
184 | }
185 |
186 | protected async end() {
187 | const summary = this.runner!.getSummary()
188 | await this.printSummary(summary)
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import type { HookHandler } from '@poppinss/hooks/types'
11 |
12 | import type { Emitter, Refiner, Runner, Suite } from '../modules/core/main.js'
13 | import type { FilteringOptions, NamedReporterContract } from '../modules/core/types.js'
14 |
15 | export * from '../modules/core/types.js'
16 |
17 | /**
18 | * Global setup hook
19 | */
20 | export type SetupHookState = [[runner: Runner], [error: Error | null, runner: Runner]]
21 | export type SetupHookHandler = HookHandler
22 |
23 | /**
24 | * Global teardown hook
25 | */
26 | export type TeardownHookState = [[runner: Runner], [error: Error | null, runner: Runner]]
27 | export type TeardownHookHandler = HookHandler
28 |
29 | /**
30 | * Global set of available hooks
31 | */
32 | export type HooksEvents = {
33 | setup: SetupHookState
34 | teardown: TeardownHookState
35 | }
36 |
37 | /**
38 | * Parsed command-line arguments
39 | */
40 | export type CLIArgs = {
41 | _?: string[]
42 | tags?: string | string[]
43 | files?: string | string[]
44 | tests?: string | string[]
45 | groups?: string | string[]
46 | timeout?: string
47 | retries?: string
48 | reporters?: string | string[]
49 | forceExit?: boolean
50 | failed?: boolean
51 | help?: boolean
52 | matchAll?: boolean
53 | bail?: boolean
54 | bailLayer?: string
55 | } & Record
56 |
57 | /**
58 | * Set of filters you can apply to run only specific tests
59 | */
60 | export type Filters = FilteringOptions & {
61 | files?: string[]
62 | suites?: string[]
63 | }
64 |
65 | /**
66 | * Plugin function receives an instance of the runner,
67 | * emitter, config and the hooks
68 | */
69 | export type PluginFn = (japa: {
70 | config: NormalizedConfig
71 | cliArgs: CLIArgs
72 | runner: Runner
73 | emitter: Emitter
74 | }) => void | Promise
75 |
76 | /**
77 | * Base configuration options
78 | */
79 | export type BaseConfig = {
80 | /**
81 | * Current working directory. It is required to search for
82 | * the test files
83 | */
84 | cwd?: string
85 |
86 | /**
87 | * The timeout to apply on all the tests, unless overwritten explicitly
88 | */
89 | timeout?: number
90 |
91 | /**
92 | * The retries to apply on all the tests, unless overwritten explicitly
93 | */
94 | retries?: number
95 |
96 | /**
97 | * Test filters to apply
98 | */
99 | filters?: Filters
100 |
101 | /**
102 | * A hook to configure suites. The callback will be called for each
103 | * suite before it gets executed.
104 | */
105 | configureSuite?: (suite: Suite) => void
106 |
107 | /**
108 | * A collection of registered reporters. Reporters are not activated by
109 | * default. Either you have to activate them using the commandline,
110 | * or using the `activated` property.
111 | */
112 | reporters?: {
113 | activated: string[]
114 | list?: NamedReporterContract[]
115 | }
116 |
117 | /**
118 | * A collection of registered plugins
119 | */
120 | plugins?: PluginFn[]
121 |
122 | /**
123 | * A custom implementation to import test files.
124 | */
125 | importer?: (filePath: URL) => void | Promise
126 |
127 | /**
128 | * Overwrite tests refiner. Check documentation for refiner
129 | * usage
130 | */
131 | refiner?: Refiner
132 |
133 | /**
134 | * Enable/disable force exiting.
135 | */
136 | forceExit?: boolean
137 |
138 | /**
139 | * Global hooks to execute before importing
140 | * the test files
141 | */
142 | setup?: SetupHookHandler[]
143 |
144 | /**
145 | * Global hooks to execute on teardown
146 | */
147 | teardown?: TeardownHookHandler[]
148 |
149 | /**
150 | * An array of directories to exclude when searching
151 | * for test files.
152 | *
153 | * For example, if you search for test files inside the entire
154 | * project, you might want to exclude "node_modules"
155 | */
156 | exclude?: string[]
157 | }
158 |
159 | /**
160 | * A collection of test files defined as a glob or a callback
161 | * function that returns an array of URLs
162 | */
163 | export type TestFiles = string | string[] | (() => URL[] | Promise)
164 |
165 | /**
166 | * A test suite to register tests under a named suite
167 | */
168 | export type TestSuite = {
169 | /**
170 | * A unique name for the suite
171 | */
172 | name: string
173 |
174 | /**
175 | * Collection of files associated with the suite. Files should be
176 | * defined as a glob or a callback function that returns an array of URLs
177 | */
178 | files: TestFiles
179 |
180 | /**
181 | * A callback functon to configure the suite. The callback is invoked only
182 | * when the runner is going to run the tests for the given suite.
183 | */
184 | configure?: (suite: Suite) => void
185 |
186 | /**
187 | * The timeout to apply on all the tests in this suite, unless overwritten explicitly
188 | */
189 | timeout?: number
190 |
191 | /**
192 | * The retries to apply on all the tests in this suite, unless overwritten explicitly
193 | */
194 | retries?: number
195 | }
196 |
197 | /**
198 | * BaseConfig after normalized by the config manager
199 | */
200 | export type NormalizedBaseConfig = Required> & {
201 | reporters: {
202 | activated: string[]
203 | list: NamedReporterContract[]
204 | }
205 | }
206 |
207 | /**
208 | * Configuration options
209 | */
210 | export type Config = BaseConfig &
211 | (
212 | | {
213 | files: TestFiles
214 | }
215 | | {
216 | suites: TestSuite[]
217 | }
218 | )
219 |
220 | /**
221 | * Config after normalized by the config manager
222 | */
223 | export type NormalizedConfig = NormalizedBaseConfig &
224 | (
225 | | {
226 | files: TestFiles
227 | }
228 | | {
229 | suites: Required[]
230 | }
231 | )
232 |
--------------------------------------------------------------------------------
/src/validator.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import { NormalizedConfig } from './types.js'
11 |
12 | /**
13 | * Validator encapsulates the validations to perform before running
14 | * the tests
15 | */
16 | class Validator {
17 | /**
18 | * Ensures the japa is configured. Otherwise raises an exception
19 | */
20 | ensureIsConfigured(config: NormalizedConfig | undefined) {
21 | if (!config) {
22 | throw new Error(
23 | `Cannot run tests. Make sure to call "configure" method before the "run" method`
24 | )
25 | }
26 | }
27 |
28 | /**
29 | * Ensures the japa is in planning phase
30 | */
31 | ensureIsInPlanningPhase(phase: 'idle' | 'planning' | 'executing') {
32 | if (phase !== 'planning') {
33 | throw new Error(
34 | `Cannot import japa test file directly. It must be imported by calling the "japa.run" method`
35 | )
36 | }
37 | }
38 |
39 | /**
40 | * Ensures the suites filter uses a subset of the user configured suites.
41 | */
42 | validateSuitesFilter(config: NormalizedConfig) {
43 | /**
44 | * Do not perform any validation if no filters are applied
45 | * in the first place
46 | */
47 | if (!config.filters.suites || !config.filters.suites.length) {
48 | return
49 | }
50 |
51 | /**
52 | * Notify user they have applied the suites filter but forgot to define
53 | * suites
54 | */
55 | if (!('suites' in config) || !config.suites.length) {
56 | throw new Error(`Cannot apply suites filter. You have not configured any test suites`)
57 | }
58 |
59 | const suites = config.suites.map(({ name }) => name)
60 |
61 | /**
62 | * Find unknown suites and report the error
63 | */
64 | const unknownSuites = config.filters.suites.filter((suite) => !suites.includes(suite))
65 | if (unknownSuites.length) {
66 | throw new Error(`Cannot apply suites filter. "${unknownSuites[0]}" suite is not configured`)
67 | }
68 | }
69 |
70 | /**
71 | * Ensure there are unique suites
72 | */
73 | validateSuitesForUniqueness(config: NormalizedConfig) {
74 | if (!('suites' in config)) {
75 | return
76 | }
77 |
78 | const suites: Set = new Set()
79 | config.suites.forEach(({ name }) => {
80 | if (suites.has(name)) {
81 | throw new Error(`Duplicate suite "${name}"`)
82 | }
83 | suites.add(name)
84 | })
85 |
86 | suites.clear()
87 | }
88 |
89 | /**
90 | * Ensure the activated reporters are in the list of defined
91 | * reporters
92 | */
93 | validateActivatedReporters(config: NormalizedConfig) {
94 | const reportersList = config.reporters.list.map(({ name }) => name)
95 | const unknownReporters = config.reporters.activated.filter(
96 | (name) => !reportersList.includes(name)
97 | )
98 |
99 | if (unknownReporters.length) {
100 | throw new Error(
101 | `Invalid reporter "${unknownReporters[0]}". Make sure to register it first inside the "reporters.list" array`
102 | )
103 | }
104 | }
105 | }
106 |
107 | export default new Validator()
108 |
--------------------------------------------------------------------------------
/tests/base_reporter.spec.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import { test } from 'node:test'
11 | import chaiSubset from 'chai-subset'
12 | import { assert, use } from 'chai'
13 |
14 | use(chaiSubset)
15 |
16 | import { wrapAssertions } from './helpers.js'
17 | import {
18 | TestEndNode,
19 | GroupEndNode,
20 | GroupOptions,
21 | SuiteEndNode,
22 | RunnerSummary,
23 | TestStartNode,
24 | SuiteStartNode,
25 | } from '../modules/core/types.js'
26 | import {
27 | Test,
28 | Suite,
29 | Group,
30 | Runner,
31 | Refiner,
32 | Emitter,
33 | TestContext,
34 | BaseReporter,
35 | } from '../modules/core/main.js'
36 |
37 | test.describe('Base reporter', () => {
38 | test('extend base reporter to create a custom reporter', async () => {
39 | const stack: string[] = []
40 | class MyReporter extends BaseReporter {
41 | protected async start() {
42 | stack.push('reporter started')
43 | }
44 | }
45 |
46 | const emitter = new Emitter()
47 | const runner = new Runner(emitter)
48 | runner.registerReporter((r, e) => new MyReporter({}).boot(r, e))
49 |
50 | await runner.start()
51 | await wrapAssertions(() => {
52 | assert.deepEqual(stack, ['reporter started'])
53 | })
54 | })
55 |
56 | test('invoke handlers when suite, groups and tests are executed', async () => {
57 | const stack: string[] = []
58 | let summary: RunnerSummary | undefined
59 |
60 | class MyReporter extends BaseReporter {
61 | protected async start() {
62 | stack.push('reporter started')
63 | }
64 |
65 | protected async end() {
66 | summary = this.runner!.getSummary()
67 | stack.push('reporter ended')
68 | }
69 |
70 | protected onTestStart(t: TestStartNode): void {
71 | assert.equal(t.title.expanded, '2 + 2')
72 | assert.equal(this.currentSuiteName, 'unit')
73 | assert.equal(this.currentGroupName, 'default')
74 | stack.push('test started')
75 | }
76 |
77 | protected onTestEnd(t: TestEndNode): void {
78 | assert.equal(t.title.expanded, '2 + 2')
79 | assert.isFalse(t.hasError)
80 | stack.push('test ended')
81 | }
82 |
83 | protected onGroupStart(g: GroupOptions): void {
84 | assert.equal(g.title, 'default')
85 | assert.equal(this.currentSuiteName, 'unit')
86 | stack.push('group started')
87 | }
88 |
89 | protected onGroupEnd(g: GroupEndNode): void {
90 | assert.equal(g.title, 'default')
91 | assert.isFalse(g.hasError)
92 | stack.push('group ended')
93 | }
94 |
95 | protected onSuiteStart(s: SuiteStartNode): void {
96 | assert.equal(s.name, 'unit')
97 | stack.push('suite started')
98 | }
99 |
100 | protected onSuiteEnd(s: SuiteEndNode): void {
101 | assert.equal(s.name, 'unit')
102 | stack.push('suite ended')
103 | }
104 | }
105 |
106 | const emitter = new Emitter()
107 | const runner = new Runner(emitter)
108 | const refiner = new Refiner()
109 | const suite = new Suite('unit', emitter, refiner)
110 | const group = new Group('default', emitter, refiner)
111 | const t = new Test('2 + 2', (_t) => new TestContext(_t), emitter, refiner, group)
112 |
113 | group.add(t)
114 | suite.add(group)
115 | runner.add(suite)
116 |
117 | runner.registerReporter((r, e) => new MyReporter({}).boot(r, e))
118 | await runner.start()
119 | await runner.exec()
120 | await runner.end()
121 |
122 | await wrapAssertions(() => {
123 | assert.deepEqual(stack, [
124 | 'reporter started',
125 | 'suite started',
126 | 'group started',
127 | 'test started',
128 | 'test ended',
129 | 'group ended',
130 | 'suite ended',
131 | 'reporter ended',
132 | ])
133 |
134 | assert.containSubset(summary!, {
135 | aggregates: {
136 | total: 1,
137 | failed: 0,
138 | passed: 0,
139 | regression: 0,
140 | skipped: 0,
141 | todo: 1,
142 | },
143 | hasError: false,
144 | failureTree: [],
145 | failedTestsTitles: [],
146 | })
147 | })
148 | })
149 | })
150 |
--------------------------------------------------------------------------------
/tests/cli_parser.spec.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import { assert } from 'chai'
11 | import { test } from 'node:test'
12 |
13 | import { colors } from '../src/helpers.js'
14 | import type { CLIArgs } from '../src/types.js'
15 | import { CliParser } from '../src/cli_parser.js'
16 | import { wrapAssertions } from './helpers.js'
17 |
18 | const DATASET: [CLIArgs, CLIArgs][] = [
19 | [
20 | new CliParser().parse([]),
21 | {
22 | '_': [] as string[],
23 | 'files': '',
24 | 'groups': '',
25 | 'reporters': '',
26 | 'h': false,
27 | 'help': false,
28 | 'failed': false,
29 | 'bail': false,
30 | 'bailLayer': '',
31 | 'bail-layer': '',
32 | 'match-all': false,
33 | 'matchAll': false,
34 | 'retries': '',
35 | 'tags': '',
36 | 'tests': '',
37 | 'timeout': '',
38 | },
39 | ],
40 | [
41 | new CliParser().parse(['unit', 'functional']),
42 | {
43 | '_': ['unit', 'functional'] as string[],
44 | 'files': '',
45 | 'reporters': '',
46 | 'groups': '',
47 | 'h': false,
48 | 'bail': false,
49 | 'bailLayer': '',
50 | 'bail-layer': '',
51 | 'help': false,
52 | 'failed': false,
53 | 'match-all': false,
54 | 'matchAll': false,
55 | 'retries': '',
56 | 'tags': '',
57 | 'tests': '',
58 | 'timeout': '',
59 | },
60 | ],
61 | [
62 | new CliParser().parse(['--timeout', '1000']),
63 | {
64 | '_': [] as string[],
65 | 'files': '',
66 | 'reporters': '',
67 | 'groups': '',
68 | 'h': false,
69 | 'bail': false,
70 | 'bailLayer': '',
71 | 'bail-layer': '',
72 | 'help': false,
73 | 'failed': false,
74 | 'match-all': false,
75 | 'matchAll': false,
76 | 'retries': '',
77 | 'tags': '',
78 | 'tests': '',
79 | 'timeout': '1000',
80 | },
81 | ],
82 | [
83 | new CliParser().parse(['--timeout', '1000', '--retries', '2']),
84 | {
85 | '_': [] as string[],
86 | 'files': '',
87 | 'reporters': '',
88 | 'groups': '',
89 | 'h': false,
90 | 'bail': false,
91 | 'bailLayer': '',
92 | 'bail-layer': '',
93 | 'help': false,
94 | 'failed': false,
95 | 'match-all': false,
96 | 'matchAll': false,
97 | 'retries': '2',
98 | 'tags': '',
99 | 'tests': '',
100 | 'timeout': '1000',
101 | },
102 | ],
103 | [
104 | new CliParser().parse(['--match-all']),
105 | {
106 | '_': [] as string[],
107 | 'files': '',
108 | 'reporters': '',
109 | 'groups': '',
110 | 'h': false,
111 | 'bail': false,
112 | 'bailLayer': '',
113 | 'bail-layer': '',
114 | 'help': false,
115 | 'failed': false,
116 | 'match-all': true,
117 | 'matchAll': true,
118 | 'retries': '',
119 | 'tags': '',
120 | 'tests': '',
121 | 'timeout': '',
122 | },
123 | ],
124 | [
125 | new CliParser().parse(['--force-exit']),
126 | {
127 | '_': [] as string[],
128 | 'files': '',
129 | 'force-exit': true,
130 | 'forceExit': true,
131 | 'reporters': '',
132 | 'groups': '',
133 | 'h': false,
134 | 'bail': false,
135 | 'bailLayer': '',
136 | 'bail-layer': '',
137 | 'help': false,
138 | 'failed': false,
139 | 'match-all': false,
140 | 'matchAll': false,
141 | 'retries': '',
142 | 'tags': '',
143 | 'tests': '',
144 | 'timeout': '',
145 | },
146 | ],
147 | [
148 | new CliParser().parse(['--browser', 'chrome', '--browser', 'firefox']),
149 | {
150 | '_': [] as string[],
151 | 'files': '',
152 | 'browser': ['chrome', 'firefox'],
153 | 'reporters': '',
154 | 'groups': '',
155 | 'h': false,
156 | 'bail': false,
157 | 'bailLayer': '',
158 | 'bail-layer': '',
159 | 'help': false,
160 | 'failed': false,
161 | 'match-all': false,
162 | 'matchAll': false,
163 | 'retries': '',
164 | 'tags': '',
165 | 'tests': '',
166 | 'timeout': '',
167 | },
168 | ],
169 | [
170 | new CliParser().parse(['--reporters', 'spec']),
171 | {
172 | '_': [] as string[],
173 | 'files': '',
174 | 'reporters': 'spec',
175 | 'groups': '',
176 | 'h': false,
177 | 'bail': false,
178 | 'bailLayer': '',
179 | 'bail-layer': '',
180 | 'help': false,
181 | 'failed': false,
182 | 'match-all': false,
183 | 'matchAll': false,
184 | 'retries': '',
185 | 'tags': '',
186 | 'tests': '',
187 | 'timeout': '',
188 | },
189 | ],
190 | [
191 | new CliParser().parse(['--reporters', 'spec', '--reporters', 'dot']),
192 | {
193 | '_': [] as string[],
194 | 'files': '',
195 | 'reporters': ['spec', 'dot'],
196 | 'groups': '',
197 | 'h': false,
198 | 'bail': false,
199 | 'bailLayer': '',
200 | 'bail-layer': '',
201 | 'help': false,
202 | 'failed': false,
203 | 'match-all': false,
204 | 'matchAll': false,
205 | 'retries': '',
206 | 'tags': '',
207 | 'tests': '',
208 | 'timeout': '',
209 | },
210 | ],
211 | [
212 | new CliParser().parse(['--reporters', 'spec,dot']),
213 | {
214 | '_': [] as string[],
215 | 'files': '',
216 | 'reporters': 'spec,dot',
217 | 'groups': '',
218 | 'h': false,
219 | 'bail': false,
220 | 'bailLayer': '',
221 | 'bail-layer': '',
222 | 'help': false,
223 | 'failed': false,
224 | 'match-all': false,
225 | 'matchAll': false,
226 | 'retries': '',
227 | 'tags': '',
228 | 'tests': '',
229 | 'timeout': '',
230 | },
231 | ],
232 | [
233 | new CliParser().parse(['--failed']),
234 | {
235 | '_': [] as string[],
236 | 'files': '',
237 | 'reporters': '',
238 | 'groups': '',
239 | 'h': false,
240 | 'bail': false,
241 | 'bailLayer': '',
242 | 'bail-layer': '',
243 | 'help': false,
244 | 'failed': true,
245 | 'match-all': false,
246 | 'matchAll': false,
247 | 'retries': '',
248 | 'tags': '',
249 | 'tests': '',
250 | 'timeout': '',
251 | },
252 | ],
253 | [
254 | new CliParser().parse(['--bail']),
255 | {
256 | '_': [] as string[],
257 | 'files': '',
258 | 'groups': '',
259 | 'reporters': '',
260 | 'h': false,
261 | 'help': false,
262 | 'failed': false,
263 | 'bail': true,
264 | 'bailLayer': '',
265 | 'bail-layer': '',
266 | 'match-all': false,
267 | 'matchAll': false,
268 | 'retries': '',
269 | 'tags': '',
270 | 'tests': '',
271 | 'timeout': '',
272 | },
273 | ],
274 | [
275 | new CliParser().parse(['--bail', '--bail-layer=group']),
276 | {
277 | '_': [] as string[],
278 | 'files': '',
279 | 'groups': '',
280 | 'reporters': '',
281 | 'h': false,
282 | 'help': false,
283 | 'failed': false,
284 | 'bail': true,
285 | 'bail-layer': 'group',
286 | 'bailLayer': 'group',
287 | 'match-all': false,
288 | 'matchAll': false,
289 | 'retries': '',
290 | 'tags': '',
291 | 'tests': '',
292 | 'timeout': '',
293 | },
294 | ],
295 | ]
296 |
297 | test.describe('CLI parser', () => {
298 | test('parse CLI arguments', async () => {
299 | for (let [cliArgs, output] of DATASET) {
300 | await wrapAssertions(() => {
301 | assert.deepEqual(cliArgs, output)
302 | })
303 | }
304 | })
305 |
306 | if (!process.env.CI) {
307 | test('display help', async () => {
308 | console.log(new CliParser().getHelp())
309 | await wrapAssertions(() => {
310 | assert.deepEqual(new CliParser().getHelp().split('\n'), [
311 | '',
312 | colors.yellow('@japa/runner v2.3.0'),
313 | '',
314 | `${colors.green('--tests')} ${colors.dim(
315 | 'Filter tests by the test title'
316 | )}`,
317 | `${colors.green('--groups')} ${colors.dim(
318 | 'Filter tests by the group title'
319 | )}`,
320 | `${colors.green('--tags')} ${colors.dim('Filter tests by tags')}`,
321 | `${colors.green('--match-all')} ${colors.dim('Run tests that matches all the supplied tags')}`,
322 | `${colors.green('--files')} ${colors.dim(
323 | 'Filter tests by the file name'
324 | )}`,
325 | `${colors.green('--force-exit')} ${colors.dim('Forcefully exit the process')}`,
326 | `${colors.green('--timeout')} ${colors.dim(
327 | 'Define default timeout for all tests'
328 | )}`,
329 | `${colors.green('--retries')} ${colors.dim(
330 | 'Define default retries for all tests'
331 | )}`,
332 | `${colors.green('--reporters')} ${colors.dim(
333 | 'Activate one or more test reporters'
334 | )}`,
335 | `${colors.green('--failed')} ${colors.dim(
336 | 'Run tests failed during the last run'
337 | )}`,
338 | `${colors.green('--bail')} ${colors.dim(
339 | 'Exit early when a test fails'
340 | )}`,
341 | `${colors.green('--bail-layer')} ${colors.dim('Specify at which layer to enable the bail mode. Can be "group" or "suite"')}`,
342 | `${colors.green('-h, --help')} ${colors.dim('View help')}`,
343 | ``,
344 | `${colors.yellow('Examples:')}`,
345 | `${colors.dim('node bin/test.js --tags="@github"')}`,
346 | `${colors.dim('node bin/test.js --tags="~@github"')}`,
347 | `${colors.dim('node bin/test.js --tags="@github,@slow,@integration" --match-all')}`,
348 | `${colors.dim('node bin/test.js --force-exit')}`,
349 | `${colors.dim('node bin/test.js --files="user"')}`,
350 | `${colors.dim('node bin/test.js --files="functional/user"')}`,
351 | `${colors.dim('node bin/test.js --files="unit/user"')}`,
352 | `${colors.dim('node bin/test.js --failed')}`,
353 | `${colors.dim('node bin/test.js --bail')}`,
354 | `${colors.dim('node bin/test.js --bail=group')}`,
355 | ``,
356 | `${colors.yellow('Notes:')}`,
357 | `- When groups and tests filters are applied together. We will first filter the`,
358 | ` tests by group title and then apply the tests filter.`,
359 | `- The timeout defined on test object takes precedence over the ${colors.green(
360 | '--timeout'
361 | )} flag.`,
362 | `- The retries defined on test object takes precedence over the ${colors.green(
363 | '--retries'
364 | )} flag.`,
365 | `- The ${colors.green(
366 | '--files'
367 | )} flag checks for the file names ending with the filter substring.`,
368 | `- The ${colors.green('--tags')} filter runs tests that has one or more of the supplied tags.`,
369 | `- You can use the ${colors.green('--match-all')} flag to run tests that has all the supplied tags.`,
370 | ``,
371 | ])
372 | })
373 | })
374 | }
375 | })
376 |
--------------------------------------------------------------------------------
/tests/config_manager.spec.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import { test } from 'node:test'
11 | import { assert, use } from 'chai'
12 | import chaiSubset from 'chai-subset'
13 | import { Refiner } from '@japa/core'
14 |
15 | import { wrapAssertions } from './helpers.js'
16 | import { CliParser } from '../src/cli_parser.js'
17 | import type { CLIArgs, Config } from '../src/types.js'
18 | import { ConfigManager, NOOP } from '../src/config_manager.js'
19 | import { ndjson, spec, dot, github } from '../src/reporters/main.js'
20 |
21 | use(chaiSubset)
22 |
23 | const USER_DEFINED_CONFIG_DATASET: [Config, Config][] = [
24 | [
25 | {
26 | files: [],
27 | },
28 | {
29 | cwd: process.cwd(),
30 | files: [],
31 | filters: {},
32 | exclude: ['node_modules/**', '.git/**', 'coverage/**'],
33 | forceExit: false,
34 | refiner: new Refiner(),
35 | reporters: {
36 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
37 | list: [spec(), ndjson(), dot(), github()],
38 | },
39 | setup: [],
40 | teardown: [],
41 | plugins: [],
42 | retries: 0,
43 | timeout: 2000,
44 | },
45 | ],
46 | [
47 | {
48 | files: ['tests/unit/**.spec.ts'],
49 | },
50 | {
51 | cwd: process.cwd(),
52 | files: ['tests/unit/**.spec.ts'],
53 | filters: {},
54 | exclude: ['node_modules/**', '.git/**', 'coverage/**'],
55 | forceExit: false,
56 | refiner: new Refiner(),
57 | reporters: {
58 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
59 | list: [spec(), ndjson(), dot(), github()],
60 | },
61 | plugins: [],
62 | setup: [],
63 | teardown: [],
64 | retries: 0,
65 | timeout: 2000,
66 | },
67 | ],
68 | [
69 | {
70 | files: ['tests/unit/**.spec.ts'],
71 | timeout: 4000,
72 | },
73 | {
74 | cwd: process.cwd(),
75 | files: ['tests/unit/**.spec.ts'],
76 | filters: {},
77 | exclude: ['node_modules/**', '.git/**', 'coverage/**'],
78 | forceExit: false,
79 | refiner: new Refiner(),
80 | reporters: {
81 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
82 | list: [spec(), ndjson(), dot(), github()],
83 | },
84 | plugins: [],
85 | setup: [],
86 | teardown: [],
87 | retries: 0,
88 | timeout: 4000,
89 | },
90 | ],
91 | [
92 | {
93 | files: ['tests/unit/**.spec.ts'],
94 | timeout: 4000,
95 | retries: 2,
96 | },
97 | {
98 | cwd: process.cwd(),
99 | files: ['tests/unit/**.spec.ts'],
100 | filters: {},
101 | exclude: ['node_modules/**', '.git/**', 'coverage/**'],
102 | forceExit: false,
103 | refiner: new Refiner(),
104 | reporters: {
105 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
106 | list: [spec(), ndjson(), dot(), github()],
107 | },
108 | plugins: [],
109 | setup: [],
110 | teardown: [],
111 | retries: 2,
112 | timeout: 4000,
113 | },
114 | ],
115 | [
116 | {
117 | files: ['tests/unit/**.spec.ts'],
118 | timeout: 4000,
119 | retries: 2,
120 | },
121 | {
122 | cwd: process.cwd(),
123 | files: ['tests/unit/**.spec.ts'],
124 | filters: {},
125 | exclude: ['node_modules/**', '.git/**', 'coverage/**'],
126 | forceExit: false,
127 | refiner: new Refiner(),
128 | reporters: {
129 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
130 | list: [spec(), ndjson(), dot(), github()],
131 | },
132 | plugins: [],
133 | setup: [],
134 | teardown: [],
135 | retries: 2,
136 | timeout: 4000,
137 | },
138 | ],
139 | [
140 | {
141 | files: ['tests/unit/**.spec.ts'],
142 | timeout: 4000,
143 | retries: 2,
144 | filters: {
145 | tags: ['@integration', '~@slow'],
146 | tests: ['users list'],
147 | },
148 | },
149 | {
150 | cwd: process.cwd(),
151 | files: ['tests/unit/**.spec.ts'],
152 | filters: {
153 | tags: ['@integration', '~@slow'],
154 | tests: ['users list'],
155 | },
156 | exclude: ['node_modules/**', '.git/**', 'coverage/**'],
157 | forceExit: false,
158 | refiner: new Refiner(),
159 | reporters: {
160 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
161 | list: [spec(), ndjson(), dot(), github()],
162 | },
163 | plugins: [],
164 | setup: [],
165 | teardown: [],
166 | retries: 2,
167 | timeout: 4000,
168 | },
169 | ],
170 | [
171 | {
172 | files: ['tests/unit/**.spec.ts'],
173 | timeout: 4000,
174 | retries: 2,
175 | filters: {
176 | tags: ['@integration', '~@slow'],
177 | tests: ['users list'],
178 | },
179 | forceExit: true,
180 | },
181 | {
182 | cwd: process.cwd(),
183 | files: ['tests/unit/**.spec.ts'],
184 | filters: {
185 | tags: ['@integration', '~@slow'],
186 | tests: ['users list'],
187 | },
188 | exclude: ['node_modules/**', '.git/**', 'coverage/**'],
189 | forceExit: true,
190 | refiner: new Refiner(),
191 | reporters: {
192 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
193 | list: [spec(), ndjson(), dot(), github()],
194 | },
195 | plugins: [],
196 | setup: [],
197 | teardown: [],
198 | retries: 2,
199 | timeout: 4000,
200 | },
201 | ],
202 | [
203 | {
204 | files: [],
205 | reporters: {
206 | activated: ['dot'],
207 | list: [],
208 | },
209 | },
210 | {
211 | cwd: process.cwd(),
212 | files: [],
213 | filters: {},
214 | exclude: ['node_modules/**', '.git/**', 'coverage/**'],
215 | forceExit: false,
216 | refiner: new Refiner(),
217 | reporters: {
218 | activated: ['dot'],
219 | list: [],
220 | },
221 | plugins: [],
222 | setup: [],
223 | teardown: [],
224 | retries: 0,
225 | timeout: 2000,
226 | },
227 | ],
228 | [
229 | {
230 | files: [],
231 | reporters: {
232 | activated: ['dot'],
233 | },
234 | },
235 | {
236 | cwd: process.cwd(),
237 | files: [],
238 | filters: {},
239 | exclude: ['node_modules/**', '.git/**', 'coverage/**'],
240 | forceExit: false,
241 | refiner: new Refiner(),
242 | reporters: {
243 | activated: ['dot'],
244 | list: [spec(), ndjson(), dot(), github()],
245 | },
246 | plugins: [],
247 | setup: [],
248 | teardown: [],
249 | retries: 0,
250 | timeout: 2000,
251 | },
252 | ],
253 | ]
254 |
255 | const USER_DEFINED_CONFIG_DATASET_WITH_CLI_ARGS: [Config, CLIArgs, Config][] = [
256 | [
257 | {
258 | files: [],
259 | },
260 | new CliParser().parse([]),
261 | {
262 | cwd: process.cwd(),
263 | files: [],
264 | filters: {},
265 | exclude: ['node_modules/**', '.git/**', 'coverage/**'],
266 | forceExit: false,
267 | refiner: new Refiner(),
268 | reporters: {
269 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
270 | list: [spec(), ndjson(), dot(), github()],
271 | },
272 | plugins: [],
273 | setup: [],
274 | teardown: [],
275 | retries: 0,
276 | timeout: 2000,
277 | },
278 | ],
279 | [
280 | {
281 | files: ['tests/unit/**.spec.ts'],
282 | },
283 | new CliParser().parse(['--tags=@slow']),
284 | {
285 | cwd: process.cwd(),
286 | files: ['tests/unit/**.spec.ts'],
287 | filters: {
288 | tags: ['@slow'],
289 | },
290 | exclude: ['node_modules/**', '.git/**', 'coverage/**'],
291 | forceExit: false,
292 | refiner: new Refiner(),
293 | reporters: {
294 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
295 | list: [spec(), ndjson(), dot(), github()],
296 | },
297 | plugins: [],
298 | setup: [],
299 | teardown: [],
300 | retries: 0,
301 | timeout: 2000,
302 | },
303 | ],
304 | [
305 | {
306 | files: ['tests/unit/**.spec.ts'],
307 | timeout: 4000,
308 | },
309 | new CliParser().parse(['--timeout=1000']),
310 | {
311 | cwd: process.cwd(),
312 | files: ['tests/unit/**.spec.ts'],
313 | filters: {},
314 | exclude: ['node_modules/**', '.git/**', 'coverage/**'],
315 | forceExit: false,
316 | refiner: new Refiner(),
317 | reporters: {
318 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
319 | list: [spec(), ndjson(), dot(), github()],
320 | },
321 | plugins: [],
322 | setup: [],
323 | teardown: [],
324 | retries: 0,
325 | timeout: 1000,
326 | },
327 | ],
328 | [
329 | {
330 | suites: [
331 | {
332 | name: 'unit',
333 | files: 'tests/unit/**.spec.ts',
334 | timeout: 3000,
335 | },
336 | ],
337 | timeout: 4000,
338 | },
339 | new CliParser().parse(['--timeout=1000']),
340 | {
341 | cwd: process.cwd(),
342 | suites: [
343 | {
344 | name: 'unit',
345 | files: 'tests/unit/**.spec.ts',
346 | timeout: 1000,
347 | retries: 0,
348 | configure: NOOP,
349 | },
350 | ],
351 | filters: {},
352 | exclude: ['node_modules/**', '.git/**', 'coverage/**'],
353 | forceExit: false,
354 | refiner: new Refiner(),
355 | reporters: {
356 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
357 | list: [spec(), ndjson(), dot(), github()],
358 | },
359 | plugins: [],
360 | setup: [],
361 | teardown: [],
362 | retries: 0,
363 | timeout: 1000,
364 | },
365 | ],
366 | [
367 | {
368 | files: ['tests/unit/**.spec.ts'],
369 | timeout: 4000,
370 | retries: 2,
371 | },
372 | new CliParser().parse(['--retries=4']),
373 | {
374 | cwd: process.cwd(),
375 | files: ['tests/unit/**.spec.ts'],
376 | filters: {},
377 | exclude: ['node_modules/**', '.git/**', 'coverage/**'],
378 | forceExit: false,
379 | refiner: new Refiner(),
380 | reporters: {
381 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
382 | list: [spec(), ndjson(), dot(), github()],
383 | },
384 | plugins: [],
385 | setup: [],
386 | teardown: [],
387 | retries: 4,
388 | timeout: 4000,
389 | },
390 | ],
391 | [
392 | {
393 | suites: [
394 | {
395 | name: 'unit',
396 | files: 'tests/unit/**.spec.ts',
397 | retries: 2,
398 | },
399 | ],
400 | retries: 0,
401 | },
402 | new CliParser().parse(['--retries=4']),
403 | {
404 | cwd: process.cwd(),
405 | suites: [
406 | {
407 | name: 'unit',
408 | files: 'tests/unit/**.spec.ts',
409 | timeout: 2000,
410 | retries: 4,
411 | configure: NOOP,
412 | },
413 | ],
414 | filters: {},
415 | exclude: ['node_modules/**', '.git/**', 'coverage/**'],
416 | forceExit: false,
417 | refiner: new Refiner(),
418 | reporters: {
419 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
420 | list: [spec(), ndjson(), dot(), github()],
421 | },
422 | plugins: [],
423 | setup: [],
424 | teardown: [],
425 | retries: 4,
426 | timeout: 2000,
427 | },
428 | ],
429 | [
430 | {
431 | files: ['tests/unit/**.spec.ts'],
432 | timeout: 4000,
433 | retries: 2,
434 | },
435 | new CliParser().parse(['--timeout=1000', '--retries=4']),
436 | {
437 | cwd: process.cwd(),
438 | files: ['tests/unit/**.spec.ts'],
439 | filters: {},
440 | exclude: ['node_modules/**', '.git/**', 'coverage/**'],
441 | forceExit: false,
442 | refiner: new Refiner(),
443 | reporters: {
444 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
445 | list: [spec(), ndjson(), dot(), github()],
446 | },
447 | plugins: [],
448 | setup: [],
449 | teardown: [],
450 | retries: 4,
451 | timeout: 1000,
452 | },
453 | ],
454 | [
455 | {
456 | files: ['tests/unit/**.spec.ts'],
457 | timeout: 4000,
458 | retries: 2,
459 | filters: {
460 | tags: ['@integration', '~@slow'],
461 | tests: ['users list'],
462 | },
463 | },
464 | new CliParser().parse(['--tags=@slow']),
465 | {
466 | cwd: process.cwd(),
467 | files: ['tests/unit/**.spec.ts'],
468 | filters: {
469 | tags: ['@slow'],
470 | tests: ['users list'],
471 | },
472 | exclude: ['node_modules/**', '.git/**', 'coverage/**'],
473 | forceExit: false,
474 | refiner: new Refiner(),
475 | reporters: {
476 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
477 | list: [spec(), ndjson(), dot(), github()],
478 | },
479 | plugins: [],
480 | setup: [],
481 | teardown: [],
482 | retries: 2,
483 | timeout: 4000,
484 | },
485 | ],
486 | [
487 | {
488 | files: ['tests/unit/**.spec.ts'],
489 | timeout: 4000,
490 | retries: 2,
491 | filters: {
492 | tags: ['@integration', '~@slow'],
493 | tests: ['users list'],
494 | },
495 | forceExit: true,
496 | },
497 | new CliParser().parse(['--tests=users']),
498 | {
499 | cwd: process.cwd(),
500 | files: ['tests/unit/**.spec.ts'],
501 | filters: {
502 | tags: ['@integration', '~@slow'],
503 | tests: ['users'],
504 | },
505 | exclude: ['node_modules/**', '.git/**', 'coverage/**'],
506 | forceExit: true,
507 | refiner: new Refiner(),
508 | reporters: {
509 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
510 | list: [spec(), ndjson(), dot(), github()],
511 | },
512 | plugins: [],
513 | setup: [],
514 | teardown: [],
515 | retries: 2,
516 | timeout: 4000,
517 | },
518 | ],
519 | [
520 | {
521 | files: ['tests/unit/**.spec.ts'],
522 | timeout: 4000,
523 | retries: 2,
524 | filters: {
525 | tags: ['@integration', '~@slow'],
526 | groups: ['user'],
527 | },
528 | forceExit: true,
529 | },
530 | new CliParser().parse(['--groups=customers']),
531 | {
532 | cwd: process.cwd(),
533 | files: ['tests/unit/**.spec.ts'],
534 | filters: {
535 | tags: ['@integration', '~@slow'],
536 | groups: ['customers'],
537 | },
538 | exclude: ['node_modules/**', '.git/**', 'coverage/**'],
539 | forceExit: true,
540 | refiner: new Refiner(),
541 | reporters: {
542 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
543 | list: [spec(), ndjson(), dot(), github()],
544 | },
545 | plugins: [],
546 | setup: [],
547 | teardown: [],
548 | retries: 2,
549 | timeout: 4000,
550 | },
551 | ],
552 | [
553 | {
554 | files: ['tests/unit/**.spec.ts'],
555 | timeout: 4000,
556 | retries: 2,
557 | filters: {
558 | tags: ['@integration', '~@slow'],
559 | files: ['unit/users/*'],
560 | },
561 | forceExit: true,
562 | },
563 | new CliParser().parse(['--files=*']),
564 | {
565 | cwd: process.cwd(),
566 | files: ['tests/unit/**.spec.ts'],
567 | filters: {
568 | tags: ['@integration', '~@slow'],
569 | files: ['*'],
570 | },
571 | exclude: ['node_modules/**', '.git/**', 'coverage/**'],
572 | forceExit: true,
573 | refiner: new Refiner(),
574 | reporters: {
575 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
576 | list: [spec(), ndjson(), dot(), github()],
577 | },
578 | plugins: [],
579 | setup: [],
580 | teardown: [],
581 | retries: 2,
582 | timeout: 4000,
583 | },
584 | ],
585 | [
586 | {
587 | files: ['tests/unit/**.spec.ts'],
588 | timeout: 4000,
589 | retries: 2,
590 | filters: {
591 | tags: ['@integration', '~@slow'],
592 | tests: ['users list'],
593 | suites: ['unit'],
594 | },
595 | forceExit: true,
596 | },
597 | new CliParser().parse(['unit', 'functional']),
598 | {
599 | cwd: process.cwd(),
600 | files: ['tests/unit/**.spec.ts'],
601 | filters: {
602 | tags: ['@integration', '~@slow'],
603 | tests: ['users list'],
604 | suites: ['unit', 'functional'],
605 | },
606 | exclude: ['node_modules/**', '.git/**', 'coverage/**'],
607 | forceExit: true,
608 | refiner: new Refiner(),
609 | reporters: {
610 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
611 | list: [spec(), ndjson(), dot(), github()],
612 | },
613 | plugins: [],
614 | setup: [],
615 | teardown: [],
616 | retries: 2,
617 | timeout: 4000,
618 | },
619 | ],
620 | [
621 | {
622 | files: [],
623 | reporters: {
624 | activated: ['dot'],
625 | list: [spec()],
626 | },
627 | },
628 | new CliParser().parse(['--reporters', 'progress']),
629 | {
630 | cwd: process.cwd(),
631 | files: [],
632 | filters: {},
633 | exclude: ['node_modules/**', '.git/**', 'coverage/**'],
634 | forceExit: false,
635 | refiner: new Refiner(),
636 | reporters: {
637 | activated: ['progress'],
638 | list: [spec()],
639 | },
640 | plugins: [],
641 | setup: [],
642 | teardown: [],
643 | retries: 0,
644 | timeout: 2000,
645 | },
646 | ],
647 | [
648 | {
649 | files: [],
650 | reporters: {
651 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
652 | list: [spec()],
653 | },
654 | },
655 | new CliParser().parse(['--force-exit']),
656 | {
657 | cwd: process.cwd(),
658 | files: [],
659 | filters: {},
660 | exclude: ['node_modules/**', '.git/**', 'coverage/**'],
661 | forceExit: true,
662 | refiner: new Refiner(),
663 | reporters: {
664 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
665 | list: [spec()],
666 | },
667 | plugins: [],
668 | setup: [],
669 | teardown: [],
670 | retries: 0,
671 | timeout: 2000,
672 | },
673 | ],
674 | [
675 | {
676 | files: [],
677 | reporters: {
678 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
679 | list: [spec()],
680 | },
681 | },
682 | new CliParser().parse(['--force-exit=true']),
683 | {
684 | cwd: process.cwd(),
685 | files: [],
686 | filters: {},
687 | exclude: ['node_modules/**', '.git/**', 'coverage/**'],
688 | forceExit: true,
689 | refiner: new Refiner(),
690 | reporters: {
691 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
692 | list: [spec()],
693 | },
694 | plugins: [],
695 | setup: [],
696 | teardown: [],
697 | retries: 0,
698 | timeout: 2000,
699 | },
700 | ],
701 | [
702 | {
703 | files: [],
704 | reporters: {
705 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
706 | list: [spec()],
707 | },
708 | },
709 | new CliParser().parse(['--force-exit=false']),
710 | {
711 | cwd: process.cwd(),
712 | files: [],
713 | filters: {},
714 | exclude: ['node_modules/**', '.git/**', 'coverage/**'],
715 | forceExit: false,
716 | refiner: new Refiner(),
717 | reporters: {
718 | activated: ['spec'].concat(process.env.GITHUB_ACTIONS === 'true' ? ['github'] : []),
719 | list: [spec()],
720 | },
721 | plugins: [],
722 | setup: [],
723 | teardown: [],
724 | retries: 0,
725 | timeout: 2000,
726 | },
727 | ],
728 | ]
729 |
730 | test.describe('Config manager', () => {
731 | test('hydrate config from user defined config', async () => {
732 | let index = -1
733 | for (let [userConfig, output] of USER_DEFINED_CONFIG_DATASET) {
734 | index++
735 | const manager = new ConfigManager(userConfig, {})
736 |
737 | const config = manager.hydrate() as Config
738 | await wrapAssertions(() => {
739 | const actualReporters = output.reporters
740 | const expectedReporters = config.reporters
741 | const actualReportersList = actualReporters?.list?.map((r) => r.name)
742 | const expectedReportersList = expectedReporters?.list?.map((r) => r.name)
743 |
744 | delete config.importer
745 | delete config.configureSuite
746 | delete config.reporters
747 | delete output.reporters
748 |
749 | assert.deepEqual(config, output)
750 | assert.deepEqual(actualReporters?.activated, expectedReporters?.activated)
751 | assert.deepEqual(actualReportersList, expectedReportersList)
752 | })
753 | }
754 | })
755 |
756 | test('hydrate config from user defined config and CLI args', async () => {
757 | let index = -1
758 | for (let [userConfig, cliArgs, output] of USER_DEFINED_CONFIG_DATASET_WITH_CLI_ARGS) {
759 | index++
760 | const manager = new ConfigManager(userConfig, cliArgs)
761 |
762 | const config = manager.hydrate() as Config
763 | await wrapAssertions(() => {
764 | const actualReporters = output.reporters
765 | const expectedReporters = config.reporters
766 | const actualReportersList = actualReporters?.list?.map((r) => r.name)
767 | const expectedReportersList = expectedReporters?.list?.map((r) => r.name)
768 |
769 | delete config.importer
770 | delete config.configureSuite
771 | delete config.reporters
772 | delete output.reporters
773 |
774 | assert.deepEqual(config, output)
775 | assert.deepEqual(actualReporters?.activated, expectedReporters?.activated)
776 | assert.deepEqual(actualReportersList, expectedReportersList)
777 | })
778 | }
779 | })
780 | })
781 |
--------------------------------------------------------------------------------
/tests/core.spec.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import { assert } from 'chai'
11 | import { test } from 'node:test'
12 | import { wrapAssertions } from './helpers.js'
13 | import { Emitter, Refiner, Test, TestContext } from '../modules/core/main.js'
14 |
15 | test.describe('Core', () => {
16 | test('define test cleanup callback using the test context', async () => {
17 | let stack: string[] = []
18 |
19 | const context = (t: Test) => new TestContext(t)
20 | const emitter = new Emitter()
21 | const refiner = new Refiner()
22 | const t = new Test('foo', context, emitter, refiner)
23 |
24 | t.run(({ cleanup }) => {
25 | cleanup(() => {
26 | stack.push('cleanup')
27 | })
28 | stack.push('executed')
29 | })
30 |
31 | await t.exec()
32 | await wrapAssertions(() => {
33 | assert.deepEqual(stack, ['executed', 'cleanup'])
34 | })
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/tests/files_manager.spec.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import { assert } from 'chai'
11 | import { test } from 'node:test'
12 | import { fileURLToPath, pathToFileURL } from 'node:url'
13 |
14 | import { FilesManager } from '../src/files_manager.js'
15 | import { wrapAssertions } from './helpers.js'
16 |
17 | const FILTERING_DATASET: { files: URL[]; filters: string[]; output: URL[] }[] = [
18 | {
19 | files: [
20 | pathToFileURL('tests/unit/create_user.spec.js'),
21 | pathToFileURL('tests/unit/list_user.spec.js'),
22 | pathToFileURL('tests/unit/edit_user.spec.js'),
23 | pathToFileURL('tests/functional/user.spec.js'),
24 | pathToFileURL('tests/functional/register_user.spec.js'),
25 | ],
26 | filters: ['user'],
27 | output: [
28 | pathToFileURL('tests/unit/create_user.spec.js'),
29 | pathToFileURL('tests/unit/list_user.spec.js'),
30 | pathToFileURL('tests/unit/edit_user.spec.js'),
31 | pathToFileURL('tests/functional/user.spec.js'),
32 | pathToFileURL('tests/functional/register_user.spec.js'),
33 | ],
34 | },
35 | {
36 | files: [
37 | pathToFileURL('tests/unit/create_user.spec.js'),
38 | pathToFileURL('tests/unit/list_user.spec.js'),
39 | pathToFileURL('tests/unit/edit_user.spec.js'),
40 | pathToFileURL('tests/functional/user.spec.js'),
41 | pathToFileURL('tests/functional/register_user.spec.js'),
42 | ],
43 | filters: ['unit/user'],
44 | output: [
45 | pathToFileURL('tests/unit/create_user.spec.js'),
46 | pathToFileURL('tests/unit/list_user.spec.js'),
47 | pathToFileURL('tests/unit/edit_user.spec.js'),
48 | ],
49 | },
50 | {
51 | files: [
52 | pathToFileURL('tests/unit/create_user.spec.js'),
53 | pathToFileURL('tests/unit/list_user.spec.js'),
54 | pathToFileURL('tests/unit/edit_user.spec.js'),
55 | pathToFileURL('tests/functional/user.spec.js'),
56 | pathToFileURL('tests/functional/register_user.spec.js'),
57 | ],
58 | filters: ['onal/user'],
59 | output: [
60 | pathToFileURL('tests/functional/user.spec.js'),
61 | pathToFileURL('tests/functional/register_user.spec.js'),
62 | ],
63 | },
64 | {
65 | files: [
66 | pathToFileURL('tests/unit/users/create.spec.js'),
67 | pathToFileURL('tests/unit/users/edit.spec.js'),
68 | pathToFileURL('tests/unit/users/delete.spec.js'),
69 | pathToFileURL('tests/functional/users.spec.js'),
70 | pathToFileURL('tests/functional/register_user.spec.js'),
71 | ],
72 | filters: ['unit/users/*'],
73 | output: [
74 | pathToFileURL('tests/unit/users/create.spec.js'),
75 | pathToFileURL('tests/unit/users/edit.spec.js'),
76 | pathToFileURL('tests/unit/users/delete.spec.js'),
77 | ],
78 | },
79 | {
80 | files: [
81 | pathToFileURL('tests/unit/users/create.spec.js'),
82 | pathToFileURL('tests/unit/users/edit.spec.js'),
83 | pathToFileURL('tests/unit/users/delete.spec.js'),
84 | pathToFileURL('tests/functional/users.spec.js'),
85 | pathToFileURL('tests/functional/register_user.spec.js'),
86 | ],
87 | filters: ['users/create.spec.js'],
88 | output: [pathToFileURL('tests/unit/users/create.spec.js')],
89 | },
90 | ]
91 |
92 | test.describe('Files manager | grep', () => {
93 | test('apply filter on the file name', async () => {
94 | for (let { files, filters, output } of FILTERING_DATASET) {
95 | await wrapAssertions(() => {
96 | assert.deepEqual(new FilesManager().grep(files, filters), output)
97 | })
98 | }
99 | })
100 | })
101 |
102 | test.describe('Files manager | getFiles', () => {
103 | test('get files for the glob pattern', async () => {
104 | const cwd = new URL('../', import.meta.url)
105 | const files = await new FilesManager().getFiles(fileURLToPath(cwd), ['tests/**/*.spec.ts'], [])
106 |
107 | await wrapAssertions(() => {
108 | assert.deepEqual(files, [
109 | new URL('tests/base_reporter.spec.ts', cwd),
110 | new URL('tests/cli_parser.spec.ts', cwd),
111 | new URL('tests/config_manager.spec.ts', cwd),
112 | new URL('tests/core.spec.ts', cwd),
113 | new URL('tests/files_manager.spec.ts', cwd),
114 | new URL('tests/github_reporter.spec.ts', cwd),
115 | new URL('tests/planner.spec.ts', cwd),
116 | new URL('tests/runner.spec.ts', cwd),
117 | ])
118 | })
119 | })
120 |
121 | test('get files from multiple glob patterns', async () => {
122 | const cwd = new URL('../', import.meta.url)
123 | const files = await new FilesManager().getFiles(
124 | fileURLToPath(cwd),
125 | ['tests/**/*.spec.ts', 'modules/**/*.ts'],
126 | []
127 | )
128 |
129 | await wrapAssertions(() => {
130 | assert.deepEqual(files, [
131 | new URL('tests/base_reporter.spec.ts', cwd),
132 | new URL('tests/cli_parser.spec.ts', cwd),
133 | new URL('tests/config_manager.spec.ts', cwd),
134 | new URL('tests/core.spec.ts', cwd),
135 | new URL('tests/files_manager.spec.ts', cwd),
136 | new URL('tests/github_reporter.spec.ts', cwd),
137 | new URL('tests/planner.spec.ts', cwd),
138 | new URL('tests/runner.spec.ts', cwd),
139 | new URL('modules/core/main.ts', cwd),
140 | new URL('modules/core/types.ts', cwd),
141 | new URL('modules/core/reporters/base.ts', cwd),
142 | ])
143 | })
144 | })
145 |
146 | test('ignore files using the exclude pattern', async () => {
147 | const cwd = new URL('../', import.meta.url)
148 | const files = await new FilesManager().getFiles(
149 | fileURLToPath(cwd),
150 | ['tests/**/*.spec.ts', 'modules/**/*.ts'],
151 | ['modules/**']
152 | )
153 |
154 | await wrapAssertions(() => {
155 | assert.deepEqual(files, [
156 | new URL('tests/base_reporter.spec.ts', cwd),
157 | new URL('tests/cli_parser.spec.ts', cwd),
158 | new URL('tests/config_manager.spec.ts', cwd),
159 | new URL('tests/core.spec.ts', cwd),
160 | new URL('tests/files_manager.spec.ts', cwd),
161 | new URL('tests/github_reporter.spec.ts', cwd),
162 | new URL('tests/planner.spec.ts', cwd),
163 | new URL('tests/runner.spec.ts', cwd),
164 | ])
165 | })
166 | })
167 |
168 | test('get files from a custom implementation', async () => {
169 | const cwd = new URL('../', import.meta.url)
170 | const files = await new FilesManager().getFiles(
171 | fileURLToPath(cwd),
172 | () => {
173 | return [
174 | new URL('tests/cli_parser.spec.ts', cwd),
175 | new URL('tests/config_manager.spec.ts', cwd),
176 | new URL('tests/files_manager.spec.ts', cwd),
177 | new URL('tests/github_reporter.spec.ts', cwd),
178 | new URL('modules/core/main.ts', cwd),
179 | ]
180 | },
181 | []
182 | )
183 |
184 | await wrapAssertions(() => {
185 | assert.deepEqual(files, [
186 | new URL('tests/cli_parser.spec.ts', cwd),
187 | new URL('tests/config_manager.spec.ts', cwd),
188 | new URL('tests/files_manager.spec.ts', cwd),
189 | new URL('tests/github_reporter.spec.ts', cwd),
190 | new URL('modules/core/main.ts', cwd),
191 | ])
192 | })
193 | })
194 | })
195 |
--------------------------------------------------------------------------------
/tests/github_reporter.spec.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import { test } from 'node:test'
11 | import { assert, use } from 'chai'
12 | import chaiSubset from 'chai-subset'
13 | import { ErrorsPrinter } from '@japa/errors-printer'
14 |
15 | import { wrapAssertions } from './helpers.js'
16 | import { GithubReporter } from '../src/reporters/github.js'
17 |
18 | use(chaiSubset)
19 |
20 | test.describe('Github reporter', () => {
21 | test('report errors in correct format', async () => {
22 | const reporter = new GithubReporter()
23 | const errorPrinter = new ErrorsPrinter()
24 | const annotation = await reporter.getErrorAnnotation(errorPrinter, {
25 | phase: 'test',
26 | title: '2 + 2 is 4',
27 | error: new Error('Expected 5 to equal 4'),
28 | })
29 |
30 | wrapAssertions(() => {
31 | assert.equal(
32 | annotation,
33 | '::error file=tests/github_reporter.spec.ts,title=2 + 2 is 4,line=27,column=14::Expected 5 to equal 4'
34 | )
35 | })
36 | })
37 |
38 | test('do not report values other than errors', async () => {
39 | const reporter = new GithubReporter()
40 | const errorPrinter = new ErrorsPrinter()
41 | const annotation = await reporter.getErrorAnnotation(errorPrinter, {
42 | phase: 'test',
43 | title: '2 + 2 is 4',
44 | error: 22,
45 | } as any)
46 |
47 | wrapAssertions(() => {
48 | assert.isUndefined(annotation)
49 | })
50 | })
51 | })
52 |
--------------------------------------------------------------------------------
/tests/helpers.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import { ErrorsPrinter } from '@japa/errors-printer'
11 | import { Emitter } from '../modules/core/main.js'
12 | import { RunnerEvents } from '../src/types.js'
13 |
14 | export async function wrapAssertions(fn: () => void | Promise) {
15 | try {
16 | await fn()
17 | } catch (error) {
18 | await new ErrorsPrinter().printError(error)
19 | throw new Error('Assertion failure')
20 | }
21 | }
22 |
23 | /**
24 | * Promisify an event
25 | */
26 | export function pEvent(
27 | emitter: Emitter,
28 | event: Name,
29 | timeout: number = 500
30 | ) {
31 | return new Promise((resolve) => {
32 | function handler(data: RunnerEvents[Name]) {
33 | emitter.off(event, handler)
34 | resolve(data)
35 | }
36 |
37 | setTimeout(() => {
38 | emitter.off(event, handler)
39 | resolve(null)
40 | }, timeout)
41 | emitter.on(event, handler)
42 | })
43 | }
44 |
--------------------------------------------------------------------------------
/tests/planner.spec.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import { assert } from 'chai'
11 | import { test } from 'node:test'
12 | import { Planner } from '../src/planner.js'
13 | import { ConfigManager, NOOP } from '../src/config_manager.js'
14 | import { wrapAssertions } from './helpers.js'
15 |
16 | test.describe('Planner | files', () => {
17 | test('get suites for files', async () => {
18 | const config = new ConfigManager({ files: ['tests/**/*.spec.ts'] }, {}).hydrate()
19 | const { suites } = await new Planner(config).plan()
20 |
21 | await wrapAssertions(() => {
22 | assert.deepEqual(suites, [
23 | {
24 | name: 'default',
25 | files: ['tests/**/*.spec.ts'],
26 | filesURLs: [
27 | new URL('../tests/base_reporter.spec.ts', import.meta.url),
28 | new URL('../tests/cli_parser.spec.ts', import.meta.url),
29 | new URL('../tests/config_manager.spec.ts', import.meta.url),
30 | new URL('../tests/core.spec.ts', import.meta.url),
31 | new URL('../tests/files_manager.spec.ts', import.meta.url),
32 | new URL('../tests/github_reporter.spec.ts', import.meta.url),
33 | new URL('../tests/planner.spec.ts', import.meta.url),
34 | new URL('../tests/runner.spec.ts', import.meta.url),
35 | ],
36 | retries: 0,
37 | timeout: 2000,
38 | },
39 | ])
40 | })
41 | })
42 |
43 | test('apply files filter to the list', async () => {
44 | const config = new ConfigManager(
45 | { files: ['tests/**/*.spec.ts'], filters: { files: ['parser'] } },
46 | {}
47 | ).hydrate()
48 | const { suites } = await new Planner(config).plan()
49 |
50 | await wrapAssertions(() => {
51 | assert.deepEqual(suites, [
52 | {
53 | name: 'default',
54 | files: ['tests/**/*.spec.ts'],
55 | filesURLs: [new URL('../tests/cli_parser.spec.ts', import.meta.url)],
56 | retries: 0,
57 | timeout: 2000,
58 | },
59 | ])
60 | })
61 | })
62 |
63 | test('use inline global timeout', async () => {
64 | const config = new ConfigManager({ files: ['tests/**/*.spec.ts'], timeout: 1000 }, {}).hydrate()
65 | const { suites } = await new Planner(config).plan()
66 |
67 | await wrapAssertions(() => {
68 | assert.deepEqual(suites, [
69 | {
70 | name: 'default',
71 | files: ['tests/**/*.spec.ts'],
72 | filesURLs: [
73 | new URL('../tests/base_reporter.spec.ts', import.meta.url),
74 | new URL('../tests/cli_parser.spec.ts', import.meta.url),
75 | new URL('../tests/config_manager.spec.ts', import.meta.url),
76 | new URL('../tests/core.spec.ts', import.meta.url),
77 | new URL('../tests/files_manager.spec.ts', import.meta.url),
78 | new URL('../tests/github_reporter.spec.ts', import.meta.url),
79 | new URL('../tests/planner.spec.ts', import.meta.url),
80 | new URL('../tests/runner.spec.ts', import.meta.url),
81 | ],
82 | retries: 0,
83 | timeout: 1000,
84 | },
85 | ])
86 | })
87 | })
88 |
89 | test('use cli timeout', async () => {
90 | const config = new ConfigManager(
91 | { files: ['tests/**/*.spec.ts'], timeout: 1000 },
92 | {
93 | timeout: '3000',
94 | }
95 | ).hydrate()
96 | const { suites } = await new Planner(config).plan()
97 |
98 | await wrapAssertions(() => {
99 | assert.deepEqual(suites, [
100 | {
101 | name: 'default',
102 | files: ['tests/**/*.spec.ts'],
103 | filesURLs: [
104 | new URL('../tests/base_reporter.spec.ts', import.meta.url),
105 | new URL('../tests/cli_parser.spec.ts', import.meta.url),
106 | new URL('../tests/config_manager.spec.ts', import.meta.url),
107 | new URL('../tests/core.spec.ts', import.meta.url),
108 | new URL('../tests/files_manager.spec.ts', import.meta.url),
109 | new URL('../tests/github_reporter.spec.ts', import.meta.url),
110 | new URL('../tests/planner.spec.ts', import.meta.url),
111 | new URL('../tests/runner.spec.ts', import.meta.url),
112 | ],
113 | retries: 0,
114 | timeout: 3000,
115 | },
116 | ])
117 | })
118 | })
119 |
120 | test('use inline global retries', async () => {
121 | const config = new ConfigManager({ files: ['tests/**/*.spec.ts'], retries: 2 }, {}).hydrate()
122 | const { suites } = await new Planner(config).plan()
123 |
124 | await wrapAssertions(() => {
125 | assert.deepEqual(suites, [
126 | {
127 | name: 'default',
128 | files: ['tests/**/*.spec.ts'],
129 | filesURLs: [
130 | new URL('../tests/base_reporter.spec.ts', import.meta.url),
131 | new URL('../tests/cli_parser.spec.ts', import.meta.url),
132 | new URL('../tests/config_manager.spec.ts', import.meta.url),
133 | new URL('../tests/core.spec.ts', import.meta.url),
134 | new URL('../tests/files_manager.spec.ts', import.meta.url),
135 | new URL('../tests/github_reporter.spec.ts', import.meta.url),
136 | new URL('../tests/planner.spec.ts', import.meta.url),
137 | new URL('../tests/runner.spec.ts', import.meta.url),
138 | ],
139 | retries: 2,
140 | timeout: 2000,
141 | },
142 | ])
143 | })
144 | })
145 |
146 | test('use cli retries', async () => {
147 | const config = new ConfigManager(
148 | { files: ['tests/**/*.spec.ts'], retries: 2 },
149 | {
150 | retries: '3',
151 | }
152 | ).hydrate()
153 | const { suites } = await new Planner(config).plan()
154 |
155 | await wrapAssertions(() => {
156 | assert.deepEqual(suites, [
157 | {
158 | name: 'default',
159 | files: ['tests/**/*.spec.ts'],
160 | filesURLs: [
161 | new URL('../tests/base_reporter.spec.ts', import.meta.url),
162 | new URL('../tests/cli_parser.spec.ts', import.meta.url),
163 | new URL('../tests/config_manager.spec.ts', import.meta.url),
164 | new URL('../tests/core.spec.ts', import.meta.url),
165 | new URL('../tests/files_manager.spec.ts', import.meta.url),
166 | new URL('../tests/github_reporter.spec.ts', import.meta.url),
167 | new URL('../tests/planner.spec.ts', import.meta.url),
168 | new URL('../tests/runner.spec.ts', import.meta.url),
169 | ],
170 | retries: 3,
171 | timeout: 2000,
172 | },
173 | ])
174 | })
175 | })
176 |
177 | test('error when suite filter is applied with files', async () => {
178 | const config = new ConfigManager(
179 | {
180 | files: [],
181 | },
182 | {
183 | _: ['functional'],
184 | }
185 | ).hydrate()
186 |
187 | await wrapAssertions(() => {
188 | assert.throws(
189 | () => new Planner(config),
190 | 'Cannot apply suites filter. You have not configured any test suites'
191 | )
192 | })
193 | })
194 | })
195 |
196 | test.describe('Planner | suites', () => {
197 | test('get suites', async () => {
198 | const config = new ConfigManager(
199 | { suites: [{ name: 'unit', files: 'tests/**/*.spec.ts' }] },
200 | {}
201 | ).hydrate()
202 | const { suites } = await new Planner(config).plan()
203 |
204 | await wrapAssertions(() => {
205 | assert.deepEqual(suites, [
206 | {
207 | name: 'unit',
208 | files: 'tests/**/*.spec.ts',
209 | filesURLs: [
210 | new URL('../tests/base_reporter.spec.ts', import.meta.url),
211 | new URL('../tests/cli_parser.spec.ts', import.meta.url),
212 | new URL('../tests/config_manager.spec.ts', import.meta.url),
213 | new URL('../tests/core.spec.ts', import.meta.url),
214 | new URL('../tests/files_manager.spec.ts', import.meta.url),
215 | new URL('../tests/github_reporter.spec.ts', import.meta.url),
216 | new URL('../tests/planner.spec.ts', import.meta.url),
217 | new URL('../tests/runner.spec.ts', import.meta.url),
218 | ],
219 | retries: 0,
220 | timeout: 2000,
221 | configure: NOOP,
222 | },
223 | ])
224 | })
225 | })
226 |
227 | test('apply files filter to the suites', async () => {
228 | const config = new ConfigManager(
229 | { suites: [{ name: 'unit', files: 'tests/**/*.spec.ts' }], filters: { files: ['manager'] } },
230 | {}
231 | ).hydrate()
232 | const { suites } = await new Planner(config).plan()
233 |
234 | await wrapAssertions(() => {
235 | assert.deepEqual(suites, [
236 | {
237 | name: 'unit',
238 | files: 'tests/**/*.spec.ts',
239 | filesURLs: [
240 | new URL('../tests/config_manager.spec.ts', import.meta.url),
241 | new URL('../tests/files_manager.spec.ts', import.meta.url),
242 | ],
243 | retries: 0,
244 | timeout: 2000,
245 | configure: NOOP,
246 | },
247 | ])
248 | })
249 | })
250 |
251 | test('use inline timeout', async () => {
252 | const config = new ConfigManager(
253 | {
254 | suites: [{ name: 'unit', files: 'tests/**/*.spec.ts', timeout: 1000 }],
255 | },
256 | {}
257 | ).hydrate()
258 | const { suites } = await new Planner(config).plan()
259 |
260 | await wrapAssertions(() => {
261 | assert.deepEqual(suites, [
262 | {
263 | name: 'unit',
264 | files: 'tests/**/*.spec.ts',
265 | filesURLs: [
266 | new URL('../tests/base_reporter.spec.ts', import.meta.url),
267 | new URL('../tests/cli_parser.spec.ts', import.meta.url),
268 | new URL('../tests/config_manager.spec.ts', import.meta.url),
269 | new URL('../tests/core.spec.ts', import.meta.url),
270 | new URL('../tests/files_manager.spec.ts', import.meta.url),
271 | new URL('../tests/github_reporter.spec.ts', import.meta.url),
272 | new URL('../tests/planner.spec.ts', import.meta.url),
273 | new URL('../tests/runner.spec.ts', import.meta.url),
274 | ],
275 | retries: 0,
276 | timeout: 1000,
277 | configure: NOOP,
278 | },
279 | ])
280 | })
281 | })
282 |
283 | test('use cli timeout', async () => {
284 | const config = new ConfigManager(
285 | { suites: [{ name: 'unit', files: 'tests/**/*.spec.ts', timeout: 1000 }] },
286 | {
287 | timeout: '3000',
288 | }
289 | ).hydrate()
290 | const { suites } = await new Planner(config).plan()
291 |
292 | await wrapAssertions(() => {
293 | assert.deepEqual(suites, [
294 | {
295 | name: 'unit',
296 | files: 'tests/**/*.spec.ts',
297 | filesURLs: [
298 | new URL('../tests/base_reporter.spec.ts', import.meta.url),
299 | new URL('../tests/cli_parser.spec.ts', import.meta.url),
300 | new URL('../tests/config_manager.spec.ts', import.meta.url),
301 | new URL('../tests/core.spec.ts', import.meta.url),
302 | new URL('../tests/files_manager.spec.ts', import.meta.url),
303 | new URL('../tests/github_reporter.spec.ts', import.meta.url),
304 | new URL('../tests/planner.spec.ts', import.meta.url),
305 | new URL('../tests/runner.spec.ts', import.meta.url),
306 | ],
307 | retries: 0,
308 | timeout: 3000,
309 | configure: NOOP,
310 | },
311 | ])
312 | })
313 | })
314 |
315 | test('use inline retries', async () => {
316 | const config = new ConfigManager(
317 | { suites: [{ name: 'unit', files: 'tests/**/*.spec.ts', retries: 2 }] },
318 | {}
319 | ).hydrate()
320 | const { suites } = await new Planner(config).plan()
321 |
322 | await wrapAssertions(() => {
323 | assert.deepEqual(suites, [
324 | {
325 | name: 'unit',
326 | files: 'tests/**/*.spec.ts',
327 | filesURLs: [
328 | new URL('../tests/base_reporter.spec.ts', import.meta.url),
329 | new URL('../tests/cli_parser.spec.ts', import.meta.url),
330 | new URL('../tests/config_manager.spec.ts', import.meta.url),
331 | new URL('../tests/core.spec.ts', import.meta.url),
332 | new URL('../tests/files_manager.spec.ts', import.meta.url),
333 | new URL('../tests/github_reporter.spec.ts', import.meta.url),
334 | new URL('../tests/planner.spec.ts', import.meta.url),
335 | new URL('../tests/runner.spec.ts', import.meta.url),
336 | ],
337 | retries: 2,
338 | timeout: 2000,
339 | configure: NOOP,
340 | },
341 | ])
342 | })
343 | })
344 |
345 | test('use cli retries', async () => {
346 | const config = new ConfigManager(
347 | { suites: [{ name: 'unit', files: 'tests/**/*.spec.ts', retries: 2 }] },
348 | { retries: '3' }
349 | ).hydrate()
350 | const { suites } = await new Planner(config).plan()
351 |
352 | await wrapAssertions(() => {
353 | assert.deepEqual(suites, [
354 | {
355 | name: 'unit',
356 | files: 'tests/**/*.spec.ts',
357 | filesURLs: [
358 | new URL('../tests/base_reporter.spec.ts', import.meta.url),
359 | new URL('../tests/cli_parser.spec.ts', import.meta.url),
360 | new URL('../tests/config_manager.spec.ts', import.meta.url),
361 | new URL('../tests/core.spec.ts', import.meta.url),
362 | new URL('../tests/files_manager.spec.ts', import.meta.url),
363 | new URL('../tests/github_reporter.spec.ts', import.meta.url),
364 | new URL('../tests/planner.spec.ts', import.meta.url),
365 | new URL('../tests/runner.spec.ts', import.meta.url),
366 | ],
367 | retries: 3,
368 | timeout: 2000,
369 | configure: NOOP,
370 | },
371 | ])
372 | })
373 | })
374 |
375 | test('error on duplicate suites', async () => {
376 | const config = new ConfigManager(
377 | {
378 | suites: [
379 | { name: 'unit', files: 'tests/**/*.spec.ts' },
380 | { name: 'unit', files: 'tests/**/*.spec.ts' },
381 | ],
382 | },
383 | {}
384 | ).hydrate()
385 |
386 | await wrapAssertions(() => {
387 | assert.throws(() => new Planner(config), 'Duplicate suite "unit"')
388 | })
389 | })
390 |
391 | test('error when suite filter mentions non-existing suite', async () => {
392 | const config = new ConfigManager(
393 | {
394 | suites: [{ name: 'unit', files: 'tests/**/*.spec.ts' }],
395 | },
396 | {
397 | _: ['functional'],
398 | }
399 | ).hydrate()
400 |
401 | await wrapAssertions(() => {
402 | assert.throws(
403 | () => new Planner(config),
404 | 'Cannot apply suites filter. "functional" suite is not configured'
405 | )
406 | })
407 | })
408 |
409 | test('error when suite filter is applied without defining suites', async () => {
410 | const config = new ConfigManager(
411 | {
412 | suites: [],
413 | },
414 | {
415 | _: ['functional'],
416 | }
417 | ).hydrate()
418 |
419 | await wrapAssertions(() => {
420 | assert.throws(
421 | () => new Planner(config),
422 | 'Cannot apply suites filter. You have not configured any test suites'
423 | )
424 | })
425 | })
426 |
427 | test('apply suites filter', async () => {
428 | const config = new ConfigManager(
429 | {
430 | suites: [
431 | { name: 'unit', files: 'tests/**/*.spec.ts' },
432 | { name: 'functional', files: 'tests/**/*.spec.ts' },
433 | ],
434 | },
435 | {
436 | _: ['functional'],
437 | }
438 | ).hydrate()
439 |
440 | const { suites } = await new Planner(config).plan()
441 | await wrapAssertions(() => {
442 | assert.deepEqual(suites, [
443 | {
444 | name: 'functional',
445 | files: 'tests/**/*.spec.ts',
446 | filesURLs: [
447 | new URL('../tests/base_reporter.spec.ts', import.meta.url),
448 | new URL('../tests/cli_parser.spec.ts', import.meta.url),
449 | new URL('../tests/config_manager.spec.ts', import.meta.url),
450 | new URL('../tests/core.spec.ts', import.meta.url),
451 | new URL('../tests/files_manager.spec.ts', import.meta.url),
452 | new URL('../tests/github_reporter.spec.ts', import.meta.url),
453 | new URL('../tests/planner.spec.ts', import.meta.url),
454 | new URL('../tests/runner.spec.ts', import.meta.url),
455 | ],
456 | retries: 0,
457 | timeout: 2000,
458 | configure: NOOP,
459 | },
460 | ])
461 | })
462 | })
463 | })
464 |
465 | test.describe('Planner | reporters', () => {
466 | test('get collection of activated reporters', async () => {
467 | const config = new ConfigManager({ files: ['tests/**/*.spec.ts'] }, {}).hydrate()
468 | const { reporters } = await new Planner(config).plan()
469 |
470 | await wrapAssertions(() => {
471 | assert.deepEqual(
472 | reporters,
473 | [
474 | {
475 | handler: reporters[0].handler,
476 | name: 'spec',
477 | },
478 | ].concat(
479 | process.env.GITHUB_ACTIONS === 'true'
480 | ? [
481 | {
482 | handler: reporters[1].handler,
483 | name: 'github',
484 | },
485 | ]
486 | : []
487 | )
488 | )
489 | })
490 | })
491 |
492 | test('get collection of manually activated reporters', async () => {
493 | const config = new ConfigManager(
494 | {
495 | files: ['tests/**/*.spec.ts'],
496 | reporters: {
497 | activated: ['dot'],
498 | list: [
499 | {
500 | name: 'spec',
501 | handler: {} as any,
502 | },
503 | {
504 | name: 'dot',
505 | handler: {} as any,
506 | },
507 | ],
508 | },
509 | },
510 | {}
511 | ).hydrate()
512 | const { reporters } = await new Planner(config).plan()
513 |
514 | await wrapAssertions(() => {
515 | assert.deepEqual(reporters, [
516 | {
517 | handler: {} as any,
518 | name: 'dot',
519 | },
520 | ])
521 | })
522 | })
523 |
524 | test('get collection of reporters activated via CLI flag', async () => {
525 | const config = new ConfigManager(
526 | {
527 | files: ['tests/**/*.spec.ts'],
528 | reporters: {
529 | activated: ['dot'],
530 | list: [
531 | {
532 | name: 'spec',
533 | handler: {} as any,
534 | },
535 | {
536 | name: 'dot',
537 | handler: {} as any,
538 | },
539 | ],
540 | },
541 | },
542 | {
543 | reporters: 'spec',
544 | }
545 | ).hydrate()
546 | const { reporters } = await new Planner(config).plan()
547 |
548 | await wrapAssertions(() => {
549 | assert.deepEqual(reporters, [
550 | {
551 | handler: {} as any,
552 | name: 'spec',
553 | },
554 | ])
555 | })
556 | })
557 |
558 | test('report error when activated reporter is not in the list', async () => {
559 | const config = new ConfigManager(
560 | {
561 | files: ['tests/**/*.spec.ts'],
562 | reporters: {
563 | activated: ['progress'],
564 | list: [
565 | {
566 | name: 'spec',
567 | handler: {} as any,
568 | },
569 | {
570 | name: 'dot',
571 | handler: {} as any,
572 | },
573 | ],
574 | },
575 | },
576 | {}
577 | ).hydrate()
578 |
579 | await wrapAssertions(() => {
580 | assert.throws(
581 | () => new Planner(config),
582 | 'Invalid reporter "progress". Make sure to register it first inside the "reporters.list" array'
583 | )
584 | })
585 | })
586 |
587 | test('report error when CLI activated reporter is not in the list', async () => {
588 | const config = new ConfigManager(
589 | {
590 | files: ['tests/**/*.spec.ts'],
591 | reporters: {
592 | activated: ['dot'],
593 | list: [
594 | {
595 | name: 'spec',
596 | handler: {} as any,
597 | },
598 | {
599 | name: 'dot',
600 | handler: {} as any,
601 | },
602 | ],
603 | },
604 | },
605 | {
606 | reporters: 'progress',
607 | }
608 | ).hydrate()
609 |
610 | await wrapAssertions(() => {
611 | assert.throws(
612 | () => new Planner(config),
613 | 'Invalid reporter "progress". Make sure to register it first inside the "reporters.list" array'
614 | )
615 | })
616 | })
617 | })
618 |
619 | test.describe('Planner | refinerFilters', () => {
620 | test('get refinerFilters from the filters list', async () => {
621 | const config = new ConfigManager(
622 | {
623 | files: ['tests/**/*.spec.ts'],
624 | filters: {
625 | tests: ['user'],
626 | tags: ['@slow'],
627 | files: ['manager'],
628 | groups: ['customers'],
629 | },
630 | },
631 | {}
632 | ).hydrate()
633 | const { refinerFilters } = await new Planner(config).plan()
634 |
635 | await wrapAssertions(() => {
636 | assert.deepEqual(refinerFilters, [
637 | {
638 | layer: 'tests',
639 | filters: ['user'],
640 | },
641 | {
642 | layer: 'tags',
643 | filters: ['@slow'],
644 | },
645 | {
646 | layer: 'groups',
647 | filters: ['customers'],
648 | },
649 | ])
650 | })
651 | })
652 | })
653 |
--------------------------------------------------------------------------------
/tests/runner.spec.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @japa/runner
3 | *
4 | * (c) Japa
5 | *
6 | * For the full copyright and license information, please view the LICENSE
7 | * file that was distributed with this source code.
8 | */
9 |
10 | import { assert } from 'chai'
11 | import { test } from 'node:test'
12 |
13 | import { runner } from '../factories/main.js'
14 | import { GlobalHooks } from '../src/hooks.js'
15 | import { ConfigManager } from '../src/config_manager.js'
16 | import { pEvent, wrapAssertions } from './helpers.js'
17 | import { createTest, createTestGroup } from '../src/create_test.js'
18 | import { Emitter, Refiner, Runner, Suite } from '../modules/core/main.js'
19 | import { clearCache, getFailedTests, retryPlugin } from '../src/plugins/retry.js'
20 |
21 | test.describe('Runner | create tests and groups', () => {
22 | test('raise error when defining nested groups', async () => {
23 | const emitter = new Emitter()
24 | const refiner = new Refiner()
25 | const group = createTestGroup('', emitter, refiner, {})
26 |
27 | await wrapAssertions(() => {
28 | assert.throws(
29 | () => createTestGroup('', emitter, refiner, { group }),
30 | 'Nested groups are not supported by Japa'
31 | )
32 | })
33 | })
34 |
35 | test('add group to the suite when defined', async () => {
36 | const emitter = new Emitter()
37 | const refiner = new Refiner()
38 | const suite = new Suite('', emitter, refiner)
39 | const group = createTestGroup('', emitter, refiner, { suite })
40 |
41 | await wrapAssertions(() => {
42 | assert.deepEqual(suite.stack, [group])
43 | })
44 | })
45 |
46 | test('add test to the suite when defined', async () => {
47 | const emitter = new Emitter()
48 | const refiner = new Refiner()
49 | const suite = new Suite('', emitter, refiner)
50 | const t = createTest('', emitter, refiner, { suite })
51 |
52 | await wrapAssertions(() => {
53 | assert.deepEqual(suite.stack, [t])
54 | })
55 | })
56 |
57 | test('add test to the group when group and suite both are defined', async () => {
58 | const emitter = new Emitter()
59 | const refiner = new Refiner()
60 | const suite = new Suite('', emitter, refiner)
61 |
62 | const group = createTestGroup('', emitter, refiner, { suite })
63 | const t = createTest('', emitter, refiner, { suite, group })
64 |
65 | await wrapAssertions(() => {
66 | assert.deepEqual(suite.stack, [group])
67 | assert.deepEqual(group.tests, [t])
68 | })
69 | })
70 |
71 | test('define test timeout from global options', async () => {
72 | const emitter = new Emitter()
73 | const refiner = new Refiner()
74 | const t = createTest('', emitter, refiner, { timeout: 1000 })
75 |
76 | await wrapAssertions(() => {
77 | assert.equal(t.options.timeout, 1000)
78 | })
79 | })
80 |
81 | test('define test retries from global options', async () => {
82 | const emitter = new Emitter()
83 | const refiner = new Refiner()
84 | const t = createTest('', emitter, refiner, { retries: 4 })
85 |
86 | await wrapAssertions(() => {
87 | assert.equal(t.options.retries, 4)
88 | })
89 | })
90 |
91 | test('execute test', async () => {
92 | const stack: string[] = []
93 | const emitter = new Emitter()
94 | const refiner = new Refiner()
95 | const t = createTest('', emitter, refiner, { retries: 4 })
96 | t.run(() => {
97 | stack.push('executed')
98 | })
99 |
100 | await t.exec()
101 | await wrapAssertions(() => {
102 | assert.deepEqual(stack, ['executed'])
103 | })
104 | })
105 |
106 | test('assert test throws an exception', async () => {
107 | const emitter = new Emitter()
108 | const refiner = new Refiner()
109 | const t = createTest('', emitter, refiner, {})
110 | t.run(() => {
111 | throw new Error('Failed')
112 | }).throws('Failed')
113 |
114 | const [event] = await Promise.all([pEvent(emitter, 'test:end'), t.exec()])
115 |
116 | await wrapAssertions(() => {
117 | assert.equal(event!.hasError, false)
118 | })
119 | })
120 |
121 | test('assert error matches the regular expression', async () => {
122 | const emitter = new Emitter()
123 | const refiner = new Refiner()
124 | const t = createTest('', emitter, refiner, {})
125 | t.run(() => {
126 | throw new Error('Failed')
127 | }).throws(/ed?/)
128 |
129 | const [event] = await Promise.all([pEvent(emitter, 'test:end'), t.exec()])
130 |
131 | await wrapAssertions(() => {
132 | assert.equal(event!.hasError, false)
133 | })
134 | })
135 |
136 | test('throw error when test does not have a callback defined', async () => {
137 | const emitter = new Emitter()
138 | const refiner = new Refiner()
139 | const t = createTest('', emitter, refiner, {})
140 |
141 | await wrapAssertions(() => {
142 | assert.throws(
143 | () => t.throws(/ed?/),
144 | 'Cannot use "test.throws" method without a test callback'
145 | )
146 | })
147 | })
148 |
149 | test('assert test throws an instance of a given class', async () => {
150 | const emitter = new Emitter()
151 | const refiner = new Refiner()
152 | const t = createTest('', emitter, refiner, {})
153 | t.run(() => {
154 | throw new Error('Failed')
155 | }).throws('Failed', Error)
156 |
157 | const [event] = await Promise.all([pEvent(emitter, 'test:end'), t.exec()])
158 | await wrapAssertions(() => {
159 | assert.equal(event!.hasError, false)
160 | })
161 | })
162 |
163 | test('fail when test does not throw an exception', async () => {
164 | const emitter = new Emitter()
165 | const refiner = new Refiner()
166 | const t = createTest('', emitter, refiner, {})
167 | t.run(() => {}).throws('Failed', Error)
168 |
169 | const [event] = await Promise.all([pEvent(emitter, 'test:end'), t.exec()])
170 | await wrapAssertions(() => {
171 | assert.equal(event!.hasError, true)
172 | assert.equal(event!.errors[0].error.message, 'Expected test to throw an exception')
173 | })
174 | })
175 |
176 | test('fail when error constructor mismatch', async () => {
177 | class Exception {}
178 | const emitter = new Emitter()
179 | const refiner = new Refiner()
180 | const t = createTest('', emitter, refiner, {})
181 | t.run(() => {
182 | throw new Error('Failed')
183 | }).throws('Failed', Exception)
184 |
185 | const [event] = await Promise.all([pEvent(emitter, 'test:end'), t.exec()])
186 | await wrapAssertions(() => {
187 | assert.equal(event!.hasError, true)
188 | assert.equal(event!.errors[0].error.message, 'Expected test to throw "[class Exception]"')
189 | })
190 | })
191 |
192 | test('fail when error message mismatch', async () => {
193 | const emitter = new Emitter()
194 | const refiner = new Refiner()
195 | const t = createTest('', emitter, refiner, {})
196 | t.run(() => {
197 | throw new Error('Failed')
198 | }).throws('Failure')
199 |
200 | const [event] = await Promise.all([pEvent(emitter, 'test:end'), t.exec()])
201 | await wrapAssertions(() => {
202 | assert.equal(event!.hasError, true)
203 | assert.equal(
204 | event!.errors[0].error.message,
205 | 'Expected test to throw "Failure". Instead received "Failed"'
206 | )
207 | })
208 | })
209 |
210 | test('fail when error does not match the regular expression', async () => {
211 | const emitter = new Emitter()
212 | const refiner = new Refiner()
213 | const t = createTest('', emitter, refiner, {})
214 | t.run(() => {
215 | throw new Error('Failed')
216 | }).throws(/lure?/)
217 |
218 | const [event] = await Promise.all([pEvent(emitter, 'test:end'), t.exec()])
219 | await wrapAssertions(() => {
220 | assert.equal(event!.hasError, true)
221 | assert.equal(
222 | event!.errors[0].error.message,
223 | 'Expected test error to match "/lure?/" regular expression'
224 | )
225 | })
226 | })
227 | })
228 |
229 | test.describe('Runner | global hooks', () => {
230 | test('do not run teardown hooks when setup hooks were not executed', async () => {
231 | const hooks = new GlobalHooks()
232 | const stack: string[] = []
233 |
234 | hooks.apply(
235 | new ConfigManager(
236 | {
237 | files: [],
238 | setup: [
239 | () => {
240 | stack.push('setup')
241 | },
242 | ],
243 | teardown: [
244 | () => {
245 | stack.push('teardown')
246 | },
247 | ],
248 | },
249 | {}
250 | ).hydrate()
251 | )
252 |
253 | const emitter = new Emitter()
254 | await hooks.teardown(null, new Runner(emitter))
255 |
256 | await wrapAssertions(() => {
257 | assert.deepEqual(stack, [])
258 | })
259 | })
260 |
261 | test('run teardown hooks when setup hooks were executed', async () => {
262 | const hooks = new GlobalHooks()
263 | const stack: string[] = []
264 |
265 | hooks.apply(
266 | new ConfigManager(
267 | {
268 | files: [],
269 | setup: [
270 | () => {
271 | stack.push('setup')
272 | },
273 | ],
274 | teardown: [
275 | () => {
276 | stack.push('teardown')
277 | },
278 | ],
279 | },
280 | {}
281 | ).hydrate()
282 | )
283 |
284 | const emitter = new Emitter()
285 | await hooks.setup(new Runner(emitter))
286 | await hooks.teardown(null, new Runner(emitter))
287 |
288 | await wrapAssertions(() => {
289 | assert.deepEqual(stack, ['setup', 'teardown'])
290 | })
291 | })
292 |
293 | test('do not run teardown hooks in case of error', async () => {
294 | const hooks = new GlobalHooks()
295 | const stack: string[] = []
296 |
297 | hooks.apply(
298 | new ConfigManager(
299 | {
300 | files: [],
301 | setup: [
302 | () => {
303 | stack.push('setup')
304 | },
305 | ],
306 | teardown: [
307 | () => {
308 | stack.push('teardown')
309 | },
310 | ],
311 | },
312 | {}
313 | ).hydrate()
314 | )
315 |
316 | const emitter = new Emitter()
317 | await hooks.setup(new Runner(emitter))
318 | await hooks.teardown(new Error('foo'), new Runner(emitter))
319 |
320 | await wrapAssertions(() => {
321 | assert.deepEqual(stack, ['setup'])
322 | })
323 | })
324 |
325 | test('run teardown cleanup methods when teardown hook raises error', async () => {
326 | const hooks = new GlobalHooks()
327 | const stack: string[] = []
328 |
329 | hooks.apply(
330 | new ConfigManager(
331 | {
332 | files: [],
333 | setup: [
334 | () => {
335 | stack.push('setup')
336 | },
337 | ],
338 | teardown: [
339 | () => {
340 | stack.push('teardown')
341 | return () => {
342 | stack.push('teardown cleanup')
343 | }
344 | },
345 | () => {
346 | throw new Error('blowup')
347 | },
348 | ],
349 | },
350 | {}
351 | ).hydrate()
352 | )
353 |
354 | const emitter = new Emitter()
355 | await hooks.setup(new Runner(emitter))
356 | try {
357 | await hooks.teardown(null, new Runner(emitter))
358 | } catch (error) {
359 | await hooks.teardown(error, new Runner(emitter))
360 | }
361 |
362 | await wrapAssertions(() => {
363 | assert.deepEqual(stack, ['setup', 'teardown', 'teardown cleanup'])
364 | })
365 | })
366 | })
367 |
368 | test.describe('Runner | retryPlugin', () => {
369 | test('store failing tests inside the cache dir', async () => {
370 | const stack: string[] = []
371 |
372 | const emitter = new Emitter()
373 | const refiner = new Refiner()
374 |
375 | const suite = new Suite('same', emitter, refiner)
376 | createTest('failing test', emitter, refiner, { suite }).run(() => {
377 | stack.push('executing failing test')
378 | throw new Error('Failing')
379 | })
380 | createTest('passing test', emitter, refiner, { suite }).run(() => {
381 | stack.push('executing passing test')
382 | })
383 |
384 | await runner()
385 | .configure({
386 | files: [],
387 | refiner,
388 | reporters: {
389 | activated: [],
390 | list: [],
391 | },
392 | plugins: [retryPlugin],
393 | })
394 | .useEmitter(emitter)
395 | .runSuites(() => [suite])
396 |
397 | await wrapAssertions(async () => {
398 | assert.deepEqual(await getFailedTests(), { tests: ['failing test'] })
399 | assert.deepEqual(stack, ['executing failing test', 'executing passing test'])
400 | })
401 |
402 | await clearCache()
403 | })
404 |
405 | test('run only failed tests when failed flag is used', async () => {
406 | const stack: string[] = []
407 |
408 | const emitter = new Emitter()
409 | const refiner = new Refiner()
410 |
411 | function getSuite() {
412 | const suite = new Suite('same', emitter, refiner)
413 | createTest('failing test', emitter, refiner, { suite }).run(() => {
414 | stack.push('executing failing test')
415 | throw new Error('Failing')
416 | })
417 | createTest('passing test', emitter, refiner, { suite }).run(() => {
418 | stack.push('executing passing test')
419 | })
420 |
421 | return suite
422 | }
423 |
424 | function getExecutor(argv?: string[]) {
425 | return runner()
426 | .configure(
427 | {
428 | files: [],
429 | refiner,
430 | reporters: {
431 | activated: [],
432 | list: [],
433 | },
434 | plugins: [retryPlugin],
435 | },
436 | argv
437 | )
438 | .useEmitter(emitter)
439 | }
440 |
441 | await getExecutor().runSuites(() => [getSuite()])
442 | await wrapAssertions(async () => {
443 | assert.deepEqual(await getFailedTests(), { tests: ['failing test'] })
444 | assert.deepEqual(stack, ['executing failing test', 'executing passing test'])
445 | })
446 |
447 | await getExecutor(['--failed']).runSuites(() => [getSuite()])
448 | await wrapAssertions(async () => {
449 | assert.deepEqual(await getFailedTests(), { tests: ['failing test'] })
450 | assert.deepEqual(stack, [
451 | 'executing failing test',
452 | 'executing passing test',
453 | 'executing failing test',
454 | ])
455 | })
456 |
457 | await clearCache()
458 | })
459 |
460 | test('run all tests when failed flag is not used', async () => {
461 | const stack: string[] = []
462 |
463 | const emitter = new Emitter()
464 | const refiner = new Refiner()
465 |
466 | function getSuite() {
467 | const suite = new Suite('same', emitter, refiner)
468 | createTest('failing test', emitter, refiner, { suite }).run(() => {
469 | stack.push('executing failing test')
470 | throw new Error('Failing')
471 | })
472 | createTest('passing test', emitter, refiner, { suite }).run(() => {
473 | stack.push('executing passing test')
474 | })
475 |
476 | return suite
477 | }
478 |
479 | function getExecutor(argv?: string[]) {
480 | return runner()
481 | .configure(
482 | {
483 | files: [],
484 | refiner,
485 | reporters: {
486 | activated: [],
487 | list: [],
488 | },
489 | plugins: [retryPlugin],
490 | },
491 | argv
492 | )
493 | .useEmitter(emitter)
494 | }
495 |
496 | await getExecutor().runSuites(() => [getSuite()])
497 | await wrapAssertions(async () => {
498 | assert.deepEqual(await getFailedTests(), { tests: ['failing test'] })
499 | assert.deepEqual(stack, ['executing failing test', 'executing passing test'])
500 | })
501 |
502 | await getExecutor([]).runSuites(() => [getSuite()])
503 | await wrapAssertions(async () => {
504 | assert.deepEqual(await getFailedTests(), { tests: ['failing test'] })
505 | assert.deepEqual(stack, [
506 | 'executing failing test',
507 | 'executing passing test',
508 | 'executing failing test',
509 | 'executing passing test',
510 | ])
511 | })
512 |
513 | await clearCache()
514 | })
515 |
516 | test('run all tests when there are no failing tests', async () => {
517 | const stack: string[] = []
518 |
519 | const emitter = new Emitter()
520 | const refiner = new Refiner()
521 |
522 | function getSuite() {
523 | const suite = new Suite('same', emitter, refiner)
524 | createTest('passing test', emitter, refiner, { suite }).run(() => {
525 | stack.push('executing passing test')
526 | })
527 |
528 | return suite
529 | }
530 |
531 | function getExecutor(argv?: string[]) {
532 | return runner()
533 | .configure(
534 | {
535 | files: [],
536 | refiner,
537 | reporters: {
538 | activated: [],
539 | list: [],
540 | },
541 | plugins: [retryPlugin],
542 | },
543 | argv
544 | )
545 | .useEmitter(emitter)
546 | }
547 |
548 | await getExecutor().runSuites(() => [getSuite()])
549 | await wrapAssertions(async () => {
550 | assert.deepEqual(await getFailedTests(), { tests: [] })
551 | assert.deepEqual(stack, ['executing passing test'])
552 | })
553 |
554 | await getExecutor(['--failed']).runSuites(() => [getSuite()])
555 | await wrapAssertions(async () => {
556 | assert.deepEqual(await getFailedTests(), { tests: [] })
557 | assert.deepEqual(stack, ['executing passing test', 'executing passing test'])
558 | })
559 |
560 | await clearCache()
561 | })
562 | })
563 |
564 | test.describe('Runner | bail', () => {
565 | test('stop after a failing test', async () => {
566 | const stack: string[] = []
567 |
568 | const emitter = new Emitter()
569 | const refiner = new Refiner()
570 |
571 | const suite = new Suite('same', emitter, refiner)
572 | createTest('failing test', emitter, refiner, { suite }).run(() => {
573 | stack.push('executing failing test')
574 | throw new Error('Failing')
575 | })
576 | createTest('passing test', emitter, refiner, { suite }).run(() => {
577 | stack.push('executing passing test')
578 | })
579 |
580 | await runner()
581 | .bail()
582 | .configure({
583 | files: [],
584 | refiner,
585 | reporters: {
586 | activated: [],
587 | list: [],
588 | },
589 | })
590 | .useEmitter(emitter)
591 | .runSuites(() => [suite])
592 |
593 | await wrapAssertions(async () => {
594 | assert.deepEqual(stack, ['executing failing test'])
595 | })
596 | })
597 |
598 | test('run all suites when bailLayer is set to suite', async () => {
599 | const stack: string[] = []
600 |
601 | const emitter = new Emitter()
602 | const refiner = new Refiner()
603 |
604 | const unit = new Suite('unit', emitter, refiner)
605 | unit.bail()
606 | const functional = new Suite('functional', emitter, refiner)
607 | functional.bail()
608 |
609 | createTest('failing unit test', emitter, refiner, { suite: unit }).run(() => {
610 | stack.push('executing failing unit test')
611 | throw new Error('Failing')
612 | })
613 | createTest('passing unit test', emitter, refiner, { suite: unit }).run(() => {
614 | stack.push('executing passing unit test')
615 | })
616 |
617 | createTest('failing functional test', emitter, refiner, { suite: functional }).run(() => {
618 | stack.push('executing failing functional test')
619 | throw new Error('Failing')
620 | })
621 | createTest('passing functional test', emitter, refiner, { suite: functional }).run(() => {
622 | stack.push('executing passing functional test')
623 | })
624 |
625 | await runner()
626 | .configure({
627 | files: [],
628 | refiner,
629 | reporters: {
630 | activated: [],
631 | list: [],
632 | },
633 | })
634 | .useEmitter(emitter)
635 | .runSuites(() => [unit, functional])
636 |
637 | await wrapAssertions(async () => {
638 | assert.deepEqual(stack, ['executing failing unit test', 'executing failing functional test'])
639 | })
640 | })
641 |
642 | test('run all groups when bailLayer is set to group', async () => {
643 | const stack: string[] = []
644 |
645 | const emitter = new Emitter()
646 | const refiner = new Refiner()
647 |
648 | const unit = new Suite('unit', emitter, refiner)
649 |
650 | const group1 = createTestGroup('group 1', emitter, refiner, { suite: unit })
651 | group1.bail()
652 |
653 | const group2 = createTestGroup('group 2', emitter, refiner, { suite: unit })
654 | group2.bail()
655 |
656 | createTest('failing group 1 test', emitter, refiner, { suite: unit, group: group1 }).run(() => {
657 | stack.push('executing failing group 1 test')
658 | throw new Error('Failing')
659 | })
660 | createTest('passing group 1 test', emitter, refiner, { suite: unit, group: group1 }).run(() => {
661 | stack.push('executing passing group 1 test')
662 | })
663 |
664 | createTest('failing group 2 test', emitter, refiner, { suite: unit, group: group2 }).run(() => {
665 | stack.push('executing failing group 2 test')
666 | throw new Error('Failing')
667 | })
668 | createTest('passing group 1 test', emitter, refiner, { suite: unit, group: group2 }).run(() => {
669 | stack.push('executing passing group 2 test')
670 | })
671 |
672 | await runner()
673 | .configure({
674 | files: [],
675 | refiner,
676 | reporters: {
677 | activated: [],
678 | list: [],
679 | },
680 | })
681 | .useEmitter(emitter)
682 | .runSuites(() => [unit])
683 |
684 | await wrapAssertions(async () => {
685 | assert.deepEqual(stack, ['executing failing group 1 test', 'executing failing group 2 test'])
686 | })
687 | })
688 | })
689 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@adonisjs/tsconfig/tsconfig.package.json",
3 | "compilerOptions": {
4 | "rootDir": "./",
5 | "outDir": "./build"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------