diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 05a07e125484..73f4cc2aec65 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,13 +88,25 @@ DEBUG=vite:[name] astro dev # debug specific process, e.g. "vite:deps" or "vit # run this in the top-level project root to run all tests pnpm run test # run only a few tests in the `astro` package, great for working on a single feature -# (example - `pnpm run test:match "cli"` runs `cli.test.js`) +# (example - `pnpm run test:match "cli"` runs tests with "cli" in the name) pnpm run test:match "$STRING_MATCH" # run tests on another package # (example - `pnpm --filter @astrojs/rss run test` runs `packages/astro-rss/test/rss.test.js`) pnpm --filter $STRING_MATCH run test ``` +Most tests use [`mocha`](https://mochajs.org) as the test runner. We're slowly migrating to use [`node:test`](https://nodejs.org/api/test.html) instead through the custom [`astro-scripts test`](./scripts/cmd/test.js) command. For packages that use `node:test`, you can run these commands in their directories: + +```shell +# run all of the package's tests +pnpm run test +# run only a few tests in the package +# (example - `pnpm run test -m "cli"` runs tests with "cli" in the name) +pnpm run test -m "$STRING_MATCH" +# run a single test file, you can use `node --test` directly +node --test ./test/foo.test.js +``` + #### E2E tests Certain features, like HMR and client hydration, need end-to-end tests to verify functionality in the dev server. [Playwright](https://playwright.dev/) is used to test against the dev server. diff --git a/packages/upgrade/package.json b/packages/upgrade/package.json index 802b180157ec..a15c90616ed7 100644 --- a/packages/upgrade/package.json +++ b/packages/upgrade/package.json @@ -20,7 +20,7 @@ "build": "astro-scripts build \"src/index.ts\" --bundle && tsc", "build:ci": "astro-scripts build \"src/index.ts\" --bundle", "dev": "astro-scripts dev \"src/**/*.ts\"", - "test": "mocha --exit --timeout 20000 --parallel" + "test": "astro-scripts test \"test/**/*.test.js\"" }, "files": [ "dist", @@ -39,8 +39,6 @@ "@types/which-pm-runs": "^1.0.0", "arg": "^5.0.2", "astro-scripts": "workspace:*", - "chai": "^4.3.7", - "mocha": "^10.2.0", "strip-ansi": "^7.1.0" }, "engines": { diff --git a/packages/upgrade/test/context.test.js b/packages/upgrade/test/context.test.js index 5b6b8c6b2201..714a7b64ac43 100644 --- a/packages/upgrade/test/context.test.js +++ b/packages/upgrade/test/context.test.js @@ -1,19 +1,20 @@ -import { expect } from 'chai'; +import { describe, it } from 'node:test'; +import * as assert from 'node:assert/strict'; import { getContext } from '../dist/index.js'; describe('context', () => { it('no arguments', async () => { const ctx = await getContext([]); - expect(ctx.version).to.eq('latest'); - expect(ctx.dryRun).to.be.undefined; + assert.equal(ctx.version, 'latest'); + assert.equal(ctx.dryRun, undefined); }); it('tag', async () => { const ctx = await getContext(['beta']); - expect(ctx.version).to.eq('beta'); - expect(ctx.dryRun).to.be.undefined; + assert.equal(ctx.version, 'beta'); + assert.equal(ctx.dryRun, undefined); }); it('dry run', async () => { const ctx = await getContext(['--dry-run']); - expect(ctx.dryRun).to.eq(true); + assert.equal(ctx.dryRun, true); }); }); diff --git a/packages/upgrade/test/install.test.js b/packages/upgrade/test/install.test.js index 05c46cdce9ef..b4158d264845 100644 --- a/packages/upgrade/test/install.test.js +++ b/packages/upgrade/test/install.test.js @@ -1,4 +1,5 @@ -import { expect } from 'chai'; +import { describe, it } from 'node:test'; +import * as assert from 'node:assert/strict'; import { setup } from './utils.js'; import { install } from '../dist/index.js'; @@ -23,7 +24,7 @@ describe('install', () => { ], }; await install(context); - expect(fixture.hasMessage('◼ astro is up to date on v1.0.0')).to.be.true; + assert.equal(fixture.hasMessage('◼ astro is up to date on v1.0.0'), true); }); it('patch', async () => { @@ -38,7 +39,7 @@ describe('install', () => { ], }; await install(context); - expect(fixture.hasMessage('● astro can be updated to v1.0.1')).to.be.true; + assert.equal(fixture.hasMessage('● astro can be updated to v1.0.1'), true); }); it('minor', async () => { @@ -53,7 +54,7 @@ describe('install', () => { ], }; await install(context); - expect(fixture.hasMessage('● astro can be updated to v1.2.0')).to.be.true; + assert.equal(fixture.hasMessage('● astro can be updated to v1.2.0'), true); }); it('major (reject)', async () => { @@ -80,10 +81,10 @@ describe('install', () => { ], }; await install(context); - expect(fixture.hasMessage('▲ astro can be updated to v2.0.0')).to.be.true; - expect(prompted).to.be.true; - expect(exitCode).to.eq(0); - expect(fixture.hasMessage('check Be sure to follow the CHANGELOG.')).to.be.false; + assert.equal(fixture.hasMessage('▲ astro can be updated to v2.0.0'), true); + assert.equal(prompted, true); + assert.equal(exitCode, 0); + assert.equal(fixture.hasMessage('check Be sure to follow the CHANGELOG.'), false); }); it('major (accept)', async () => { @@ -110,10 +111,10 @@ describe('install', () => { ], }; await install(context); - expect(fixture.hasMessage('▲ astro can be updated to v2.0.0')).to.be.true; - expect(prompted).to.be.true; - expect(exitCode).to.be.undefined; - expect(fixture.hasMessage('check Be sure to follow the CHANGELOG.')).to.be.true; + assert.equal(fixture.hasMessage('▲ astro can be updated to v2.0.0'), true); + assert.equal(prompted, true); + assert.equal(exitCode, undefined); + assert.equal(fixture.hasMessage('check Be sure to follow the CHANGELOG.'), true); }); it('multiple major', async () => { @@ -148,14 +149,14 @@ describe('install', () => { ], }; await install(context); - expect(fixture.hasMessage('▲ a can be updated to v2.0.0')).to.be.true; - expect(fixture.hasMessage('▲ b can be updated to v7.0.0')).to.be.true; - expect(prompted).to.be.true; - expect(exitCode).to.be.undefined; + assert.equal(fixture.hasMessage('▲ a can be updated to v2.0.0'), true); + assert.equal(fixture.hasMessage('▲ b can be updated to v7.0.0'), true); + assert.equal(prompted, true); + assert.equal(exitCode, undefined); const [changelog, a, b] = fixture.messages().slice(-5); - expect(changelog).to.match(/^check/); - expect(a).to.match(/^a/); - expect(b).to.match(/^b/); + assert.match(changelog, /^check/); + assert.match(a, /^a/); + assert.match(b, /^b/); }); it('current patch minor major', async () => { @@ -197,15 +198,15 @@ describe('install', () => { ], }; await install(context); - expect(fixture.hasMessage('◼ current is up to date on v1.0.0')).to.be.true; - expect(fixture.hasMessage('● patch can be updated to v1.0.1')).to.be.true; - expect(fixture.hasMessage('● minor can be updated to v1.2.0')).to.be.true; - expect(fixture.hasMessage('▲ major can be updated to v3.0.0')).to.be.true; - expect(prompted).to.be.true; - expect(exitCode).to.be.undefined; - expect(fixture.hasMessage('check Be sure to follow the CHANGELOG.')).to.be.true; + assert.equal(fixture.hasMessage('◼ current is up to date on v1.0.0'), true); + assert.equal(fixture.hasMessage('● patch can be updated to v1.0.1'), true); + assert.equal(fixture.hasMessage('● minor can be updated to v1.2.0'), true); + assert.equal(fixture.hasMessage('▲ major can be updated to v3.0.0'), true); + assert.equal(prompted, true); + assert.equal(exitCode, undefined); + assert.equal(fixture.hasMessage('check Be sure to follow the CHANGELOG.'), true); const [changelog, major] = fixture.messages().slice(-4); - expect(changelog).to.match(/^check/); - expect(major).to.match(/^major/); + assert.match(changelog, /^check/); + assert.match(major, /^major/) }); }); diff --git a/packages/upgrade/test/utils.js b/packages/upgrade/test/utils.js index ff5d5dd832af..691e63d90a81 100644 --- a/packages/upgrade/test/utils.js +++ b/packages/upgrade/test/utils.js @@ -1,3 +1,4 @@ +import { before, beforeEach } from 'node:test'; import { setStdout } from '../dist/index.js'; import stripAnsi from 'strip-ansi'; diff --git a/packages/upgrade/test/verify.test.js b/packages/upgrade/test/verify.test.js index a54cb6bb5085..3b9d4b3bc12d 100644 --- a/packages/upgrade/test/verify.test.js +++ b/packages/upgrade/test/verify.test.js @@ -1,4 +1,5 @@ -import { expect } from 'chai'; +import { describe, it, beforeEach } from 'node:test'; +import * as assert from 'node:assert/strict'; import { collectPackageInfo } from '../dist/index.js'; describe('collectPackageInfo', () => { @@ -16,61 +17,61 @@ describe('collectPackageInfo', () => { it('detects astro', async () => { collectPackageInfo(context, { astro: '1.0.0' }, {}); - expect(context.packages).deep.equal([ + assert.deepEqual(context.packages, [ { name: 'astro', currentVersion: '1.0.0', targetVersion: 'latest' }, ]); }); it('detects @astrojs', async () => { collectPackageInfo(context, { '@astrojs/preact': '1.0.0' }, {}); - expect(context.packages).deep.equal([ + assert.deepEqual(context.packages, [ { name: '@astrojs/preact', currentVersion: '1.0.0', targetVersion: 'latest' }, ]); }); it('supports ^ prefixes', async () => { collectPackageInfo(context, { astro: '^1.0.0' }, {}); - expect(context.packages).deep.equal([ + assert.deepEqual(context.packages, [ { name: 'astro', currentVersion: '^1.0.0', targetVersion: 'latest' }, ]); }); it('supports ~ prefixes', async () => { collectPackageInfo(context, { astro: '~1.0.0' }, {}); - expect(context.packages).deep.equal([ + assert.deepEqual(context.packages, [ { name: 'astro', currentVersion: '~1.0.0', targetVersion: 'latest' }, ]); }); it('supports prereleases', async () => { collectPackageInfo(context, { astro: '1.0.0-beta.0' }, {}); - expect(context.packages).deep.equal([ + assert.deepEqual(context.packages, [ { name: 'astro', currentVersion: '1.0.0-beta.0', targetVersion: 'latest' }, ]); }); it('ignores self', async () => { collectPackageInfo(context, { '@astrojs/upgrade': '0.0.1' }, {}); - expect(context.packages).deep.equal([]); + assert.deepEqual(context.packages, []); }); it('ignores linked packages', async () => { collectPackageInfo(context, { '@astrojs/preact': 'link:../packages/preact' }, {}); - expect(context.packages).deep.equal([]); + assert.deepEqual(context.packages, []); }); it('ignores workspace packages', async () => { collectPackageInfo(context, { '@astrojs/preact': 'workspace:*' }, {}); - expect(context.packages).deep.equal([]); + assert.deepEqual(context.packages, []); }); it('ignores github packages', async () => { collectPackageInfo(context, { '@astrojs/preact': 'github:withastro/astro' }, {}); - expect(context.packages).deep.equal([]); + assert.deepEqual(context.packages, []); }); it('ignores tag', async () => { collectPackageInfo(context, { '@astrojs/preact': 'beta' }, {}); - expect(context.packages).deep.equal([]); + assert.deepEqual(context.packages, []); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1a32f4441b3..ab455435e4b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5148,12 +5148,6 @@ importers: astro-scripts: specifier: workspace:* version: link:../../scripts - chai: - specifier: ^4.3.7 - version: 4.3.10 - mocha: - specifier: ^10.2.0 - version: 10.2.0 strip-ansi: specifier: ^7.1.0 version: 7.1.0 diff --git a/scripts/cmd/test.js b/scripts/cmd/test.js index e69de29bb2d1..84f6d9742187 100644 --- a/scripts/cmd/test.js +++ b/scripts/cmd/test.js @@ -0,0 +1,51 @@ +import { run } from 'node:test'; +import { spec } from 'node:test/reporters'; +import arg from 'arg'; +import glob from 'tiny-glob'; + +const isCI = !!process.env.CI; +const defaultTimeout = isCI ? 30000 : 20000; + +export default async function test() { + const args = arg({ + '--match': String, // aka --test-name-pattern: https://nodejs.org/api/test.html#filtering-tests-by-name + '--only': Boolean, // aka --test-only: https://nodejs.org/api/test.html#only-tests + '--parallel': Boolean, // aka --test-concurrency: https://nodejs.org/api/test.html#test-runner-execution-model + '--watch': Boolean, // experimental: https://nodejs.org/api/test.html#watch-mode + '--timeout': Number, // Test timeout in milliseconds (default: 30000ms) + '--setup': String, // Test setup file + // Aliases + '-m': '--match', + '-o': '--only', + '-p': '--parallel', + '-w': '--watch', + '-t': '--timeout', + '-s': '--setup', + }); + + const pattern = args._[1]; + if (!pattern) throw new Error('Missing test glob pattern'); + + const files = await glob(pattern, { filesOnly: true, absolute: true }); + + // For some reason, the `only` option does not work and we need to explicitly set the CLI flag instead. + // Node.js requires opt-in to run .only tests :( + // https://nodejs.org/api/test.html#only-tests + if (args['--only']) { + process.env.NODE_OPTIONS ??= ''; + process.env.NODE_OPTIONS += ' --test-only'; + } + + // https://nodejs.org/api/test.html#runoptions + run({ + files, + testNamePatterns: args['--match'], + concurrency: args['--parallel'], + only: args['--only'], + setup: args['--setup'], + watch: args['--watch'], + timeout: args['--timeout'] ?? defaultTimeout, // Node.js defaults to Infinity, so set better fallback + }) + .pipe(new spec()) + .pipe(process.stdout); +} diff --git a/scripts/index.js b/scripts/index.js index 249eac53d135..381500ac4e69 100755 --- a/scripts/index.js +++ b/scripts/index.js @@ -18,6 +18,11 @@ export default async function run() { await prebuild(...args); break; } + case 'test': { + const { default: test } = await import('./cmd/test.js'); + await test(...args); + break; + } } }