├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── 1-bug-report.en-US.yml │ ├── 2-feature-request.en-US.yml │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md ├── pr-labeler.yml ├── release.yml ├── renovate.json5 └── workflows │ ├── lint.yml │ ├── pr-label.yaml │ ├── preview.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── biome.json ├── cspell.config.cjs ├── examples └── node │ ├── package.json │ ├── rstest.config.ts │ ├── src │ └── index.ts │ ├── test │ └── index.test.ts │ └── tsconfig.json ├── nx.json ├── package.json ├── packages └── core │ ├── LICENSE │ ├── README.md │ ├── bin │ └── rstest.js │ ├── globals.d.ts │ ├── importMeta.d.ts │ ├── package.json │ ├── rslib.config.ts │ ├── src │ ├── cli │ │ ├── commands.ts │ │ ├── index.ts │ │ └── prepare.ts │ ├── config.ts │ ├── core │ │ ├── context.ts │ │ ├── index.ts │ │ ├── listTests.ts │ │ ├── plugins │ │ │ ├── entry.ts │ │ │ └── ignoreResolveError.ts │ │ ├── rsbuild.ts │ │ └── runTests.ts │ ├── env.d.ts │ ├── node.ts │ ├── pool │ │ ├── forks.ts │ │ └── index.ts │ ├── public.ts │ ├── reporter │ │ ├── index.ts │ │ ├── statusRenderer.ts │ │ ├── summary.ts │ │ └── windowedRenderer.ts │ ├── runtime │ │ ├── api │ │ │ ├── expect.ts │ │ │ ├── index.ts │ │ │ ├── poll.ts │ │ │ ├── public.ts │ │ │ ├── snapshot.ts │ │ │ ├── spy.ts │ │ │ └── utilities.ts │ │ ├── runner │ │ │ ├── fixtures.ts │ │ │ ├── index.ts │ │ │ ├── runner.ts │ │ │ ├── runtime.ts │ │ │ └── task.ts │ │ ├── util.ts │ │ └── worker │ │ │ ├── console.ts │ │ │ ├── index.ts │ │ │ ├── loadModule.ts │ │ │ ├── rpc.ts │ │ │ ├── setup.ts │ │ │ └── snapshot.ts │ ├── types │ │ ├── api.ts │ │ ├── config.ts │ │ ├── core.ts │ │ ├── expect.ts │ │ ├── index.ts │ │ ├── mock.ts │ │ ├── reporter.ts │ │ ├── runner.ts │ │ ├── testSuite.ts │ │ ├── utils.ts │ │ └── worker.ts │ └── utils │ │ ├── constants.ts │ │ ├── error.ts │ │ ├── helper.ts │ │ ├── index.ts │ │ ├── logger.ts │ │ └── testFiles.ts │ ├── tests │ ├── core │ │ ├── __snapshots__ │ │ │ └── rsbuild.test.ts.snap │ │ └── rsbuild.test.ts │ ├── runner │ │ ├── runner.test.ts │ │ ├── runtime.test.ts │ │ └── util.test.ts │ ├── tsconfig.json │ └── utils │ │ ├── helper.test.ts │ │ └── testFiles.test.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── rstest.config.ts ├── scripts ├── dictionary.txt ├── rstest.setup.ts └── tsconfig │ ├── base.json │ └── package.json ├── tests ├── assets │ └── icon.png ├── basic │ ├── src │ │ ├── index.ts │ │ └── meta.ts │ └── test │ │ ├── dynamicImport.test.ts │ │ ├── index.test.ts │ │ ├── meta.test.ts │ │ └── nodeApi.test.ts ├── build │ ├── fixtures │ │ ├── alias │ │ │ ├── index.test.ts │ │ │ ├── rstest.config.ts │ │ │ └── src │ │ │ │ ├── a.ts │ │ │ │ ├── b.ts │ │ │ │ └── index.ts │ │ ├── cssModules │ │ │ ├── index.test.ts │ │ │ ├── rstest.config.ts │ │ │ └── src │ │ │ │ ├── env.d.ts │ │ │ │ ├── index.module.css │ │ │ │ └── index.ts │ │ ├── decorators │ │ │ ├── index.test.js │ │ │ ├── rstest.config.ts │ │ │ └── src │ │ │ │ └── index.js │ │ ├── define │ │ │ ├── env.d.ts │ │ │ ├── index.test.ts │ │ │ └── rstest.config.ts │ │ ├── plugin │ │ │ ├── index.test.ts │ │ │ ├── rstest.config.ts │ │ │ └── src │ │ │ │ ├── a.ts │ │ │ │ └── index.ts │ │ └── tools │ │ │ └── rspack │ │ │ ├── index.test.ts │ │ │ ├── rstest.config.ts │ │ │ └── src │ │ │ ├── a.ts │ │ │ ├── b.ts │ │ │ └── index.ts │ ├── index.test.ts │ └── runtimeImport │ │ ├── a.js │ │ ├── b.ts │ │ ├── index.test.ts │ │ └── package.json ├── cli │ ├── fixtures │ │ ├── error.config.ts │ │ ├── fail.test.ts │ │ └── success.test.ts │ ├── index.test.ts │ └── package.json ├── describe │ ├── chain.test.ts │ ├── cli.test.ts │ ├── concurrent.test.ts │ ├── condition.test.ts │ ├── each.test.ts │ ├── fixtures │ │ ├── skip.test.ts │ │ ├── todo.test.ts │ │ └── undefined.test.ts │ ├── for.test.ts │ ├── only.each.test.ts │ ├── only.test.ts │ └── skip.each.test.ts ├── diff │ ├── fixtures │ │ └── index.test.ts │ └── index.test.ts ├── expect │ └── test │ │ ├── assert.test.ts │ │ ├── fixtures │ │ └── soft.test.ts │ │ ├── index.test.ts │ │ ├── poll.test.ts │ │ └── soft.test.ts ├── externals │ ├── fixtures │ │ ├── index.test.ts │ │ └── test-pkg │ │ │ ├── index.ts │ │ │ └── package.json │ ├── index.test.ts │ └── package.json ├── filter │ ├── default.test.ts │ ├── fixtures │ │ ├── testNamePattern.config.ts │ │ └── testNamePattern.test.ts │ ├── regex.test.ts │ └── testNamePattern.test.ts ├── globals │ ├── fixtures │ │ ├── index.test.ts │ │ ├── package.json │ │ └── tsconfig.json │ └── index.test.ts ├── in-source │ ├── fixtures │ │ ├── rslib.config.ts │ │ ├── rstest.config.ts │ │ ├── src │ │ │ ├── a.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ └── index.test.ts ├── isolate │ ├── src │ │ └── index.ts │ └── test │ │ ├── index.test.ts │ │ └── index1.test.ts ├── lifecycle │ ├── afterAll.test.ts │ ├── afterEach.test.ts │ ├── afterHooksError.test.ts │ ├── beforeAll.test.ts │ ├── beforeEach.test.ts │ ├── beforeHooksError.test.ts │ ├── fixtures │ │ ├── afterAll.test.ts │ │ ├── afterEach.test.ts │ │ ├── beforeAll.test.ts │ │ ├── beforeEach.test.ts │ │ ├── cleanup.test.ts │ │ ├── error │ │ │ ├── afterAll.test.ts │ │ │ ├── afterEach.test.ts │ │ │ ├── beforeAll.test.ts │ │ │ ├── beforeAllRoot.test.ts │ │ │ └── beforeEach.test.ts │ │ ├── skip.test.ts │ │ └── timeout.test.ts │ ├── index.test.ts │ ├── package.json │ ├── rstest.config.ts │ └── timeout.test.ts ├── list │ ├── fixtures │ │ ├── a.test.ts │ │ └── b.test.ts │ ├── index.test.ts │ └── json.test.ts ├── log │ ├── fixtures │ │ ├── consoleLogFalse.config.ts │ │ ├── log.test.ts │ │ ├── logSrc.test.ts │ │ ├── src │ │ │ └── index.ts │ │ └── trace.test.ts │ ├── index.test.ts │ └── trace.test.ts ├── mock │ ├── package.json │ ├── src │ │ ├── b.ts │ │ └── index.ts │ └── tests │ │ ├── external.test.ts │ │ └── index.test.ts ├── noTests │ ├── fixtures │ │ ├── noTestInSuite.test.ts │ │ ├── noTests.test.ts │ │ └── suiteFnUndefined.test.ts │ └── index.test.ts ├── package.json ├── reporter │ ├── fixtures │ │ └── index.test.ts │ ├── index.test.ts │ ├── package.json │ ├── rstest.config.ts │ ├── rstest.customReporterConfig.ts │ └── rstest.emptyReporterConfig.ts ├── rstest.config.ts ├── runner │ └── test │ │ ├── async.test.ts │ │ └── index.test.ts ├── scripts │ ├── index.ts │ └── utils.ts ├── setup │ ├── fixtures │ │ ├── basic │ │ │ ├── index.test.ts │ │ │ ├── rstest.config.ts │ │ │ └── rstest.setup.ts │ │ └── error │ │ │ ├── index.test.ts │ │ │ ├── rstest.config.ts │ │ │ └── rstest.setup.ts │ └── index.test.ts ├── snapshot │ ├── __image_snapshots__ │ │ └── extend-test-ts-test-to-match-image-snapshot-correctly-1-snap.png │ ├── __snapshots__ │ │ ├── file.output.txt │ │ └── file.test.ts.snap │ ├── extend.test.ts │ ├── fail.test.ts │ ├── file.test.ts │ ├── fixtures │ │ ├── __snapshots__ │ │ │ ├── fail.test.ts.snap │ │ │ ├── obsolete.test.ts.snap │ │ │ └── skip.test.ts.snap │ │ ├── fail.test.ts │ │ ├── index.test.ts │ │ ├── inlineSnapshot.each.test.ts │ │ ├── obsolete.test.ts │ │ └── skip.test.ts │ ├── index.test.ts │ └── obsolete.test.ts ├── spy │ ├── clearAllMocks.test.ts │ ├── config.test.ts │ ├── fixtures │ │ ├── clearMocks.test.ts │ │ └── restoreMocks.test.ts │ ├── index.test.ts │ ├── invocationCallOrder.test.ts │ ├── spyOn.test.ts │ ├── stubEnv.test.ts │ ├── stubGlobal.test.ts │ └── withImplementation.test.ts ├── ssr │ ├── fixtures │ │ ├── package.json │ │ ├── rsbuild.config.ts │ │ ├── rstest.config.ts │ │ ├── src │ │ │ ├── App.css │ │ │ ├── App.tsx │ │ │ ├── env.d.ts │ │ │ ├── index.server.tsx │ │ │ └── index.tsx │ │ ├── test │ │ │ └── index.test.ts │ │ └── tsconfig.json │ └── index.test.ts ├── test-api │ ├── __snapshots__ │ │ └── concurrentContext.test.ts.snap │ ├── chain.test.ts │ ├── concurrent.test.ts │ ├── concurrentContext.test.ts │ ├── concurrentIsolated.test.ts │ ├── concurrentLimit.test.ts │ ├── concurrentNested.test.ts │ ├── condition.test.ts │ ├── each.test.ts │ ├── edgeCase.test.ts │ ├── extend.auto.test.ts │ ├── extend.depends.test.ts │ ├── extend.extend.test.ts │ ├── extend.onDemand.test.ts │ ├── fixtures │ │ ├── concurrentLimit.test.ts │ │ ├── error.test.ts │ │ ├── moduleNotFound.test.ts │ │ ├── onlyInSkip.test.ts │ │ ├── retry.test.ts │ │ ├── timeout.test.ts │ │ └── undefined.test.ts │ ├── for.test.ts │ ├── index.test.ts │ ├── only.each.test.ts │ ├── only.fails.test.ts │ ├── only.test.ts │ ├── retry.test.ts │ ├── sequential.test.ts │ ├── timeout.test.ts │ └── timeoutConfig.test.ts └── watch │ ├── fixtures │ ├── index.test.ts │ └── src │ │ └── index.ts │ ├── index.test.ts │ ├── package.json │ └── rstest.config.ts └── website ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── en │ ├── _meta.json │ ├── guide │ │ ├── _meta.json │ │ ├── advanced │ │ │ ├── _meta.json │ │ │ └── profiling.mdx │ │ ├── basic │ │ │ ├── _meta.json │ │ │ └── cli.mdx │ │ └── start │ │ │ ├── _meta.json │ │ │ ├── index.mdx │ │ │ └── quick-start.mdx │ └── index.md ├── public │ └── netlify.toml └── zh │ ├── _meta.json │ ├── guide │ ├── _meta.json │ ├── advanced │ │ ├── _meta.json │ │ └── profiling.mdx │ ├── basic │ │ ├── _meta.json │ │ └── cli.mdx │ └── start │ │ ├── _meta.json │ │ ├── index.mdx │ │ └── quick-start.mdx │ └── index.md ├── env.d.ts ├── i18n.json ├── package.json ├── rspress.config.ts ├── theme ├── components │ ├── Copyright.module.scss │ ├── Copyright.tsx │ ├── Hero.module.scss │ ├── Hero.tsx │ ├── ImageAlt.tsx │ ├── NextSteps.module.scss │ ├── NextSteps.tsx │ ├── Overview.module.scss │ ├── Overview.tsx │ ├── RsbuildDocBadge.tsx │ ├── Step.module.scss │ ├── Step.tsx │ ├── ToolStack.tsx │ └── utils.ts ├── env.d.ts ├── index.scss ├── index.tsx └── pages │ └── index.tsx └── tsconfig.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Let GitHub languages ignores MDX files 2 | *.mdx linguist-documentation 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-feature-request.en-US.yml: -------------------------------------------------------------------------------- 1 | name: '💡 Feature Request' 2 | description: Submit a new feature request to Rstest 3 | title: '[Feature]: ' 4 | type: Enhancement 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for submitting new feature requests! Before submitting, please note: 10 | 11 | - Confirmed that this is a common feature and cannot be implemented through existing APIs. 12 | - Make sure you searched in the [Issues](https://github.com/web-infra-dev/rstest/issues) and didn't find the same request. 13 | - You can discuss the feature in [Discussions](https://github.com/web-infra-dev/rstest/discussions) first. 14 | 15 | - type: textarea 16 | id: description 17 | attributes: 18 | label: What problem does this feature solve? 19 | description: Please describe the usage scenario for this feature. 20 | validations: 21 | required: true 22 | 23 | - type: textarea 24 | id: api 25 | attributes: 26 | label: What does the proposed API look like? 27 | description: Describe the new API, provide some code examples. 28 | validations: 29 | required: true 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a question 4 | url: https://github.com/web-infra-dev/rstest/discussions 5 | about: Ask a question about Rstest 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | ## Related Links 4 | 5 | 6 | 7 | ## Checklist 8 | 9 | 10 | 11 | - [ ] Tests updated (or not required). 12 | - [ ] Documentation updated (or not required). 13 | -------------------------------------------------------------------------------- /.github/pr-labeler.yml: -------------------------------------------------------------------------------- 1 | 'change: feat': 2 | - '/^(feat|types|style)/' 3 | 'change: fix': 4 | - '/^fix/' 5 | 'change: perf': 6 | - '/^perf/' 7 | 'change: breaking': 8 | - '/^breaking change/' 9 | 'change: docs': 10 | - '/^docs/' 11 | 'release': 12 | - '/^release/' 13 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | # .github/release.yml 2 | 3 | changelog: 4 | exclude: 5 | authors: 6 | # Ignore the release PR created by github-actions 7 | - github-actions 8 | categories: 9 | - title: Breaking Changes 🍭 10 | labels: 11 | - 'change: breaking' 12 | - title: New Features 🎉 13 | labels: 14 | - 'change: feat' 15 | - title: Performance 🚀 16 | labels: 17 | - 'change: perf' 18 | - title: Bug Fixes 🐞 19 | labels: 20 | - 'change: fix' 21 | - title: Document 📖 22 | labels: 23 | - 'change: docs' 24 | - title: Other Changes 25 | labels: 26 | - '*' 27 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | push: 8 | branches: [main] 9 | 10 | workflow_dispatch: 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | lint: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | with: 23 | fetch-depth: 1 24 | 25 | - name: Install pnpm 26 | run: | 27 | npm install -g corepack@latest --force 28 | corepack enable 29 | 30 | - name: Setup Node.js 31 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 32 | with: 33 | node-version: 22 34 | cache: 'pnpm' 35 | 36 | - name: Install Dependencies 37 | run: pnpm install --ignore-scripts 38 | 39 | - name: Lint 40 | run: pnpm run lint 41 | 42 | - name: Check Dependency Version 43 | run: pnpm run check-dependency-version 44 | 45 | - name: Check pnpm Dedupe 46 | run: pnpm dedupe --check 47 | -------------------------------------------------------------------------------- /.github/workflows/pr-label.yaml: -------------------------------------------------------------------------------- 1 | name: PR Labeler 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | 9 | permissions: 10 | # Permits `github/issue-labeler` to add a label to a pull request 11 | pull-requests: write 12 | contents: read 13 | 14 | jobs: 15 | change-labeling: 16 | name: Labeling for changes 17 | runs-on: ubuntu-latest 18 | if: github.repository == 'web-infra-dev/rstest' 19 | steps: 20 | - uses: github/issue-labeler@c1b0f9f52a63158c4adc09425e858e87b32e9685 # v3.4 21 | with: 22 | repo-token: '${{ secrets.GITHUB_TOKEN }}' 23 | configuration-path: .github/pr-labeler.yml 24 | enable-versioned-regex: 0 25 | include-title: 1 26 | sync-labels: 1 27 | -------------------------------------------------------------------------------- /.github/workflows/preview.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/stackblitz-labs/pkg.pr.new 2 | name: Preview Release 3 | 4 | on: 5 | # push: 6 | # branches: [main] 7 | workflow_dispatch: 8 | inputs: 9 | branch: 10 | description: 'Branch to release' 11 | required: true 12 | default: 'main' 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | preview: 19 | if: github.repository == 'web-infra-dev/rstest' 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 25 | with: 26 | fetch-depth: 1 27 | ref: ${{ github.event.inputs.branch }} 28 | 29 | - name: Install pnpm 30 | run: | 31 | npm install -g corepack@latest --force 32 | corepack enable 33 | 34 | - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 35 | id: changes 36 | with: 37 | predicate-quantifier: 'every' 38 | filters: | 39 | changed: 40 | - "packages/**" 41 | - "!packages/document/**" 42 | 43 | - name: Setup Node.js 44 | if: steps.changes.outputs.changed == 'true' 45 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 46 | with: 47 | node-version: 22.x 48 | cache: 'pnpm' 49 | 50 | - name: Install Dependencies 51 | if: steps.changes.outputs.changed == 'true' 52 | run: pnpm install 53 | 54 | - name: Publish Preview 55 | if: steps.changes.outputs.changed == 'true' 56 | run: pnpx pkg-pr-new publish --pnpm ./packages/* 57 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | npm_tag: 7 | type: choice 8 | description: 'Specify npm tag' 9 | required: true 10 | default: 'alpha' 11 | options: 12 | - alpha 13 | - beta 14 | - rc 15 | - canary 16 | - latest 17 | branch: 18 | description: 'Branch to release' 19 | required: true 20 | default: 'main' 21 | 22 | permissions: 23 | # Provenance generation in GitHub Actions requires "write" access to the "id-token" 24 | id-token: write 25 | 26 | jobs: 27 | release: 28 | name: Release 29 | if: github.repository == 'web-infra-dev/rstest' && github.event_name == 'workflow_dispatch' 30 | runs-on: ubuntu-latest 31 | environment: production 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 35 | with: 36 | fetch-depth: 1 37 | ref: ${{ github.event.inputs.branch }} 38 | 39 | - name: Install pnpm 40 | run: | 41 | npm install -g corepack@latest --force 42 | corepack enable 43 | 44 | - name: Setup Node.js 45 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 46 | with: 47 | node-version: 22 48 | cache: 'pnpm' 49 | 50 | - name: Install Dependencies 51 | run: pnpm install 52 | 53 | - name: Publish to npm 54 | env: 55 | NPM_TOKEN: ${{ secrets.RSLIB_NPM_TOKEN }} 56 | run: | 57 | npm config set "//registry.npmjs.org/:_authToken" "${NPM_TOKEN}" 58 | pnpm -r publish --tag ${{ github.event.inputs.npm_tag }} --publish-branch ${{ github.event.inputs.branch }} 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log* 3 | *.cpuprofile 4 | node_modules/ 5 | 6 | dist/ 7 | dist-* 8 | compiled/ 9 | coverage/ 10 | doc_build/ 11 | playwright-report/ 12 | tsconfig.tsbuildinfo 13 | .swc 14 | 15 | # CSS Modules dist files 16 | *.css.d.ts 17 | *.less.d.ts 18 | *.scss.d.ts 19 | 20 | # Test temp files 21 | test-temp-* 22 | 23 | .vscode/**/* 24 | !.vscode/settings.json 25 | !.vscode/extensions.json 26 | .idea/ 27 | .nx/ 28 | .history/ 29 | .env.local 30 | .env.*.local 31 | 32 | # ignore directory for artifacts produced by tests 33 | test-results/ 34 | fixtures-test/ 35 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry = 'https://registry.npmjs.org/' 2 | strict-peer-dependencies=false 3 | auto-install-peers=false 4 | hoist-pattern[]=[] 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v22 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | dist 3 | compiled 4 | doc_build 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "biomejs.biome", 4 | "esbenp.prettier-vscode", 5 | "unifiedjs.vscode-mdx" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.useIgnoreFiles": true, 3 | "search.exclude": { 4 | "**/.git": true, 5 | "**/dist": true, 6 | "**/dist-*": true, 7 | "**/coverage": true, 8 | "**/compiled": true, 9 | "**/doc_build": true, 10 | "**/node_modules": true, 11 | "**/tsconfig.tsbuildinfo": true 12 | }, 13 | "files.exclude": { 14 | "**/.DS_Store": true 15 | }, 16 | "editor.codeActionsOnSave": { 17 | "source.organizeImports.biome": "explicit" 18 | }, 19 | "[javascript]": { 20 | "editor.defaultFormatter": "biomejs.biome" 21 | }, 22 | "[javascriptreact]": { 23 | "editor.defaultFormatter": "biomejs.biome" 24 | }, 25 | "[json5]": { 26 | "editor.defaultFormatter": "esbenp.prettier-vscode" 27 | }, 28 | "[json]": { 29 | "editor.defaultFormatter": "esbenp.prettier-vscode" 30 | }, 31 | "[jsonc]": { 32 | "editor.defaultFormatter": "esbenp.prettier-vscode" 33 | }, 34 | "[mdx]": { 35 | "editor.defaultFormatter": "esbenp.prettier-vscode" 36 | }, 37 | "[md]": { 38 | "editor.defaultFormatter": "esbenp.prettier-vscode" 39 | }, 40 | "[typescript]": { 41 | "editor.defaultFormatter": "biomejs.biome" 42 | }, 43 | "[typescriptreact]": { 44 | "editor.defaultFormatter": "biomejs.biome" 45 | }, 46 | "[css]": { 47 | "editor.defaultFormatter": "esbenp.prettier-vscode" 48 | }, 49 | "[sass]": { 50 | "editor.defaultFormatter": "esbenp.prettier-vscode" 51 | }, 52 | "[less]": { 53 | "editor.defaultFormatter": "esbenp.prettier-vscode" 54 | }, 55 | "typescript.tsdk": "node_modules/typescript/lib" 56 | } 57 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Rstest contributing guide 2 | 3 | Thanks for that you are interested in contributing to Rstest. Before starting your contribution, please take a moment to read the following guidelines. 4 | 5 | ## Install Node.js 6 | 7 | Use [fnm](https://github.com/Schniz/fnm) or [nvm](https://github.com/nvm-sh/nvm) to run the command below. This will switch to the Node.js version specified in the project's `.nvmrc` file. 8 | 9 | ```bash 10 | # with fnm 11 | fnm use 12 | 13 | # with nvm 14 | nvm use 15 | ``` 16 | 17 | ## Install dependencies 18 | 19 | Enable [pnpm](https://pnpm.io/) with corepack: 20 | 21 | ```bash 22 | corepack enable 23 | ``` 24 | 25 | Install dependencies: 26 | 27 | ```bash 28 | pnpm install 29 | ``` 30 | 31 | What this will do: 32 | 33 | - Install all dependencies. 34 | - Create symlinks between packages in the monorepo 35 | - Run the prepare script to build all packages, powered by [nx](https://nx.dev/). 36 | 37 | ## Making changes and building 38 | 39 | Once you have set up the local development environment in your forked repository, we can start development. 40 | 41 | ### Checkout a new branch 42 | 43 | It is recommended to develop on a new branch, as it will make things easier later when you submit a pull request: 44 | 45 | ```sh 46 | git checkout -b MY_BRANCH_NAME 47 | ``` 48 | 49 | ### Build the package 50 | 51 | Use [nx build](https://nx.dev/nx-api/nx/documents/run) to build the package you want to change: 52 | 53 | ```sh 54 | npx nx build @rstest/core 55 | ``` 56 | 57 | Build all packages: 58 | 59 | ```sh 60 | pnpm run build 61 | ``` 62 | 63 | You can also use the watch mode to automatically rebuild the package when you make changes: 64 | 65 | ```sh 66 | npx nx build @rstest/core --watch 67 | ``` 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present Bytedance, Inc. and its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | ## Reporting a vulnerability in Rstest 4 | 5 | Report a security vulnerability in Rstest via web-infra-security@bytedance.com. 6 | 7 | Normally, your report will be acknowledged within 24 hours, and you'll receive a more detailed response to your report within 5 days indicating the next steps in handling your submission. 8 | 9 | After the initial reply to your report, the security team will endeavor to keep you informed of the progress being made towards a fix and full announcement, and may ask for additional information or guidance surrounding the reported issue. 10 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "organizeImports": { 4 | "enabled": true, 5 | "include": [ 6 | "./**/*.js", 7 | "./**/*.jsx", 8 | "./**/*.ts", 9 | "./**/*.tsx", 10 | "./**/*.mjs", 11 | "./**/*.cjs" 12 | ] 13 | }, 14 | "vcs": { 15 | "enabled": true, 16 | "defaultBranch": "main", 17 | "clientKind": "git", 18 | "useIgnoreFile": true 19 | }, 20 | "files": { 21 | "ignoreUnknown": true 22 | }, 23 | "formatter": { 24 | "ignore": [], 25 | "indentStyle": "space" 26 | }, 27 | "javascript": { 28 | "formatter": { 29 | "quoteStyle": "single" 30 | }, 31 | "jsxRuntime": "reactClassic" 32 | }, 33 | "json": { 34 | "formatter": { 35 | "enabled": false 36 | } 37 | }, 38 | "css": { 39 | "formatter": { 40 | "enabled": false 41 | } 42 | }, 43 | "linter": { 44 | "enabled": true, 45 | "rules": { 46 | "recommended": true, 47 | "style": { 48 | "noNonNullAssertion": "off", 49 | "useFilenamingConvention": { 50 | "level": "error", 51 | "options": { 52 | "filenameCases": ["camelCase", "PascalCase", "export"] 53 | } 54 | } 55 | }, 56 | "suspicious": { 57 | "noExplicitAny": "off", 58 | "noConfusingVoidType": "off" 59 | }, 60 | "performance": { 61 | "noDelete": "off" 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /cspell.config.cjs: -------------------------------------------------------------------------------- 1 | const { banWords } = require('cspell-ban-words'); 2 | 3 | module.exports = { 4 | version: '0.2', 5 | language: 'en', 6 | files: ['**/*.{ts,tsx,js,jsx,md,mdx}'], 7 | enableFiletypes: ['mdx'], 8 | ignoreRegExpList: [ 9 | // ignore markdown anchors such as [modifyRsbuildConfig](#modifyrsbuildconfig) 10 | '#.*?\\)', 11 | ], 12 | ignorePaths: [ 13 | 'dist', 14 | 'dist-*', 15 | 'compiled', 16 | 'coverage', 17 | 'doc_build', 18 | 'node_modules', 19 | 'pnpm-lock.yaml', 20 | ], 21 | flagWords: banWords, 22 | dictionaries: ['dictionary'], 23 | dictionaryDefinitions: [ 24 | { 25 | name: 'dictionary', 26 | path: './scripts/dictionary.txt', 27 | addWords: true, 28 | }, 29 | ], 30 | }; 31 | -------------------------------------------------------------------------------- /examples/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@examples/node", 3 | "private": true, 4 | "type": "commonjs", 5 | "scripts": { 6 | "build": "rsbuild build", 7 | "test": "rstest run", 8 | "test:watch": "rstest" 9 | }, 10 | "devDependencies": { 11 | "@rstest/core": "workspace:*", 12 | "@types/node": "^22.13.8", 13 | "typescript": "^5.8.3" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/node/rstest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@rstest/core'; 2 | 3 | export default defineConfig({}); 4 | -------------------------------------------------------------------------------- /examples/node/src/index.ts: -------------------------------------------------------------------------------- 1 | export const sayHi = () => 'hi'; 2 | -------------------------------------------------------------------------------- /examples/node/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@rstest/core'; 2 | import { sayHi } from '../src/index'; 3 | 4 | describe('Index', () => { 5 | it('should add two numbers correctly', () => { 6 | expect(1 + 1).toBe(2); 7 | }); 8 | 9 | it('should test source code correctly', () => { 10 | expect(sayHi()).toBe('hi'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /examples/node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["ES2020"], 5 | "module": "ESNext", 6 | "strict": true, 7 | "skipLibCheck": true, 8 | "isolatedModules": true, 9 | "resolveJsonModule": true, 10 | "moduleResolution": "bundler", 11 | "useDefineForClassFields": true 12 | }, 13 | "include": ["src"] 14 | } 15 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 3 | "namedInputs": { 4 | "default": ["{projectRoot}/src/**/*"], 5 | "build": [ 6 | "default", 7 | "!{projectRoot}/**/*.{md,mdx}", 8 | "{projectRoot}/tsconfig.json", 9 | "{projectRoot}/package.json", 10 | "{projectRoot}/scripts/**/*" 11 | ], 12 | "prebundle": [ 13 | "{projectRoot}/package.json", 14 | "{projectRoot}/prebundle.config.mjs" 15 | ] 16 | }, 17 | "targetDefaults": { 18 | "build": { 19 | "cache": true, 20 | "dependsOn": ["^build", "prebundle"], 21 | "inputs": ["build", "^build"], 22 | "outputs": ["{projectRoot}/dist", "{projectRoot}/dist-types"] 23 | }, 24 | "prebundle": { 25 | "cache": true, 26 | "inputs": ["prebundle"], 27 | "outputs": ["{projectRoot}/compiled"] 28 | } 29 | }, 30 | "defaultBase": "main" 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present Bytedance, Inc. and its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | 2 | Rstest Banner 3 | 4 | 5 | # Rstest 6 | 7 | Rstest is a testing framework powered by Rspack. 8 | 9 | ## 📖 License 10 | 11 | Rstest is licensed under the [MIT License](https://github.com/web-infra-dev/rstest/blob/main/LICENSE). 12 | -------------------------------------------------------------------------------- /packages/core/bin/rstest.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import nodeModule from 'node:module'; 3 | 4 | // enable on-disk code caching of all modules loaded by Node.js 5 | // requires Nodejs >= 22.8.0 6 | const { enableCompileCache } = nodeModule; 7 | if (enableCompileCache) { 8 | try { 9 | enableCompileCache(); 10 | } catch { 11 | // ignore errors 12 | } 13 | } 14 | 15 | async function main() { 16 | const { runCLI } = await import('../dist/cli.js'); 17 | runCLI(); 18 | } 19 | 20 | main(); 21 | -------------------------------------------------------------------------------- /packages/core/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | const test: typeof import('@rstest/core')['test']; 3 | const describe: typeof import('@rstest/core')['describe']; 4 | const it: typeof import('@rstest/core')['it']; 5 | const expect: typeof import('@rstest/core')['expect']; 6 | const assert: typeof import('@rstest/core')['assert']; 7 | const beforeAll: typeof import('@rstest/core')['beforeAll']; 8 | const afterAll: typeof import('@rstest/core')['afterAll']; 9 | const beforeEach: typeof import('@rstest/core')['beforeEach']; 10 | const afterEach: typeof import('@rstest/core')['afterEach']; 11 | const rstest: typeof import('@rstest/core')['rstest']; 12 | } 13 | export {}; 14 | -------------------------------------------------------------------------------- /packages/core/importMeta.d.ts: -------------------------------------------------------------------------------- 1 | interface ImportMeta { 2 | readonly rstest?: typeof import('@rstest/core'); 3 | } 4 | -------------------------------------------------------------------------------- /packages/core/rslib.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@rslib/core'; 2 | 3 | export default defineConfig({ 4 | lib: [ 5 | { 6 | format: 'esm', 7 | syntax: ['node 16'], 8 | dts: { 9 | bundle: true, 10 | distPath: './dist-types', 11 | }, 12 | output: { 13 | minify: { 14 | jsOptions: { 15 | minimizerOptions: { 16 | mangle: false, 17 | minify: false, 18 | compress: { 19 | defaults: false, 20 | unused: true, 21 | dead_code: true, 22 | toplevel: true, 23 | // fix `Couldn't infer stack frame for inline snapshot` error 24 | // should keep function name used to filter stack trace 25 | keep_fnames: true, 26 | }, 27 | format: { 28 | comments: 'some', 29 | preserve_annotations: true, 30 | }, 31 | }, 32 | }, 33 | }, 34 | }, 35 | source: { 36 | entry: { 37 | public: './src/public.ts', 38 | node: './src/node.ts', 39 | cli: './src/cli/index.ts', 40 | worker: './src/runtime/worker/index.ts', 41 | }, 42 | define: { 43 | RSTEST_VERSION: JSON.stringify(require('./package.json').version), 44 | }, 45 | }, 46 | }, 47 | ], 48 | tools: { 49 | rspack: { 50 | watchOptions: { 51 | ignored: /\.git/, 52 | }, 53 | }, 54 | }, 55 | }); 56 | -------------------------------------------------------------------------------- /packages/core/src/cli/index.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '../utils/logger'; 2 | import { setupCommands } from './commands'; 3 | import { prepareCli } from './prepare'; 4 | 5 | export async function runCLI(): Promise { 6 | prepareCli(); 7 | 8 | try { 9 | setupCommands(); 10 | } catch (err) { 11 | logger.error('Failed to start Rstest CLI.'); 12 | logger.error(err); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/cli/prepare.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '../utils/logger'; 2 | 3 | function initNodeEnv() { 4 | if (!process.env.NODE_ENV) { 5 | process.env.NODE_ENV = 'test'; 6 | } 7 | } 8 | 9 | export function prepareCli(): void { 10 | initNodeEnv(); 11 | 12 | // Print a blank line to keep the greet log nice. 13 | // Some package managers automatically output a blank line, some do not. 14 | const { npm_execpath } = process.env; 15 | if ( 16 | !npm_execpath || 17 | npm_execpath.includes('npx-cli.js') || 18 | npm_execpath.includes('.bun') 19 | ) { 20 | console.log(); 21 | } 22 | } 23 | 24 | export function showRstest(): void { 25 | logger.greet(` ${`Rstest v${RSTEST_VERSION}`}`); 26 | logger.log(''); 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/core/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ListCommandOptions, 3 | RstestCommand, 4 | RstestConfig, 5 | RstestInstance, 6 | } from '../types'; 7 | import { createContext } from './context'; 8 | 9 | export function createRstest( 10 | config: RstestConfig, 11 | command: RstestCommand, 12 | fileFilters: string[], 13 | ): RstestInstance { 14 | const context = createContext({ cwd: process.cwd(), command }, config); 15 | 16 | const runTests = async (): Promise => { 17 | const { runTests } = await import('./runTests'); 18 | await runTests(context, fileFilters); 19 | }; 20 | 21 | const listTests = async (options: ListCommandOptions): Promise => { 22 | const { listTests } = await import('./listTests'); 23 | await listTests(context, fileFilters, options); 24 | }; 25 | 26 | return { 27 | context, 28 | runTests, 29 | listTests, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/src/core/plugins/ignoreResolveError.ts: -------------------------------------------------------------------------------- 1 | import type { RsbuildPlugin, Rspack } from '@rsbuild/core'; 2 | 3 | class IgnoreModuleNotFoundErrorPlugin { 4 | apply(compiler: Rspack.Compiler) { 5 | compiler.hooks.done.tap('Rstest:IgnoreModuleNotFoundPlugin', (stats) => { 6 | stats.compilation.errors = stats.compilation.errors.filter((error) => { 7 | if (/Module not found/.test(error.message)) { 8 | return false; 9 | } 10 | return true; 11 | }); 12 | }); 13 | } 14 | } 15 | 16 | /** 17 | * Module not found errors should be silent at build, and throw errors at runtime 18 | */ 19 | export const pluginIgnoreResolveError: RsbuildPlugin = { 20 | name: 'rstest:ignore-resolve-error', 21 | setup: (api) => { 22 | api.modifyRspackConfig(async (config) => { 23 | config.plugins!.push(new IgnoreModuleNotFoundErrorPlugin()); 24 | config.optimization ??= {}; 25 | config.optimization.emitOnErrors = true; 26 | 27 | config.ignoreWarnings ??= []; 28 | config.ignoreWarnings.push(/Module not found/); 29 | }); 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /packages/core/src/env.d.ts: -------------------------------------------------------------------------------- 1 | declare const RSTEST_VERSION: string; 2 | declare const RSTEST_SELF_CI: boolean; 3 | -------------------------------------------------------------------------------- /packages/core/src/node.ts: -------------------------------------------------------------------------------- 1 | export type { RstestConfig, Reporter, RstestCommand } from './types'; 2 | -------------------------------------------------------------------------------- /packages/core/src/public.ts: -------------------------------------------------------------------------------- 1 | import type { RstestConfig } from './types'; 2 | 3 | export * from './runtime/api/public'; 4 | 5 | export type { RstestConfig }; 6 | 7 | export type RstestConfigAsyncFn = () => Promise; 8 | 9 | export type RstestConfigSyncFn = () => RstestConfig; 10 | 11 | export type RstestConfigExport = 12 | | RstestConfig 13 | | RstestConfigSyncFn 14 | | RstestConfigAsyncFn; 15 | 16 | /** 17 | * This function helps you to autocomplete configuration types. 18 | * It accepts a Rsbuild config object, or a function that returns a config. 19 | */ 20 | export function defineConfig(config: RstestConfig): RstestConfig; 21 | export function defineConfig(config: RstestConfigSyncFn): RstestConfigSyncFn; 22 | export function defineConfig(config: RstestConfigAsyncFn): RstestConfigAsyncFn; 23 | export function defineConfig(config: RstestConfigExport): RstestConfigExport; 24 | export function defineConfig(config: RstestConfigExport) { 25 | return config; 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/src/reporter/statusRenderer.ts: -------------------------------------------------------------------------------- 1 | import { relative } from 'pathe'; 2 | import { color, prettyTestPath } from '../utils'; 3 | import { WindowRenderer } from './windowedRenderer'; 4 | export class StatusRenderer { 5 | private rootPath: string; 6 | private renderer: WindowRenderer; 7 | private runningModules = new Set(); 8 | 9 | constructor(rootPath: string) { 10 | this.rootPath = rootPath; 11 | this.renderer = new WindowRenderer({ 12 | getWindow: () => this.getContent(), 13 | logger: { 14 | outputStream: process.stdout, 15 | errorStream: process.stderr, 16 | getColumns: () => { 17 | return 'columns' in process.stdout ? process.stdout.columns : 80; 18 | }, 19 | }, 20 | }); 21 | } 22 | 23 | getContent(): string[] { 24 | const summary = []; 25 | for (const module of this.runningModules) { 26 | const relativePath = relative(this.rootPath, module); 27 | summary.push( 28 | `${color.bgYellow(color.bold(' RUNS '))} ${prettyTestPath(relativePath)}`, 29 | ); 30 | } 31 | summary.push(''); 32 | return summary; 33 | } 34 | 35 | addRunningModule(testPath: string): void { 36 | this.runningModules.add(testPath); 37 | this.renderer?.schedule(); 38 | } 39 | 40 | removeRunningModule(testPath: string): void { 41 | this.runningModules.delete(testPath); 42 | this.renderer?.schedule(); 43 | } 44 | 45 | clear(): void { 46 | this.runningModules.clear(); 47 | this.renderer?.finish(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/core/src/runtime/api/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Rstest, 3 | RstestExpect, 4 | RunnerHooks, 5 | Test, 6 | TestCase, 7 | TestFileResult, 8 | WorkerState, 9 | } from '../../types'; 10 | import { createRunner } from '../runner'; 11 | import { assert, GLOBAL_EXPECT, createExpect } from './expect'; 12 | import { createRstestUtilities } from './utilities'; 13 | 14 | export const createRstestRuntime = ( 15 | workerState: WorkerState, 16 | ): { 17 | runner: { 18 | runTests: ( 19 | testPath: string, 20 | hooks: RunnerHooks, 21 | api: Rstest, 22 | ) => Promise; 23 | collectTests: () => Promise; 24 | getCurrentTest: () => TestCase | undefined; 25 | }; 26 | api: Rstest; 27 | } => { 28 | const { runner, api: runnerAPI } = createRunner({ workerState }); 29 | 30 | const expect: RstestExpect = createExpect({ 31 | workerState, 32 | getCurrentTest: () => runner.getCurrentTest(), 33 | }); 34 | 35 | Object.defineProperty(globalThis, GLOBAL_EXPECT, { 36 | value: expect, 37 | writable: true, 38 | configurable: true, 39 | }); 40 | 41 | return { 42 | runner, 43 | api: { 44 | ...runnerAPI, 45 | expect, 46 | assert, 47 | rstest: createRstestUtilities(), 48 | }, 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /packages/core/src/runtime/api/public.ts: -------------------------------------------------------------------------------- 1 | import type { Rstest, RstestUtilities } from '../../types'; 2 | export type { Assertion } from '../../types/expect'; 3 | 4 | export declare const expect: Rstest['expect']; 5 | export declare const assert: Rstest['assert']; 6 | export declare const it: Rstest['it']; 7 | export declare const test: Rstest['test']; 8 | export declare const describe: Rstest['describe']; 9 | export declare const beforeAll: Rstest['beforeAll']; 10 | export declare const afterAll: Rstest['afterAll']; 11 | export declare const beforeEach: Rstest['beforeEach']; 12 | export declare const afterEach: Rstest['afterEach']; 13 | export declare const rstest: RstestUtilities; 14 | -------------------------------------------------------------------------------- /packages/core/src/runtime/runner/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Rstest, 3 | RunnerAPI, 4 | RunnerHooks, 5 | Test, 6 | TestFileResult, 7 | WorkerState, 8 | } from '../../types'; 9 | 10 | import { TestRunner } from './runner'; 11 | import { createRuntimeAPI } from './runtime'; 12 | import { traverseUpdateTest } from './task'; 13 | 14 | export function createRunner({ workerState }: { workerState: WorkerState }): { 15 | api: RunnerAPI; 16 | runner: { 17 | runTests: ( 18 | testFilePath: string, 19 | hooks: RunnerHooks, 20 | api: Rstest, 21 | ) => Promise; 22 | collectTests: () => Promise; 23 | getCurrentTest: TestRunner['getCurrentTest']; 24 | }; 25 | } { 26 | const { 27 | testPath, 28 | runtimeConfig: { testTimeout, testNamePattern }, 29 | } = workerState; 30 | const runtime = createRuntimeAPI({ 31 | testPath, 32 | testTimeout, 33 | }); 34 | const testRunner: TestRunner = new TestRunner(); 35 | 36 | return { 37 | api: runtime.api, 38 | runner: { 39 | runTests: async (testPath: string, hooks: RunnerHooks, api: Rstest) => { 40 | const tests = await runtime.instance.getTests(); 41 | traverseUpdateTest(tests, testNamePattern); 42 | 43 | const results = await testRunner.runTests({ 44 | tests, 45 | testPath, 46 | state: workerState, 47 | hooks, 48 | api, 49 | }); 50 | 51 | hooks.onTestFileResult?.(results); 52 | 53 | return results; 54 | }, 55 | collectTests: async () => { 56 | const tests = await runtime.instance.getTests(); 57 | traverseUpdateTest(tests, testNamePattern); 58 | 59 | return tests; 60 | }, 61 | getCurrentTest: () => testRunner.getCurrentTest(), 62 | }, 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /packages/core/src/runtime/worker/rpc.ts: -------------------------------------------------------------------------------- 1 | import v8 from 'node:v8'; 2 | import { type BirpcOptions, type BirpcReturn, createBirpc } from 'birpc'; 3 | import type { TinypoolWorkerMessage } from 'tinypool'; 4 | import type { RuntimeRPC, ServerRPC } from '../../types'; 5 | 6 | export type WorkerRPC = BirpcReturn; 7 | 8 | const processSend = process.send!.bind(process); 9 | const processOn = process.on!.bind(process); 10 | const processOff = process.off!.bind(process); 11 | const dispose: (() => void)[] = []; 12 | 13 | export type WorkerRpcOptions = Pick< 14 | BirpcOptions, 15 | 'on' | 'post' | 'serialize' | 'deserialize' 16 | >; 17 | 18 | export function createForksRpcOptions( 19 | nodeV8: typeof import('v8') = v8, 20 | ): WorkerRpcOptions { 21 | return { 22 | serialize: nodeV8.serialize, 23 | deserialize: (v) => nodeV8.deserialize(Buffer.from(v)), 24 | post(v) { 25 | processSend(v); 26 | }, 27 | on(fn) { 28 | const handler = (message: any, ...extras: any) => { 29 | // Do not react on Tinypool's internal messaging 30 | if ((message as TinypoolWorkerMessage)?.__tinypool_worker_message__) { 31 | return; 32 | } 33 | return fn(message, ...extras); 34 | }; 35 | processOn('message', handler); 36 | dispose.push(() => processOff('message', handler)); 37 | }, 38 | }; 39 | } 40 | 41 | export function createRuntimeRpc( 42 | options: Pick< 43 | BirpcOptions, 44 | 'on' | 'post' | 'serialize' | 'deserialize' 45 | >, 46 | ): { rpc: WorkerRPC } { 47 | const rpc = createBirpc({}, options); 48 | 49 | return { 50 | rpc, 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /packages/core/src/runtime/worker/setup.ts: -------------------------------------------------------------------------------- 1 | const gracefulExit: boolean = process.execArgv.some( 2 | (execArg) => 3 | execArg.startsWith('--perf') || 4 | execArg.startsWith('--prof') || 5 | execArg.startsWith('--cpu-prof') || 6 | execArg.startsWith('--heap-prof') || 7 | execArg.startsWith('--diagnostic-dir'), 8 | ); 9 | 10 | if (gracefulExit) { 11 | // gracefully handle SIGTERM to generate CPU profile 12 | // https://github.com/nodejs/node/issues/55094 13 | process.on('SIGTERM', () => { 14 | process.exit(); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/src/runtime/worker/snapshot.ts: -------------------------------------------------------------------------------- 1 | import { NodeSnapshotEnvironment } from '@vitest/snapshot/environment'; 2 | 3 | export class RstestSnapshotEnvironment extends NodeSnapshotEnvironment {} 4 | -------------------------------------------------------------------------------- /packages/core/src/types/core.ts: -------------------------------------------------------------------------------- 1 | import type { SnapshotManager } from '@vitest/snapshot/manager'; 2 | import type { NormalizedConfig, RstestConfig } from './config'; 3 | import type { Reporter } from './reporter'; 4 | 5 | export type RstestCommand = 'watch' | 'run' | 'list'; 6 | 7 | export type RstestContext = { 8 | /** The Rstest core version. */ 9 | version: string; 10 | /** The root path of current project. */ 11 | rootPath: string; 12 | /** The original Rstest config passed from the createRstest method. */ 13 | originalConfig: Readonly; 14 | /** The normalized Rstest config. */ 15 | normalizedConfig: NormalizedConfig; 16 | /** 17 | * The command type. 18 | * 19 | * - dev: `rstest dev` 20 | * - run: `rstest run` 21 | */ 22 | command: RstestCommand; 23 | reporters: Reporter[]; 24 | snapshotManager: SnapshotManager; 25 | }; 26 | 27 | export type ListCommandOptions = { 28 | filesOnly?: boolean; 29 | json?: boolean | string; 30 | }; 31 | 32 | export type RstestInstance = { 33 | context: RstestContext; 34 | runTests: () => Promise; 35 | listTests: (options: ListCommandOptions) => Promise; 36 | }; 37 | -------------------------------------------------------------------------------- /packages/core/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export type * from './config'; 2 | export type * from './core'; 3 | export type * from './worker'; 4 | export type * from './testSuite'; 5 | export type * from './api'; 6 | export type * from './expect'; 7 | export type * from './mock'; 8 | export type * from './runner'; 9 | export type * from './reporter'; 10 | export type * from './utils'; 11 | -------------------------------------------------------------------------------- /packages/core/src/types/runner.ts: -------------------------------------------------------------------------------- 1 | import type { TestFileInfo, TestFileResult, TestResult } from './testSuite'; 2 | 3 | export type RunnerHooks = { 4 | /** 5 | * Called before test file run. 6 | */ 7 | onTestFileStart?: (test: TestFileInfo) => Promise; 8 | /** 9 | * Called after the test file is finished running. 10 | */ 11 | onTestFileResult: (test: TestFileResult) => Promise; 12 | /** 13 | * Called after the test is finished running. 14 | */ 15 | onTestCaseResult?: (result: TestResult) => Promise; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/core/src/types/utils.ts: -------------------------------------------------------------------------------- 1 | export type MaybePromise = T | Promise; 2 | 3 | export type FunctionLike = (...args: any) => any; 4 | 5 | /** The test file output path */ 6 | export type DistPath = string; 7 | /** The test file original path */ 8 | export type TestPath = string; 9 | -------------------------------------------------------------------------------- /packages/core/src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | import type { Rstest } from '../types'; 2 | 3 | export const DEFAULT_CONFIG_NAME = 'rstest.config'; 4 | 5 | export const TEST_DELIMITER = '>'; 6 | 7 | export const ROOT_SUITE_NAME = 'Rstest:_internal_root_suite'; 8 | 9 | export const TEMP_RSTEST_OUTPUT_DIR = 'dist/.rstest-temp'; 10 | 11 | export const TEMP_RSTEST_OUTPUT_DIR_GLOB = '**/dist/.rstest-temp'; 12 | 13 | export const DEFAULT_CONFIG_EXTENSIONS = [ 14 | '.js', 15 | '.ts', 16 | '.mjs', 17 | '.mts', 18 | '.cjs', 19 | '.cts', 20 | ] as const; 21 | 22 | export const globalApis: (keyof Rstest)[] = [ 23 | 'test', 24 | 'describe', 25 | 'it', 26 | 'expect', 27 | 'afterAll', 28 | 'afterEach', 29 | 'beforeAll', 30 | 'beforeEach', 31 | 'rstest', 32 | 'assert', 33 | ]; 34 | -------------------------------------------------------------------------------- /packages/core/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './helper'; 2 | export * from './logger'; 3 | export * from './testFiles'; 4 | export * from './constants'; 5 | -------------------------------------------------------------------------------- /packages/core/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Logging message case convention: 3 | * 4 | * Info, ready, success and debug messages: 5 | * - Start with lowercase 6 | * - Example: "info build started..." 7 | * 8 | * Errors and warnings: 9 | * - Start with uppercase 10 | * - Example: "error Failed to build" 11 | * 12 | * This convention helps distinguish between normal operations 13 | * and important alerts that require attention. 14 | */ 15 | import { type Logger, logger } from 'rslog'; 16 | import { color } from './helper'; 17 | 18 | export const isDebug = (): boolean => { 19 | if (!process.env.DEBUG) { 20 | return false; 21 | } 22 | 23 | const values = process.env.DEBUG.toLocaleLowerCase().split(','); 24 | return ['rstest', 'rsbuild', 'builder', '*'].some((key) => 25 | values.includes(key), 26 | ); 27 | }; 28 | 29 | // setup the logger level 30 | if (isDebug()) { 31 | logger.level = 'verbose'; 32 | } 33 | 34 | function getTime() { 35 | const now = new Date(); 36 | const hours = String(now.getHours()).padStart(2, '0'); 37 | const minutes = String(now.getMinutes()).padStart(2, '0'); 38 | const seconds = String(now.getSeconds()).padStart(2, '0'); 39 | 40 | return `${hours}:${minutes}:${seconds}`; 41 | } 42 | 43 | logger.override({ 44 | debug: (message, ...args) => { 45 | if (logger.level !== 'verbose') { 46 | return; 47 | } 48 | const time = color.gray(`${getTime()}`); 49 | console.log(` ${color.magenta('rstest')} ${time} ${message}`, ...args); 50 | }, 51 | }); 52 | 53 | export { logger }; 54 | export type { Logger }; 55 | -------------------------------------------------------------------------------- /packages/core/tests/core/rsbuild.test.ts: -------------------------------------------------------------------------------- 1 | import { prepareRsbuild } from '../../src/core/rsbuild'; 2 | import type { RstestContext } from '../../src/types'; 3 | 4 | describe('prepareRsbuild', () => { 5 | it('should generate rspack config correctly', async () => { 6 | const rsbuildInstance = await prepareRsbuild( 7 | { 8 | normalizedConfig: { 9 | name: 'test', 10 | plugins: [], 11 | resolve: {}, 12 | source: {}, 13 | output: {}, 14 | tools: {}, 15 | }, 16 | } as unknown as RstestContext, 17 | async () => ({}), 18 | {}, 19 | ); 20 | expect(rsbuildInstance).toBeDefined(); 21 | const { 22 | origin: { bundlerConfigs }, 23 | } = await rsbuildInstance.inspectConfig(); 24 | 25 | expect(bundlerConfigs[0]).toMatchSnapshot(); 26 | }); 27 | 28 | it('should generate swc config correctly with user customize', async () => { 29 | const rsbuildInstance = await prepareRsbuild( 30 | { 31 | normalizedConfig: { 32 | name: 'test', 33 | plugins: [], 34 | resolve: {}, 35 | source: { 36 | decorators: { 37 | version: 'legacy', 38 | }, 39 | include: [/node_modules[\\/]query-string[\\/]/], 40 | }, 41 | output: {}, 42 | tools: {}, 43 | }, 44 | } as unknown as RstestContext, 45 | async () => ({}), 46 | {}, 47 | ); 48 | expect(rsbuildInstance).toBeDefined(); 49 | const { 50 | origin: { bundlerConfigs }, 51 | } = await rsbuildInstance.inspectConfig(); 52 | 53 | expect( 54 | bundlerConfigs[0]?.module?.rules?.filter( 55 | (rule) => 56 | rule && 57 | typeof rule === 'object' && 58 | rule.test && 59 | rule.test instanceof RegExp && 60 | rule.test.test('a.js'), 61 | ), 62 | ).toMatchSnapshot(); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /packages/core/tests/runner/util.test.ts: -------------------------------------------------------------------------------- 1 | import { formatName } from '../../src/runtime/util'; 2 | 3 | it('test formatName', () => { 4 | expect(formatName('test index %#', [1, 2, 3], 1)).toBe('test index 1'); 5 | 6 | expect(formatName('test %i + %i -> %i', [1, 2, 3], 0)).toBe( 7 | 'test 1 + 2 -> 3', 8 | ); 9 | 10 | expect(formatName('test $a', { a: 1 }, 0)).toBe('test 1'); 11 | 12 | expect(formatName('test $a.b', { a: { b: 1 } }, 0)).toBe('test 1'); 13 | 14 | expect(formatName('test $c', { a: { b: 1 } }, 0)).toBe('test undefined'); 15 | 16 | expect(formatName('%j', { a: { b: 1 } }, 0)).toBe('{"a":{"b":1}}'); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/core/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@rstest/tsconfig/base", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "types": ["@rstest/core/globals"] 6 | }, 7 | "include": ["./**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/tests/utils/helper.test.ts: -------------------------------------------------------------------------------- 1 | import { sep } from 'node:path'; 2 | import { parsePosix } from '../../src/utils/helper'; 3 | 4 | it('parsePosix correctly', () => { 5 | const splitPaths = ['packages', 'core', 'tests', 'index.test.ts']; 6 | 7 | expect(parsePosix(splitPaths.join(sep))).toEqual({ 8 | dir: 'packages/core/tests', 9 | base: 'index.test.ts', 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/core/tests/utils/testFiles.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { filterFiles } from '../../src/utils/testFiles'; 3 | 4 | describe('test filterFiles', () => { 5 | it('should filter files correctly', () => { 6 | const testFiles = ['index.test.ts', 'index1.test.ts', 'index2.test.ts'].map( 7 | (filename) => path.join(__dirname, filename), 8 | ); 9 | 10 | expect(filterFiles(testFiles, ['index.test.ts'], __dirname)).toEqual([ 11 | testFiles[0], 12 | ]); 13 | 14 | expect( 15 | filterFiles( 16 | testFiles, 17 | [path.join(__dirname, 'index.test.ts')], 18 | __dirname, 19 | ), 20 | ).toEqual([testFiles[0]]); 21 | 22 | expect(filterFiles(testFiles, ['index'], __dirname)).toEqual(testFiles); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@rstest/tsconfig/base", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "baseUrl": "./", 6 | "rootDir": "src", 7 | "declarationDir": "./dist-types", 8 | "composite": true, 9 | "isolatedDeclarations": true 10 | }, 11 | "include": ["src"] 12 | } 13 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'website' 3 | - 'scripts/**' 4 | - 'packages/**' 5 | - 'examples/**' 6 | - 'tests/**' 7 | -------------------------------------------------------------------------------- /rstest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@rstest/core'; 2 | 3 | export default defineConfig({ 4 | include: ['packages/**/tests/**/*.test.ts'], 5 | globals: true, 6 | setupFiles: ['./scripts/rstest.setup.ts'], 7 | }); 8 | -------------------------------------------------------------------------------- /scripts/rstest.setup.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { expect } from '@rstest/core'; 3 | import { createSnapshotSerializer } from 'path-serializer'; 4 | 5 | expect.addSnapshotSerializer( 6 | createSnapshotSerializer({ 7 | workspace: path.join(__dirname, '..'), 8 | }), 9 | ); 10 | -------------------------------------------------------------------------------- /scripts/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "exactOptionalPropertyTypes": false, 4 | "target": "ES2021", 5 | "lib": ["DOM", "ESNext"], 6 | "allowJs": false, 7 | "checkJs": false, 8 | "module": "ES2020", 9 | "strict": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "jsx": "preserve", 16 | "resolveJsonModule": true, 17 | "moduleResolution": "Bundler", 18 | "useDefineForClassFields": true, 19 | "noPropertyAccessFromIndexSignature": false, 20 | "allowUnusedLabels": false, 21 | "allowUnreachableCode": false, 22 | "noFallthroughCasesInSwitch": true, 23 | "noImplicitOverride": true, 24 | "noImplicitReturns": true, 25 | "noUncheckedIndexedAccess": true 26 | }, 27 | "$schema": "https://json.schemastore.org/tsconfig", 28 | "display": "Base" 29 | } 30 | -------------------------------------------------------------------------------- /scripts/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rstest/tsconfig", 3 | "version": "0.0.1", 4 | "private": true, 5 | "type": "module" 6 | } 7 | -------------------------------------------------------------------------------- /tests/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-infra-dev/rstest/046266ebc142de8df3a12a4553411c08b2a52f9b/tests/assets/icon.png -------------------------------------------------------------------------------- /tests/basic/src/index.ts: -------------------------------------------------------------------------------- 1 | export const sayHi = () => 'hi'; 2 | -------------------------------------------------------------------------------- /tests/basic/src/meta.ts: -------------------------------------------------------------------------------- 1 | export const aFileName = __filename; 2 | 3 | export const aDirName = __dirname; 4 | -------------------------------------------------------------------------------- /tests/basic/test/dynamicImport.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@rstest/core'; 2 | 3 | describe('Dynamic import', () => { 4 | it('should test correctly with dynamic import', async () => { 5 | const { sayHi } = await import('../src/index'); 6 | expect(sayHi()).toBe('hi'); 7 | }); 8 | 9 | // TODO 10 | it.todo( 11 | 'should get source file meta correctly with dynamic import', 12 | async () => { 13 | const { aDirName, aFileName } = await import('../src/meta'); 14 | expect(aDirName.endsWith('/basic/src')).toBeTruthy(); 15 | expect(aFileName.endsWith('/basic/src/meta.ts')).toBeTruthy(); 16 | }, 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/basic/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { describe, expect, it } from '@rstest/core'; 3 | import { sayHi } from '../src/index'; 4 | 5 | describe('Index', () => { 6 | it('should add two numbers correctly', () => { 7 | expect(1 + 1).toBe(2); 8 | }); 9 | 10 | it('should test source code correctly', () => { 11 | expect(sayHi()).toBe('hi'); 12 | }); 13 | 14 | it('should use node API correctly', async () => { 15 | expect( 16 | path.posix 17 | .resolve(__dirname, '../src/index.ts') 18 | .endsWith('/basic/src/index.ts'), 19 | ).toBeTruthy(); 20 | }); 21 | 22 | it('should use require.resolve correctly', async () => { 23 | expect( 24 | require.resolve('../src/index.ts').endsWith('/basic/src/index.ts'), 25 | ).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/basic/test/meta.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@rstest/core'; 2 | import { aDirName, aFileName } from '../src/meta'; 3 | 4 | describe('Meta', () => { 5 | it('should get test file meta correctly', async () => { 6 | expect(__dirname.endsWith('/rstest/tests/basic/test')).toBeTruthy(); 7 | expect(__filename.endsWith('/basic/test/meta.test.ts')).toBeTruthy(); 8 | }); 9 | 10 | it('should get test file import meta correctly', async () => { 11 | expect(import.meta.dirname.includes('/basic/test')).toBeTruthy(); 12 | expect( 13 | import.meta.filename.endsWith('/basic/test/meta.test.ts'), 14 | ).toBeTruthy(); 15 | }); 16 | 17 | // TODO 18 | it.todo('should get source file meta correctly', async () => { 19 | expect(aDirName.endsWith('/basic/src')).toBeTruthy(); 20 | expect(aFileName.endsWith('/basic/src/meta.ts')).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/basic/test/nodeApi.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { describe, expect, it } from '@rstest/core'; 3 | 4 | describe('Node API', () => { 5 | it('should use node path API correctly', async () => { 6 | expect( 7 | path.posix 8 | .resolve(__dirname, './index.test.ts') 9 | .endsWith(path.posix.join('basic', 'test', 'index.test.ts')), 10 | ).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/build/fixtures/alias/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from '@rstest/core'; 2 | import { count } from './src/index'; 3 | 4 | it('Alias config should work correctly', () => { 5 | expect(count).toBe(2); 6 | }); 7 | -------------------------------------------------------------------------------- /tests/build/fixtures/alias/rstest.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { defineConfig } from '@rstest/core'; 3 | 4 | export default defineConfig({ 5 | name: 'node', 6 | resolve: { 7 | alias: { 8 | './a': path.join(__dirname, './src/b'), 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /tests/build/fixtures/alias/src/a.ts: -------------------------------------------------------------------------------- 1 | export const count = 1; 2 | -------------------------------------------------------------------------------- /tests/build/fixtures/alias/src/b.ts: -------------------------------------------------------------------------------- 1 | export const count = 2; 2 | -------------------------------------------------------------------------------- /tests/build/fixtures/alias/src/index.ts: -------------------------------------------------------------------------------- 1 | export { count } from './a'; 2 | -------------------------------------------------------------------------------- /tests/build/fixtures/cssModules/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from '@rstest/core'; 2 | import { style } from './src/index'; 3 | 4 | it('css modules should work correctly', () => { 5 | expect(style.titleClass).toBe('index-module__title-class'); 6 | }); 7 | -------------------------------------------------------------------------------- /tests/build/fixtures/cssModules/rstest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@rstest/core'; 2 | 3 | export default defineConfig({ 4 | name: 'node', 5 | output: { 6 | cssModules: { 7 | localIdentName: '[name]__[local]', 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /tests/build/fixtures/cssModules/src/env.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.css' { 2 | const classes: { readonly [key: string]: string }; 3 | export default classes; 4 | } 5 | -------------------------------------------------------------------------------- /tests/build/fixtures/cssModules/src/index.module.css: -------------------------------------------------------------------------------- 1 | .title-class { 2 | font-size: 14px; 3 | } 4 | -------------------------------------------------------------------------------- /tests/build/fixtures/cssModules/src/index.ts: -------------------------------------------------------------------------------- 1 | import style from './index.module.css'; 2 | 3 | export { style }; 4 | -------------------------------------------------------------------------------- /tests/build/fixtures/decorators/index.test.js: -------------------------------------------------------------------------------- 1 | import { expect, it } from '@rstest/core'; 2 | import './src/index'; 3 | 4 | it('decorator should work correctly', () => { 5 | expect(global.ccc).toBe('hello world'); 6 | expect(global.aaa).toBe('hello'); 7 | expect(global.bbb).toBe('world'); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/build/fixtures/decorators/rstest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@rstest/core'; 2 | 3 | export default defineConfig({ 4 | name: 'node', 5 | source: { 6 | decorators: { 7 | version: '2022-03', 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /tests/build/fixtures/decorators/src/index.js: -------------------------------------------------------------------------------- 1 | function propertyDecorator() { 2 | global.aaa = 'hello'; 3 | } 4 | 5 | function methodDecorator() { 6 | global.bbb = 'world'; 7 | } 8 | 9 | class C { 10 | @propertyDecorator 11 | message = 'hello world'; 12 | 13 | @methodDecorator 14 | m() { 15 | return this.message; 16 | } 17 | } 18 | 19 | global.ccc = new C().m(); 20 | -------------------------------------------------------------------------------- /tests/build/fixtures/define/env.d.ts: -------------------------------------------------------------------------------- 1 | declare const RSBUILD_VERSION: string; 2 | -------------------------------------------------------------------------------- /tests/build/fixtures/define/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from '@rstest/core'; 2 | 3 | it('Define config should work correctly', () => { 4 | expect(RSBUILD_VERSION).toBeDefined(); 5 | }); 6 | -------------------------------------------------------------------------------- /tests/build/fixtures/define/rstest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@rstest/core'; 2 | 3 | export default defineConfig({ 4 | name: 'node', 5 | source: { 6 | define: { 7 | RSBUILD_VERSION: JSON.stringify(require('@rsbuild/core').version), 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /tests/build/fixtures/plugin/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from '@rstest/core'; 2 | import { count } from './src/index'; 3 | 4 | it('Rsbuild plugin should work correctly', () => { 5 | expect(count).toBe(2); 6 | }); 7 | -------------------------------------------------------------------------------- /tests/build/fixtures/plugin/rstest.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { defineConfig } from '@rstest/core'; 3 | 4 | export default defineConfig({ 5 | name: 'node', 6 | plugins: [ 7 | { 8 | name: 'plugin', 9 | setup(api) { 10 | api.transform({ test: /a.ts$/ }, ({ code }) => { 11 | return code.replace('count = 1', 'count = 2'); 12 | }); 13 | }, 14 | }, 15 | ], 16 | }); 17 | -------------------------------------------------------------------------------- /tests/build/fixtures/plugin/src/a.ts: -------------------------------------------------------------------------------- 1 | export const count = 1; 2 | -------------------------------------------------------------------------------- /tests/build/fixtures/plugin/src/index.ts: -------------------------------------------------------------------------------- 1 | export { count } from './a'; 2 | -------------------------------------------------------------------------------- /tests/build/fixtures/tools/rspack/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from '@rstest/core'; 2 | import { count } from './src/index'; 3 | 4 | it('tools.rspack config should work correctly', () => { 5 | expect(count).toBe(2); 6 | }); 7 | -------------------------------------------------------------------------------- /tests/build/fixtures/tools/rspack/rstest.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { defineConfig } from '@rstest/core'; 3 | 4 | export default defineConfig({ 5 | name: 'node', 6 | tools: { 7 | rspack: { 8 | resolve: { 9 | alias: { 10 | './a': path.join(__dirname, './src/b'), 11 | }, 12 | }, 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /tests/build/fixtures/tools/rspack/src/a.ts: -------------------------------------------------------------------------------- 1 | export const count = 1; 2 | -------------------------------------------------------------------------------- /tests/build/fixtures/tools/rspack/src/b.ts: -------------------------------------------------------------------------------- 1 | export const count = 2; 2 | -------------------------------------------------------------------------------- /tests/build/fixtures/tools/rspack/src/index.ts: -------------------------------------------------------------------------------- 1 | export { count } from './a'; 2 | -------------------------------------------------------------------------------- /tests/build/index.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { describe, expect, it } from '@rstest/core'; 4 | import { runRstestCli } from '../scripts/'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | describe('test build config', () => { 10 | it.concurrent.each([ 11 | { name: 'define' }, 12 | { name: 'alias' }, 13 | { name: 'plugin' }, 14 | { name: 'tools/rspack' }, 15 | { name: 'decorators' }, 16 | ])('$name config should work correctly', async ({ name }) => { 17 | const { cli } = await runRstestCli({ 18 | command: 'rstest', 19 | args: [ 20 | 'run', 21 | `fixtures/${name}/index.test.ts`, 22 | '-c', 23 | `fixtures/${name}/rstest.config.ts`, 24 | ], 25 | options: { 26 | nodeOptions: { 27 | cwd: __dirname, 28 | }, 29 | }, 30 | }); 31 | 32 | await cli.exec; 33 | expect(cli.exec.process?.exitCode).toBe(0); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/build/runtimeImport/a.js: -------------------------------------------------------------------------------- 1 | export const a = 1; 2 | -------------------------------------------------------------------------------- /tests/build/runtimeImport/b.ts: -------------------------------------------------------------------------------- 1 | export const b = 1; 2 | -------------------------------------------------------------------------------- /tests/build/runtimeImport/index.test.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'node:path'; 2 | import { expect, it } from '@rstest/core'; 3 | 4 | const customImport = async (name: string) => { 5 | return import(join(__dirname, name)); 6 | }; 7 | 8 | it('should load runtime deps correctly', async () => { 9 | const res = await customImport('./a.js'); 10 | expect(res.a).toBe(1); 11 | }); 12 | 13 | it('should load compile-time deps correctly', async () => { 14 | const res = await import('./b.ts'); 15 | expect(res.b).toBe(1); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/build/runtimeImport/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@rstest/tests-runtime-import", 4 | "type": "module", 5 | "version": "1.0.0" 6 | } 7 | -------------------------------------------------------------------------------- /tests/cli/fixtures/error.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@rstest/core'; 2 | 3 | export default defineConfig({ 4 | pool: { 5 | type: 'forks', 6 | maxWorkers: 4, 7 | minWorkers: 5, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /tests/cli/fixtures/fail.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from '@rstest/core'; 2 | 3 | it('should failed', () => { 4 | expect(1 + 1).toBe(3); 5 | }); 6 | -------------------------------------------------------------------------------- /tests/cli/fixtures/success.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from '@rstest/core'; 2 | 3 | it('should succeed', () => { 4 | expect(1 + 1).toBe(2); 5 | }); 6 | -------------------------------------------------------------------------------- /tests/cli/index.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { describe, expect, it } from '@rstest/core'; 4 | import { runRstestCli } from '../scripts/'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | 8 | const __dirname = dirname(__filename); 9 | 10 | describe.concurrent('test exit code', () => { 11 | it('should return code 0 when test succeed', async () => { 12 | const { cli } = await runRstestCli({ 13 | command: 'rstest', 14 | args: ['run', 'success.test.ts'], 15 | options: { 16 | nodeOptions: { 17 | cwd: __dirname, 18 | }, 19 | }, 20 | }); 21 | 22 | await cli.exec; 23 | expect(cli.exec.process?.exitCode).toBe(0); 24 | }); 25 | 26 | it('should return code 1 when test failed', async () => { 27 | const { cli } = await runRstestCli({ 28 | command: 'rstest', 29 | args: ['run', 'fail.test.ts'], 30 | options: { 31 | nodeOptions: { 32 | cwd: __dirname, 33 | }, 34 | }, 35 | }); 36 | await cli.exec; 37 | expect(cli.exec.process?.exitCode).toBe(1); 38 | }); 39 | 40 | it('should return code 1 and print error correctly when test config error', async () => { 41 | const { cli } = await runRstestCli({ 42 | command: 'rstest', 43 | args: ['run', 'success.test.ts', '-c', 'fixtures/error.config.ts'], 44 | options: { 45 | nodeOptions: { 46 | cwd: __dirname, 47 | }, 48 | }, 49 | }); 50 | await cli.exec; 51 | expect(cli.exec.process?.exitCode).toBe(1); 52 | 53 | const logs = cli.stdout.split('\n').filter(Boolean); 54 | 55 | expect( 56 | logs.find((log) => log.includes('Invalid pool configuration')), 57 | ).toBeDefined(); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /tests/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@rstest/tests-cli", 4 | "version": "1.0.0", 5 | "devDependencies": { 6 | "@rstest/core": "workspace:*" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/describe/chain.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@rstest/core'; 2 | 3 | it('Describe chain API enumerable', async () => { 4 | expect(Object.keys(describe)).toMatchInlineSnapshot(` 5 | [ 6 | "only", 7 | "todo", 8 | "skip", 9 | "concurrent", 10 | "sequential", 11 | "skipIf", 12 | "runIf", 13 | "each", 14 | "for", 15 | ] 16 | `); 17 | expect(Object.keys(describe.only)).toMatchInlineSnapshot(` 18 | [ 19 | "only", 20 | "todo", 21 | "skip", 22 | "concurrent", 23 | "sequential", 24 | "skipIf", 25 | "runIf", 26 | "each", 27 | "for", 28 | ] 29 | `); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/describe/concurrent.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, describe, expect, it } from '@rstest/core'; 2 | 3 | const logs: string[] = []; 4 | 5 | afterAll(() => { 6 | expect(logs).toEqual([ 7 | '[log] concurrent test 1', 8 | '[log] concurrent test 2', 9 | '[log] concurrent test 3', 10 | '[log] concurrent test 4', 11 | '[log] concurrent test 5', 12 | '[log] concurrent test 2 - 1', 13 | '[log] concurrent test B 1', 14 | '[log] concurrent test 3 - 1', 15 | '[log] concurrent test 4 - 1', 16 | '[log] concurrent test 5 - 1', 17 | '[log] concurrent test 1 - 1', 18 | '[log] concurrent test B 1 - 1', 19 | ]); 20 | }); 21 | 22 | describe.concurrent('suite', () => { 23 | it('test 1', async () => { 24 | logs.push('[log] concurrent test 1'); 25 | await new Promise((resolve) => setTimeout(resolve, 200)); 26 | logs.push('[log] concurrent test 1 - 1'); 27 | }); 28 | 29 | it('test 2', async () => { 30 | logs.push('[log] concurrent test 2'); 31 | await new Promise((resolve) => setTimeout(resolve, 100)); 32 | logs.push('[log] concurrent test 2 - 1'); 33 | }); 34 | 35 | describe('suite 1', () => { 36 | it('test 3', async () => { 37 | logs.push('[log] concurrent test 3'); 38 | await new Promise((resolve) => setTimeout(resolve, 100)); 39 | logs.push('[log] concurrent test 3 - 1'); 40 | }); 41 | 42 | it('test 4', async () => { 43 | logs.push('[log] concurrent test 4'); 44 | await new Promise((resolve) => setTimeout(resolve, 100)); 45 | logs.push('[log] concurrent test 4 - 1'); 46 | }); 47 | }); 48 | 49 | it('test 5', async () => { 50 | logs.push('[log] concurrent test 5'); 51 | await new Promise((resolve) => setTimeout(resolve, 100)); 52 | logs.push('[log] concurrent test 5 - 1'); 53 | }); 54 | }); 55 | 56 | describe.concurrent('suite B', () => { 57 | it('test B 1', async () => { 58 | logs.push('[log] concurrent test B 1'); 59 | await new Promise((resolve) => setTimeout(resolve, 200)); 60 | logs.push('[log] concurrent test B 1 - 1'); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /tests/describe/condition.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, describe, expect, it } from '@rstest/core'; 2 | 3 | describe('Describe skipIf & runIf API', async () => { 4 | const logs: string[] = []; 5 | 6 | afterAll(() => { 7 | expect(logs.length).toBe(1); 8 | }); 9 | 10 | describe.skipIf(1 + 1 === 2).each([ 11 | { a: 1, b: 1, expected: 2 }, 12 | { a: 1, b: 2, expected: 3 }, 13 | { a: 2, b: 1, expected: 3 }, 14 | ])('add two numbers correctly', ({ a, b, expected }) => { 15 | it(`should return ${expected}`, () => { 16 | logs.push('executed'); 17 | expect(a + b).toBe(expected); 18 | }); 19 | }); 20 | 21 | describe.runIf(1 + 1 === 2)('add two numbers correctly', () => { 22 | it('add two numbers correctly', () => { 23 | logs.push('executed'); 24 | expect(1 + 1).toBe(2); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/describe/each.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, describe, expect, it } from '@rstest/core'; 2 | 3 | const logs: string[] = []; 4 | 5 | afterAll(() => { 6 | expect(logs.length).toBe(6); 7 | }); 8 | 9 | describe.each([ 10 | { a: 1, b: 1, expected: 2 }, 11 | { a: 1, b: 2, expected: 3 }, 12 | { a: 2, b: 1, expected: 3 }, 13 | ])('add two numbers correctly', ({ a, b, expected }) => { 14 | it(`should return ${expected}`, () => { 15 | expect(a + b).toBe(expected); 16 | logs.push('executed'); 17 | }); 18 | }); 19 | 20 | describe.each([ 21 | [2, 1, 3], 22 | [2, 2, 4], 23 | [3, 1, 4], 24 | ])('add two numbers correctly', (a, b, expected) => { 25 | it(`should return ${expected}`, () => { 26 | expect(a + b).toBe(expected); 27 | logs.push('executed'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/describe/fixtures/skip.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, beforeEach, describe, expect, it } from '@rstest/core'; 2 | 3 | beforeAll(() => { 4 | console.log('[beforeAll] should not run'); 5 | }); 6 | 7 | beforeEach(() => { 8 | console.log('[beforeEach] should not run'); 9 | }); 10 | 11 | describe.skip('should skip', () => { 12 | it('test 1', () => { 13 | console.log('[test 1] should not run'); 14 | expect(1 + 1).toBe(2); 15 | }); 16 | 17 | it('test 2', () => { 18 | expect(1 + 1).toBe(2); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/describe/fixtures/todo.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, beforeEach, describe, expect, it } from '@rstest/core'; 2 | 3 | beforeAll(() => { 4 | console.log('[beforeAll] should not run'); 5 | }); 6 | 7 | beforeEach(() => { 8 | console.log('[beforeEach] should not run'); 9 | }); 10 | 11 | describe.todo('should skip', () => { 12 | it('test 1', () => { 13 | console.log('[test 1] should not run'); 14 | expect(1 + 1).toBe(2); 15 | }); 16 | 17 | it('test 2', () => { 18 | expect(1 + 1).toBe(2); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/describe/fixtures/undefined.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, beforeEach, describe } from '@rstest/core'; 2 | 3 | beforeAll(() => { 4 | console.log('[beforeAll] should not run'); 5 | }); 6 | 7 | beforeEach(() => { 8 | console.log('[beforeEach] should not run'); 9 | }); 10 | 11 | describe.skip('should skip'); 12 | describe.todo('should skip - 1'); 13 | -------------------------------------------------------------------------------- /tests/describe/for.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, describe, expect, it } from '@rstest/core'; 2 | 3 | const logs: string[] = []; 4 | 5 | afterAll(() => { 6 | expect(logs.length).toBe(6); 7 | }); 8 | 9 | describe.for([ 10 | { a: 1, b: 1, expected: 2 }, 11 | { a: 1, b: 2, expected: 3 }, 12 | { a: 2, b: 1, expected: 3 }, 13 | ])('add two numbers correctly', ({ a, b, expected }) => { 14 | it(`should return ${expected}`, () => { 15 | expect(a + b).toBe(expected); 16 | logs.push('executed'); 17 | }); 18 | }); 19 | 20 | describe.for([ 21 | [2, 1, 3], 22 | [2, 2, 4], 23 | [3, 1, 4], 24 | ])('add two numbers correctly', ([a, b, expected]) => { 25 | it(`should return ${expected}`, () => { 26 | expect(a + b).toBe(expected); 27 | logs.push('executed'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/describe/only.each.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, describe, expect, it } from '@rstest/core'; 2 | 3 | const logs: string[] = []; 4 | 5 | afterAll(() => { 6 | expect(logs.length).toBe(3); 7 | }); 8 | 9 | describe.only.each([ 10 | { a: 1, b: 1, expected: 2 }, 11 | { a: 1, b: 2, expected: 3 }, 12 | { a: 2, b: 1, expected: 3 }, 13 | ])('add two numbers correctly', ({ a, b, expected }) => { 14 | it(`should return ${expected}`, () => { 15 | expect(a + b).toBe(expected); 16 | logs.push('executed'); 17 | }); 18 | }); 19 | 20 | describe.each([ 21 | [2, 1, 3], 22 | [2, 2, 4], 23 | ])('add two numbers correctly', (a, b, expected) => { 24 | it(`should return ${expected}`, () => { 25 | expect(a + b).toBe(expected); 26 | logs.push('executed'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/describe/only.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, beforeEach, describe, expect, it } from '@rstest/core'; 2 | 3 | const logs: string[] = []; 4 | 5 | beforeEach(() => { 6 | logs.push('[beforeEach] root'); 7 | }); 8 | 9 | afterAll(() => { 10 | expect(logs).toEqual([ 11 | '[beforeEach] root', 12 | '[test] in level B-A', 13 | '[beforeEach] root', 14 | '[test] in level B-C-A', 15 | '[beforeEach] root', 16 | '[test] in level E-A', 17 | ]); 18 | }); 19 | 20 | describe('level A', () => { 21 | it('it in level A', () => { 22 | logs.push('[test] in level A'); 23 | expect(1 + 1).toBe(2); 24 | }); 25 | 26 | // biome-ignore lint/suspicious/noFocusedTests: 27 | describe.only('level B', () => { 28 | it('it in level B-A', () => { 29 | logs.push('[test] in level B-A'); 30 | expect(2 + 1).toBe(3); 31 | }); 32 | 33 | it.skip('it in level B-B', () => { 34 | logs.push('[test] in level B-B'); 35 | expect(2 + 1).toBe(3); 36 | }); 37 | 38 | describe('level B-C', () => { 39 | it('it in level B-C-A', () => { 40 | logs.push('[test] in level B-C-A'); 41 | expect(2 + 1).toBe(3); 42 | }); 43 | }); 44 | }); 45 | 46 | it('it in level C', () => { 47 | logs.push('[test] in level C'); 48 | expect(2 + 2).toBe(4); 49 | }); 50 | 51 | describe('level D', () => { 52 | it('it in level D-A', () => { 53 | logs.push('[test] in level D-A'); 54 | expect(2 + 1).toBe(3); 55 | }); 56 | }); 57 | }); 58 | 59 | // biome-ignore lint/suspicious/noFocusedTests: 60 | describe.only('level E', () => { 61 | it('it in level E-A', () => { 62 | logs.push('[test] in level E-A'); 63 | expect(2 + 1).toBe(3); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /tests/describe/skip.each.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, describe, expect, it } from '@rstest/core'; 2 | 3 | const logs: string[] = []; 4 | 5 | afterAll(() => { 6 | expect(logs.length).toBe(2); 7 | }); 8 | 9 | describe.skip.each([ 10 | { a: 1, b: 1, expected: 2 }, 11 | { a: 1, b: 2, expected: 3 }, 12 | { a: 2, b: 1, expected: 3 }, 13 | ])('add two numbers correctly', ({ a, b, expected }) => { 14 | it(`should return ${expected}`, () => { 15 | expect(a + b).toBe(expected); 16 | logs.push('executed'); 17 | }); 18 | }); 19 | 20 | describe.each([ 21 | [2, 1, 3], 22 | [2, 2, 4], 23 | ])('add two numbers correctly', (a, b, expected) => { 24 | it(`should return ${expected}`, () => { 25 | expect(a + b).toBe(expected); 26 | logs.push('executed'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/diff/fixtures/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@rstest/core'; 2 | 3 | describe('Diff', () => { 4 | it('diff object', () => { 5 | expect({ 6 | a: 1, 7 | b: 2, 8 | c: { 9 | cA: 1, 10 | }, 11 | }).toEqual({ 12 | a: 1, 13 | b: 3, 14 | c: { 15 | cA: 3, 16 | }, 17 | }); 18 | }); 19 | 20 | it('diff string', () => { 21 | expect('hi').toBe('hii'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/diff/index.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { describe, expect, it } from '@rstest/core'; 4 | import { runRstestCli } from '../scripts'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | describe('diff', () => { 10 | it('should print diff info correctly', async () => { 11 | const { cli } = await runRstestCli({ 12 | command: 'rstest', 13 | args: ['run', 'fixtures/index.test.ts'], 14 | options: { 15 | nodeOptions: { 16 | cwd: __dirname, 17 | }, 18 | }, 19 | }); 20 | 21 | await cli.exec; 22 | expect(cli.exec.process?.exitCode).toBe(1); 23 | 24 | const logs = cli.stdout 25 | .split('\n') 26 | .map((log) => log.replace(/\s/g, '')) 27 | .filter(Boolean); 28 | 29 | // should diff object correctly 30 | expect(logs.find((log) => log.includes('-"b":3,'))).toBeDefined(); 31 | expect(logs.find((log) => log.includes('+"b":2,'))).toBeDefined(); 32 | expect(logs.find((log) => log.includes('-"cA":3,'))).toBeDefined(); 33 | expect(logs.find((log) => log.includes('+"cA":1,'))).toBeDefined(); 34 | 35 | // should diff string correctly 36 | expect(logs.find((log) => log.includes('-hii'))).toBeDefined(); 37 | expect(logs.find((log) => log.includes('+hi'))).toBeDefined(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/expect/test/assert.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, it } from '@rstest/core'; 2 | 3 | it('assert', () => { 4 | assert( 5 | 'hello world'.includes('world'), 6 | 'Expected "hello world" to include "world"', 7 | ); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/expect/test/fixtures/soft.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@rstest/core'; 2 | 3 | test('expect.soft test', () => { 4 | expect.soft(1 + 1).toBe(3); // should mark the test as fail and continue 5 | expect.soft(1 + 2).toBe(4); // should mark the test as fail and continue 6 | expect.soft(1 + 3).toBe(4); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/expect/test/poll.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@rstest/core'; 2 | 3 | describe('Expect Poll API', () => { 4 | it('should run expect poll succeed', async () => { 5 | const logs: string[] = []; 6 | setTimeout(() => { 7 | logs.push('hello world'); 8 | }, 100); 9 | 10 | await expect 11 | .poll(() => logs.some((log) => log.includes('hello world'))) 12 | .toBeTruthy(); 13 | }); 14 | 15 | it.fails('should run expect poll failed when unmatched', async () => { 16 | const logs: string[] = []; 17 | setTimeout(() => { 18 | logs.push('hello world'); 19 | }, 100); 20 | 21 | await expect 22 | .poll(() => logs.some((log) => log.includes('hello world!')), { 23 | timeout: 300, 24 | }) 25 | .toBeTruthy(); 26 | }); 27 | 28 | it.fails('should run expect poll failed when timeout', async () => { 29 | const logs: string[] = []; 30 | setTimeout(() => { 31 | logs.push('hello world'); 32 | }, 100); 33 | 34 | await expect 35 | .poll(() => logs.some((log) => log.includes('hello world!')), { 36 | timeout: 50, 37 | }) 38 | .toBeTruthy(); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/expect/test/soft.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { describe, expect, it } from '@rstest/core'; 4 | import { runRstestCli } from '../../scripts'; 5 | 6 | describe('Expect Soft API', () => { 7 | it('should mark the test as fail and continue', async () => { 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = dirname(__filename); 10 | 11 | const { cli } = await runRstestCli({ 12 | command: 'rstest', 13 | args: ['run', 'fixtures/soft.test.ts'], 14 | options: { 15 | nodeOptions: { 16 | cwd: __dirname, 17 | }, 18 | }, 19 | }); 20 | await cli.exec; 21 | expect(cli.exec.process?.exitCode).toBe(1); 22 | 23 | const logs = cli.stdout.split('\n').filter(Boolean); 24 | 25 | expect( 26 | logs.find((log) => log.includes('AssertionError: expected 2 to be 3')), 27 | ).toBeTruthy(); 28 | expect( 29 | logs.find((log) => log.includes('AssertionError: expected 3 to be 4')), 30 | ).toBeTruthy(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/externals/fixtures/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from '@rstest/core'; 2 | import stripAnsi from 'strip-ansi'; 3 | 4 | it('should load esm correctly', () => { 5 | expect(stripAnsi('\u001B[4mUnicorn\u001B[0m')).toBe('Unicorn'); 6 | }); 7 | 8 | it('should load esm dynamic correctly', async () => { 9 | const { default: stripAnsi } = await import('strip-ansi'); 10 | expect(stripAnsi('\u001B[4mUnicorn\u001B[0m')).toBe('Unicorn'); 11 | }); 12 | 13 | it('should load cjs with require correctly', () => { 14 | const picocolors = require('picocolors'); 15 | expect(picocolors.green).toBeDefined(); 16 | }); 17 | 18 | it('should load pkg from some other pkgs correctly', async () => { 19 | const { pathe } = await import('./test-pkg/index'); 20 | expect(pathe.basename('test-pkg/index.ts')).toBe('index.ts'); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/externals/fixtures/test-pkg/index.ts: -------------------------------------------------------------------------------- 1 | import pathe from 'pathe'; 2 | 3 | export { pathe }; 4 | -------------------------------------------------------------------------------- /tests/externals/fixtures/test-pkg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@rstest/tests-externals-fixtures", 4 | "version": "1.0.0", 5 | "devDependencies": { 6 | "pathe": "^2.0.3" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/externals/index.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import { dirname, join } from 'node:path'; 3 | import { fileURLToPath } from 'node:url'; 4 | import { describe, expect, it } from '@rstest/core'; 5 | import { runRstestCli } from '../scripts/'; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = dirname(__filename); 9 | 10 | describe('test externals', () => { 11 | it('should external node_modules by default', async () => { 12 | process.env.DEBUG_RSTEST_OUTPUTS = 'true'; 13 | const { cli } = await runRstestCli({ 14 | command: 'rstest', 15 | args: ['run', './fixtures/index.test.ts'], 16 | options: { 17 | nodeOptions: { 18 | cwd: __dirname, 19 | }, 20 | }, 21 | }); 22 | 23 | await cli.exec; 24 | expect(cli.exec.process?.exitCode).toBe(0); 25 | 26 | const outputPath = join( 27 | __dirname, 28 | 'dist/.rstest-temp/fixtures/index.test.ts.js', 29 | ); 30 | 31 | expect(fs.existsSync(outputPath)).toBeTruthy(); 32 | const content = fs.readFileSync(outputPath, 'utf-8'); 33 | 34 | expect(content).toEqual(expect.stringMatching(/require\S+picocolors/)); 35 | expect(content).toEqual(expect.stringMatching(/import\S+strip\-ansi/)); 36 | expect(content).toEqual(expect.stringMatching(/import\S+pathe/)); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/externals/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@rstest/tests-externals", 4 | "version": "1.0.0", 5 | "devDependencies": { 6 | "@rstest/core": "workspace:*", 7 | "picocolors": "^1.1.1", 8 | "strip-ansi": "^7.1.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/filter/default.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { describe, expect, it } from '@rstest/core'; 4 | import { runRstestCli } from '../scripts/'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | it('should run success without filter', async () => { 10 | const { cli } = await runRstestCli({ 11 | command: 'rstest', 12 | args: ['run', 'fixtures/testNamePattern.test.ts'], 13 | options: { 14 | nodeOptions: { 15 | cwd: __dirname, 16 | }, 17 | }, 18 | }); 19 | 20 | await cli.exec; 21 | expect(cli.exec.process?.exitCode).toBe(0); 22 | 23 | const logs = cli.stdout.split('\n').filter(Boolean); 24 | 25 | expect(logs.filter((log) => log.startsWith('['))).toMatchInlineSnapshot(` 26 | [ 27 | "[test] in level-B-A", 28 | "[test] in level-B-C-A", 29 | "[test] in level-C", 30 | "[test] in level-D-A", 31 | ] 32 | `); 33 | }); 34 | -------------------------------------------------------------------------------- /tests/filter/fixtures/testNamePattern.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@rstest/core'; 2 | 3 | export default defineConfig({ 4 | testNamePattern: /B/, 5 | }); 6 | -------------------------------------------------------------------------------- /tests/filter/fixtures/testNamePattern.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@rstest/core'; 2 | 3 | // biome-ignore lint/suspicious/noFocusedTests: 4 | describe.only('level-A', () => { 5 | describe('level-B', () => { 6 | it('it in level-B-A', () => { 7 | console.log('[test] in level-B-A'); 8 | expect(2 + 1).toBe(3); 9 | }); 10 | 11 | it.skip('it in level-B-B', () => { 12 | console.log('[test] in level-B-B'); 13 | expect(2 + 1).toBe(3); 14 | }); 15 | 16 | describe('level-B-C', () => { 17 | it('it in level-B-C-A', () => { 18 | console.log('[test] in level-B-C-A'); 19 | expect(2 + 1).toBe(3); 20 | }); 21 | }); 22 | }); 23 | 24 | it('it in level-C', () => { 25 | console.log('[test] in level-C'); 26 | expect(2 + 2).toBe(4); 27 | }); 28 | 29 | describe('level-D', () => { 30 | it('it in level-D-A', () => { 31 | console.log('[test] in level-D-A'); 32 | expect(2 + 1).toBe(3); 33 | }); 34 | }); 35 | }); 36 | 37 | describe('level-E', () => { 38 | it('it in level-E-A', () => { 39 | console.log('[test] in level-E-A'); 40 | expect(2 + 1).toBe(3); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/filter/regex.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { expect, it } from '@rstest/core'; 4 | import { runRstestCli } from '../scripts/'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | it('should filter test suite name success use regex', async () => { 10 | const { cli } = await runRstestCli({ 11 | command: 'rstest', 12 | args: [ 13 | 'run', 14 | 'fixtures/testNamePattern.test.ts', 15 | '-c=fixtures/testNamePattern.config.ts', 16 | ], 17 | options: { 18 | nodeOptions: { 19 | cwd: __dirname, 20 | }, 21 | }, 22 | }); 23 | 24 | await cli.exec; 25 | expect(cli.exec.process?.exitCode).toBe(0); 26 | 27 | const logs = cli.stdout.split('\n').filter(Boolean); 28 | 29 | expect(logs.filter((log) => log.startsWith('['))).toMatchInlineSnapshot(` 30 | [ 31 | "[test] in level-B-A", 32 | "[test] in level-B-C-A", 33 | ] 34 | `); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/globals/fixtures/index.test.ts: -------------------------------------------------------------------------------- 1 | beforeAll(() => { 2 | console.log('[beforeAll]'); 3 | }); 4 | 5 | afterAll(() => { 6 | console.log('[afterAll]'); 7 | }); 8 | 9 | beforeEach(() => { 10 | console.log('[beforeEach]'); 11 | }); 12 | 13 | afterEach(() => { 14 | console.log('[afterEach]'); 15 | }); 16 | 17 | describe('Index', () => { 18 | it('should add two numbers correctly', () => { 19 | expect(1 + 1).toBe(2); 20 | expect(rstest.fn).toBeDefined(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/globals/fixtures/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@rstest/tests-global-fixtures", 4 | "version": "1.0.0", 5 | "devDependencies": { 6 | "@rstest/core": "workspace:*", 7 | "@rstest/tsconfig": "workspace:*" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/globals/fixtures/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@rstest/tsconfig/base", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "types": ["@rstest/core/globals"] 6 | }, 7 | "include": ["./**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /tests/globals/index.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { describe, expect, it } from '@rstest/core'; 4 | import { runRstestCli } from '../scripts/'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | describe('test global APIs', () => { 10 | it('should run with global API succeed', async () => { 11 | const { cli } = await runRstestCli({ 12 | command: 'rstest', 13 | args: ['run', 'fixtures/index.test.ts', '--globals'], 14 | options: { 15 | nodeOptions: { 16 | cwd: __dirname, 17 | }, 18 | }, 19 | }); 20 | 21 | await cli.exec; 22 | expect(cli.exec.process?.exitCode).toBe(0); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/in-source/fixtures/rslib.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@rslib/core'; 2 | 3 | export default defineConfig({ 4 | lib: [ 5 | { 6 | bundle: false, 7 | format: 'esm', 8 | }, 9 | ], 10 | source: { 11 | define: { 12 | 'import.meta.rstest': undefined, 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /tests/in-source/fixtures/rstest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@rstest/core'; 2 | 3 | export default defineConfig({ 4 | includeSource: ['src/**/*.{js,ts}'], 5 | }); 6 | -------------------------------------------------------------------------------- /tests/in-source/fixtures/src/a.ts: -------------------------------------------------------------------------------- 1 | export const a = '1'; 2 | -------------------------------------------------------------------------------- /tests/in-source/fixtures/src/index.ts: -------------------------------------------------------------------------------- 1 | export const sayHi = () => 'hi'; 2 | 3 | if (import.meta.rstest) { 4 | const { it, expect } = import.meta.rstest; 5 | it('should test source code correctly', () => { 6 | expect(sayHi()).toBe('hi'); 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /tests/in-source/fixtures/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["DOM", "ES2020"], 5 | "module": "ESNext", 6 | "jsx": "react-jsx", 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "isolatedModules": true, 10 | "resolveJsonModule": true, 11 | "moduleResolution": "bundler", 12 | "useDefineForClassFields": true, 13 | "types": ["@rstest/core/importMeta"] 14 | }, 15 | "include": ["src"] 16 | } 17 | -------------------------------------------------------------------------------- /tests/in-source/index.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { describe, expect, it } from '@rstest/core'; 4 | import { runRstestCli } from '../scripts'; 5 | 6 | describe('In-Source testing', () => { 7 | it('should run in-source testing correctly', async () => { 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = dirname(__filename); 10 | 11 | const { cli } = await runRstestCli({ 12 | command: 'rstest', 13 | args: ['run'], 14 | options: { 15 | nodeOptions: { 16 | cwd: join(__dirname, 'fixtures'), 17 | }, 18 | }, 19 | }); 20 | await cli.exec; 21 | expect(cli.exec.process?.exitCode).toBe(0); 22 | 23 | const logs = cli.stdout.split('\n').filter(Boolean); 24 | 25 | expect( 26 | logs.find((log) => log.includes('Test Files 1 passed')), 27 | ).toBeTruthy(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/isolate/src/index.ts: -------------------------------------------------------------------------------- 1 | let count = 0; 2 | 3 | export function increment() { 4 | count++; 5 | } 6 | 7 | export function getCount() { 8 | return count; 9 | } 10 | -------------------------------------------------------------------------------- /tests/isolate/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@rstest/core'; 2 | import { sleep } from '../../scripts/utils'; 3 | import { getCount, increment } from '../src/index'; 4 | 5 | process.env.index = '1'; 6 | globalThis.index = '1'; 7 | 8 | describe('Test isolate', () => { 9 | it('should get process.env index correctly', async () => { 10 | await sleep(200); 11 | expect(process.env.index).toBe('1'); 12 | expect(process.env.index1).toBeUndefined(); 13 | }); 14 | 15 | it('should get globalThis index correctly', async () => { 16 | await sleep(200); 17 | expect(globalThis.index).toBe('1'); 18 | expect(globalThis.index1).toBeUndefined(); 19 | }); 20 | 21 | it('should call source code isolate', async () => { 22 | increment(); 23 | expect(getCount()).toBe(1); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/isolate/test/index1.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@rstest/core'; 2 | import { sleep } from '../../scripts/utils'; 3 | import { getCount, increment } from '../src/index'; 4 | 5 | process.env.index1 = '1'; 6 | globalThis.index1 = '1'; 7 | 8 | describe('Test isolate - 1', () => { 9 | it('should get process.env index1 correctly', async () => { 10 | await sleep(200); 11 | expect(process.env.index1).toBe('1'); 12 | expect(process.env.index).toBeUndefined(); 13 | }); 14 | 15 | it('should get global index1 correctly', async () => { 16 | await sleep(200); 17 | expect(globalThis.index1).toBe('1'); 18 | expect(globalThis.index).toBeUndefined(); 19 | }); 20 | 21 | it('should call source code isolate - 1', async () => { 22 | increment(); 23 | expect(getCount()).toBe(1); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/lifecycle/afterAll.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { describe, expect, it } from '@rstest/core'; 4 | import { runRstestCli } from '../scripts'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | describe('afterAll', () => { 10 | it('afterAll should be invoked in the correct order', async () => { 11 | const { cli } = await runRstestCli({ 12 | command: 'rstest', 13 | args: ['run', 'afterAll.test'], 14 | options: { 15 | nodeOptions: { 16 | cwd: __dirname, 17 | }, 18 | }, 19 | }); 20 | 21 | await cli.exec; 22 | const logs = cli.stdout.split('\n').filter(Boolean); 23 | 24 | expect(logs.filter((log) => log.startsWith('[afterAll]'))).toEqual([ 25 | '[afterAll] in level B-A', 26 | '[afterAll] in level B-B', 27 | '[afterAll] in level A', 28 | '[afterAll] root', 29 | ]); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /tests/lifecycle/afterEach.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { describe, expect, it } from '@rstest/core'; 4 | import { runRstestCli } from '../scripts'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | describe('afterEach', () => { 10 | it('afterEach should be invoked in the correct order', async () => { 11 | const { cli } = await runRstestCli({ 12 | command: 'rstest', 13 | args: ['run', 'afterEach.test'], 14 | options: { 15 | nodeOptions: { 16 | cwd: __dirname, 17 | }, 18 | }, 19 | }); 20 | 21 | await cli.exec; 22 | const logs = cli.stdout.split('\n').filter(Boolean); 23 | 24 | expect(logs.filter((log) => log.startsWith('[afterEach]'))).toEqual([ 25 | '[afterEach] in level A', 26 | '[afterEach] root', 27 | 28 | '[afterEach] in level B-A', 29 | '[afterEach] in level A', 30 | '[afterEach] root', 31 | 32 | '[afterEach] in level B-B', 33 | '[afterEach] in level A', 34 | '[afterEach] root', 35 | ]); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/lifecycle/beforeAll.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { describe, expect, it } from '@rstest/core'; 4 | import { runRstestCli } from '../scripts'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | describe('beforeAll', () => { 10 | it('beforeAll should be invoked in the correct order', async () => { 11 | const { cli } = await runRstestCli({ 12 | command: 'rstest', 13 | args: ['run', 'beforeAll.test'], 14 | options: { 15 | nodeOptions: { 16 | cwd: __dirname, 17 | }, 18 | }, 19 | }); 20 | 21 | await cli.exec; 22 | const logs = cli.stdout.split('\n').filter(Boolean); 23 | 24 | expect(logs.filter((log) => log.startsWith('[beforeAll]'))).toEqual([ 25 | '[beforeAll] root', 26 | '[beforeAll] root async', 27 | '[beforeAll] in level A', 28 | '[beforeAll] in level B-A', 29 | '[beforeAll] in level B-B', 30 | ]); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/lifecycle/beforeEach.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { describe, expect, it } from '@rstest/core'; 4 | import { runRstestCli } from '../scripts'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | describe('beforeEach', () => { 10 | it('beforeEach should be invoked in the correct order', async () => { 11 | const { cli } = await runRstestCli({ 12 | command: 'rstest', 13 | args: ['run', 'beforeEach.test'], 14 | options: { 15 | nodeOptions: { 16 | cwd: __dirname, 17 | }, 18 | }, 19 | }); 20 | 21 | await cli.exec; 22 | const logs = cli.stdout.split('\n').filter(Boolean); 23 | 24 | expect(logs.filter((log) => log.startsWith('[beforeEach]'))).toEqual([ 25 | '[beforeEach] root', 26 | '[beforeEach] root async', 27 | '[beforeEach] in level A', 28 | 29 | '[beforeEach] root', 30 | '[beforeEach] root async', 31 | '[beforeEach] in level A', 32 | '[beforeEach] in level B-A', 33 | 34 | '[beforeEach] root', 35 | '[beforeEach] root async', 36 | '[beforeEach] in level A', 37 | '[beforeEach] in level B-B', 38 | ]); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/lifecycle/fixtures/afterAll.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, describe, expect, it } from '@rstest/core'; 2 | 3 | afterAll(() => { 4 | console.log('[afterAll] root'); 5 | }); 6 | 7 | describe('level A', () => { 8 | it('it in level A', () => { 9 | expect(1 + 1).toBe(2); 10 | }); 11 | 12 | afterAll(() => { 13 | console.log('[afterAll] in level A'); 14 | }); 15 | 16 | describe('level B-A', () => { 17 | it('it in level B-A', () => { 18 | expect(2 + 1).toBe(3); 19 | }); 20 | 21 | afterAll(() => { 22 | console.log('[afterAll] in level B-A'); 23 | }); 24 | }); 25 | 26 | describe('level B-B', () => { 27 | it('it in level B-B', () => { 28 | expect(2 + 2).toBe(4); 29 | }); 30 | 31 | afterAll(() => { 32 | console.log('[afterAll] in level B-B'); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/lifecycle/fixtures/afterEach.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, describe, expect, it } from '@rstest/core'; 2 | 3 | afterEach(() => { 4 | console.log('[afterEach] root'); 5 | }); 6 | 7 | describe('level A', () => { 8 | it('it in level A', () => { 9 | expect(1 + 1).toBe(2); 10 | }); 11 | 12 | afterEach(() => { 13 | console.log('[afterEach] in level A'); 14 | }); 15 | 16 | describe('level B-A', () => { 17 | it('it in level B-A', () => { 18 | expect(2 + 1).toBe(3); 19 | }); 20 | 21 | afterEach(() => { 22 | console.log('[afterEach] in level B-A'); 23 | }); 24 | }); 25 | 26 | describe('level B-B', () => { 27 | it('it in level B-B', () => { 28 | expect(2 + 2).toBe(4); 29 | }); 30 | 31 | afterEach(() => { 32 | console.log('[afterEach] in level B-B'); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/lifecycle/fixtures/beforeAll.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from '@rstest/core'; 2 | import { sleep } from '../../scripts'; 3 | 4 | beforeAll((ctx) => { 5 | console.log('[beforeAll] root'); 6 | expect(ctx.filepath).toBe(__filename); 7 | }); 8 | 9 | beforeAll(async () => { 10 | await sleep(100); 11 | console.log('[beforeAll] root async'); 12 | }); 13 | 14 | describe('level A', () => { 15 | it('it in level A', () => { 16 | expect(1 + 1).toBe(2); 17 | }); 18 | 19 | beforeAll(() => { 20 | console.log('[beforeAll] in level A'); 21 | }); 22 | 23 | describe('level B-A', () => { 24 | it('it in level B-A', () => { 25 | expect(2 + 1).toBe(3); 26 | }); 27 | 28 | beforeAll(() => { 29 | console.log('[beforeAll] in level B-A'); 30 | }); 31 | }); 32 | 33 | describe('level B-B', () => { 34 | it('it in level B-B', () => { 35 | expect(2 + 2).toBe(4); 36 | }); 37 | 38 | beforeAll(() => { 39 | console.log('[beforeAll] in level B-B'); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/lifecycle/fixtures/beforeEach.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it } from '@rstest/core'; 2 | import { sleep } from '../../scripts'; 3 | 4 | beforeEach(() => { 5 | console.log('[beforeEach] root'); 6 | }); 7 | 8 | beforeEach(async () => { 9 | await sleep(100); 10 | console.log('[beforeEach] root async'); 11 | }); 12 | 13 | describe('level A', () => { 14 | it('it in level A', () => { 15 | expect(1 + 1).toBe(2); 16 | }); 17 | 18 | beforeEach(() => { 19 | console.log('[beforeEach] in level A'); 20 | }); 21 | 22 | describe('level B-A', () => { 23 | it('it in level B-A', () => { 24 | expect(2 + 1).toBe(3); 25 | }); 26 | 27 | beforeEach(() => { 28 | console.log('[beforeEach] in level B-A'); 29 | }); 30 | }); 31 | 32 | describe('level B-B', () => { 33 | it('it in level B-B', () => { 34 | expect(2 + 2).toBe(4); 35 | }); 36 | 37 | beforeEach(() => { 38 | console.log('[beforeEach] in level B-B'); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /tests/lifecycle/fixtures/cleanup.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | afterAll, 3 | beforeAll, 4 | beforeEach, 5 | describe, 6 | expect, 7 | it, 8 | } from '@rstest/core'; 9 | import { sleep } from '../../scripts'; 10 | 11 | beforeAll(() => { 12 | return async () => { 13 | await sleep(100); 14 | console.log('[beforeAll] cleanup root'); 15 | }; 16 | }); 17 | 18 | afterAll(() => { 19 | console.log('[afterAll] root'); 20 | }); 21 | 22 | beforeAll(() => { 23 | return () => { 24 | console.log('[beforeAll] cleanup root1'); 25 | }; 26 | }); 27 | 28 | beforeEach(() => { 29 | return () => { 30 | console.log('[beforeEach] cleanup root'); 31 | }; 32 | }); 33 | 34 | describe('level A', () => { 35 | it('it in level A', () => { 36 | expect(1 + 1).toBe(2); 37 | }); 38 | 39 | beforeAll(() => { 40 | return () => { 41 | console.log('[beforeAll] cleanup in level A'); 42 | }; 43 | }); 44 | 45 | beforeEach(() => { 46 | return () => { 47 | console.log('[beforeEach] cleanup in level A'); 48 | }; 49 | }); 50 | 51 | describe('level B-A', () => { 52 | it('it in level B-A', () => { 53 | expect(2 + 1).toBe(3); 54 | }); 55 | 56 | beforeAll(() => { 57 | return () => { 58 | console.log('[beforeAll] cleanup in level B-A'); 59 | }; 60 | }); 61 | }); 62 | 63 | describe('level B-B', () => { 64 | it('it in level B-B', () => { 65 | expect(2 + 2).toBe(4); 66 | }); 67 | 68 | beforeAll(() => { 69 | return () => { 70 | console.log('[beforeAll] cleanup in level B-B'); 71 | }; 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /tests/lifecycle/fixtures/error/afterAll.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, afterEach, describe, expect, it } from '@rstest/core'; 2 | 3 | describe('test afterAll error', () => { 4 | it('it in level A', () => { 5 | console.log('[test] should run'); 6 | expect(1 + 1).toBe(2); 7 | }); 8 | 9 | afterAll(() => { 10 | console.log('[afterAll - 2] should not run'); 11 | }); 12 | 13 | // biome-ignore lint/suspicious/noDuplicateTestHooks: test 14 | afterAll(() => { 15 | throw new Error('afterAll error'); 16 | }); 17 | 18 | // biome-ignore lint/suspicious/noDuplicateTestHooks: test 19 | afterAll(() => { 20 | console.log('[afterAll - 0] should run'); 21 | }); 22 | 23 | afterEach(() => { 24 | console.log('[afterEach] should run'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/lifecycle/fixtures/error/afterEach.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, afterEach, describe, expect, it } from '@rstest/core'; 2 | 3 | describe('test afterEach error', () => { 4 | it('it in level A', () => { 5 | console.log('[test] should run'); 6 | expect(1 + 1).toBe(2); 7 | }); 8 | 9 | afterAll(() => { 10 | console.log('[afterAll] should run'); 11 | }); 12 | 13 | afterEach(() => { 14 | console.log('[afterEach - 2] should not run'); 15 | }); 16 | 17 | // biome-ignore lint/suspicious/noDuplicateTestHooks: test 18 | afterEach(() => { 19 | throw new Error('afterEach error'); 20 | }); 21 | 22 | // biome-ignore lint/suspicious/noDuplicateTestHooks: test 23 | afterEach(() => { 24 | console.log('[afterEach - 0] should run'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/lifecycle/fixtures/error/beforeAll.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | afterAll, 3 | afterEach, 4 | beforeAll, 5 | beforeEach, 6 | describe, 7 | expect, 8 | it, 9 | } from '@rstest/core'; 10 | 11 | describe('test beforeAll error', () => { 12 | beforeAll(() => { 13 | console.log('[beforeAll - 0] should run'); 14 | }); 15 | 16 | // biome-ignore lint/suspicious/noDuplicateTestHooks: test 17 | beforeAll(() => { 18 | throw new Error('beforeAll error'); 19 | }); 20 | 21 | // biome-ignore lint/suspicious/noDuplicateTestHooks: test 22 | beforeAll(() => { 23 | console.log('[beforeAll - 2] should not run'); 24 | }); 25 | 26 | beforeEach(() => { 27 | console.log('[beforeEach] should not run'); 28 | }); 29 | 30 | it('it in level A', () => { 31 | console.log('[test] should not run'); 32 | expect(1 + 1).toBe(2); 33 | }); 34 | 35 | afterEach(() => { 36 | console.log('[afterEach] should not run'); 37 | }); 38 | 39 | afterAll(() => { 40 | console.log('[afterAll] should run'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/lifecycle/fixtures/error/beforeAllRoot.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | afterAll, 3 | afterEach, 4 | beforeAll, 5 | beforeEach, 6 | describe, 7 | expect, 8 | it, 9 | } from '@rstest/core'; 10 | 11 | beforeAll(() => { 12 | throw new Error('beforeAll error'); 13 | }); 14 | 15 | afterAll(() => { 16 | console.log('[afterAll] should run root'); 17 | }); 18 | 19 | describe('test beforeAll error', () => { 20 | beforeAll(() => { 21 | console.log('[beforeAll - 0] should not run'); 22 | }); 23 | 24 | beforeEach(() => { 25 | console.log('[beforeEach] should not run'); 26 | }); 27 | 28 | it('it in level A', () => { 29 | console.log('[test] should not run'); 30 | expect(1 + 1).toBe(2); 31 | }); 32 | 33 | it('it in level A - B', () => { 34 | console.log('[test -1] should not run'); 35 | expect(1 + 1).toBe(2); 36 | }); 37 | 38 | afterEach(() => { 39 | console.log('[afterEach] should not run'); 40 | }); 41 | 42 | afterAll(() => { 43 | console.log('[afterAll -1] should not run'); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/lifecycle/fixtures/error/beforeEach.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | afterAll, 3 | afterEach, 4 | beforeAll, 5 | beforeEach, 6 | describe, 7 | expect, 8 | it, 9 | } from '@rstest/core'; 10 | 11 | describe('test beforeEach error', () => { 12 | it('it in level A', () => { 13 | console.log('[test] should not run'); 14 | expect(1 + 1).toBe(2); 15 | }); 16 | 17 | beforeAll(() => { 18 | console.log('[beforeAll] should run'); 19 | }); 20 | 21 | beforeEach(() => { 22 | console.log('[beforeEach - 0] should run'); 23 | }); 24 | 25 | // biome-ignore lint/suspicious/noDuplicateTestHooks: test 26 | beforeEach(() => { 27 | throw new Error('beforeEach error'); 28 | }); 29 | 30 | // biome-ignore lint/suspicious/noDuplicateTestHooks: test 31 | beforeEach(() => { 32 | console.log('[beforeEach - 2] should not run'); 33 | }); 34 | 35 | afterAll(() => { 36 | console.log('[afterAll] should run'); 37 | }); 38 | 39 | afterEach(() => { 40 | console.log('[afterEach] should run'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/lifecycle/fixtures/skip.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | afterAll, 3 | afterEach, 4 | beforeAll, 5 | beforeEach, 6 | describe, 7 | expect, 8 | it, 9 | } from '@rstest/core'; 10 | 11 | beforeAll(() => { 12 | console.log('[beforeAll] should not run root'); 13 | }); 14 | 15 | afterAll(() => { 16 | console.log('[afterAll] should not run root'); 17 | }); 18 | 19 | describe('level A', () => { 20 | beforeAll(() => { 21 | console.log('[beforeAll] should not run'); 22 | }); 23 | 24 | beforeEach(() => { 25 | console.log('[beforeEach] should not run'); 26 | }); 27 | 28 | it.skip('it in level A', () => { 29 | expect(2 + 2).toBe(4); 30 | }); 31 | 32 | it.todo('it in level A', () => { 33 | expect(2 + 2).toBe(4); 34 | }); 35 | 36 | afterEach(() => { 37 | console.log('[afterEach] should not run'); 38 | }); 39 | 40 | afterAll(() => { 41 | console.log('[afterAll] should not run'); 42 | }); 43 | }); 44 | 45 | it.skip('it in level B', () => { 46 | expect(2 + 2).toBe(4); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/lifecycle/fixtures/timeout.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, beforeEach, describe, expect, it } from '@rstest/core'; 2 | import { sleep } from '../../scripts'; 3 | 4 | beforeAll(async () => { 5 | console.log('[beforeAll] root'); 6 | await sleep(100); 7 | }, 10); 8 | 9 | describe('level A', () => { 10 | it('it in level A', () => { 11 | expect(1 + 1).toBe(2); 12 | }); 13 | 14 | beforeEach(() => { 15 | console.log('[beforeEach] in level A'); 16 | }); 17 | 18 | describe('level B-A', () => { 19 | it('it in level B-A', () => { 20 | expect(2 + 1).toBe(3); 21 | }); 22 | 23 | beforeEach(() => { 24 | console.log('[beforeEach] in level B-A'); 25 | }); 26 | }); 27 | 28 | describe('level B-B', () => { 29 | it('it in level B-B', () => { 30 | expect(2 + 2).toBe(4); 31 | }); 32 | 33 | beforeEach(() => { 34 | console.log('[beforeEach] in level B-B'); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/lifecycle/index.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { describe, expect, it } from '@rstest/core'; 4 | import { runRstestCli } from '../scripts'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | it('cleanup function should be invoked in the correct order', async () => { 10 | const { cli } = await runRstestCli({ 11 | command: 'rstest', 12 | args: ['run', 'cleanup.test'], 13 | options: { 14 | nodeOptions: { 15 | cwd: __dirname, 16 | }, 17 | }, 18 | }); 19 | 20 | await cli.exec; 21 | const logs = cli.stdout.split('\n').filter(Boolean); 22 | 23 | expect( 24 | logs.filter((log) => log.startsWith('[before') || log.startsWith('[after')), 25 | ).toEqual([ 26 | '[beforeEach] cleanup root', 27 | '[beforeEach] cleanup in level A', 28 | 29 | '[beforeEach] cleanup root', 30 | '[beforeEach] cleanup in level A', 31 | '[beforeAll] cleanup in level B-A', 32 | 33 | '[beforeEach] cleanup root', 34 | '[beforeEach] cleanup in level A', 35 | '[beforeAll] cleanup in level B-B', 36 | 37 | '[beforeAll] cleanup in level A', 38 | 39 | '[afterAll] root', 40 | '[beforeAll] cleanup root', 41 | '[beforeAll] cleanup root1', 42 | ]); 43 | }); 44 | 45 | describe('skipped', () => { 46 | it('should not run hooks when no test case execution', async () => { 47 | const { cli } = await runRstestCli({ 48 | command: 'rstest', 49 | args: ['run', 'skip.test'], 50 | options: { 51 | nodeOptions: { 52 | cwd: __dirname, 53 | }, 54 | }, 55 | }); 56 | 57 | await cli.exec; 58 | const logs = cli.stdout.split('\n').filter(Boolean); 59 | 60 | expect(logs.find((log) => log.includes('[afterAll]'))).toBeFalsy(); 61 | expect(logs.find((log) => log.includes('[beforeAll]'))).toBeFalsy(); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /tests/lifecycle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@rstest/tests-life-cycle", 4 | "version": "1.0.0", 5 | "devDependencies": { 6 | "@rstest/core": "workspace:*" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/lifecycle/rstest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@rstest/core'; 2 | 3 | export default defineConfig({ 4 | include: ['**/fixtures/**'], 5 | }); 6 | -------------------------------------------------------------------------------- /tests/lifecycle/timeout.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { describe, expect, it } from '@rstest/core'; 4 | import { runRstestCli } from '../scripts'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | describe('test timeout', () => { 10 | it('should throw timeout error when hook timeout', async () => { 11 | const { cli } = await runRstestCli({ 12 | command: 'rstest', 13 | args: ['run', 'timeout.test'], 14 | options: { 15 | nodeOptions: { 16 | cwd: __dirname, 17 | }, 18 | }, 19 | }); 20 | 21 | await cli.exec; 22 | expect(cli.exec.process?.exitCode).toBe(1); 23 | const logs = cli.stdout.split('\n').filter(Boolean); 24 | 25 | expect(logs.filter((log) => log.startsWith('['))).toMatchInlineSnapshot(` 26 | [ 27 | "[beforeAll] root", 28 | ] 29 | `); 30 | 31 | expect( 32 | logs.find((log) => 33 | log.includes('Error: beforeAll hook timed out in 10ms'), 34 | ), 35 | ).toBeTruthy(); 36 | expect( 37 | logs.find((log) => log.includes('timeout.test.ts:4:10')), 38 | ).toBeTruthy(); 39 | expect( 40 | logs.find((log) => log.includes('Test Files 1 failed')), 41 | ).toBeTruthy(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /tests/list/fixtures/a.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@rstest/core'; 2 | 3 | describe('test a', () => { 4 | it('test a-1', () => { 5 | expect(1 + 1).toBe(2); 6 | }); 7 | }); 8 | 9 | it('test a-2', () => { 10 | expect(2 - 1).toBe(1); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/list/fixtures/b.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@rstest/core'; 2 | 3 | describe('test b', () => { 4 | it('test b-1', () => { 5 | expect(1 + 1).toBe(2); 6 | }); 7 | }); 8 | 9 | it('test b-2', () => { 10 | expect(2 - 1).toBe(1); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/log/fixtures/consoleLogFalse.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@rstest/core'; 2 | 3 | export default defineConfig({ 4 | onConsoleLog: () => false, 5 | }); 6 | -------------------------------------------------------------------------------- /tests/log/fixtures/log.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@rstest/core'; 2 | 3 | test('log output', () => { 4 | console.log("I'm log"); 5 | }); 6 | 7 | test('warn output', () => { 8 | console.warn("I'm warn"); 9 | }); 10 | 11 | test('error output', () => { 12 | console.error("I'm error"); 13 | }); 14 | 15 | test('info output', () => { 16 | console.info("I'm info"); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/log/fixtures/logSrc.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@rstest/core'; 2 | 3 | test('src log output', async () => { 4 | await import('./src/index'); 5 | expect(1).toBe(1); 6 | }); 7 | -------------------------------------------------------------------------------- /tests/log/fixtures/src/index.ts: -------------------------------------------------------------------------------- 1 | console.log("I'm src log"); 2 | 3 | export {}; 4 | -------------------------------------------------------------------------------- /tests/log/fixtures/trace.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@rstest/core'; 2 | 3 | test('trace output', () => { 4 | console.trace("I'm trace"); 5 | }); 6 | -------------------------------------------------------------------------------- /tests/log/index.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { describe, expect, it } from '@rstest/core'; 4 | import { runRstestCli } from '../scripts'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | describe('console log', () => { 10 | it('should not console log when onConsoleLog return false', async () => { 11 | const { cli } = await runRstestCli({ 12 | command: 'rstest', 13 | args: ['run', 'log.test', '-c', 'consoleLogFalse.config.ts'], 14 | options: { 15 | nodeOptions: { 16 | cwd: join(__dirname, 'fixtures'), 17 | }, 18 | }, 19 | }); 20 | 21 | await cli.exec; 22 | const logs = cli.stdout.split('\n').filter(Boolean); 23 | 24 | expect(logs.filter((log) => log.startsWith('I'))).toEqual([]); 25 | }); 26 | 27 | it('should onConsoleLog will not take effect when disableConsoleIntercept', async () => { 28 | const { cli } = await runRstestCli({ 29 | command: 'rstest', 30 | args: [ 31 | 'run', 32 | 'log.test', 33 | '-c', 34 | 'consoleLogFalse.config.ts', 35 | '--disableConsoleIntercept', 36 | ], 37 | options: { 38 | nodeOptions: { 39 | cwd: join(__dirname, 'fixtures'), 40 | }, 41 | }, 42 | }); 43 | 44 | await cli.exec; 45 | const logs = cli.stdout.split('\n').filter(Boolean); 46 | 47 | expect(logs.some((log) => log.startsWith('I'))).toBeTruthy(); 48 | expect(logs.some((log) => log.includes('log.test.ts:4:11'))).toBeFalsy(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/mock/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@rstest/test-mock", 4 | "version": "1.0.0", 5 | "devDependencies": { 6 | "@rstest/core": "workspace:*", 7 | "picocolors": "^1.1.1", 8 | "strip-ansi": "^7.1.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/mock/src/b.ts: -------------------------------------------------------------------------------- 1 | export const b = 2; 2 | -------------------------------------------------------------------------------- /tests/mock/src/index.ts: -------------------------------------------------------------------------------- 1 | export { b } from './b'; 2 | export const a = 1; 3 | -------------------------------------------------------------------------------- /tests/mock/tests/external.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it, rstest } from '@rstest/core'; 2 | 3 | rstest.mock('picocolors', () => { 4 | return { 5 | sayHi: () => 'hi', 6 | }; 7 | }); 8 | 9 | // TODO 10 | it.todo('should mock external module correctly', async () => { 11 | // @ts-expect-error 12 | const { sayHi } = await import('picocolors'); 13 | 14 | expect(sayHi?.()).toBe('hi'); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/mock/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it, rstest } from '@rstest/core'; 2 | 3 | rstest.mock('../src/b', () => { 4 | return { 5 | b: 3, 6 | }; 7 | }); 8 | 9 | // TODO 10 | it.todo('should mock relative path module correctly', async () => { 11 | const { b } = await import('../src/index'); 12 | expect(b).toBe(3); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/noTests/fixtures/noTestInSuite.test.ts: -------------------------------------------------------------------------------- 1 | import { describe } from '@rstest/core'; 2 | 3 | describe('no tests', () => {}); 4 | -------------------------------------------------------------------------------- /tests/noTests/fixtures/noTests.test.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-infra-dev/rstest/046266ebc142de8df3a12a4553411c08b2a52f9b/tests/noTests/fixtures/noTests.test.ts -------------------------------------------------------------------------------- /tests/noTests/fixtures/suiteFnUndefined.test.ts: -------------------------------------------------------------------------------- 1 | import { describe } from '@rstest/core'; 2 | 3 | describe('no tests'); 4 | -------------------------------------------------------------------------------- /tests/noTests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { describe, expect, it } from '@rstest/core'; 4 | import { runRstestCli } from '../scripts'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | describe('no tests', () => { 10 | it('should error when no tests', async () => { 11 | const { cli } = await runRstestCli({ 12 | command: 'rstest', 13 | args: ['run', 'fixtures/'], 14 | options: { 15 | nodeOptions: { 16 | cwd: __dirname, 17 | }, 18 | }, 19 | }); 20 | 21 | await cli.exec; 22 | 23 | expect(cli.exec.process?.exitCode).toBe(1); 24 | 25 | const logs = cli.stdout.split('\n').filter(Boolean); 26 | 27 | expect( 28 | logs.find((log) => log.includes('Test Files 3 failed')), 29 | ).toBeDefined(); 30 | expect(logs.find((log) => log.includes('Tests no tests'))).toBeDefined(); 31 | }); 32 | 33 | it('should passWithNoTests with passWithNoTests flag', async () => { 34 | const { cli } = await runRstestCli({ 35 | command: 'rstest', 36 | args: ['run', 'fixtures/', '--passWithNoTests'], 37 | options: { 38 | nodeOptions: { 39 | cwd: __dirname, 40 | }, 41 | }, 42 | }); 43 | 44 | await cli.exec; 45 | 46 | expect(cli.exec.process?.exitCode).toBe(0); 47 | 48 | const logs = cli.stdout.split('\n').filter(Boolean); 49 | 50 | expect( 51 | logs.find((log) => log.includes('Test Files 3 passed')), 52 | ).toBeDefined(); 53 | expect(logs.find((log) => log.includes('Tests no tests'))).toBeDefined(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@rstest/tests", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "test": "rstest run" 7 | }, 8 | "devDependencies": { 9 | "@rsbuild/core": "^1.3.22", 10 | "@rslib/core": "0.9.1", 11 | "@rstest/core": "workspace:*", 12 | "jest-image-snapshot": "^6.5.1", 13 | "strip-ansi": "^7.1.0", 14 | "tinyexec": "^1.0.1", 15 | "typescript": "^5.8.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/reporter/fixtures/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@rstest/core'; 2 | 3 | describe('basic', () => { 4 | it('a', () => { 5 | expect(1 + 1).toBe(2); 6 | }); 7 | 8 | it('b', () => { 9 | expect(1 + 1).not.toBe(2); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/reporter/index.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { describe, expect, it } from '@rstest/core'; 4 | import { runRstestCli } from '../scripts'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | describe.concurrent('reporters', () => { 10 | it('default', async () => { 11 | const { cli } = await runRstestCli({ 12 | command: 'rstest', 13 | args: ['run'], 14 | options: { 15 | nodeOptions: { 16 | cwd: __dirname, 17 | }, 18 | }, 19 | }); 20 | 21 | await cli.exec; 22 | expect(cli.stdout).toContain('✗ basic > b'); 23 | }); 24 | 25 | it('custom', async () => { 26 | const { cli } = await runRstestCli({ 27 | command: 'rstest', 28 | args: ['run', '-c', './rstest.customReporterConfig.ts'], 29 | options: { 30 | nodeOptions: { 31 | cwd: __dirname, 32 | }, 33 | }, 34 | }); 35 | 36 | await cli.exec; 37 | expect(cli.stdout).toContain('[custom reporter] onTestFileStart'); 38 | expect( 39 | cli.stdout.match(/\[custom reporter\] onTestCaseResult/g)?.length, 40 | ).toBe(2); 41 | expect(cli.stdout).toContain('[custom reporter] onTestRunEnd'); 42 | }); 43 | 44 | it('empty', async () => { 45 | const { cli } = await runRstestCli({ 46 | command: 'rstest', 47 | args: ['run', '-c', './rstest.emptyReporterConfig.ts'], 48 | options: { 49 | nodeOptions: { 50 | cwd: __dirname, 51 | }, 52 | }, 53 | }); 54 | 55 | await cli.exec; 56 | expect(cli.stdout).not.toContain('✗ basic > b'); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /tests/reporter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@rstest/tests-repoter", 4 | "version": "1.0.0", 5 | "devDependencies": { 6 | "@rstest/core": "workspace:*" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/reporter/rstest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@rstest/core'; 2 | 3 | export default defineConfig({ 4 | include: ['**/fixtures/**'], 5 | }); 6 | -------------------------------------------------------------------------------- /tests/reporter/rstest.customReporterConfig.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@rstest/core'; 2 | import type { Reporter } from '@rstest/core/node'; 3 | 4 | export const reporterResult: string[] = []; 5 | 6 | class MyReporter implements Reporter { 7 | onTestFileStart(file) { 8 | reporterResult.push('[custom reporter] onTestFileStart'); 9 | } 10 | 11 | onTestCaseResult(result) { 12 | reporterResult.push('[custom reporter] onTestCaseResult'); 13 | } 14 | 15 | onTestRunEnd({ results, testResults }) { 16 | reporterResult.push('[custom reporter] onTestRunEnd'); 17 | console.log(reporterResult); 18 | } 19 | } 20 | 21 | export default defineConfig({ 22 | include: ['**/fixtures/**'], 23 | reporters: [new MyReporter()], 24 | }); 25 | -------------------------------------------------------------------------------- /tests/reporter/rstest.emptyReporterConfig.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@rstest/core'; 2 | 3 | export default defineConfig({ 4 | include: ['**/fixtures/**'], 5 | reporters: [], 6 | }); 7 | -------------------------------------------------------------------------------- /tests/rstest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@rstest/core'; 2 | 3 | export default defineConfig({ 4 | setupFiles: ['../scripts/rstest.setup.ts'], 5 | testTimeout: process.env.CI ? 10_000 : 5_000, 6 | slowTestThreshold: 2_000, 7 | exclude: [ 8 | '**/node_modules/**', 9 | '**/dist/**', 10 | '**/fixtures/**', 11 | '**/fixtures-test/**', 12 | ], 13 | }); 14 | -------------------------------------------------------------------------------- /tests/runner/test/async.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, describe, expect, it } from '@rstest/core'; 2 | import { sleep } from '../../scripts/utils'; 3 | 4 | const logs: string[] = []; 5 | 6 | afterAll(() => { 7 | expect(logs).toEqual([ 8 | 'run 0-0', 9 | 'run 0-1-0', 10 | 'run 0-1-1-0', 11 | 'run 0-2-0', 12 | 'run 0-3', 13 | 'run 1-0', 14 | 'run 2-0-0', 15 | 'run 2-1', 16 | 'run 3-0', 17 | ]); 18 | }); 19 | 20 | describe('should run async suite in the correct order', () => { 21 | describe('0', async () => { 22 | await sleep(100); 23 | 24 | it('0-0', () => { 25 | logs.push('run 0-0'); 26 | expect(1 + 1).toBe(2); 27 | }); 28 | 29 | describe('0-1', async () => { 30 | it('0-1-0', () => { 31 | logs.push('run 0-1-0'); 32 | expect(1 + 1).toBe(2); 33 | }); 34 | 35 | describe('0-1-1', () => { 36 | it('0-1-1-0', () => { 37 | logs.push('run 0-1-1-0'); 38 | expect(1 + 1).toBe(2); 39 | }); 40 | }); 41 | }); 42 | 43 | describe('0-2', () => { 44 | it('0-2-0', () => { 45 | logs.push('run 0-2-0'); 46 | expect(1 + 1).toBe(2); 47 | }); 48 | }); 49 | 50 | it('0-3', () => { 51 | logs.push('run 0-3'); 52 | expect(1 + 1).toBe(2); 53 | }); 54 | }); 55 | 56 | it('1', () => { 57 | logs.push('run 1-0'); 58 | expect(1 + 1).toBe(2); 59 | }); 60 | 61 | describe('2', () => { 62 | describe('2-0', async () => { 63 | it('2-0-0', () => { 64 | logs.push('run 2-0-0'); 65 | expect(1 + 1).toBe(2); 66 | }); 67 | }); 68 | 69 | it('2-1', () => { 70 | logs.push('run 2-1'); 71 | expect(1 + 1).toBe(2); 72 | }); 73 | }); 74 | 75 | it('3', () => { 76 | logs.push('run 3-0'); 77 | expect(1 + 1).toBe(2); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /tests/runner/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@rstest/core'; 2 | 3 | it('should allow run test without suite wrapped', () => { 4 | expect(1 + 1).toBe(2); 5 | }); 6 | 7 | describe('Test Suite', () => { 8 | it('should allow run test in suite', () => { 9 | expect(1 + 1).toBe(2); 10 | }); 11 | 12 | describe('Test Suite Nested', () => { 13 | it('should allow run test in nested suite', () => { 14 | expect(1 + 1).toBe(2); 15 | }); 16 | }); 17 | 18 | it('should allow run test in suite - 1', () => { 19 | expect(1 + 1).toBe(2); 20 | }); 21 | }); 22 | 23 | describe('Test Suite - 1', () => { 24 | it('should ok', () => { 25 | expect(1 + 1).toBe(2); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/scripts/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import { expect } from '@rstest/core'; 3 | 4 | export const getTestName = (log: string, prefix: string) => 5 | log.slice(0, log.lastIndexOf('(')).split(prefix)[1].trim(); 6 | 7 | export const expectFile = (filePath: string, timeout = 3000) => 8 | expect 9 | .poll(() => fs.existsSync(filePath), { 10 | timeout, 11 | }) 12 | .toBeTruthy(); 13 | 14 | export const sleep = (ms: number) => { 15 | return new Promise((resolve) => { 16 | setTimeout(resolve, ms); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /tests/setup/fixtures/basic/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from '@rstest/core'; 2 | 3 | it('should run setup file correctly', () => { 4 | expect(process.env.RETEST_SETUP_FLAG).toBe('1'); 5 | expect(process.env.NODE_ENV).toBe('rstest:production'); 6 | }); 7 | 8 | it('addSnapshotSerializer should works', () => { 9 | expect(__filename).toMatchInlineSnapshot(`"/basic/index.test.ts"`); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/setup/fixtures/basic/rstest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@rstest/core'; 2 | 3 | export default defineConfig({ 4 | passWithNoTests: true, 5 | setupFiles: ['./rstest.setup.ts'], 6 | exclude: ['**/node_modules/**', '**/dist/**'], 7 | }); 8 | -------------------------------------------------------------------------------- /tests/setup/fixtures/basic/rstest.setup.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { afterAll, beforeAll, expect } from '@rstest/core'; 3 | import { createSnapshotSerializer } from 'path-serializer'; 4 | 5 | process.env.RETEST_SETUP_FLAG = '1'; 6 | 7 | process.env.NODE_ENV = 'rstest:production'; 8 | 9 | beforeAll((ctx) => { 10 | console.log('[beforeAll] root'); 11 | expect(ctx.filepath).toContain('index.test.ts'); 12 | }); 13 | 14 | expect.addSnapshotSerializer( 15 | createSnapshotSerializer({ 16 | workspace: path.join(__dirname, '..'), 17 | }), 18 | ); 19 | 20 | afterAll(() => { 21 | console.log('[afterAll] setup'); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/setup/fixtures/error/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from '@rstest/core'; 2 | 3 | it('should not run', () => { 4 | expect(1 + 1).toBe(1); 5 | }); 6 | -------------------------------------------------------------------------------- /tests/setup/fixtures/error/rstest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@rstest/core'; 2 | 3 | export default defineConfig({ 4 | passWithNoTests: true, 5 | setupFiles: ['./rstest.setup.ts'], 6 | exclude: ['**/node_modules/**', '**/dist/**'], 7 | }); 8 | -------------------------------------------------------------------------------- /tests/setup/fixtures/error/rstest.setup.ts: -------------------------------------------------------------------------------- 1 | throw new Error('Rstest setup error'); 2 | -------------------------------------------------------------------------------- /tests/setup/index.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join, sep } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { describe, expect, it } from '@rstest/core'; 4 | import { runRstestCli } from '../scripts'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | describe('test setup file', async () => { 10 | it('should run setup file correctly', async () => { 11 | const { cli } = await runRstestCli({ 12 | command: 'rstest', 13 | args: ['run'], 14 | options: { 15 | nodeOptions: { 16 | cwd: join(__dirname, 'fixtures/basic'), 17 | }, 18 | }, 19 | }); 20 | 21 | await cli.exec; 22 | const logs = cli.stdout 23 | .split('\n') 24 | .filter((log) => log.startsWith('[afterAll]')); 25 | expect(cli.exec.process?.exitCode).toBe(0); 26 | expect(logs).toEqual(['[afterAll] setup']); 27 | }); 28 | 29 | it('should test error when run setup file failed', async () => { 30 | const { cli } = await runRstestCli({ 31 | command: 'rstest', 32 | args: ['run'], 33 | options: { 34 | nodeOptions: { 35 | cwd: join(__dirname, 'fixtures/error'), 36 | }, 37 | }, 38 | }); 39 | 40 | await cli.exec; 41 | expect(cli.exec.process?.exitCode).toBe(1); 42 | const logs = cli.stdout.split('\n').filter(Boolean); 43 | // test error log 44 | expect(logs.find((log) => log.includes('Rstest setup error'))).toBeTruthy(); 45 | expect( 46 | logs.find((log) => log.includes('rstest.setup.ts:1:7')), 47 | ).toBeTruthy(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/snapshot/__image_snapshots__/extend-test-ts-test-to-match-image-snapshot-correctly-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-infra-dev/rstest/046266ebc142de8df3a12a4553411c08b2a52f9b/tests/snapshot/__image_snapshots__/extend-test-ts-test-to-match-image-snapshot-correctly-1-snap.png -------------------------------------------------------------------------------- /tests/snapshot/__snapshots__/file.output.txt: -------------------------------------------------------------------------------- 1 | hello world -------------------------------------------------------------------------------- /tests/snapshot/__snapshots__/file.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Snapshot v1 2 | 3 | exports[`test snapshot > test snapshot file state > test toMatchSnapshot API - 1 1`] = `"hello world - 1"`; 4 | 5 | exports[`test snapshot > test snapshot file state > test toMatchSnapshot API 1`] = `"hello world"`; 6 | 7 | exports[`test snapshot > test snapshot file state > test toMatchSnapshot API 2`] = `"hello Rstest"`; 8 | 9 | exports[`test snapshot > test snapshot file state > test toMatchSnapshot API 3`] = `"hello world 1"`; 10 | 11 | exports[`test snapshot > test snapshot file state > test toMatchSnapshot name > say hi 1`] = `"hi"`; 12 | -------------------------------------------------------------------------------- /tests/snapshot/extend.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import { join } from 'node:path'; 3 | import { expect, it } from '@rstest/core'; 4 | 5 | declare module '@rstest/core' { 6 | interface Assertion { 7 | toMatchImageSnapshot(): void; 8 | } 9 | } 10 | 11 | it('test toMatchImageSnapshot correctly', async () => { 12 | const { toMatchImageSnapshot } = await import('jest-image-snapshot'); 13 | 14 | expect.extend({ toMatchImageSnapshot }); 15 | const testFilePath = join(__dirname, '../assets/icon.png'); 16 | 17 | expect(fs.readFileSync(testFilePath)).toMatchImageSnapshot(); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/snapshot/fail.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { expect, it } from '@rstest/core'; 4 | import { runRstestCli } from '../scripts'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | it('should failed when snapshot unmatched', async () => { 10 | const { cli } = await runRstestCli({ 11 | command: 'rstest', 12 | args: ['run', 'fixtures/fail.test.ts'], 13 | options: { 14 | nodeOptions: { 15 | cwd: __dirname, 16 | }, 17 | }, 18 | }); 19 | 20 | await cli.exec; 21 | expect(cli.exec.process?.exitCode).toBe(1); 22 | 23 | const logs = cli.stdout.split('\n').filter(Boolean); 24 | 25 | expect(logs.find((log) => log.includes('Snapshots 1 failed'))).toBeTruthy(); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/snapshot/fixtures/__snapshots__/fail.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Snapshot v1 2 | 3 | exports[`test snapshot > should failed when snapshot unmatched 1`] = `"hello world"`; 4 | -------------------------------------------------------------------------------- /tests/snapshot/fixtures/__snapshots__/obsolete.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Snapshot v1 2 | 3 | exports[`test snapshot obsolete > test snapshot generate 1`] = `"hello world"`; 4 | -------------------------------------------------------------------------------- /tests/snapshot/fixtures/__snapshots__/skip.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Snapshot v1 2 | 3 | exports[`test snapshot obsolete > test snapshot generate 1`] = `"hello world"`; 4 | -------------------------------------------------------------------------------- /tests/snapshot/fixtures/fail.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@rstest/core'; 2 | 3 | describe('test snapshot', () => { 4 | it('should failed when snapshot unmatched', () => { 5 | expect('hello world!').toMatchSnapshot(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/snapshot/fixtures/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@rstest/core'; 2 | 3 | describe('test snapshot', () => { 4 | it('test snapshot generate', () => { 5 | expect('hello world').toMatchSnapshot(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/snapshot/fixtures/inlineSnapshot.each.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@rstest/core'; 2 | 3 | describe('test inlineSnapshot in each', () => { 4 | it.each([ 5 | { a: 1, b: 1 }, 6 | { a: 1, b: 2 }, 7 | { a: 2, b: 1 }, 8 | ])('add two numbers correctly', ({ a, b }) => { 9 | expect(a + b).toMatchInlineSnapshot(); 10 | }); 11 | }); 12 | 13 | describe.each([ 14 | { a: 1, b: 1, expected: 2 }, 15 | { a: 1, b: 2, expected: 3 }, 16 | { a: 2, b: 1, expected: 3 }, 17 | ])('add two numbers correctly', ({ a, b, expected }) => { 18 | it(`should return ${expected}`, () => { 19 | expect(a + b).toMatchInlineSnapshot(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/snapshot/fixtures/obsolete.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@rstest/core'; 2 | 3 | describe('test snapshot obsolete', () => { 4 | it('test snapshot generate', () => { 5 | expect(1).toBe(1); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/snapshot/fixtures/skip.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@rstest/core'; 2 | 3 | describe('test snapshot obsolete', () => { 4 | it.skip('test snapshot generate', () => { 5 | expect(1).toBe(1); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/snapshot/index.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { dirname } from 'node:path'; 3 | import { fileURLToPath } from 'node:url'; 4 | import { describe, expect, it } from '@rstest/core'; 5 | import { createSnapshotSerializer } from 'path-serializer'; 6 | import { runRstestCli } from '../scripts'; 7 | 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = dirname(__filename); 10 | 11 | describe('test snapshot', () => { 12 | it('test toMatchInlineSnapshot API', () => { 13 | expect('hello world').toMatchInlineSnapshot(`"hello world"`); 14 | expect({ a: 1, b: 2 }).toMatchInlineSnapshot(` 15 | { 16 | "a": 1, 17 | "b": 2, 18 | } 19 | `); 20 | }); 21 | 22 | it('test custom serializer', () => { 23 | expect.addSnapshotSerializer( 24 | createSnapshotSerializer({ 25 | workspace: path.join(__dirname, '..'), 26 | }), 27 | ); 28 | expect(__filename).toMatchInlineSnapshot( 29 | `"/snapshot/index.test.ts"`, 30 | ); 31 | }); 32 | 33 | it('should failed when use inline snapshot in each', async () => { 34 | const { cli } = await runRstestCli({ 35 | command: 'rstest', 36 | args: ['run', 'fixtures/inlineSnapshot.each.test.ts'], 37 | options: { 38 | nodeOptions: { 39 | cwd: __dirname, 40 | }, 41 | }, 42 | }); 43 | 44 | await cli.exec; 45 | expect(cli.exec.process?.exitCode).toBe(1); 46 | 47 | const logs = cli.stdout.split('\n').filter(Boolean); 48 | 49 | expect( 50 | logs.find((log) => 51 | log.includes( 52 | 'InlineSnapshot cannot be used inside of test.each or describe.each', 53 | ), 54 | ), 55 | ).toBeTruthy(); 56 | 57 | expect(logs.find((log) => log.includes('Tests 6 failed'))).toBeTruthy(); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /tests/snapshot/obsolete.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { dirname } from 'node:path'; 3 | import { fileURLToPath } from 'node:url'; 4 | import { describe, expect, it } from '@rstest/core'; 5 | import { createSnapshotSerializer } from 'path-serializer'; 6 | import { runRstestCli } from '../scripts'; 7 | 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = dirname(__filename); 10 | 11 | describe('test snapshot', () => { 12 | it('should mark snapshot obsolete ', async () => { 13 | const { cli } = await runRstestCli({ 14 | command: 'rstest', 15 | args: ['run', 'fixtures/obsolete.test.ts'], 16 | options: { 17 | nodeOptions: { 18 | cwd: __dirname, 19 | }, 20 | }, 21 | }); 22 | 23 | await cli.exec; 24 | expect(cli.exec.process?.exitCode).toBe(0); 25 | 26 | const logs = cli.stdout.split('\n').filter(Boolean); 27 | 28 | expect(logs.find((log) => log.includes('1 obsolete'))).toBeTruthy(); 29 | }); 30 | 31 | it('should not mark snapshot obsolete when case skipped', async () => { 32 | const { cli } = await runRstestCli({ 33 | command: 'rstest', 34 | args: ['run', 'fixtures/skip.test.ts'], 35 | options: { 36 | nodeOptions: { 37 | cwd: __dirname, 38 | }, 39 | }, 40 | }); 41 | 42 | await cli.exec; 43 | expect(cli.exec.process?.exitCode).toBe(0); 44 | 45 | const logs = cli.stdout.split('\n').filter(Boolean); 46 | 47 | expect(logs.find((log) => log.includes('1 obsolete'))).toBeFalsy(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/spy/clearAllMocks.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, describe, expect, it, rstest } from '@rstest/core'; 2 | 3 | describe('clearAllMocks', () => { 4 | const sayHi = rstest.fn(); 5 | 6 | afterEach(() => { 7 | rstest.clearAllMocks(); 8 | }); 9 | it('spy', () => { 10 | sayHi.mockImplementation(() => 'hi'); 11 | 12 | expect(sayHi('bob')).toBe('hi'); 13 | 14 | expect(sayHi).toHaveBeenCalledTimes(1); 15 | }); 16 | 17 | it('spy - 1', () => { 18 | sayHi.mockImplementation(() => 'hello'); 19 | 20 | expect(sayHi('bob')).toBe('hello'); 21 | 22 | expect(sayHi).toHaveBeenCalledTimes(1); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/spy/config.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { expect, it } from '@rstest/core'; 4 | import { runRstestCli } from '../scripts'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | it('test clearMocks config', async () => { 10 | const { cli } = await runRstestCli({ 11 | command: 'rstest', 12 | args: ['run', 'fixtures/clearMocks.test', '--clearMocks'], 13 | options: { 14 | nodeOptions: { 15 | cwd: __dirname, 16 | }, 17 | }, 18 | }); 19 | 20 | await cli.exec; 21 | expect(cli.exec.process?.exitCode).toBe(0); 22 | }); 23 | 24 | it('test restoreMocks config', async () => { 25 | const { cli } = await runRstestCli({ 26 | command: 'rstest', 27 | args: ['run', 'fixtures/restoreMocks.test', '--restoreMocks'], 28 | options: { 29 | nodeOptions: { 30 | cwd: __dirname, 31 | }, 32 | }, 33 | }); 34 | 35 | await cli.exec; 36 | expect(cli.exec.process?.exitCode).toBe(0); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/spy/fixtures/clearMocks.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, rstest } from '@rstest/core'; 2 | 3 | describe('auto clearMocks', () => { 4 | const sayHi = rstest.fn(); 5 | 6 | it('spy', () => { 7 | sayHi.mockImplementation(() => 'hi'); 8 | 9 | expect(sayHi('bob')).toBe('hi'); 10 | 11 | expect(sayHi).toHaveBeenCalledTimes(1); 12 | }); 13 | 14 | it('spy - 1', () => { 15 | sayHi.mockImplementation(() => 'hello'); 16 | 17 | expect(sayHi('bob')).toBe('hello'); 18 | 19 | expect(sayHi).toHaveBeenCalledTimes(1); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/spy/fixtures/restoreMocks.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, rstest } from '@rstest/core'; 2 | 3 | describe('auto restoreMocks', () => { 4 | const hi = { 5 | sayHi: () => 'hi', 6 | }; 7 | 8 | it('spy', () => { 9 | const spy = rstest.spyOn(hi, 'sayHi'); 10 | 11 | spy.mockImplementation(() => 'hello'); 12 | 13 | expect(hi.sayHi()).toBe('hello'); 14 | }); 15 | 16 | it('spy - 1', () => { 17 | expect(hi.sayHi()).toBe('hi'); 18 | expect(rstest.isMockFunction(hi.sayHi)).toBeFalsy(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/spy/invocationCallOrder.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it, rstest } from '@rstest/core'; 2 | 3 | it('rstest.fn -> mock.invocationCallOrder', () => { 4 | const sayHi = rstest.fn(); 5 | const sayHello = rstest.fn(); 6 | 7 | sayHi(); 8 | sayHello(); 9 | sayHi(); 10 | 11 | expect(sayHi.mock.invocationCallOrder).toEqual([1, 3]); 12 | expect(sayHello.mock.invocationCallOrder).toEqual([2]); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/spy/spyOn.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, rstest } from '@rstest/core'; 2 | 3 | describe('test spyOn', () => { 4 | it('spyOn', () => { 5 | const sayHi = () => 'hi'; 6 | const hi = { 7 | sayHi, 8 | }; 9 | const spy = rstest.spyOn(hi, 'sayHi'); 10 | 11 | expect(hi.sayHi()).toBe('hi'); 12 | 13 | expect(spy).toHaveBeenCalled(); 14 | 15 | spy.mockImplementation(() => 'hello'); 16 | 17 | expect(hi.sayHi()).toBe('hello'); 18 | 19 | spy.mockRestore(); 20 | 21 | expect(hi.sayHi()).toBe('hi'); 22 | 23 | expect(hi.sayHi).toEqual(sayHi); 24 | 25 | spy.mockImplementation(() => 'mocked'); 26 | 27 | expect(hi.sayHi()).toBe('hi'); 28 | }); 29 | 30 | it('isMockFunction', () => { 31 | const hi = { 32 | sayHi: () => 'hi', 33 | }; 34 | const spy = rstest.spyOn(hi, 'sayHi'); 35 | 36 | expect(rstest.isMockFunction(spy)).toBeTruthy(); 37 | expect(rstest.isMockFunction(hi.sayHi)).toBeTruthy(); 38 | 39 | spy.mockRestore(); 40 | expect(rstest.isMockFunction(hi.sayHi)).toBeFalsy(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/spy/stubEnv.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it, rstest } from '@rstest/core'; 2 | 3 | it('test stubEnv & unstubAllEnvs', () => { 4 | const env = process.env.NODE_ENV; 5 | const mockEnv = env === 'production' ? 'development' : 'production'; 6 | rstest.stubEnv('NODE_ENV', mockEnv); 7 | 8 | rstest.stubEnv('TEST_111', '111'); 9 | 10 | expect(process.env.NODE_ENV).toBe(mockEnv); 11 | 12 | expect(process.env.TEST_111).toBe('111'); 13 | 14 | rstest.stubEnv('NODE_ENV', undefined); 15 | expect(process.env.NODE_ENV).toBeUndefined(); 16 | 17 | rstest.unstubAllEnvs(); 18 | 19 | expect(process.env.NODE_ENV).toBe(env); 20 | expect(process.env.TEST_111).toBeUndefined(); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/spy/stubGlobal.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it, rstest } from '@rstest/core'; 2 | 3 | function checkGlobalThis() { 4 | // @ts-expect-error 5 | expect(__test_flag__).toBeTruthy(); 6 | } 7 | 8 | it('test stubGlobal & unstubAllGlobals', () => { 9 | const testFlag = '__test_flag__'; 10 | expect(globalThis[testFlag]).toBeUndefined(); 11 | 12 | rstest.stubGlobal(testFlag, true); 13 | 14 | checkGlobalThis(); 15 | 16 | expect(globalThis[testFlag]).toBeTruthy(); 17 | 18 | rstest.unstubAllGlobals(); 19 | 20 | expect(globalThis[testFlag]).toBeUndefined(); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/ssr/fixtures/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@rstest/tests-ssr", 4 | "version": "1.0.0", 5 | "dependencies": { 6 | "react": "^19.1.0", 7 | "react-dom": "^19.1.0" 8 | }, 9 | "devDependencies": { 10 | "@rsbuild/plugin-react": "^1.3.2", 11 | "@types/react": "^19.1.6", 12 | "@types/react-dom": "^19.1.6", 13 | "typescript": "^5.8.3" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/ssr/fixtures/rsbuild.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@rsbuild/core'; 2 | import { pluginReact } from '@rsbuild/plugin-react'; 3 | 4 | export default defineConfig({ 5 | plugins: [pluginReact()], 6 | }); 7 | -------------------------------------------------------------------------------- /tests/ssr/fixtures/rstest.config.ts: -------------------------------------------------------------------------------- 1 | import config from './rsbuild.config'; 2 | 3 | export default config; 4 | -------------------------------------------------------------------------------- /tests/ssr/fixtures/src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | color: #fff; 4 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 5 | background-image: linear-gradient(to bottom, #020917, #101725); 6 | } 7 | 8 | .content { 9 | display: flex; 10 | min-height: 100vh; 11 | line-height: 1.1; 12 | text-align: center; 13 | flex-direction: column; 14 | justify-content: center; 15 | } 16 | 17 | .content h1 { 18 | font-size: 3.6rem; 19 | font-weight: 700; 20 | } 21 | 22 | .content p { 23 | font-size: 1.2rem; 24 | font-weight: 400; 25 | opacity: 0.5; 26 | } 27 | -------------------------------------------------------------------------------- /tests/ssr/fixtures/src/App.tsx: -------------------------------------------------------------------------------- 1 | import './App.css'; 2 | 3 | const App = () => { 4 | return ( 5 |
6 |

Rsbuild with React

7 |

Start building amazing things with Rsbuild.

8 |
9 | ); 10 | }; 11 | 12 | export default App; 13 | -------------------------------------------------------------------------------- /tests/ssr/fixtures/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tests/ssr/fixtures/src/index.server.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOMServer from 'react-dom/server'; 3 | import App from './App'; 4 | 5 | export function render() { 6 | return ReactDOMServer.renderToString( 7 | 8 | 9 | , 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /tests/ssr/fixtures/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | 5 | ReactDOM.hydrateRoot( 6 | document.getElementById('root')!, 7 | 8 | 9 | , 10 | ); 11 | -------------------------------------------------------------------------------- /tests/ssr/fixtures/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from '@rstest/core'; 2 | import { render } from '../src/index.server'; 3 | 4 | it('ssr render', () => { 5 | expect(render()).toMatchInlineSnapshot( 6 | `"

Rsbuild with React

Start building amazing things with Rsbuild.

"`, 7 | ); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/ssr/fixtures/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["DOM", "ES2020"], 5 | "module": "ESNext", 6 | "jsx": "react-jsx", 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "isolatedModules": true, 10 | "resolveJsonModule": true, 11 | "moduleResolution": "bundler", 12 | "useDefineForClassFields": true 13 | }, 14 | "include": ["src"] 15 | } 16 | -------------------------------------------------------------------------------- /tests/ssr/index.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { describe, expect, it } from '@rstest/core'; 4 | import { runRstestCli } from '../scripts/'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | describe('test ssr', () => { 10 | it('should run ssr test succeed', async () => { 11 | const { cli } = await runRstestCli({ 12 | command: 'rstest', 13 | args: ['run', 'test/index.test.ts'], 14 | options: { 15 | nodeOptions: { 16 | cwd: join(__dirname, 'fixtures'), 17 | }, 18 | }, 19 | }); 20 | 21 | await cli.exec; 22 | 23 | const logs = cli.stdout.split('\n').filter(Boolean); 24 | 25 | expect(cli.exec.process?.exitCode).toBe(0); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/test-api/__snapshots__/concurrentContext.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Snapshot v1 2 | 3 | exports[`concurrent test 2 1`] = `"hello world"`; 4 | -------------------------------------------------------------------------------- /tests/test-api/chain.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { describe, expect, it } from '@rstest/core'; 4 | import { runRstestCli } from '../scripts'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | describe('Test Chain', () => { 10 | it('chain API enumerable', async () => { 11 | expect(Object.keys(it)).toMatchInlineSnapshot(` 12 | [ 13 | "fails", 14 | "concurrent", 15 | "sequential", 16 | "skip", 17 | "todo", 18 | "only", 19 | "runIf", 20 | "skipIf", 21 | "each", 22 | "for", 23 | "extend", 24 | ] 25 | `); 26 | expect(Object.keys(it.only)).toMatchInlineSnapshot(` 27 | [ 28 | "fails", 29 | "concurrent", 30 | "sequential", 31 | "skip", 32 | "todo", 33 | "only", 34 | "runIf", 35 | "skipIf", 36 | "each", 37 | "for", 38 | ] 39 | `); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /tests/test-api/concurrentContext.test.ts: -------------------------------------------------------------------------------- 1 | import { it } from '@rstest/core'; 2 | 3 | it.concurrent('concurrent test 1', async ({ expect }) => { 4 | expect.assertions(1); 5 | await new Promise((resolve) => setTimeout(resolve, 200)); 6 | expect(1 + 1).toBe(2); 7 | }); 8 | 9 | it.concurrent('concurrent test 2', async ({ expect }) => { 10 | expect(1 + 1).toBe(2); 11 | await new Promise((resolve) => setTimeout(resolve, 100)); 12 | expect('hello world').toMatchSnapshot(); 13 | }); 14 | it.concurrent('concurrent test 3', async ({ expect }) => { 15 | expect.assertions(2); 16 | await new Promise((resolve) => setTimeout(resolve, 100)); 17 | expect(1 + 1).toBe(2); 18 | expect(1 + 2).toBe(3); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/test-api/concurrentIsolated.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, describe, expect, it } from '@rstest/core'; 2 | const logs: string[] = []; 3 | 4 | afterAll(() => { 5 | expect(logs).toEqual([ 6 | '[log] concurrent test 1', 7 | '[log] concurrent test 2', 8 | '[log] concurrent test 2 - 1', 9 | '[log] concurrent test 1 - 1', 10 | '[log] concurrent test B 1', 11 | '[log] concurrent test B 2', 12 | '[log] concurrent test B 2 - 1', 13 | '[log] concurrent test B 1 - 1', 14 | ]); 15 | }); 16 | 17 | describe('suite', () => { 18 | it.concurrent('concurrent test 1', async () => { 19 | logs.push('[log] concurrent test 1'); 20 | await new Promise((resolve) => setTimeout(resolve, 200)); 21 | logs.push('[log] concurrent test 1 - 1'); 22 | }); 23 | 24 | it.concurrent('concurrent test 2', async () => { 25 | logs.push('[log] concurrent test 2'); 26 | await new Promise((resolve) => setTimeout(resolve, 100)); 27 | logs.push('[log] concurrent test 2 - 1'); 28 | }); 29 | }); 30 | 31 | describe('suite B', () => { 32 | it.concurrent('concurrent test B 1', async () => { 33 | logs.push('[log] concurrent test B 1'); 34 | await new Promise((resolve) => setTimeout(resolve, 200)); 35 | logs.push('[log] concurrent test B 1 - 1'); 36 | }); 37 | 38 | it.concurrent('concurrent test B 2', async () => { 39 | logs.push('[log] concurrent test B 2'); 40 | await new Promise((resolve) => setTimeout(resolve, 100)); 41 | logs.push('[log] concurrent test B 2 - 1'); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /tests/test-api/concurrentLimit.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { expect, it } from '@rstest/core'; 4 | import { runRstestCli } from '../scripts'; 5 | 6 | it('should run concurrent cases correctly with limit', async () => { 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = dirname(__filename); 9 | 10 | const { cli } = await runRstestCli({ 11 | command: 'rstest', 12 | args: ['run', 'fixtures/concurrentLimit.test.ts', '--maxConcurrency=4'], 13 | options: { 14 | nodeOptions: { 15 | cwd: __dirname, 16 | }, 17 | }, 18 | }); 19 | await cli.exec; 20 | expect(cli.exec.process?.exitCode).toBe(0); 21 | 22 | const logs = cli.stdout.split('\n').filter(Boolean); 23 | 24 | expect(logs.filter((log) => log.includes('[log]'))).toMatchInlineSnapshot(` 25 | [ 26 | "[log] concurrent test 1", 27 | "[log] concurrent test 2", 28 | "[log] concurrent test 3", 29 | "[log] concurrent test 4", 30 | "[log] concurrent test 2 - 1", 31 | "[log] concurrent test 5", 32 | "[log] concurrent test 3 - 1", 33 | "[log] concurrent test 6", 34 | "[log] concurrent test 4 - 1", 35 | "[log] concurrent test 7", 36 | "[log] concurrent test 1 - 1", 37 | "[log] concurrent test 5 - 1", 38 | "[log] concurrent test 6 - 1", 39 | "[log] concurrent test 7 - 1", 40 | ] 41 | `); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/test-api/condition.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, describe, expect, it } from '@rstest/core'; 2 | 3 | describe('Test Condition (runIf & skipIf)', () => { 4 | const logs: string[] = []; 5 | 6 | afterAll(() => { 7 | expect(logs.length).toBe(1); 8 | }); 9 | 10 | it.skipIf(1 + 1 === 2).each([ 11 | { a: 1, b: 1, expected: 2 }, 12 | { a: 1, b: 2, expected: 3 }, 13 | { a: 2, b: 1, expected: 3 }, 14 | ])('add($a, $b) -> $expected', ({ a, b, expected }) => { 15 | logs.push('executed'); 16 | expect(a + b).toBe(expected); 17 | }); 18 | 19 | it.runIf(1 + 1 === 2)('add two numbers correctly', () => { 20 | logs.push('executed'); 21 | expect(1 + 1).toBe(2); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/test-api/each.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, expect, it } from '@rstest/core'; 2 | 3 | const logs: string[] = []; 4 | 5 | afterAll(() => { 6 | expect(logs.length).toBe(6); 7 | }); 8 | 9 | it.each([ 10 | { a: 1, b: 1, expected: 2 }, 11 | { a: 1, b: 2, expected: 3 }, 12 | { a: 2, b: 1, expected: 3 }, 13 | ])('add($a, $b) -> $expected', ({ a, b, expected }) => { 14 | expect(a + b).toBe(expected); 15 | logs.push('executed'); 16 | }); 17 | 18 | it.each([ 19 | [2, 1, 3], 20 | [2, 2, 4], 21 | [3, 1, 4], 22 | ])('case-%# add(%i, %i) -> %i', (a, b, expected) => { 23 | expect(a + b).toBe(expected); 24 | logs.push('executed'); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/test-api/extend.auto.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, expect, test } from '@rstest/core'; 2 | 3 | const logs: string[] = []; 4 | 5 | afterAll(() => { 6 | // should init on demand 7 | expect(logs.length).toBe(4); 8 | }); 9 | 10 | const todos: number[] = []; 11 | const archive: number[] = []; 12 | 13 | const myTest = test.extend<{ 14 | todos: number[]; 15 | archive: number[]; 16 | }>({ 17 | todos: [ 18 | async (_, use) => { 19 | logs.push('init todos'); 20 | todos.push(1, 2, 3); 21 | await use(todos); 22 | // cleanup after each test function 23 | todos.length = 0; 24 | }, 25 | { 26 | auto: true, 27 | }, 28 | ], 29 | archive: [ 30 | async (_, use) => { 31 | logs.push('init archive'); 32 | await use(archive); 33 | // cleanup after each test function 34 | archive.length = 0; 35 | }, 36 | { 37 | auto: true, 38 | }, 39 | ], 40 | }); 41 | 42 | myTest('add todo', ({ todos }) => { 43 | expect(todos.length).toBe(3); 44 | 45 | todos.push(4); 46 | expect(todos.length).toBe(4); 47 | }); 48 | 49 | myTest('add archive', ({ archive }) => { 50 | expect(archive.length).toBe(0); 51 | 52 | archive.push(1, 2); 53 | expect(archive.length).toBe(2); 54 | }); 55 | -------------------------------------------------------------------------------- /tests/test-api/extend.depends.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, expect, test } from '@rstest/core'; 2 | 3 | const todos: number[] = []; 4 | const archive: number[] = []; 5 | 6 | const logs: string[] = []; 7 | 8 | afterAll(() => { 9 | expect(logs).toEqual(['clean getList', 'clean archive', 'clean todos']); 10 | }); 11 | 12 | const myTest = test.extend<{ 13 | todos: number[]; 14 | archive: number[]; 15 | getList: () => { 16 | todos: number[]; 17 | archive: number[]; 18 | }; 19 | }>({ 20 | todos: async (_, use) => { 21 | await new Promise((resolve) => setTimeout(resolve, 10)); 22 | todos.push(1, 2, 3); 23 | await use(todos); 24 | logs.push('clean todos'); 25 | // cleanup after each test function 26 | todos.length = 0; 27 | }, 28 | archive: async (_, use) => { 29 | archive.push(1, 2, 3); 30 | await use(archive); 31 | logs.push('clean archive'); 32 | // cleanup after each test function 33 | archive.length = 0; 34 | }, 35 | getList: async ({ todos, archive }, use) => { 36 | await use(() => ({ todos, archive })); 37 | logs.push('clean getList'); 38 | }, 39 | }); 40 | 41 | myTest('add todo', ({ getList }) => { 42 | expect(getList()).toEqual({ 43 | todos: [1, 2, 3], 44 | archive: [1, 2, 3], 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /tests/test-api/extend.extend.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@rstest/core'; 2 | 3 | const todos: number[] = []; 4 | const archive: number[] = []; 5 | 6 | const myTest = test.extend<{ 7 | todos: number[]; 8 | }>({ 9 | todos: async (_, use) => { 10 | await new Promise((resolve) => setTimeout(resolve, 10)); 11 | todos.push(1, 2, 3); 12 | await use(todos); 13 | // cleanup after each test function 14 | todos.length = 0; 15 | }, 16 | }); 17 | 18 | const myTest1 = myTest.extend<{ 19 | archive: number[]; 20 | }>({ 21 | archive: async (_, use) => { 22 | archive.push(1, 2, 3); 23 | await use(archive); 24 | // cleanup after each test function 25 | archive.length = 0; 26 | }, 27 | }); 28 | 29 | myTest1('add todo', ({ todos }) => { 30 | expect(todos.length).toBe(3); 31 | 32 | todos.push(4); 33 | expect(todos.length).toBe(4); 34 | }); 35 | 36 | myTest1('add archive', ({ archive }) => { 37 | expect(archive.length).toBe(3); 38 | 39 | archive.push(4, 5); 40 | expect(archive.length).toBe(5); 41 | }); 42 | -------------------------------------------------------------------------------- /tests/test-api/extend.onDemand.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, expect, test } from '@rstest/core'; 2 | 3 | const logs: string[] = []; 4 | 5 | afterAll(() => { 6 | // should init on demand 7 | expect(logs.length).toBe(3); 8 | }); 9 | 10 | const todos: number[] = []; 11 | 12 | const myTest = test.extend<{ 13 | todos: number[]; 14 | archive: number[]; 15 | }>({ 16 | todos: async (_, use) => { 17 | logs.push('init todos'); 18 | await new Promise((resolve) => setTimeout(resolve, 10)); 19 | todos.push(1, 2, 3); 20 | await use(todos); 21 | // cleanup after each test function 22 | todos.length = 0; 23 | }, 24 | archive: [], 25 | }); 26 | 27 | myTest('add todo 1', ({ todos }) => { 28 | expect(todos.length).toBe(3); 29 | 30 | todos.push(4); 31 | expect(todos.length).toBe(4); 32 | }); 33 | 34 | myTest('add todo 2', ({ todos }) => { 35 | expect(todos.length).toBe(3); 36 | 37 | todos.push(4, 5); 38 | expect(todos.length).toBe(5); 39 | }); 40 | 41 | myTest.fails('add todo 3 - failed', ({ todos }) => { 42 | expect(todos.length).toBe(3); 43 | 44 | todos.push(4, 5); 45 | expect(todos.length).toBe(6); 46 | }); 47 | 48 | myTest('add archive', ({ archive }) => { 49 | expect(archive.length).toBe(0); 50 | 51 | archive.push(1, 2); 52 | expect(archive.length).toBe(2); 53 | }); 54 | -------------------------------------------------------------------------------- /tests/test-api/fixtures/concurrentLimit.test.ts: -------------------------------------------------------------------------------- 1 | import { it } from '@rstest/core'; 2 | 3 | it.concurrent('concurrent test 1', async () => { 4 | console.log('[log] concurrent test 1'); 5 | await new Promise((resolve) => setTimeout(resolve, 200)); 6 | console.log('[log] concurrent test 1 - 1'); 7 | }); 8 | 9 | it.concurrent('concurrent test 2', async () => { 10 | console.log('[log] concurrent test 2'); 11 | await new Promise((resolve) => setTimeout(resolve, 100)); 12 | console.log('[log] concurrent test 2 - 1'); 13 | }); 14 | it.concurrent('concurrent test 3', async () => { 15 | console.log('[log] concurrent test 3'); 16 | await new Promise((resolve) => setTimeout(resolve, 100)); 17 | console.log('[log] concurrent test 3 - 1'); 18 | }); 19 | 20 | it.concurrent('concurrent test 4', async () => { 21 | console.log('[log] concurrent test 4'); 22 | await new Promise((resolve) => setTimeout(resolve, 100)); 23 | console.log('[log] concurrent test 4 - 1'); 24 | }); 25 | 26 | it.concurrent('concurrent test 5', async () => { 27 | console.log('[log] concurrent test 5'); 28 | await new Promise((resolve) => setTimeout(resolve, 100)); 29 | console.log('[log] concurrent test 5 - 1'); 30 | }); 31 | 32 | it.concurrent('concurrent test 6', async () => { 33 | console.log('[log] concurrent test 6'); 34 | await new Promise((resolve) => setTimeout(resolve, 100)); 35 | console.log('[log] concurrent test 6 - 1'); 36 | }); 37 | 38 | it.concurrent('concurrent test 7', async () => { 39 | console.log('[log] concurrent test 7'); 40 | await new Promise((resolve) => setTimeout(resolve, 100)); 41 | console.log('[log] concurrent test 7 - 1'); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/test-api/fixtures/error.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from '@rstest/core'; 2 | 3 | it('test asymmetricMatcher error', () => { 4 | expect({ 5 | text: 'hello world', 6 | }).toEqual({ 7 | text: expect.stringMatching('hhh'), 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /tests/test-api/fixtures/moduleNotFound.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from '@rstest/core'; 2 | 3 | const expectNotFound = async () => { 4 | try { 5 | // @ts-expect-error 6 | const res = await import('404'); 7 | return res; 8 | } catch (err) { 9 | return null; 10 | } 11 | }; 12 | 13 | const unexpectNotFound = async () => { 14 | // @ts-expect-error 15 | return import('aaa'); 16 | }; 17 | 18 | it('test expectNotFound error', async () => { 19 | await expect(expectNotFound()).resolves.toBeNull(); 20 | }); 21 | 22 | it('test expectNotFound error', async () => { 23 | await expect(unexpectNotFound()).rejects.toThrowError( 24 | /Cannot find module \'aaa\'/, 25 | ); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/test-api/fixtures/onlyInSkip.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@rstest/core'; 2 | 3 | describe.skip('level A', () => { 4 | // biome-ignore lint/suspicious/noFocusedTests: 5 | it.only('it in level A', () => { 6 | console.log('[test] in level A'); 7 | expect(1 + 1).toBe(2); 8 | }); 9 | 10 | it('it in level B', () => { 11 | console.log('[test] in level B'); 12 | expect(2 + 2).toBe(4); 13 | }); 14 | }); 15 | 16 | describe('level E', () => { 17 | it('it in level E-A', () => { 18 | console.log('[test] in level E-A'); 19 | expect(2 + 1).toBe(3); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/test-api/fixtures/retry.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from '@rstest/core'; 2 | 3 | let count = 1; 4 | it('should run success with retry', () => { 5 | expect(count++).toBe(5); 6 | }); 7 | -------------------------------------------------------------------------------- /tests/test-api/fixtures/timeout.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@rstest/core'; 2 | import { sleep } from '../../scripts'; 3 | 4 | describe('level A', () => { 5 | it('it in level A', async () => { 6 | await sleep(100); 7 | expect(1 + 1).toBe(2); 8 | }, 50); 9 | 10 | it('it in level B', async () => { 11 | await sleep(5100); 12 | expect(1 + 1).toBe(2); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/test-api/fixtures/undefined.test.ts: -------------------------------------------------------------------------------- 1 | import { it } from '@rstest/core'; 2 | 3 | it('should passed'); 4 | 5 | it.skip('should skip'); 6 | 7 | it.todo('should todo'); 8 | 9 | it.fails('should failed'); 10 | -------------------------------------------------------------------------------- /tests/test-api/for.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, expect, it } from '@rstest/core'; 2 | 3 | const logs: string[] = []; 4 | 5 | afterAll(() => { 6 | expect(logs.length).toBe(6); 7 | }); 8 | 9 | it.for([ 10 | { a: 1, b: 1, expected: 2 }, 11 | { a: 1, b: 2, expected: 3 }, 12 | { a: 2, b: 1, expected: 3 }, 13 | ])('add($a, $b) -> $expected', ({ a, b, expected }, { expect }) => { 14 | expect(a + b).toBe(expected); 15 | logs.push('executed'); 16 | }); 17 | 18 | it.for([ 19 | [2, 1, 3], 20 | [2, 2, 4], 21 | [3, 1, 4], 22 | ])('case-%# add(%i, %i) -> %i', ([a, b, expected], { expect }) => { 23 | expect(a + b).toBe(expected); 24 | logs.push('executed'); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/test-api/index.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { describe, expect, it } from '@rstest/core'; 4 | import { runRstestCli } from '../scripts'; 5 | 6 | describe('Test API', () => { 7 | it('test function undefined', async () => { 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = dirname(__filename); 10 | 11 | const { cli } = await runRstestCli({ 12 | command: 'rstest', 13 | args: ['run', 'fixtures/undefined.test.ts'], 14 | options: { 15 | nodeOptions: { 16 | cwd: __dirname, 17 | }, 18 | }, 19 | }); 20 | await cli.exec; 21 | expect(cli.exec.process?.exitCode).toBe(1); 22 | 23 | const logs = cli.stdout.split('\n').filter(Boolean); 24 | 25 | expect( 26 | logs.find((log) => log.includes('Test Files 1 failed')), 27 | ).toBeTruthy(); 28 | expect( 29 | logs.find((log) => 30 | log.includes('Tests 1 failed | 1 passed | 1 skipped | 1 todo'), 31 | ), 32 | ).toBeTruthy(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/test-api/only.each.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, expect, it } from '@rstest/core'; 2 | const logs: string[] = []; 3 | 4 | afterAll(() => { 5 | expect(logs.length).toBe(3); 6 | }); 7 | 8 | it.only.each([ 9 | [1, 1, 2], 10 | [1, 2, 3], 11 | [2, 1, 3], 12 | ])('%i + %i should be %i', (a, b, expected) => { 13 | expect(a + b).toBe(expected); 14 | logs.push('executed'); 15 | }); 16 | 17 | it('will not be run', () => { 18 | expect(1 + 1).toBe(1); 19 | logs.push('executed'); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/test-api/only.fails.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, expect, it } from '@rstest/core'; 2 | const logs: string[] = []; 3 | 4 | afterAll(() => { 5 | expect(logs.length).toBe(1); 6 | }); 7 | 8 | it.only.fails('will pass when failed', () => { 9 | logs.push('executed'); 10 | expect(1 + 1).toBe(1); 11 | }); 12 | 13 | it('will not run', () => { 14 | logs.push('executed'); 15 | expect(1 + 1).toBe(1); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/test-api/only.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, beforeEach, describe, expect, it } from '@rstest/core'; 2 | 3 | const logs: string[] = []; 4 | 5 | afterAll(() => { 6 | expect(logs).toEqual([ 7 | '[beforeEach] root', 8 | '[test] in level A', 9 | '[beforeEach] root', 10 | '[test] in level B-B', 11 | '[beforeEach] root', 12 | '[test] in level D', 13 | ]); 14 | }); 15 | 16 | beforeEach(() => { 17 | logs.push('[beforeEach] root'); 18 | }); 19 | 20 | describe('level A', () => { 21 | // biome-ignore lint/suspicious/noFocusedTests: 22 | it.only('it in level A', () => { 23 | logs.push('[test] in level A'); 24 | expect(1 + 1).toBe(2); 25 | }); 26 | 27 | describe('level B', () => { 28 | it('it in level B-A', () => { 29 | logs.push('[test] in level B-A'); 30 | expect(2 + 1).toBe(3); 31 | }); 32 | 33 | // biome-ignore lint/suspicious/noFocusedTests: 34 | it.only('it in level B-B', () => { 35 | logs.push('[test] in level B-B'); 36 | expect(2 + 1).toBe(3); 37 | }); 38 | }); 39 | 40 | it('it in level C', () => { 41 | logs.push('[test] in level C'); 42 | expect(2 + 2).toBe(4); 43 | }); 44 | }); 45 | 46 | // biome-ignore lint/suspicious/noFocusedTests: 47 | it.only('it in level D', () => { 48 | logs.push('[test] in level D'); 49 | expect(1 + 1).toBe(2); 50 | }); 51 | 52 | describe('level E', () => { 53 | it('it in level E-A', () => { 54 | logs.push('[test] in level E-A'); 55 | expect(2 + 1).toBe(3); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tests/test-api/retry.test.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { describe, expect, it } from '@rstest/core'; 4 | import { runRstestCli } from '../scripts'; 5 | 6 | describe('Test Retry', () => { 7 | it('should run success with retry', async () => { 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = dirname(__filename); 10 | 11 | const { cli } = await runRstestCli({ 12 | command: 'rstest', 13 | args: ['run', 'fixtures/retry.test.ts', '--retry=4'], 14 | options: { 15 | nodeOptions: { 16 | cwd: __dirname, 17 | }, 18 | }, 19 | }); 20 | await cli.exec; 21 | expect(cli.exec.process?.exitCode).toBe(0); 22 | }); 23 | 24 | it('should error when retry times exhausted', async () => { 25 | const __filename = fileURLToPath(import.meta.url); 26 | const __dirname = dirname(__filename); 27 | 28 | const { cli } = await runRstestCli({ 29 | command: 'rstest', 30 | args: ['run', 'fixtures/retry.test.ts', '--retry=3'], 31 | options: { 32 | nodeOptions: { 33 | cwd: __dirname, 34 | }, 35 | }, 36 | }); 37 | await cli.exec; 38 | expect(cli.exec.process?.exitCode).toBe(1); 39 | 40 | const logs = cli.stdout.split('\n').filter(Boolean); 41 | 42 | expect( 43 | logs.find((log) => log.includes('Test Files 1 failed')), 44 | ).toBeTruthy(); 45 | expect(logs.find((log) => log.includes('Tests 1 failed'))).toBeTruthy(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/test-api/sequential.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, describe, expect, it } from '@rstest/core'; 2 | const logs: string[] = []; 3 | 4 | afterAll(() => { 5 | expect(logs).toEqual([ 6 | '[log] test', 7 | '[log] test 1', 8 | '[log] test 0 - 1', 9 | '[log] test 1 - 1', 10 | '[log] test 2', 11 | '[log] test 2 - 1', 12 | '[log] test 3', 13 | '[log] test 4', 14 | '[log] test 3 - 1', 15 | '[log] test 4 - 1', 16 | ]); 17 | }); 18 | 19 | describe.concurrent('suite', () => { 20 | it('test', async () => { 21 | logs.push('[log] test'); 22 | await new Promise((resolve) => setTimeout(resolve, 10)); 23 | logs.push('[log] test 0 - 1'); 24 | }); 25 | it('test 1', async () => { 26 | logs.push('[log] test 1'); 27 | await new Promise((resolve) => setTimeout(resolve, 200)); 28 | logs.push('[log] test 1 - 1'); 29 | }); 30 | 31 | it.sequential('test 2', async () => { 32 | logs.push('[log] test 2'); 33 | await new Promise((resolve) => setTimeout(resolve, 100)); 34 | logs.push('[log] test 2 - 1'); 35 | }); 36 | 37 | it('test 3', async () => { 38 | logs.push('[log] test 3'); 39 | await new Promise((resolve) => setTimeout(resolve, 100)); 40 | logs.push('[log] test 3 - 1'); 41 | }); 42 | 43 | it('test 4', async () => { 44 | logs.push('[log] test 4'); 45 | await new Promise((resolve) => setTimeout(resolve, 100)); 46 | logs.push('[log] test 4 - 1'); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /tests/test-api/timeout.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@rstest/core'; 2 | import { runRstestCli } from '../scripts'; 3 | 4 | describe('Test timeout', () => { 5 | it('should throw timeout error when test timeout', async () => { 6 | const { cli } = await runRstestCli({ 7 | command: 'rstest', 8 | args: ['run', 'fixtures/timeout.test'], 9 | options: { 10 | nodeOptions: { 11 | cwd: __dirname, 12 | }, 13 | }, 14 | }); 15 | 16 | await cli.exec; 17 | expect(cli.exec.process?.exitCode).toBe(1); 18 | const logs = cli.stdout.split('\n').filter(Boolean); 19 | 20 | expect( 21 | logs.find((log) => log.includes('Error: test timed out in 50ms')), 22 | ).toBeTruthy(); 23 | expect( 24 | logs.find((log) => log.includes('timeout.test.ts:5:5')), 25 | ).toBeTruthy(); 26 | expect( 27 | logs.find((log) => log.includes('Error: test timed out in 5000ms')), 28 | ).toBeTruthy(); 29 | expect( 30 | logs.find((log) => log.includes('timeout.test.ts:10:5')), 31 | ).toBeTruthy(); 32 | expect(logs.find((log) => log.includes('Tests 2 failed'))).toBeTruthy(); 33 | }, 10000); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/test-api/timeoutConfig.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@rstest/core'; 2 | import { runRstestCli } from '../scripts'; 3 | 4 | describe('Test timeout configuration', () => { 5 | it('should not throw timeout error when update timeout time via testTimeout configuration', async () => { 6 | const { cli } = await runRstestCli({ 7 | command: 'rstest', 8 | args: ['run', 'fixtures/timeout.test', '--testTimeout=10000'], 9 | options: { 10 | nodeOptions: { 11 | cwd: __dirname, 12 | }, 13 | }, 14 | }); 15 | 16 | await cli.exec; 17 | expect(cli.exec.process?.exitCode).toBe(1); 18 | const logs = cli.stdout.split('\n').filter(Boolean); 19 | 20 | // The timeout set by the API is higher than the global configuration item 21 | expect( 22 | logs.find((log) => log.includes('Error: test timed out in 50ms')), 23 | ).toBeTruthy(); 24 | expect( 25 | logs.find((log) => log.includes('timeout.test.ts:5:5')), 26 | ).toBeTruthy(); 27 | 28 | expect( 29 | logs.find((log) => log.includes('Error: test timed out in 5000ms')), 30 | ).toBeFalsy(); 31 | expect( 32 | logs.find((log) => log.includes('Tests 1 failed | 1 passed')), 33 | ).toBeTruthy(); 34 | }, 12000); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/watch/fixtures/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from '@rstest/core'; 2 | import { sayHi } from './src/index'; 3 | 4 | describe('index', () => { 5 | it('should test source code correctly', () => { 6 | expect(sayHi()).toBe('hi'); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/watch/fixtures/src/index.ts: -------------------------------------------------------------------------------- 1 | export const sayHi = () => 'hi'; 2 | -------------------------------------------------------------------------------- /tests/watch/index.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { describe, expect, it } from '@rstest/core'; 4 | import { prepareFixtures, runRstestCli } from '../scripts/'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = path.dirname(__filename); 8 | 9 | describe('watch', () => { 10 | it('test files should be ran when create / update / delete', async () => { 11 | const { fs } = await prepareFixtures({ 12 | fixturesPath: `${__dirname}/fixtures`, 13 | }); 14 | 15 | const { cli } = await runRstestCli({ 16 | command: 'rstest', 17 | args: ['watch'], 18 | options: { 19 | nodeOptions: { 20 | cwd: __dirname, 21 | }, 22 | }, 23 | }); 24 | 25 | // initial 26 | await cli.waitForStdout('Duration'); 27 | expect(cli.stdout).toMatch('Tests 1 passed'); 28 | 29 | // create 30 | cli.resetStd(); 31 | fs.create( 32 | './fixtures-test/bar.test.ts', 33 | `import { describe, expect, it } from '@rstest/core'; 34 | describe('bar', () => { 35 | it('bar should be to bar', () => { 36 | expect('bar').toBe('bar'); 37 | }); 38 | });`, 39 | ); 40 | 41 | await cli.waitForStdout('Duration'); 42 | expect(cli.stdout).toMatch('Tests 2 passed'); 43 | 44 | // update 45 | cli.resetStd(); 46 | fs.update('./fixtures-test/bar.test.ts', (content) => { 47 | return content.replace("toBe('bar')", "toBe('BAR')"); 48 | }); 49 | 50 | await cli.waitForStdout('Duration'); 51 | expect(cli.stdout).toMatch('Test Files 1 failed | 1 passed'); 52 | expect(cli.stdout).toMatch('✗ bar > bar should be to bar'); 53 | 54 | // delete 55 | cli.resetStd(); 56 | fs.delete('./fixtures-test/bar.test.ts'); 57 | await cli.waitForStdout('Duration'); 58 | expect(cli.stdout).toMatch('Test Files 1 passed'); 59 | 60 | cli.exec.kill(); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /tests/watch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@rstest/tests-watch", 4 | "version": "1.0.0", 5 | "devDependencies": { 6 | "@rstest/core": "workspace:*" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/watch/rstest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@rstest/core'; 2 | 3 | export default defineConfig({ 4 | include: ['**/fixtures-test/**/*.test.*'], 5 | }); 6 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Local 2 | .DS_Store 3 | *.local 4 | *.log* 5 | 6 | # Dist 7 | node_modules 8 | dist/ 9 | doc_build/ 10 | 11 | # IDE 12 | .vscode/* 13 | !.vscode/extensions.json 14 | .idea 15 | -------------------------------------------------------------------------------- /website/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-present Bytedance, Inc. and its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Rstest website 2 | 3 | This website is built with [Rspress](https://github.com/web-infra-dev/rspress), the document content can be written using markdown or mdx syntax. You can refer to the [Rspress Website](https://rspress.rs/) for detailed usage. 4 | 5 | ## Contributing 6 | 7 | Currently Rstest provides documentation in English and Chinese. If you can use Chinese, please update both documents at the same time. Otherwise, just update the English documentation. 8 | 9 | ## Writing style guide 10 | 11 | The same as Rspack: [Writing style guide](https://github.com/web-infra-dev/rspack/tree/main/website#writing-style-guide). 12 | 13 | ### Image assets 14 | 15 | For images you use in the document, it's better to upload them to the [rspack-contrib/rstack-design-resources](https://github.com/rspack-contrib/rstack-design-resources) repository, so the size of the current repository doesn't get too big. 16 | 17 | After you upload the images there, they will be automatically deployed under the . 18 | -------------------------------------------------------------------------------- /website/docs/en/_meta.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "text": "Guide", 4 | "link": "/guide/start/", 5 | "activeMatch": "/guide/" 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /website/docs/en/guide/_meta.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "dir", 4 | "name": "start", 5 | "label": "Getting Started" 6 | }, 7 | { 8 | "type": "dir", 9 | "name": "basic", 10 | "label": "Basic" 11 | }, 12 | { 13 | "type": "dir", 14 | "name": "advanced", 15 | "label": "Advanced" 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /website/docs/en/guide/advanced/_meta.json: -------------------------------------------------------------------------------- 1 | ["profiling"] 2 | -------------------------------------------------------------------------------- /website/docs/en/guide/basic/_meta.json: -------------------------------------------------------------------------------- 1 | ["cli"] 2 | -------------------------------------------------------------------------------- /website/docs/en/guide/start/_meta.json: -------------------------------------------------------------------------------- 1 | ["index", "quick-start"] 2 | -------------------------------------------------------------------------------- /website/docs/en/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | pageType: home 3 | 4 | hero: 5 | name: Rstest 6 | text: Rspack based testing framework 7 | --- 8 | -------------------------------------------------------------------------------- /website/docs/public/netlify.toml: -------------------------------------------------------------------------------- 1 | # Redirect rstest.dev to rstest.rs 2 | [[redirects]] 3 | from = "https://rstest.dev/*" 4 | to = "https://rstest.rs/:splat" 5 | status = 301 6 | force = true 7 | -------------------------------------------------------------------------------- /website/docs/zh/_meta.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "text": "指南", 4 | "link": "/guide/start/", 5 | "activeMatch": "/guide/" 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /website/docs/zh/guide/_meta.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "dir", 4 | "name": "start", 5 | "label": "开始" 6 | }, 7 | { 8 | "type": "dir", 9 | "name": "basic", 10 | "label": "基础" 11 | }, 12 | { 13 | "type": "dir", 14 | "name": "advanced", 15 | "label": "进阶" 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /website/docs/zh/guide/advanced/_meta.json: -------------------------------------------------------------------------------- 1 | ["profiling"] 2 | -------------------------------------------------------------------------------- /website/docs/zh/guide/advanced/profiling.mdx: -------------------------------------------------------------------------------- 1 | # 性能分析 2 | 3 | ## 使用 Rsdoctor 4 | 5 | [Rsdoctor](https://rsdoctor.rs/) 是一款为 Rspack 生态量身打造的构建分析工具。 6 | 7 | 当你需要调试 Rstest 的构建产物或构建过程时,可以借助 Rsdoctor 来提升排查问题的效率。 8 | 9 | ### 快速上手 10 | 11 | 在 Rstest 中,你可以通过以下步骤开启 Rsdoctor 分析: 12 | 13 | 1. 安装 Rsdoctor 插件: 14 | 15 | import { PackageManagerTabs } from '@theme'; 16 | 17 | 18 | 19 | 2. 在 CLI 命令前添加 `RSDOCTOR=true` 环境变量: 20 | 21 | ```json title="package.json" 22 | { 23 | "scripts": { 24 | "test:rsdoctor": "RSDOCTOR=true rstest run" 25 | } 26 | } 27 | ``` 28 | 29 | 由于 Windows 不支持上述用法,你也可以使用 [cross-env](https://npmjs.com/package/cross-env) 来设置环境变量,这可以确保在不同的操作系统中都能正常使用: 30 | 31 | ```json title="package.json" 32 | { 33 | "scripts": { 34 | "test:rsdoctor": "cross-env RSDOCTOR=true rstest run" 35 | }, 36 | "devDependencies": { 37 | "cross-env": "^7.0.0" 38 | } 39 | } 40 | ``` 41 | 42 | 在项目内执行上述命令后,Rstest 会自动注册 Rsdoctor 的插件,并在构建完成后打开本次构建的分析页面,请参考 [Rsdoctor 文档](https://rsdoctor.rs/) 来了解完整功能。 43 | 44 | ![rsdoctor-rstest-outputs](https://assets.rspack.rs/rstest/assets/rsdoctor-rstest-outputs.png) 45 | 46 | ## CPU profiling 47 | 48 | ### Samply 49 | 50 | > 注意:为了能在 macOS 中对 Node.js 侧代码进行 profiling 需要 22.16+ 版本。 51 | 52 | [Samply](https://github.com/mstange/samply) 支持同时对 Rstest 主进程和测试进程进行性能分析,可通过如下步骤进行完整的性能分析: 53 | 54 | 运行以下命令启动性能分析: 55 | 56 | ```bash 57 | samply record -- node --perf-prof --perf-basic-prof --interpreted-frames-native-stack {your_node_modules_folder}/@rstest/core/bin/rstest.js 58 | ``` 59 | 60 | 命令执行完毕后会自动打开分析结果。 61 | 62 | Rstest 的 JavaScript 代码通常执行在 Node.js 线程里,选择 Node.js 线程查看 Node.js 侧的耗时分布。 63 | 64 | ![rstest-samply-profiling](https://assets.rspack.rs/rstest/assets/rstest-samply-profiling.png) 65 | -------------------------------------------------------------------------------- /website/docs/zh/guide/basic/_meta.json: -------------------------------------------------------------------------------- 1 | ["cli"] 2 | -------------------------------------------------------------------------------- /website/docs/zh/guide/basic/cli.mdx: -------------------------------------------------------------------------------- 1 | # 命令行工具 2 | 3 | Rstest 提供了一个轻量级的命令行工具,包含 [rstest watch](#rstest-watch) 和 [rstest run](#rstest-run) 等命令。 4 | 5 | ## rstest -h 6 | 7 | `rstest -h` 可帮助你查看所有可用的 CLI 命令及选项: 8 | 9 | ```bash 10 | npx rstest -h 11 | ``` 12 | 13 | 输出如下: 14 | 15 | ```bash 16 | Usage: 17 | $ rstest [...filters] 18 | 19 | Commands: 20 | [...filters] run tests 21 | run [...filters] run tests without watch mode 22 | watch [...filters] run tests in watch mode 23 | list [...filters] lists all test files that Rstest will run 24 | 25 | Options: 26 | -h, --help Display this message 27 | -v, --version Display version number 28 | -c, --config 29 | ... 30 | ``` 31 | 32 | ## rstest [...filters] 33 | 34 | 直接运行 `rstest` 命令将会在当前目录执行 Rstest 测试。在开发环境下会自动进入监听模式 (等同于 `rstest watch`),而在 CI 环境或非终端交互模式下会执行单次测试 (等同于 `rstest run`)。 35 | 36 | ```bash 37 | $ npx rstest 38 | 39 | ✓ test/index.test.ts (2 tests) 1ms 40 | 41 | Test Files 1 passed (1) 42 | Tests 2 passed (2) 43 | Duration 189 ms (build 22 ms, tests 167 ms) 44 | ``` 45 | 46 | ## rstest run 47 | 48 | `rstest run` 将会执行单次测试,该命令适用于 CI 环境或不需要一边修改一边执行测试的场景。 49 | 50 | ## rstest watch 51 | 52 | `rstest watch` 将会启动监听模式并执行测试,当测试或依赖文件修改时,将重新执行关联的测试文件。 53 | 54 | ## rstest list 55 | 56 | `rstest list` 将会打印所有匹配条件的测试列表。默认情况下,它将打印所有匹配条件的测试名称。 57 | 58 | ```bash 59 | $ npx rstest list 60 | 61 | # 输出如下: 62 | a.test.ts > test a > test a-1 63 | a.test.ts > test a-2 64 | b.test.ts > test b > test b-1 65 | b.test.ts > test b-2 66 | ``` 67 | 68 | `rstest list` 命令继承所有 `rstest` 过滤选项,你可以直接过滤文件或使用 `-t` 过滤指定的测试名称。 69 | 70 | ```bash 71 | $ npx rstest list -t='test a' 72 | 73 | # 输出如下: 74 | a.test.ts > test a > test a-1 75 | a.test.ts > test a-2 76 | ``` 77 | 78 | 你可以使用 `--filesOnly` 使其仅打印测试文件: 79 | 80 | ```bash 81 | $ npx rstest list --filesOnly 82 | 83 | # 输出如下: 84 | a.test.ts 85 | b.test.ts 86 | ``` 87 | 88 | 你可以使用 `--json` 使其以 JSON 格式打印测试或将结果保存到单独的文件中: 89 | 90 | ```bash 91 | $ npx rstest list --json 92 | 93 | $ npx rstest list --json=./output.json 94 | ``` 95 | -------------------------------------------------------------------------------- /website/docs/zh/guide/start/_meta.json: -------------------------------------------------------------------------------- 1 | ["index", "quick-start"] 2 | -------------------------------------------------------------------------------- /website/docs/zh/guide/start/quick-start.mdx: -------------------------------------------------------------------------------- 1 | # 快速上手 2 | 3 | ## 环境准备 4 | 5 | 开始之前,需要先安装 [Node.js](https://nodejs.org/) >= 18 版本,推荐使用 Node.js LTS 版本。 6 | 7 | 通过以下命令检查当前的 Node.js 版本: 8 | 9 | ```bash 10 | node -v 11 | ``` 12 | 13 | 如果你的环境中尚未安装 Node.js,或是版本太低,可以通过 [nvm](https://github.com/nvm-sh/nvm) 或 [fnm](https:///github.com/Schniz/fnm) 安装。 14 | 15 | 下面是通过 nvm 安装的例子: 16 | 17 | ```bash 18 | # 安装 Node.js LTS 19 | nvm install --lts 20 | # 切换 Node.js LTS 21 | nvm use --lts 22 | ``` 23 | 24 | ## 使用 Rstest 25 | 26 | 你可以通过如下命令安装 Rstest: 27 | 28 | import { PackageManagerTabs } from '@theme'; 29 | 30 | 31 | 32 | 下一步,你需要在 package.json 的 npm scripts 中添加 Rstest 命令: 33 | 34 | ```json title=package.json 35 | { 36 | "scripts": { 37 | "test": "rstest" 38 | } 39 | } 40 | ``` 41 | 42 | 完成以上步骤后,你即可通过 `npm run test`、`yarn test` 或 `pnpm test` 来运行 Rstest 测试。当然,你也可以直接使用 `npx rstest` 来运行 Rstest 测试。 43 | 44 | Rstest 内置了 `watch`、`run` 等命令,请参考 [CLI 工具](/guide/basic/cli) 来了解所有可用命令以及选项。 45 | 46 | ## 编写测试 47 | 48 | 作为一个简单的例子,我们有一个 `sayHi` 方法。为了对它进行测试,你可以创建一个名为 `index.test.ts` 的测试文件或使用与 [Rust 测试](https://doc.rust-lang.org/book/ch11-03-test-organization.html#the-tests-module-and-cfgtest) 类似的 In-Source 测试。 49 | 50 | ```ts file=index.ts 51 | export const sayHi = () => 'hi'; 52 | ``` 53 | 54 | ```ts file=index.test.ts 55 | import { expect, test } from '@rstest/core'; 56 | import { sayHi } from '../src/index'; 57 | 58 | test('should sayHi correctly', () => { 59 | expect(sayHi()).toBe('hi'); 60 | }); 61 | ``` 62 | 63 | 接下来,你可以通过 [使用 Rstest](#使用-rstest) 中配置好的命令执行测试。Rstest 会打印如下内容: 64 | 65 | ```bash 66 | ✓ test/index.test.ts (1) 67 | 68 | Test Files 1 passed 69 | Tests 1 passed 70 | Duration 140 ms (build 17 ms, tests 123 ms) 71 | ``` 72 | -------------------------------------------------------------------------------- /website/docs/zh/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | pageType: home 3 | titleSuffix: ' - 基于 Rspack 的测试框架' 4 | 5 | hero: 6 | name: Rstest 7 | text: 基于 Rspack 的测试框架 8 | --- 9 | -------------------------------------------------------------------------------- /website/env.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.mdx' { 2 | let MDXComponent: () => JSX.Element; 3 | export default MDXComponent; 4 | } 5 | 6 | declare module '*.module.scss' { 7 | const classes: { readonly [key: string]: string }; 8 | export default classes; 9 | } 10 | -------------------------------------------------------------------------------- /website/i18n.json: -------------------------------------------------------------------------------- 1 | { 2 | "introduction": { 3 | "en": "Introduction", 4 | "zh": "介绍" 5 | }, 6 | "quickStart": { 7 | "en": "Quick Start", 8 | "zh": "快速上手" 9 | }, 10 | "subtitle": { 11 | "en": "Rspack-based Testing Framework", 12 | "zh": "由 Rspack 驱动的测试框架" 13 | }, 14 | "slogan": { 15 | "en": "Provide first-class support for Rspack ecosystem", 16 | "zh": "为 Rspack 生态提供全面、一流的支持" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rstest-website", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "rspress build", 8 | "dev": "rspress dev", 9 | "preview": "rspress preview" 10 | }, 11 | "devDependencies": { 12 | "@rsbuild/plugin-sass": "^1.3.2", 13 | "@rstack-dev/doc-ui": "1.10.5", 14 | "@rstest/tsconfig": "workspace:*", 15 | "@types/node": "^22.13.8", 16 | "@types/react": "^19.1.6", 17 | "@types/react-dom": "^19.1.6", 18 | "react": "^19.1.0", 19 | "react-dom": "^19.1.0", 20 | "rsbuild-plugin-google-analytics": "^1.0.3", 21 | "rspress": "^2.0.0-beta.10", 22 | "rspress-plugin-font-open-sans": "^1.0.0", 23 | "typescript": "^5.8.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /website/theme/components/Copyright.module.scss: -------------------------------------------------------------------------------- 1 | @media (min-width: 640px) { 2 | .copyRight { 3 | padding: 2rem; 4 | } 5 | } 6 | 7 | .copyRight { 8 | bottom: 0; 9 | margin-top: 6rem; 10 | padding-top: 2rem; 11 | padding-bottom: 2rem; 12 | padding-left: 1.5rem; 13 | padding-right: 1.5rem; 14 | 15 | width: 100%; 16 | border-top: 1px solid var(--rp-c-divider-light); 17 | } 18 | 19 | .copyRightInner { 20 | margin: 0 auto; 21 | width: 100%; 22 | text-align: center; 23 | } 24 | 25 | .copyRightText { 26 | font-weight: 400; 27 | font-size: 0.875rem; 28 | color: var(--rp-c-text-2); 29 | } 30 | -------------------------------------------------------------------------------- /website/theme/components/Copyright.tsx: -------------------------------------------------------------------------------- 1 | import styles from './Copyright.module.scss'; 2 | 3 | export const CopyRight = () => { 4 | return ( 5 |
6 |
7 |
8 |

9 | Rstest is free and open source software released under the MIT 10 | license. 11 |

12 |

© 2024-present ByteDance Inc.

13 |
14 |
15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /website/theme/components/Hero.module.scss: -------------------------------------------------------------------------------- 1 | :global { 2 | .rs-oval { 3 | width: 70% !important; 4 | height: 70% !important; 5 | top: calc(50% + 20px) !important; 6 | left: calc(50% + 5px) !important; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /website/theme/components/Hero.tsx: -------------------------------------------------------------------------------- 1 | import { Hero as BaseHero } from '@rstack-dev/doc-ui/hero'; 2 | import { useI18n, useNavigate } from 'rspress/runtime'; 3 | import { useI18nUrl } from './utils'; 4 | import './Hero.module.scss'; 5 | 6 | export function Hero() { 7 | const navigate = useNavigate(); 8 | const tUrl = useI18nUrl(); 9 | const t = useI18n(); 10 | const onClickGetStarted = () => { 11 | navigate(tUrl('/guide/start/quick-start')); 12 | }; 13 | return ( 14 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /website/theme/components/ImageAlt.tsx: -------------------------------------------------------------------------------- 1 | export const ImageAlt = (props: { children?: React.ReactNode }) => { 2 | return ( 3 |

11 | {props.children} 12 |

13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /website/theme/components/NextSteps.module.scss: -------------------------------------------------------------------------------- 1 | .next-steps { 2 | display: flex; 3 | flex-wrap: wrap; 4 | justify-content: space-between; 5 | margin-top: 3rem; 6 | } 7 | -------------------------------------------------------------------------------- /website/theme/components/NextSteps.tsx: -------------------------------------------------------------------------------- 1 | import styles from './NextSteps.module.scss'; 2 | 3 | const NextSteps = (props: { children?: React.ReactNode }) => { 4 | return
{props.children}
; 5 | }; 6 | 7 | export default NextSteps; 8 | -------------------------------------------------------------------------------- /website/theme/components/Overview.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | margin-top: 64px; 3 | 4 | h2 { 5 | font-size: 23px; 6 | font-weight: 600; 7 | line-height: 1; 8 | margin: 0 0 20px; 9 | color: var(--rp-c-text-1); 10 | transition: color 0.5s; 11 | } 12 | 13 | a { 14 | letter-spacing: -0.01em; 15 | margin-bottom: 1em; 16 | transition: color 0.5s; 17 | margin: 0; 18 | font-size: 15px; 19 | font-weight: 500; 20 | line-height: 2.2; 21 | margin-top: 8px; 22 | color: var(--rp-c-text-code); 23 | opacity: 0.9; 24 | transition: color 0.5s; 25 | word-break: break-all; 26 | } 27 | 28 | a:hover { 29 | transition: color 0.5s; 30 | color: var(--rp-c-brand-dark); 31 | } 32 | } 33 | 34 | .group { 35 | break-inside: avoid; 36 | margin-bottom: 28px; 37 | background-color: var(--rp-c-bg-soft); 38 | border-radius: 12px; 39 | padding: 28px 32px; 40 | transition: background-color 0.5s; 41 | } 42 | 43 | @media (max-width: 768px) { 44 | .root a { 45 | font-size: 14px; 46 | } 47 | } 48 | 49 | @media (min-width: 768px) { 50 | .root { 51 | columns: 2; 52 | min-width: 648px; 53 | } 54 | } 55 | 56 | @media (min-width: 1024px) { 57 | .root { 58 | columns: 3; 59 | min-width: 904px; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /website/theme/components/Overview.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'rspress/theme'; 2 | import styles from './Overview.module.scss'; 3 | import { useUrl } from './utils'; 4 | 5 | export interface GroupItem { 6 | text: string; 7 | link: string; 8 | } 9 | 10 | export interface Group { 11 | name: string; 12 | items: GroupItem[]; 13 | } 14 | 15 | declare const OVERVIEW_GROUPS: Group[]; 16 | 17 | export default function Overview() { 18 | const Nodes = OVERVIEW_GROUPS.map((group) => ( 19 |
20 |
21 |

{group.name}

22 |
    23 | {group.items.map((item) => ( 24 |
  • 25 | {item.text} 26 |
  • 27 | ))} 28 |
29 |
30 |
31 | )); 32 | 33 | return
{Nodes}
; 34 | } 35 | -------------------------------------------------------------------------------- /website/theme/components/RsbuildDocBadge.tsx: -------------------------------------------------------------------------------- 1 | import { Badge, Link } from '@theme'; 2 | import { useLang } from 'rspress/runtime'; 3 | 4 | type Props = { 5 | /** Rsbuild doc URL pathname, i18n prefix stripped. */ 6 | path: string; 7 | /** Badge text. */ 8 | text: string; 9 | /** Badge image alt text. */ 10 | alt?: string; 11 | }; 12 | 13 | export function RsbuildDocBadge({ path, text, alt }: Props) { 14 | const langPrefix = useLang() === 'en' ? '' : '/zh'; 15 | const href = `https://rsbuild.rs${langPrefix}${path}`; 16 | 17 | return ( 18 | 24 | 25 | {alt 30 | {text} 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /website/theme/components/Step.module.scss: -------------------------------------------------------------------------------- 1 | .step { 2 | border: 1px solid var(--rp-c-bg-soft); 3 | background-color: var(--rp-c-bg-soft); 4 | transition: 5 | color 0.5s, 6 | background-color 0.5s; 7 | padding: 28px 36px; 8 | border-radius: 8px; 9 | flex: 0 32%; 10 | font-size: 14px; 11 | font-weight: 500; 12 | 13 | &:hover { 14 | border-color: var(--rp-c-brand); 15 | transition: border-color 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); 16 | } 17 | } 18 | 19 | .title { 20 | font-size: 20px; 21 | line-height: 1.4; 22 | letter-spacing: -0.02em; 23 | margin: 0 0 0.75em !important; 24 | display: block; 25 | } 26 | 27 | .description { 28 | margin: 0 !important; 29 | line-height: 1.7 !important; 30 | color: var(--rp-c-text-2); 31 | transition: color 0.5s; 32 | } 33 | -------------------------------------------------------------------------------- /website/theme/components/Step.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'rspress/theme'; 2 | import styles from './Step.module.scss'; 3 | import { useUrl } from './utils'; 4 | 5 | const Step = (props: { href: string; title: string; description: string }) => { 6 | return ( 7 | 8 |

{props.title}

9 |

{props.description}

10 | 11 | ); 12 | }; 13 | 14 | export default Step; 15 | -------------------------------------------------------------------------------- /website/theme/components/ToolStack.tsx: -------------------------------------------------------------------------------- 1 | import { containerStyle } from '@rstack-dev/doc-ui/section-style'; 2 | import { ToolStack as BaseToolStack } from '@rstack-dev/doc-ui/tool-stack'; 3 | import { useLang } from 'rspress/runtime'; 4 | 5 | export function ToolStack() { 6 | const lang = useLang(); 7 | return ( 8 |
9 | 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /website/theme/components/utils.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { useLang, usePageData, withBase } from 'rspress/runtime'; 3 | 4 | export function useUrl(url: string) { 5 | const lang = useLang(); 6 | const { 7 | siteData: { lang: defaultLang }, 8 | } = usePageData(); 9 | return withBase(lang === defaultLang ? url : `/${lang}${url}`); 10 | } 11 | 12 | export function useI18nUrl() { 13 | const lang = useLang(); 14 | const { 15 | siteData: { lang: defaultLang }, 16 | } = usePageData(); 17 | return useCallback( 18 | (url: string) => withBase(lang === defaultLang ? url : `/${lang}${url}`), 19 | [lang, defaultLang], 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /website/theme/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /website/theme/index.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --rp-c-brand: #ff5e00; 3 | --rp-c-brand-dark: #ff704d; 4 | --rp-c-brand-darker: #ff704d; 5 | --rp-c-brand-light: #ff7524; 6 | --rp-c-brand-lighter: #ff7524; 7 | --rp-c-link: var(--rp-c-brand); 8 | --rp-c-brand-tint: rgba(255, 94, 0, 0.07); 9 | } 10 | 11 | .dark { 12 | --rp-c-link: var(--rp-c-brand-light); 13 | } 14 | -------------------------------------------------------------------------------- /website/theme/index.tsx: -------------------------------------------------------------------------------- 1 | import { NavIcon } from '@rstack-dev/doc-ui/nav-icon'; 2 | import { Layout as BaseLayout } from 'rspress/theme'; 3 | import { HomeLayout } from './pages'; 4 | import './index.scss'; 5 | 6 | const Layout = () => { 7 | return } />; 8 | }; 9 | 10 | export { Layout, HomeLayout }; 11 | 12 | export * from 'rspress/theme'; 13 | -------------------------------------------------------------------------------- /website/theme/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { BackgroundImage } from '@rstack-dev/doc-ui/background-image'; 2 | import { CopyRight } from '../components/Copyright'; 3 | import { Hero } from '../components/Hero'; 4 | import { ToolStack } from '../components/ToolStack'; 5 | 6 | export function HomeLayout() { 7 | return ( 8 |
9 | 10 | 11 | 12 | 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@rstest/tsconfig/base", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "baseUrl": "./", 6 | "paths": { 7 | "@theme": ["./theme"], 8 | "@components/*": ["./theme/components/*"], 9 | "@zh/*": ["./docs/zh/*"], 10 | "@en/*": ["./docs/en/*"], 11 | "i18n": ["./i18n.json"] 12 | } 13 | }, 14 | "include": ["docs", "rspress.config.ts", "theme", "env.d.ts"], 15 | "mdx": { 16 | "checkMdx": true 17 | } 18 | } 19 | --------------------------------------------------------------------------------