diff --git a/bin/d2l-test-runner.js b/bin/d2l-test-runner.js index c4d07e25..6ac43891 100755 --- a/bin/d2l-test-runner.js +++ b/bin/d2l-test-runner.js @@ -1,7 +1,34 @@ #!/usr/bin/env node -import { argv } from 'node:process'; +import commandLineArgs from 'command-line-args'; +import process from 'node:process'; import { runner } from '../src/server/cli/test-runner.js'; -const options = await runner.getOptions(argv); +const { argv, stdout } = process; +const cli = commandLineArgs({ name: 'subcommand', defaultOption: true }, { stopAtFirstUnknown: true, argv }); -await runner.start(options); +if (cli.subcommand === 'vdiff') { + const vdiff = commandLineArgs({ name: 'subcommand', defaultOption: true }, { stopAtFirstUnknown: true, argv: cli._unknown || [] }); + + if (!vdiff.subcommand) { + runTests(); + } else if (vdiff.subcommand === 'golden') { + argv.splice(argv.findIndex(a => a === 'golden'), 1, '--golden'); + stdout.write('\nGenerating vdiff goldens...\n'); + runTests(); + } else if (vdiff.subcommand === 'report') { + const { report } = await import('../src/server/cli/vdiff/report.js'); + await report.start(); + } else if (vdiff.subcommand === 'migrate') { + const { migrate } = await import('../src/server/cli/vdiff/migrate.js'); + await migrate.start(vdiff._unknown); + } else { + stdout.write(`\nfatal: unknown subcomamnd: ${vdiff.subcommand}\n`); + } +} else { + runTests(); +} + +async function runTests() { + const options = await runner.getOptions(argv); + await runner.start(options); +} diff --git a/bin/d2l-test-vdiff-report.js b/bin/d2l-test-vdiff-report.js deleted file mode 100755 index 3ee7b9a9..00000000 --- a/bin/d2l-test-vdiff-report.js +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env node -import { PATHS } from '../src/server/visual-diff-plugin.js'; -import { startDevServer } from '@web/dev-server'; - -await startDevServer({ - config: { - nodeResolve: false, - open: `./${PATHS.REPORT_ROOT}/`, - rootDir: `${PATHS.VDIFF_ROOT}`, - preserveSymlinks: false, - watch: true - }, - readCliArgs: false, - readFileConfig: false -}); diff --git a/bin/migrate-goldens.js b/bin/migrate-goldens.js deleted file mode 100755 index 2977d990..00000000 --- a/bin/migrate-goldens.js +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env node -import { appendFile, mkdir, readFile, rename, rm } from 'node:fs/promises'; -import { join, normalize, parse } from 'node:path'; -import commandLineArgs from 'command-line-args'; -import { glob } from 'glob'; -import { PATHS } from '../src/server/visual-diff-plugin.js'; -import { stdout } from 'node:process'; - -const { pattern = './test/**' } = commandLineArgs({ name: 'pattern', type: String, defaultOption: true }, { partial: true }); -const oldSuffix = 'screenshots/ci/golden'; -const dirs = await glob(`${pattern}/${oldSuffix}`, { ignore: 'node_modules/**', posix: true }); -let fileCount = 0; - -const gitignore = await readFile('.gitignore', { encoding: 'UTF8' }).catch(() => ''); -if (!new RegExp(`${PATHS.VDIFF_ROOT}/(\n|$)`).test(gitignore)) { - const newline = gitignore.endsWith('\n') ? '' : '\n'; - await appendFile('.gitignore', `${newline}${PATHS.VDIFF_ROOT}/\n`); -} - -await Promise.all(dirs.map(async dir => { - const files = await glob(`${dir}/*/*.png`, { posix: true }); - - await Promise.all(files.map(async file => { - fileCount += 1; - const { base: name, dir } = parse(file); - const dirName = parse(dir).name; - - const newName = name - .replace(/^d2l-/, '') - .replace(new RegExp(`^${dirName}-`), ''); - - const newDir = dir.replace(`${oldSuffix}/${dirName}`, `${PATHS.GOLDEN}/${dirName}/chromium`); - - await mkdir(newDir, { recursive: true }); - return rename(file, join(newDir, newName)); - })); - return rm(normalize(join(dir, '..', '..')), { recursive: true }); -})); - -stdout.write(`\nMigrated ${fileCount} ${fileCount === 1 ? 'golden' : 'goldens'} found in ${dirs.length} test ${dirs.length === 1 ? 'directory' : 'directories'}\n`); diff --git a/package.json b/package.json index 5f6b48dc..e06b5678 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,7 @@ }, "bin": { "d2l-test-runner": "./bin/d2l-test-runner.js", - "d2l-test-vdiff-report": "./bin/d2l-test-vdiff-report.js", - "migrate-goldens": "./bin/migrate-goldens.js" + "dtr": "./bin/d2l-test-runner.js" }, "author": "D2L Corporation", "license": "Apache-2.0", diff --git a/src/server/cli/test-runner.js b/src/server/cli/test-runner.js index 0bc64c59..5f9f8126 100755 --- a/src/server/cli/test-runner.js +++ b/src/server/cli/test-runner.js @@ -68,9 +68,7 @@ async function getTestRunnerOptions(argv = []) { }, { name: 'golden', - type: Boolean, - description: 'Generate new golden screenshots. Ignored unless group is "vdiff".', - order: 14 + type: Boolean }, { name: 'grep', @@ -83,7 +81,7 @@ async function getTestRunnerOptions(argv = []) { name: 'help', type: Boolean, description: 'Print usage information and exit', - order: 15 + order: 14 }, { name: 'open', @@ -134,19 +132,37 @@ async function getTestRunnerOptions(argv = []) { }, { header: 'Usage', - content: 'd2l-test-runner [options]', + content: 'd2l-test-runner [options]\nd2l-test-runner [options]\n', }, { header: 'Options', optionList: optionDefinitions .map(o => { - o.description += '\n'; const longAlias = optionDefinitions.find(clone => clone !== o && clone.longAlias === o.name)?.name; if (longAlias) o.name += `, --${longAlias}`; return o; }) .filter(o => 'order' in o) .sort((a, b) => (a.order > b.order ? 1 : -1)) + }, + { + header: 'Commands', + content: [{ + example: 'vdiff', + desc: 'Run tests for the vdiff group' + }, + { + example: 'vdiff report', + desc: 'Open the latest vdiff report' + }, + { + example: 'vdiff golden', + desc: 'Generate new golden screenshots' + }, + { + example: 'vdiff migrate [directory]', + desc: 'Migrate from @brightspace-ui/visual-diff. Restrict which goldens are migrated with a directory glob.' + }] } ]); process.stdout.write(help); diff --git a/src/server/cli/vdiff/migrate.js b/src/server/cli/vdiff/migrate.js new file mode 100755 index 00000000..430af78c --- /dev/null +++ b/src/server/cli/vdiff/migrate.js @@ -0,0 +1,46 @@ +#!/usr/bin/env node +import { appendFile, mkdir, readFile, rename, rm } from 'node:fs/promises'; +import { join, normalize, parse } from 'node:path'; +import commandLineArgs from 'command-line-args'; +import { glob } from 'glob'; +import { PATHS } from '../../visual-diff-plugin.js'; +import { stdout } from 'node:process'; + +async function start(argv = []) { + const { pattern = './**' } = commandLineArgs({ name: 'pattern', type: String, defaultOption: true }, { partial: true, argv }); + const oldSuffix = 'screenshots/ci/golden'; + const dirs = await glob(`${pattern}/${oldSuffix}`, { ignore: 'node_modules/**', posix: true }); + let fileCount = 0; + + const gitignore = await readFile('.gitignore', { encoding: 'UTF8' }).catch(() => ''); + if (!new RegExp(`${PATHS.VDIFF_ROOT}/(\n|$)`).test(gitignore)) { + const newline = gitignore.endsWith('\n') ? '' : '\n'; + await appendFile('.gitignore', `${newline}${PATHS.VDIFF_ROOT}/\n`); + } + + await Promise.all(dirs.map(async dir => { + const files = await glob(`${dir}/*/*.png`, { posix: true }); + + await Promise.all(files.map(async file => { + fileCount += 1; + const { base: name, dir } = parse(file); + const dirName = parse(dir).name; + + const newName = name + .replace(/^d2l-/, '') + .replace(new RegExp(`^${dirName}-`), ''); + + const newDir = dir.replace(`${oldSuffix}/${dirName}`, `${PATHS.GOLDEN}/${dirName}/chromium`); + + await mkdir(newDir, { recursive: true }); + return rename(file, join(newDir, newName)); + })); + return rm(normalize(join(dir, '..', '..')), { recursive: true }); + })); + + stdout.write(`\nMigrated ${fileCount} ${fileCount === 1 ? 'golden' : 'goldens'} found in ${dirs.length} test ${dirs.length === 1 ? 'directory' : 'directories'}\n`); +} + +export const migrate = { + start +}; diff --git a/src/server/cli/vdiff/report.js b/src/server/cli/vdiff/report.js new file mode 100644 index 00000000..79d468b7 --- /dev/null +++ b/src/server/cli/vdiff/report.js @@ -0,0 +1,19 @@ +#!/usr/bin/env node +import { PATHS } from '../../visual-diff-plugin.js'; +import { startDevServer } from '@web/dev-server'; + +export const report = { + start() { + return startDevServer({ + config: { + nodeResolve: false, + open: `./${PATHS.REPORT_ROOT}/`, + rootDir: `${PATHS.VDIFF_ROOT}`, + preserveSymlinks: false, + watch: true + }, + readCliArgs: false, + readFileConfig: false + }); + } +}; diff --git a/test/bin/d2l-test-runner.test.js b/test/bin/d2l-test-runner.test.js index 425751ec..ce7d3e42 100644 --- a/test/bin/d2l-test-runner.test.js +++ b/test/bin/d2l-test-runner.test.js @@ -1,18 +1,72 @@ import { assert, restore, stub } from 'sinon'; -import { argv } from 'node:process'; +import { expect } from 'chai'; +import { migrate } from '../../src/server/cli/vdiff/migrate.js'; +import process from 'node:process'; +import { report } from '../../src/server/cli/vdiff/report.js'; import { runner } from '../../src/server/cli/test-runner.js'; +const { argv, stdout } = process; + +const run = async() => { + await import(`../../bin/d2l-test-runner.js?${Math.random()}`); +}; + describe('d2l-test-runner', () => { + afterEach(() => { + restore(); + }); + it('starts test runner with options', async() => { const opts = { my: 'options' }; const optionsStub = stub(runner, 'getOptions').returns(opts); const startStub = stub(runner, 'start'); - await import('../../bin/d2l-test-runner.js'); + await run(); assert.calledOnceWithExactly(optionsStub, argv); assert.calledOnceWithExactly(startStub, opts); restore(); }); + + it('starts report server', async() => { + const reportStub = stub(report, 'start'); + const optionsStub = stub(runner, 'getOptions'); + const startStub = stub(runner, 'start'); + + argv.splice(0, argv.length, 'fake-node', 'fake-test-runner', 'vdiff', 'report'); + await run(); + + assert.calledOnce(reportStub); + assert.notCalled(optionsStub); + assert.notCalled(startStub); + }); + + it('generates goldens', async() => { + const optionsStub = stub(runner, 'getOptions'); + const startStub = stub(runner, 'start'); + const stdoutStub = stub(stdout, 'write'); + + argv.splice(0, argv.length, 'fake-node', 'fake-test-runner', 'vdiff', 'golden'); + await run(); + + expect(argv).to.deep.equal(['fake-node', 'fake-test-runner', 'vdiff', '--golden']); + assert.calledOnceWithExactly(optionsStub, argv); + assert.calledOnce(startStub); + assert.calledOnceWithExactly(stdoutStub, '\nGenerating vdiff goldens...\n'); + }); + + it('starts migration', async() => { + const migrateStub = stub(migrate, 'start'); + const optionsStub = stub(runner, 'getOptions'); + const startStub = stub(runner, 'start'); + + argv.splice(0, argv.length, 'fake-node', 'fake-test-runner', 'vdiff', 'migrate', './test/**/dir'); + await run(); + + assert.calledOnceWithExactly(migrateStub, ['./test/**/dir']); + assert.notCalled(optionsStub); + assert.notCalled(startStub); + }); + });