diff --git a/plugins/node-test/jest.config.js b/plugins/node-test/jest.config.js new file mode 100644 index 000000000..2ea38bb31 --- /dev/null +++ b/plugins/node-test/jest.config.js @@ -0,0 +1,5 @@ +const base = require('../../jest.config.base') + +module.exports = { + ...base.config +} diff --git a/plugins/node-test/package.json b/plugins/node-test/package.json index 75eb82581..5a735a6a5 100644 --- a/plugins/node-test/package.json +++ b/plugins/node-test/package.json @@ -3,7 +3,7 @@ "version": "2.0.1", "main": "lib", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "cd ../../ ; npx jest --silent --projects plugins/node-test" }, "keywords": [], "author": "FT.com Platforms Team ", diff --git a/plugins/node-test/src/tasks/node-test.ts b/plugins/node-test/src/tasks/node-test.ts index d91cd7225..c8a87c5b1 100644 --- a/plugins/node-test/src/tasks/node-test.ts +++ b/plugins/node-test/src/tasks/node-test.ts @@ -15,7 +15,7 @@ import { z } from 'zod' // between Node.js 20 and 22. In future (when we drop Node.js 20 support) we will be able to remove // this and rely on the built-in patterns. // See https://nodejs.org/api/test.html#running-tests-from-the-command-line -const defaultFilePatterns = [ +export const defaultFilePatterns = [ '**/*.test.?(c|m)js', '**/*-test.?(c|m)js', '**/*_test.?(c|m)js', @@ -25,7 +25,7 @@ const defaultFilePatterns = [ ] // We don't want to run tests against files under "node_modules" -const defaultIgnorePatterns = ['**/node_modules/**'] +export const defaultIgnorePatterns = ['**/node_modules/**'] // TODO:IM:20250407 This function has been copied wholesale from // plugins/jest/src/tasks/jest.ts. There isn't a clear shared library to put it @@ -113,7 +113,7 @@ export default class NodeTest extends Task<{ task: typeof NodeTestSchema }> { if (concurrency === true && process.env.CIRCLECI) { concurrency = (await guessCircleCiThreads()) - 1 } - const files = await glob(filePatterns, { cwd, ignore }) + const files = await glob(filePatterns, { absolute: true, cwd, ignore }) let success = true const testStream = run(Object.assign({ concurrency, files, forceExit, watch }, customOptions)) diff --git a/plugins/node-test/test/files/failing-js/example.test.js b/plugins/node-test/test/files/failing-js/example.test.js new file mode 100644 index 000000000..faa828f1a --- /dev/null +++ b/plugins/node-test/test/files/failing-js/example.test.js @@ -0,0 +1,8 @@ +const assert = require('node:assert/strict') +const { describe, it } = require('node:test') + +describe('failing test suite', () => { + it('has tests that fail', () => { + assert.ok(false) + }) +}) diff --git a/plugins/node-test/test/files/failing-js/throw.js b/plugins/node-test/test/files/failing-js/throw.js new file mode 100644 index 000000000..83b9aa042 --- /dev/null +++ b/plugins/node-test/test/files/failing-js/throw.js @@ -0,0 +1,2 @@ +// This file does not match the default file patterns +throw new Error('Should not be run as part of the test suite'); diff --git a/plugins/node-test/test/files/file-pattern/example.foo.js b/plugins/node-test/test/files/file-pattern/example.foo.js new file mode 100644 index 000000000..18231fa01 --- /dev/null +++ b/plugins/node-test/test/files/file-pattern/example.foo.js @@ -0,0 +1,8 @@ +const assert = require('node:assert/strict') +const { describe, it } = require('node:test') + +describe('passing test suite', () => { + it('has tests that pass', () => { + assert.ok(true) + }) +}) diff --git a/plugins/node-test/test/files/file-pattern/example.test.js b/plugins/node-test/test/files/file-pattern/example.test.js new file mode 100644 index 000000000..f7eb9bf72 --- /dev/null +++ b/plugins/node-test/test/files/file-pattern/example.test.js @@ -0,0 +1,2 @@ +// This file does not match the configured file patterns +throw new Error('Should not be run as part of the test suite'); diff --git a/plugins/node-test/test/files/passing-js/example.test.js b/plugins/node-test/test/files/passing-js/example.test.js new file mode 100644 index 000000000..18231fa01 --- /dev/null +++ b/plugins/node-test/test/files/passing-js/example.test.js @@ -0,0 +1,8 @@ +const assert = require('node:assert/strict') +const { describe, it } = require('node:test') + +describe('passing test suite', () => { + it('has tests that pass', () => { + assert.ok(true) + }) +}) diff --git a/plugins/node-test/test/files/passing-js/subfolder/example.test.cjs b/plugins/node-test/test/files/passing-js/subfolder/example.test.cjs new file mode 100644 index 000000000..8fb3c138c --- /dev/null +++ b/plugins/node-test/test/files/passing-js/subfolder/example.test.cjs @@ -0,0 +1,8 @@ +const assert = require('node:assert/strict') +const { describe, it } = require('node:test') + +describe("passing test suite (CJS)", () => { + it('has tests that pass', () => { + assert.ok(true) + }) +}) diff --git a/plugins/node-test/test/files/passing-js/subfolder/example.test.mjs b/plugins/node-test/test/files/passing-js/subfolder/example.test.mjs new file mode 100644 index 000000000..c941359b1 --- /dev/null +++ b/plugins/node-test/test/files/passing-js/subfolder/example.test.mjs @@ -0,0 +1,8 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +describe("passing test suite (MJS)", () => { + it('has tests that pass', () => { + assert.ok(true) + }) +}) diff --git a/plugins/node-test/test/files/passing-js/test/example.js b/plugins/node-test/test/files/passing-js/test/example.js new file mode 100644 index 000000000..fe82c0c83 --- /dev/null +++ b/plugins/node-test/test/files/passing-js/test/example.js @@ -0,0 +1,8 @@ +const assert = require('node:assert/strict') +const { describe, it } = require('node:test') + +describe("passing test suite (test folder)", () => { + it('has tests that pass', () => { + assert.ok(true) + }) +}) diff --git a/plugins/node-test/test/files/passing-js/throw.js b/plugins/node-test/test/files/passing-js/throw.js new file mode 100644 index 000000000..83b9aa042 --- /dev/null +++ b/plugins/node-test/test/files/passing-js/throw.js @@ -0,0 +1,2 @@ +// This file does not match the default file patterns +throw new Error('Should not be run as part of the test suite'); diff --git a/plugins/node-test/test/files/timeout/example.test.js b/plugins/node-test/test/files/timeout/example.test.js new file mode 100644 index 000000000..4802d0a98 --- /dev/null +++ b/plugins/node-test/test/files/timeout/example.test.js @@ -0,0 +1,10 @@ +const assert = require('node:assert/strict') +const { describe, it } = require('node:test') +const { setTimeout } = require('node:timers/promises') + +describe('passing test suite', () => { + it('has tests that pass', async () => { + await setTimeout(1000); + assert.ok(true) + }) +}) diff --git a/plugins/node-test/test/tasks/node-test.test.ts b/plugins/node-test/test/tasks/node-test.test.ts new file mode 100644 index 000000000..91dff3c0b --- /dev/null +++ b/plugins/node-test/test/tasks/node-test.test.ts @@ -0,0 +1,79 @@ +import { describe, it, expect } from '@jest/globals' +import { join, resolve } from 'node:path' +import NodeTest, { defaultFilePatterns, defaultIgnorePatterns } from '../../src/tasks/node-test' +import winston, { Logger } from 'winston' +import { ToolKitError } from '@dotcom-tool-kit/error' +import { PassThrough } from 'node:stream' + +const fixturesPath = resolve(__dirname, '..', 'files') + +const defaultOptions = { + concurrency: false, + files: defaultFilePatterns, + forceExit: false, + ignore: defaultIgnorePatterns, + watch: false +} + +describe('NodeTest', () => { + let logger: Logger + let log: jest.Mock + + beforeEach(() => { + log = jest.fn() + const logStream = new PassThrough() + logStream.on('data', (data) => log(data.toString())) + + logger = winston.createLogger({ + transports: [new winston.transports.Stream({ stream: logStream })] + }) + }) + + it('does not error when the tests pass', async () => { + const task = new NodeTest(logger, 'NodeTest', {}, defaultOptions) + await expect(task.run({ command: 'test:local', cwd: join(fixturesPath, 'passing-js') })).resolves.toBe( + undefined + ) + expect(log).toHaveBeenCalledWith(expect.stringMatching(/passing test suite/)) + expect(log).toHaveBeenCalledWith(expect.stringMatching(/pass 4/)) + expect(log).toHaveBeenCalledWith(expect.stringMatching(/fail 0/)) + }) + + it('errors when the tests fail', async () => { + const task = new NodeTest(logger, 'NodeTest', {}, defaultOptions) + await expect(task.run({ command: 'test:local', cwd: join(fixturesPath, 'failing-js') })).rejects.toThrow( + ToolKitError + ) + expect(log).toHaveBeenCalledWith(expect.stringMatching(/failing test suite/)) + expect(log).toHaveBeenCalledWith(expect.stringMatching(/pass 0/)) + expect(log).toHaveBeenCalledWith(expect.stringMatching(/fail 1/)) + }) + + it('finds tests based on the given file patterns', async () => { + const task = new NodeTest(logger, 'NodeTest', {}, { ...defaultOptions, files: ['**/*.foo.js'] }) + await expect(task.run({ command: 'test:local', cwd: join(fixturesPath, 'file-pattern') })).resolves.toBe( + undefined + ) + expect(log).toHaveBeenCalledWith(expect.stringMatching(/passing test suite/)) + expect(log).toHaveBeenCalledWith(expect.stringMatching(/pass 1/)) + expect(log).toHaveBeenCalledWith(expect.stringMatching(/fail 0/)) + }) + + it('ignores tests based on the given file patterns', async () => { + const task = new NodeTest(logger, 'NodeTest', {}, { ...defaultOptions, ignore: ['subfolder/*'] }) + await expect(task.run({ command: 'test:local', cwd: join(fixturesPath, 'passing-js') })).resolves.toBe( + undefined + ) + expect(log).toHaveBeenCalledWith(expect.stringMatching(/passing test suite/)) + expect(log).toHaveBeenCalledWith(expect.stringMatching(/pass 2/)) + expect(log).toHaveBeenCalledWith(expect.stringMatching(/fail 0/)) + }) + + it('can accept custom options (timeout)', async () => { + const task = new NodeTest(logger, 'NodeTest', {}, {...defaultOptions, customOptions: { timeout: 100 }}) + await expect(task.run({ command: 'test:local', cwd: join(fixturesPath, 'timeout') })).rejects.toThrow( + ToolKitError + ) + expect(log).toHaveBeenCalledWith(expect.stringMatching(/✖/)) + }) +})