diff --git a/biome.jsonc b/biome.jsonc index 0f269bb4..006da53a 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -5,8 +5,7 @@ "**", "!**/*.snap.cjs", "!**/fixtures", - "!**/expected", - "!**/input" + "!**/tests" ] }, "assist": { "actions": { "source": { "organizeImports": "off" } } }, diff --git a/package-lock.json b/package-lock.json index bd620faf..4cba130a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1552,6 +1552,10 @@ "resolved": "recipes/slow-buffer-to-buffer-alloc-unsafe-slow", "link": true }, + "node_modules/@nodejs/tape-to-node-test": { + "resolved": "recipes/tape-to-node-test", + "link": true + }, "node_modules/@nodejs/tmpdir-to-tmpdir": { "resolved": "recipes/tmpdir-to-tmpdir", "link": true @@ -4485,6 +4489,14 @@ "@codemod.com/jssg-types": "^1.3.0" } }, + "recipes/tape-to-node-test": { + "name": "@nodejs/tape-to-node-test", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@nodejs/codemod-utils": "*" + } + }, "recipes/tmpdir-to-tmpdir": { "name": "@nodejs/tmpdir-to-tmpdir", "version": "1.0.0", diff --git a/recipes/tape-to-node-test/README.md b/recipes/tape-to-node-test/README.md new file mode 100644 index 00000000..4dcbebef --- /dev/null +++ b/recipes/tape-to-node-test/README.md @@ -0,0 +1,118 @@ +# Tape to Node.js Test Runner Codemod + +This codemod migrates tests written using [`tape`](https://github.com/tape-testing/tape) v5 to the native Node.js test runner ([`node:test`](https://nodejs.org/api/test.html)). + +## Features + +- Replaces `tape` imports with `node:test` and `node:assert`. +- Converts `test(name, (t) => ...)` to `test(name, async (t) => ...)`. +- Maps `tape` assertions to `node:assert` equivalents, including many aliases (e.g., `t.is`, `t.equals`, `t.deepEquals`). +- Handles `t.plan` (by commenting it out). +- Handles `t.end` (removes it for async tests, converts to `done` callback for callback-style tests). +- Handles `t.test` subtests (adds `await`). +- Converts `t.teardown` to `t.after`. +- Converts `t.comment` to `t.diagnostic`. +- Migrates `t.timeoutAfter(ms)` to `{ timeout: ms }` test option. +- Supports `test.skip` and `test.only`. +- Handles `test.onFinish` and `test.onFailure` (by commenting them out with a TODO). +- Supports loose equality assertions (e.g., `t.looseEqual` -> `assert.equal`). + +## Example + +### Basic Equality + +```diff +- import test from "tape"; ++ import { test } from 'node:test'; ++ import assert from 'node:assert'; + +- test("basic equality", (t) => { ++ test("basic equality", async (t) => { +- t.plan(4); ++ // t.plan(4); +- t.equal(1, 1, "equal numbers"); ++ assert.strictEqual(1, 1, "equal numbers"); +- t.notEqual(1, 2, "not equal numbers"); ++ assert.notStrictEqual(1, 2, "not equal numbers"); +- t.strictEqual(true, true, "strict equality"); ++ assert.strictEqual(true, true, "strict equality"); +- t.notStrictEqual("1", 1, "not strict equality"); ++ assert.notStrictEqual("1", 1, "not strict equality"); +- t.end(); ++ // t.end(); + }); +``` + +### Async Tests + +```diff +- import test from "tape"; ++ import { test } from 'node:test'; ++ import assert from 'node:assert'; + + function someAsyncThing() { + return new Promise((resolve) => setTimeout(() => resolve(true), 50)); + } + +- test("async test with promises", async (t) => { ++ test("async test with promises", async (t) => { +- t.plan(1); ++ // t.plan(1); + const result = await someAsyncThing(); +- t.ok(result, "async result is truthy"); ++ assert.ok(result, "async result is truthy"); + }); +``` + +### Callback Style + +```diff +- import test from "tape"; ++ import { test } from 'node:test'; ++ import assert from 'node:assert/strict'; + +- test("callback style", (t) => { ++ test("callback style", (t, done) => { + setTimeout(() => { +- t.ok(true); ++ assert.ok(true); +- t.end(); ++ done(); + }, 100); + }); +``` + +### Timeout Handling + +```diff +- import test from "tape"; ++ import { test } from 'node:test'; ++ import assert from 'node:assert/strict'; + +- test("timeout test", (t) => { ++ test("timeout test", { timeout: 100 }, async (t) => { +- t.timeoutAfter(100); +- t.ok(true); ++ assert.ok(true); +- t.end(); ++ // t.end(); + }); +``` + +### Dynamic Import + +```diff + async function run() { +- const test = await import("tape"); ++ const { test } = await import('node:test'); ++ const { default: assert } = await import('node:assert/strict'); + +- test("dynamic import", (t) => { ++ test("dynamic import", async (t) => { +- t.ok(true); ++ assert.ok(true); +- t.end(); ++ // t.end(); + }); + } +``` diff --git a/recipes/tape-to-node-test/codemod.yaml b/recipes/tape-to-node-test/codemod.yaml new file mode 100644 index 00000000..e5128dd7 --- /dev/null +++ b/recipes/tape-to-node-test/codemod.yaml @@ -0,0 +1,23 @@ +schema_version: "1.0" +name: "@nodejs/tape-to-node-test" +version: "1.0.0" +description: Migrates Tape tests to Node.js native test runner +author: Node.js +license: MIT +workflow: workflow.yaml +category: migration + +targets: + languages: + - javascript + - typescript + +keywords: + - transformation + - migration + - tape + - node:test + +capabilities: + - fs + - child_process diff --git a/recipes/tape-to-node-test/package.json b/recipes/tape-to-node-test/package.json new file mode 100644 index 00000000..0dfefeff --- /dev/null +++ b/recipes/tape-to-node-test/package.json @@ -0,0 +1,20 @@ +{ + "name": "@nodejs/tape-to-node-test", + "version": "1.0.0", + "description": "Migrates Tape tests to Node.js native test runner", + "type": "module", + "scripts": { + "test": "npx codemod jssg test -l typescript ./src/workflow.ts" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nodejs/userland-migrations.git", + "directory": "recipes/tape-to-node-test", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "Node.js", + "license": "MIT", + "dependencies": { + "@nodejs/codemod-utils": "*" + } +} diff --git a/recipes/tape-to-node-test/src/remove-dependencies.ts b/recipes/tape-to-node-test/src/remove-dependencies.ts new file mode 100644 index 00000000..5c0ed0c6 --- /dev/null +++ b/recipes/tape-to-node-test/src/remove-dependencies.ts @@ -0,0 +1,8 @@ +import removeDependencies from '@nodejs/codemod-utils/remove-dependencies'; + +/** + * Remove tape and @types/tape dependencies from package.json + */ +export default function removeTapeDependencies(): string | null { + return removeDependencies(['tape', '@types/tape']); +} diff --git a/recipes/tape-to-node-test/src/workflow.ts b/recipes/tape-to-node-test/src/workflow.ts new file mode 100644 index 00000000..eae05673 --- /dev/null +++ b/recipes/tape-to-node-test/src/workflow.ts @@ -0,0 +1,521 @@ +import { EOL } from 'node:os'; +import { + getNodeImportStatements, + getNodeImportCalls, +} from '@nodejs/codemod-utils/ast-grep/import-statement'; +import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call'; +import type { SgRoot, SgNode, Edit } from '@codemod.com/jssg-types/main'; +import type Js from '@codemod.com/jssg-types/langs/javascript'; + +/** + * Mapping of Tape assertions to Node.js assert module methods + */ +const ASSERTION_MAPPING: Record = { + equal: 'strictEqual', + notEqual: 'notStrictEqual', + strictEqual: 'strictEqual', + notStrictEqual: 'notStrictEqual', + deepEqual: 'deepStrictEqual', + notDeepEqual: 'notDeepStrictEqual', + looseEqual: 'equal', + notLooseEqual: 'notEqual', + ok: 'ok', + ifError: 'ifError', + error: 'ifError', + throws: 'throws', + doesNotThrow: 'doesNotThrow', + match: 'match', + doesNotMatch: 'doesNotMatch', + fail: 'fail', + same: 'deepStrictEqual', + notSame: 'notDeepStrictEqual', + // Aliases + assert: 'ok', + ifErr: 'ifError', + iferror: 'ifError', + equals: 'strictEqual', + isEqual: 'strictEqual', + strictEquals: 'strictEqual', + is: 'strictEqual', + notEquals: 'notStrictEqual', + isNotEqual: 'notStrictEqual', + doesNotEqual: 'notStrictEqual', + isInequal: 'notStrictEqual', + notStrictEquals: 'notStrictEqual', + isNot: 'notStrictEqual', + not: 'notStrictEqual', + looseEquals: 'equal', + notLooseEquals: 'notEqual', + deepEquals: 'deepStrictEqual', + isEquivalent: 'deepStrictEqual', + notDeepEquals: 'notDeepStrictEqual', + notEquivalent: 'notDeepStrictEqual', + notDeeply: 'notDeepStrictEqual', + isNotDeepEqual: 'notDeepStrictEqual', + isNotDeeply: 'notDeepStrictEqual', + isNotEquivalent: 'notDeepStrictEqual', + isInequivalent: 'notDeepStrictEqual', + deepLooseEqual: 'deepEqual', + notDeepLooseEqual: 'notDeepEqual', +}; + +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const edits: Edit[] = []; + + const tapeImports = getNodeImportStatements(root, 'tape'); + const tapeRequires = getNodeRequireCalls(root, 'tape'); + const tapeImportCalls = getNodeImportCalls(root, 'tape'); + + if ( + tapeImports.length === 0 && + tapeRequires.length === 0 && + tapeImportCalls.length === 0 + ) { + return null; + } + + let testVarName = 'test'; + + // 1. Replace imports + for (const imp of tapeImports) { + const defaultImport = imp.find({ + rule: { kind: 'import_clause', has: { kind: 'identifier' } }, + }); + if (defaultImport) { + const id = defaultImport.find({ rule: { kind: 'identifier' } }); + if (id) testVarName = id.text(); + edits.push( + imp.replace( + `import { test } from 'node:test';${EOL}import assert from 'node:assert';`, + ), + ); + } + } + + for (const req of tapeRequires) { + const id = req.find({ + rule: { kind: 'identifier', inside: { kind: 'variable_declarator' } }, + }); + if (id) testVarName = id.text(); + const declaration = req + .ancestors() + .find( + (a) => + a.kind() === 'variable_declaration' || + a.kind() === 'lexical_declaration', + ); + if (declaration) { + edits.push( + declaration.replace( + `const { test } = require('node:test');${EOL}const assert = require('node:assert');`, + ), + ); + } + } + + for (const call of tapeImportCalls) { + const id = call.find({ + rule: { kind: 'identifier', inside: { kind: 'variable_declarator' } }, + }); + if (id) testVarName = id.text(); + const declaration = call + .ancestors() + .find( + (a) => + a.kind() === 'variable_declaration' || + a.kind() === 'lexical_declaration', + ); + if (declaration) { + edits.push( + declaration.replace( + `const { test } = await import('node:test');${EOL}const { default: assert } = await import('node:assert');`, + ), + ); + } + } + + const testCalls = rootNode.findAll({ + rule: { + kind: 'call_expression', + has: { + field: 'function', + regex: `^${testVarName}(\\.(skip|only))?$`, + }, + }, + }); + + // 2. Transform test calls and assertions + for (const call of testCalls) { + const func = call.field('function'); + if (func && testVarName !== 'test') { + if (func.kind() === 'identifier' && func.text() === testVarName) { + edits.push(func.replace('test')); + } else if (func.kind() === 'member_expression') { + const obj = func.field('object'); + if (obj && obj.text() === testVarName) { + edits.push(obj.replace('test')); + } + } + } + + const args = call.field('arguments'); + if (!args) continue; + + const callback = args + .children() + .find( + (c) => + c.kind() === 'arrow_function' || c.kind() === 'function_expression', + ); + if (callback) { + const params = callback.field('parameters'); + let tName = 't'; + const paramId = params?.find({ rule: { kind: 'identifier' } }); + if (paramId) { + tName = paramId.text(); + } + + const body = callback.field('body'); + let usesEndInCallback = false; + if (body) { + const endCalls = body.findAll({ + rule: { + kind: 'call_expression', + has: { + field: 'function', + kind: 'member_expression', + has: { + field: 'object', + pattern: tName, + }, + }, + }, + }); + + for (const endCall of endCalls) { + let curr = endCall.parent(); + while (curr && curr.id() !== body.id()) { + if ( + curr.kind() === 'arrow_function' || + curr.kind() === 'function_expression' || + curr.kind() === 'function_declaration' + ) { + usesEndInCallback = true; + break; + } + curr = curr.parent(); + } + } + } + + const isAsync = callback.text().startsWith('async'); + let useDone = false; + + if (usesEndInCallback && !isAsync) { + useDone = true; + if (params) { + const text = params.text(); + if (text.startsWith('(') && text.endsWith(')')) { + edits.push({ + startPos: params.range().end.index - 1, + endPos: params.range().end.index - 1, + insertedText: ', done', + }); + } else { + edits.push(params.replace(`(${text}, done)`)); + } + } + } + + if (body) { + transformAssertions(body, tName, edits, call, useDone); + } + + if (!usesEndInCallback && !isAsync) { + if (params) { + edits.push({ + startPos: callback.range().start.index, + endPos: params.range().start.index, + insertedText: 'async ', + }); + } + } + } + } + + // 3. Handle test.onFinish and test.onFailure + const lifecycleCalls = rootNode.findAll({ + rule: { + kind: 'call_expression', + has: { + field: 'function', + regex: `^${testVarName}\\.(onFinish|onFailure)$`, + }, + }, + }); + + for (const call of lifecycleCalls) { + const lines = call.text().split(/\r?\n/); + const newText = lines + .map((line, i) => (i === 0 ? `// TODO: ${line}` : `// ${line}`)) + .join(EOL); + edits.push(call.replace(newText)); + } + + return rootNode.commitEdits(edits); +} + +/** + * Transform Tape assertions to Node.js assert module assertions + * + * @param node the AST node to transform + * @param tName the name of the test object (usually 't') + * @param edits the list of edits to apply + * @param testCall the AST node of the test function call + * @param useDone whether to use the done callback for ending tests + */ +function transformAssertions( + node: SgNode, + tName: string, + edits: Edit[], + testCall: SgNode, + useDone = false, +) { + const calls = node.findAll({ + rule: { + kind: 'call_expression', + has: { + field: 'function', + kind: 'member_expression', + has: { + field: 'object', + pattern: tName, + }, + }, + }, + }); + + for (const call of calls) { + const method = call.field('function')?.field('property')?.text(); + if (!method) continue; + + const args = call.field('arguments'); + const func = call.field('function'); + + if (ASSERTION_MAPPING[method]) { + const newMethod = ASSERTION_MAPPING[method]; + if (func) { + edits.push(func.replace(`assert.${newMethod}`)); + } + continue; + } + + switch (method.toLowerCase()) { + case 'notok': + // t.notOk(val, msg) -> assert.ok(!val, msg) + if (args) { + const val = args.child(1); // child(0) is '(' + if (val) { + edits.push({ + startPos: val.range().start.index, + endPos: val.range().start.index, + insertedText: '!', + }); + const func = call.field('function'); + if (func) edits.push(func.replace('assert.ok')); + } + } + break; + case 'comment': + if (func) edits.push(func.replace(`${tName}.diagnostic`)); + break; + case 'true': + if (func) edits.push(func.replace('assert.ok')); + break; + case 'false': + if (args) { + const val = args.child(1); + if (val) { + edits.push({ + startPos: val.range().start.index, + endPos: val.range().start.index, + insertedText: '!', + }); + if (func) edits.push(func.replace('assert.ok')); + } + } + break; + case 'pass': + if (args) { + // Insert 'true' as first arg + // args text is like "('msg')" or "()" + const openParen = args.child(0); + if (openParen) { + edits.push({ + startPos: openParen.range().end.index, + endPos: openParen.range().end.index, + insertedText: args.children().length > 2 ? 'true, ' : 'true', + }); + if (func) edits.push(func.replace('assert.ok')); + } + } + break; + case 'plan': + edits.push(call.replace(`// ${call.text()}`)); + break; + case 'end': + if (useDone) { + edits.push(call.replace('done()')); + } else { + edits.push(call.replace(`// ${call.text()}`)); + } + break; + case 'test': { + edits.push({ + startPos: call.range().start.index, + endPos: call.range().start.index, + insertedText: 'await ', + }); + const cb = args + ?.children() + .find( + (c) => + c.kind() === 'arrow_function' || + c.kind() === 'function_expression', + ); + if (cb) { + const p = cb.field('parameters'); + let stName = 't'; + const paramId = p?.find({ rule: { kind: 'identifier' } }); + if (paramId) stName = paramId.text(); + + const b = cb.field('body'); + if (b) transformAssertions(b, stName, edits, call); + + if (!cb.text().startsWith('async')) { + if (p) { + edits.push({ + startPos: cb.range().start.index, + endPos: p.range().start.index, + insertedText: 'async ', + }); + } + } + } + break; + } + case 'teardown': + if (func) edits.push(func.replace(`${tName}.after`)); + break; + case 'timeoutafter': { + const timeoutArg = args?.child(1); // child(0) is '(' + if (timeoutArg) { + const timeoutVal = timeoutArg.text(); + + // Add to test options + const testArgs = testCall.field('arguments'); + if (testArgs) { + const children = testArgs.children(); + // children[0] is '(', children[last] is ')' + // args are in between. + // We expect: + // 1. test('name', cb) -> insert options + // 2. test('name', opts, cb) -> update options + + // Filter out punctuation to get actual args + const actualArgs = children.filter( + (c) => + c.kind() !== '(' && + c.kind() !== ')' && + c.kind() !== ',' && + c.kind() !== 'comment', + ); + + if (actualArgs.length === 2) { + // test('name', cb) + // Insert options as 2nd arg + const cbArg = actualArgs[1]; + edits.push({ + startPos: cbArg.range().start.index, + endPos: cbArg.range().start.index, + insertedText: `{ timeout: ${timeoutVal} }, `, + }); + // remove the original timeout call + const parent = call.parent(); + if (parent && parent.kind() === 'expression_statement') { + edits.push(parent.replace('')); + } else { + edits.push(call.replace('')); + } + } else if (actualArgs.length === 3) { + // test('name', opts, cb) + const optsArg = actualArgs[1]; + if (optsArg.kind() === 'object') { + // Add property to object + const props = optsArg + .children() + .filter((c) => c.kind() === 'pair'); + if (props.length > 0) { + const lastProp = props[props.length - 1]; + edits.push({ + startPos: lastProp.range().end.index, + endPos: lastProp.range().end.index, + insertedText: `, timeout: ${timeoutVal}`, + }); + // remove the original timeout call + const parent = call.parent(); + if (parent && parent.kind() === 'expression_statement') { + edits.push(parent.replace('')); + } else { + edits.push(call.replace('')); + } + } else { + // Empty object {} + // We need to find where to insert. + // It's safer to replace the whole object if it's empty, or find the closing brace. + const closingBrace = optsArg + .children() + .find((c) => c.text() === '}'); + if (closingBrace) { + edits.push({ + startPos: closingBrace.range().start.index, + endPos: closingBrace.range().start.index, + insertedText: ` timeout: ${timeoutVal} `, + }); + // remove the original timeout call + const parent = call.parent(); + if (parent && parent.kind() === 'expression_statement') { + edits.push(parent.replace('')); + } else { + edits.push(call.replace('')); + } + } + } + } else { + // Options is a variable or expression — replace the timeout call with a TODO comment + edits.push( + call.replace( + `// TODO: Add timeout: ${timeoutVal} to test options manually`, + ), + ); + } + } + } else { + // If we couldn't find the test call args, remove the timeout call + const parent = call.parent(); + if (parent && parent.kind() === 'expression_statement') { + edits.push(parent.replace('')); + } else { + edits.push(call.replace('')); + } + } + } + + break; + } + default: + console.log('method not handled'); + } + } +} diff --git a/recipes/tape-to-node-test/tests/advanced-assertions/expected.js b/recipes/tape-to-node-test/tests/advanced-assertions/expected.js new file mode 100644 index 00000000..44681e4f --- /dev/null +++ b/recipes/tape-to-node-test/tests/advanced-assertions/expected.js @@ -0,0 +1,13 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; + +test('advanced assertions', async (t) => { + assert.throws(() => { throw new Error('fail'); }, /fail/); + assert.doesNotThrow(() => { }); + assert.match('string', /ring/); + assert.doesNotMatch('string', /gnirt/); + assert.fail('this should fail'); + assert.ifError(null); + assert.ifError(null); + // t.end(); +}); diff --git a/recipes/tape-to-node-test/tests/advanced-assertions/input.js b/recipes/tape-to-node-test/tests/advanced-assertions/input.js new file mode 100644 index 00000000..2be9e2ea --- /dev/null +++ b/recipes/tape-to-node-test/tests/advanced-assertions/input.js @@ -0,0 +1,12 @@ +import test from 'tape'; + +test('advanced assertions', (t) => { + t.throws(() => { throw new Error('fail'); }, /fail/); + t.doesNotThrow(() => { }); + t.match('string', /ring/); + t.doesNotMatch('string', /gnirt/); + t.fail('this should fail'); + t.error(null); + t.ifError(null); + t.end(); +}); diff --git a/recipes/tape-to-node-test/tests/aliased-import/expected.js b/recipes/tape-to-node-test/tests/aliased-import/expected.js new file mode 100644 index 00000000..2a9c33a5 --- /dev/null +++ b/recipes/tape-to-node-test/tests/aliased-import/expected.js @@ -0,0 +1,7 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; + +test('aliased test', async (t) => { + assert.strictEqual(1, 1); + // t.end(); +}); diff --git a/recipes/tape-to-node-test/tests/aliased-import/input.js b/recipes/tape-to-node-test/tests/aliased-import/input.js new file mode 100644 index 00000000..e00f6825 --- /dev/null +++ b/recipes/tape-to-node-test/tests/aliased-import/input.js @@ -0,0 +1,6 @@ +import myTest from 'tape'; + +myTest('aliased test', (t) => { + t.equal(1, 1); + t.end(); +}); diff --git a/recipes/tape-to-node-test/tests/async-test/expected.js b/recipes/tape-to-node-test/tests/async-test/expected.js new file mode 100644 index 00000000..40cb081e --- /dev/null +++ b/recipes/tape-to-node-test/tests/async-test/expected.js @@ -0,0 +1,12 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; + +function someAsyncThing() { + return new Promise((resolve) => setTimeout(() => resolve(true), 50)); +} + +test("async test with promises", async (t) => { + // t.plan(1); + const result = await someAsyncThing(); + assert.ok(result, "async result is truthy"); +}); diff --git a/recipes/tape-to-node-test/tests/async-test/input.js b/recipes/tape-to-node-test/tests/async-test/input.js new file mode 100644 index 00000000..2ff9beb7 --- /dev/null +++ b/recipes/tape-to-node-test/tests/async-test/input.js @@ -0,0 +1,11 @@ +import test from "tape"; + +function someAsyncThing() { + return new Promise((resolve) => setTimeout(() => resolve(true), 50)); +} + +test("async test with promises", async (t) => { + t.plan(1); + const result = await someAsyncThing(); + t.ok(result, "async result is truthy"); +}); diff --git a/recipes/tape-to-node-test/tests/basic-equality/expected.js b/recipes/tape-to-node-test/tests/basic-equality/expected.js new file mode 100644 index 00000000..67f743fc --- /dev/null +++ b/recipes/tape-to-node-test/tests/basic-equality/expected.js @@ -0,0 +1,11 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; + +test("basic equality", async (t) => { + // t.plan(4); + assert.strictEqual(1, 1, "equal numbers"); + assert.notStrictEqual(1, 2, "not equal numbers"); + assert.strictEqual(true, true, "strict equality"); + assert.notStrictEqual("1", 1, "not strict equality"); + // t.end(); +}); diff --git a/recipes/tape-to-node-test/tests/basic-equality/input.js b/recipes/tape-to-node-test/tests/basic-equality/input.js new file mode 100644 index 00000000..3d3a7533 --- /dev/null +++ b/recipes/tape-to-node-test/tests/basic-equality/input.js @@ -0,0 +1,10 @@ +import test from "tape"; + +test("basic equality", (t) => { + t.plan(4); + t.equal(1, 1, "equal numbers"); + t.notEqual(1, 2, "not equal numbers"); + t.strictEqual(true, true, "strict equality"); + t.notStrictEqual("1", 1, "not strict equality"); + t.end(); +}); diff --git a/recipes/tape-to-node-test/tests/callback-style/expected.js b/recipes/tape-to-node-test/tests/callback-style/expected.js new file mode 100644 index 00000000..d68f60fa --- /dev/null +++ b/recipes/tape-to-node-test/tests/callback-style/expected.js @@ -0,0 +1,9 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; + +test("callback style", (t, done) => { + setTimeout(() => { + assert.ok(true); + done(); + }, 100); +}); diff --git a/recipes/tape-to-node-test/tests/callback-style/input.js b/recipes/tape-to-node-test/tests/callback-style/input.js new file mode 100644 index 00000000..bbbe722e --- /dev/null +++ b/recipes/tape-to-node-test/tests/callback-style/input.js @@ -0,0 +1,8 @@ +import test from "tape"; + +test("callback style", (t) => { + setTimeout(() => { + t.ok(true); + t.end(); + }, 100); +}); diff --git a/recipes/tape-to-node-test/tests/cjs-destructuring/expected.js b/recipes/tape-to-node-test/tests/cjs-destructuring/expected.js new file mode 100644 index 00000000..b8fced1e --- /dev/null +++ b/recipes/tape-to-node-test/tests/cjs-destructuring/expected.js @@ -0,0 +1,7 @@ +const { test } = require('node:test'); +const assert = require('node:assert'); + +test('cjs destructuring', async (t) => { + assert.ok(true); + // t.end(); +}); diff --git a/recipes/tape-to-node-test/tests/cjs-destructuring/input.js b/recipes/tape-to-node-test/tests/cjs-destructuring/input.js new file mode 100644 index 00000000..36d7268c --- /dev/null +++ b/recipes/tape-to-node-test/tests/cjs-destructuring/input.js @@ -0,0 +1,6 @@ +const { test } = require('tape'); + +test('cjs destructuring', (t) => { + t.ok(true); + t.end(); +}); diff --git a/recipes/tape-to-node-test/tests/deep-equality/expected.js b/recipes/tape-to-node-test/tests/deep-equality/expected.js new file mode 100644 index 00000000..33e66855 --- /dev/null +++ b/recipes/tape-to-node-test/tests/deep-equality/expected.js @@ -0,0 +1,8 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; + +test("deep equality", async (t) => { + // t.plan(2); + assert.deepStrictEqual({ a: 1 }, { a: 1 }, "objects are deeply equal"); + assert.notDeepStrictEqual({ a: 1 }, { a: 2 }, "objects are not deeply equal"); +}); diff --git a/recipes/tape-to-node-test/tests/deep-equality/input.js b/recipes/tape-to-node-test/tests/deep-equality/input.js new file mode 100644 index 00000000..9ec78c68 --- /dev/null +++ b/recipes/tape-to-node-test/tests/deep-equality/input.js @@ -0,0 +1,7 @@ +import test from "tape"; + +test("deep equality", (t) => { + t.plan(2); + t.deepEqual({ a: 1 }, { a: 1 }, "objects are deeply equal"); + t.notDeepEqual({ a: 1 }, { a: 2 }, "objects are not deeply equal"); +}); diff --git a/recipes/tape-to-node-test/tests/dynamic-import/expected.js b/recipes/tape-to-node-test/tests/dynamic-import/expected.js new file mode 100644 index 00000000..30a80504 --- /dev/null +++ b/recipes/tape-to-node-test/tests/dynamic-import/expected.js @@ -0,0 +1,9 @@ +async function run() { + const { test } = await import('node:test'); +const { default: assert } = await import('node:assert'); + + test("dynamic import", async (t) => { + assert.ok(true); + // t.end(); + }); +} diff --git a/recipes/tape-to-node-test/tests/dynamic-import/input.js b/recipes/tape-to-node-test/tests/dynamic-import/input.js new file mode 100644 index 00000000..055e41ae --- /dev/null +++ b/recipes/tape-to-node-test/tests/dynamic-import/input.js @@ -0,0 +1,8 @@ +async function run() { + const test = await import("tape"); + + test("dynamic import", (t) => { + t.ok(true); + t.end(); + }); +} diff --git a/recipes/tape-to-node-test/tests/lifecycle/expected.js b/recipes/tape-to-node-test/tests/lifecycle/expected.js new file mode 100644 index 00000000..fd056b67 --- /dev/null +++ b/recipes/tape-to-node-test/tests/lifecycle/expected.js @@ -0,0 +1,10 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; + +let teardownState = 1; + +test("teardown registers and runs after test", async (t) => { + // t.plan(1); + t.after(() => { teardownState = 0; }); + assert.strictEqual(teardownState, 1, "state before teardown"); +}); diff --git a/recipes/tape-to-node-test/tests/lifecycle/input.js b/recipes/tape-to-node-test/tests/lifecycle/input.js new file mode 100644 index 00000000..3116d00c --- /dev/null +++ b/recipes/tape-to-node-test/tests/lifecycle/input.js @@ -0,0 +1,9 @@ +import test from "tape"; + +let teardownState = 1; + +test("teardown registers and runs after test", (t) => { + t.plan(1); + t.teardown(() => { teardownState = 0; }); + t.equal(teardownState, 1, "state before teardown"); +}); diff --git a/recipes/tape-to-node-test/tests/nested-test/expected.js b/recipes/tape-to-node-test/tests/nested-test/expected.js new file mode 100644 index 00000000..3fa5ea21 --- /dev/null +++ b/recipes/tape-to-node-test/tests/nested-test/expected.js @@ -0,0 +1,10 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; + +test("nested tests", async (t) => { + // t.plan(1); + await t.test("inner test 1", async (st) => { + // st.plan(1); + assert.strictEqual(1, 1, "inner assertion"); + }); +}); diff --git a/recipes/tape-to-node-test/tests/nested-test/input.js b/recipes/tape-to-node-test/tests/nested-test/input.js new file mode 100644 index 00000000..4b585062 --- /dev/null +++ b/recipes/tape-to-node-test/tests/nested-test/input.js @@ -0,0 +1,9 @@ +import test from "tape"; + +test("nested tests", (t) => { + t.plan(1); + t.test("inner test 1", (st) => { + st.plan(1); + st.equal(1, 1, "inner assertion"); + }); +}); diff --git a/recipes/tape-to-node-test/tests/new-features/expected.js b/recipes/tape-to-node-test/tests/new-features/expected.js new file mode 100644 index 00000000..f123825d --- /dev/null +++ b/recipes/tape-to-node-test/tests/new-features/expected.js @@ -0,0 +1,18 @@ +const { test } = require('node:test'); +const assert = require('node:assert'); + +test('new features', async (t) => { + assert.strictEqual(1, 1, 'equals alias'); + assert.strictEqual(1, 1, 'is alias'); + assert.notStrictEqual(1, 2, 'notEquals alias'); + assert.equal(1, '1', 'looseEqual'); + assert.notEqual(1, '2', 'notLooseEqual'); + assert.deepEqual({ a: 1 }, { a: '1' }, 'deepLooseEqual'); + t.diagnostic('this is a comment'); + assert.ok(!false, 'notOk'); + // t.end(); +}); + +// TODO: test.onFinish(() => { +// console.log('finished'); +// }); diff --git a/recipes/tape-to-node-test/tests/new-features/input.js b/recipes/tape-to-node-test/tests/new-features/input.js new file mode 100644 index 00000000..c463782b --- /dev/null +++ b/recipes/tape-to-node-test/tests/new-features/input.js @@ -0,0 +1,17 @@ +const test = require('tape'); + +test('new features', (t) => { + t.equals(1, 1, 'equals alias'); + t.is(1, 1, 'is alias'); + t.notEquals(1, 2, 'notEquals alias'); + t.looseEqual(1, '1', 'looseEqual'); + t.notLooseEqual(1, '2', 'notLooseEqual'); + t.deepLooseEqual({ a: 1 }, { a: '1' }, 'deepLooseEqual'); + t.comment('this is a comment'); + t.notOk(false, 'notOk'); + t.end(); +}); + +test.onFinish(() => { + console.log('finished'); +}); diff --git a/recipes/tape-to-node-test/tests/no-callback-args/expected.js b/recipes/tape-to-node-test/tests/no-callback-args/expected.js new file mode 100644 index 00000000..50dc6bfc --- /dev/null +++ b/recipes/tape-to-node-test/tests/no-callback-args/expected.js @@ -0,0 +1,10 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; + +test('sync test with no args', async () => { + const a = 1; +}); + +test('async test with no args', async () => { + const a = 1; +}); diff --git a/recipes/tape-to-node-test/tests/no-callback-args/input.js b/recipes/tape-to-node-test/tests/no-callback-args/input.js new file mode 100644 index 00000000..a91e3219 --- /dev/null +++ b/recipes/tape-to-node-test/tests/no-callback-args/input.js @@ -0,0 +1,9 @@ +import test from 'tape'; + +test('sync test with no args', () => { + const a = 1; +}); + +test('async test with no args', async () => { + const a = 1; +}); diff --git a/recipes/tape-to-node-test/tests/require-import/expected.js b/recipes/tape-to-node-test/tests/require-import/expected.js new file mode 100644 index 00000000..630eed4f --- /dev/null +++ b/recipes/tape-to-node-test/tests/require-import/expected.js @@ -0,0 +1,7 @@ +const { test } = require('node:test'); +const assert = require('node:assert'); + +test("require test", async (t) => { + assert.strictEqual(1, 1); + // t.end(); +}); diff --git a/recipes/tape-to-node-test/tests/require-import/input.js b/recipes/tape-to-node-test/tests/require-import/input.js new file mode 100644 index 00000000..ce8e9b3e --- /dev/null +++ b/recipes/tape-to-node-test/tests/require-import/input.js @@ -0,0 +1,6 @@ +const test = require("tape"); + +test("require test", (t) => { + t.equal(1, 1); + t.end(); +}); diff --git a/recipes/tape-to-node-test/tests/test-options/expected.js b/recipes/tape-to-node-test/tests/test-options/expected.js new file mode 100644 index 00000000..0dd1bc4a --- /dev/null +++ b/recipes/tape-to-node-test/tests/test-options/expected.js @@ -0,0 +1,12 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; + +test.skip('skipped test', async (t) => { + assert.fail('should not run'); + // t.end(); +}); + +test.only('only test', async (t) => { + assert.ok(true, 'should run'); + // t.end(); +}); diff --git a/recipes/tape-to-node-test/tests/test-options/input.js b/recipes/tape-to-node-test/tests/test-options/input.js new file mode 100644 index 00000000..3a5424f0 --- /dev/null +++ b/recipes/tape-to-node-test/tests/test-options/input.js @@ -0,0 +1,11 @@ +import test from 'tape'; + +test.skip('skipped test', (t) => { + t.fail('should not run'); + t.end(); +}); + +test.only('only test', (t) => { + t.pass('should run'); + t.end(); +}); diff --git a/recipes/tape-to-node-test/tests/timeout-non-object/expected.js b/recipes/tape-to-node-test/tests/timeout-non-object/expected.js new file mode 100644 index 00000000..7990c57a --- /dev/null +++ b/recipes/tape-to-node-test/tests/timeout-non-object/expected.js @@ -0,0 +1,9 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; + +const opts = { skip: false }; + +test('timeout with variable opts', opts, async (t) => { + // TODO: Add timeout: 123 to test options manually; + // t.end(); +}); diff --git a/recipes/tape-to-node-test/tests/timeout-non-object/input.js b/recipes/tape-to-node-test/tests/timeout-non-object/input.js new file mode 100644 index 00000000..8a4141f5 --- /dev/null +++ b/recipes/tape-to-node-test/tests/timeout-non-object/input.js @@ -0,0 +1,8 @@ +import test from 'tape'; + +const opts = { skip: false }; + +test('timeout with variable opts', opts, (t) => { + t.timeoutAfter(123); + t.end(); +}); diff --git a/recipes/tape-to-node-test/tests/timeout/expected.js b/recipes/tape-to-node-test/tests/timeout/expected.js new file mode 100644 index 00000000..62d486eb --- /dev/null +++ b/recipes/tape-to-node-test/tests/timeout/expected.js @@ -0,0 +1,19 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; + +test('timeout test', { timeout: 100 }, async (t) => { + + // t.end(); +}); + +test('timeout test with options', { skip: false, timeout: 200 }, async (t) => { + + // t.end(); +}); + +test('nested timeout', async (t) => { + await t.test('inner', { timeout: 50 }, async (st) => { + + // st.end(); + }); +}); diff --git a/recipes/tape-to-node-test/tests/timeout/input.js b/recipes/tape-to-node-test/tests/timeout/input.js new file mode 100644 index 00000000..7e055130 --- /dev/null +++ b/recipes/tape-to-node-test/tests/timeout/input.js @@ -0,0 +1,18 @@ +import test from 'tape'; + +test('timeout test', (t) => { + t.timeoutAfter(100); + t.end(); +}); + +test('timeout test with options', { skip: false }, (t) => { + t.timeoutAfter(200); + t.end(); +}); + +test('nested timeout', (t) => { + t.test('inner', (st) => { + st.timeoutAfter(50); + st.end(); + }); +}); diff --git a/recipes/tape-to-node-test/tests/truthiness/expected.js b/recipes/tape-to-node-test/tests/truthiness/expected.js new file mode 100644 index 00000000..fc7b2962 --- /dev/null +++ b/recipes/tape-to-node-test/tests/truthiness/expected.js @@ -0,0 +1,11 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; + +test("truthiness", async (t) => { + // t.plan(4); + assert.ok(true, "true is ok"); + assert.ok(!false, "false is not ok"); + assert.ok(true, "explicitly true"); + assert.ok(!false, "explicitly false"); + assert.ok(true, "this passed"); +}); diff --git a/recipes/tape-to-node-test/tests/truthiness/input.js b/recipes/tape-to-node-test/tests/truthiness/input.js new file mode 100644 index 00000000..c951686c --- /dev/null +++ b/recipes/tape-to-node-test/tests/truthiness/input.js @@ -0,0 +1,10 @@ +import test from "tape"; + +test("truthiness", (t) => { + t.plan(4); + t.ok(true, "true is ok"); + t.notOk(false, "false is not ok"); + t.true(true, "explicitly true"); + t.false(false, "explicitly false"); + t.pass("this passed"); +}); diff --git a/recipes/tape-to-node-test/workflow.yaml b/recipes/tape-to-node-test/workflow.yaml new file mode 100644 index 00000000..7b331fdb --- /dev/null +++ b/recipes/tape-to-node-test/workflow.yaml @@ -0,0 +1,39 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod-com/codemod/refs/heads/main/schemas/workflow.json + +version: "1" + +nodes: + - id: apply-transforms + name: Apply AST Transformations + type: automatic + runtime: + type: direct + steps: + - name: Migrates Tape tests to Node.js native test runner + js-ast-grep: + js_file: src/workflow.ts + base_path: . + include: + - "**/*.cjs" + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.ts" + - "**/*.tsx" + + - id: remove-dependencies + name: Remove chalk dependency + type: automatic + steps: + - name: Detect package manager and remove chalk dependency + js-ast-grep: + js_file: src/remove-dependencies.ts + base_path: . + include: + - "**/package.json" + exclude: + - "**/node_modules/**" + language: typescript + capabilities: + - child_process + - fs