Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
"dist:mac": "npm run build && electron-builder --mac",
"dist:win": "npm run build && electron-builder --win",
"dist:mac:unsigned": "npm run build && electron-builder --mac -c.mac.forceCodeSigning=false -c.mac.notarize=false -c.mac.identity=null",
"test": "node scripts/test.js"
"test": "npm run test:markers && npm run test:apprenders && npm run test:bridge && npm run test:outlines && npm run test:navigator && npm run test:classattr && npm run test:gridtracks && npm run test:windowbounds && npm run test:transformunits && npm run test:popoverdismiss && npm run test:nodestates && npm run test:spacing && npm run test:gapbands && npm run test:gaphover && npm run test:popoverdropdown && npm run test:panelfield && npm run test:branchname && npm run test:sound && npm run test:panelsound && npm run test:segmented && npm run test:coloralpha && npm run test:colorlive && npm run test:enterstays && npm run test:popupscroll && npm run test:focus && npm run test:tokenfield && npm run test:canvasclick && npm run test:gitbranches && npm run test:conflicts && npm run test:githistory && npm run test:historypanel && npm run test:undoreach && npm run test:filebrowser && npm run test:assetdelete && npm run test:gradient && npm run test:gitsnapshot && npm run test:previewworktree && npm run test:starter && npm run test:content && npm run test:entries && npm run test:refs && npm run test:fields && npm run test:view && npm run test:panel && npm run test:dropdown && npm run test:injectedroutes && npm run test:csscode && npm run test:computedcolor && npm run test:styletarget && npm run test:thumbs && npm run test:welcome && npm run test:cssvars && npm run test:cssvarsrename && npm run test:fluid && npm run test:varsview && npm run test:varsrowheight && npm run test:varsdrop && npm run test:customvalue && npm run test:codeprop && npm run test:arrayvalue && npm run test:listfield && npm run test:insertbinding && npm run test:frontmattermove && npm run test:slotmove && npm run test:movingpage && npm run test:pageorder && npm run test:closeproject && npm run test:outsideedit && npm run test:hovercost && npm run test:fragmentidentity && npm run test:unpackedparser && npm run test:pressscroll && npm run test:hoverwithin && npm run test:insertsearch && npm run test:varconnectopen && npm run test:insetpresets && npm run test:hideable && npm run test:transformsettings && npm run test:fieldfocus && npm run test:panelsurface && npm run test:computedhold && npm run test:sectiondot && npm run test:previewrecovery && npm run test:morph && npm run test:openedclass && npm run test:openedclick && npm run test:instanceroot && npm run test:chipedit && npm run test:jsguard && npm run test:spacingpick && npm run test:rawattrs && npm run test:richspan && npm run test:varspanel && npm run test:binding && npm run test:loopsource && npm run test:selectorslive && npm run test:selectorwell && npm run test:popuplayers && npm run test:scopecompletions && npm run test:querylist && npm run test:codesearch && npm run test:componentcreate && npm run test:componentfolders && npm run test:commentregion && npm run test:condbranches && npm run test:instanceprops && npm run test:variantoptions && npm run test:panelsections && npm run test:htmltext && npm run test:roundtrip"
},
"dependencies": {
Expand Down
118 changes: 118 additions & 0 deletions scripts/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env node
//
// Run the whole test suite.
//
// node scripts/test.js — everything under test/
// node scripts/test.js focus color — only files whose name contains a term
// node scripts/test.js --jobs=4 — cap the number of parallel workers
//
// `npm test` used to be a && chain of 105 `npm run` links: a fresh npm per
// file, strictly serial, dead at the first failure — and a hand-edit in two
// places for every new test. This discovers test/*.js instead, so a new test
// file is in the gate the moment it exists. Files run in parallel (they all
// use listen(0) and mkdtemp, so they don't collide), every failure is
// reported rather than the first, and the exit code says whether any failed.
//
// The per-file npm scripts (test:focus, …) remain for running one at a time.

const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawn } = require('node:child_process');

const TEST_DIR = path.join(__dirname, '..', 'test');

// A few files need a different launcher than plain `node file`:
// - *.test.js are node:test suites and want the test runner's reporter
// - thumbs.js exercises Electron's nativeImage, so it needs the real binary
// Everything else runs as a plain script; the --disable-warning flag keeps
// the typeless-package.json warning out of a hundred captured outputs.
function commandFor(file) {
if (file.endsWith('.test.js')) {
return [process.execPath, ['--test', path.join('test', file)]];
}
if (file === 'thumbs.js') {
return [require('electron'), [path.join('test', file)]];
}
return [
process.execPath,
['--disable-warning=MODULE_TYPELESS_PACKAGE_JSON', path.join('test', file)],
];
}

function runOne(file) {
return new Promise((resolve) => {
const [bin, args] = commandFor(file);
const started = Date.now();
const child = spawn(bin, args, {
cwd: path.join(__dirname, '..'),
env: process.env,
});
const chunks = [];
child.stdout.on('data', (d) => chunks.push(d));
child.stderr.on('data', (d) => chunks.push(d));
child.on('error', (err) => {
resolve({ file, ok: false, output: String(err), ms: Date.now() - started });
});
child.on('close', (code) => {
resolve({
file,
ok: code === 0,
output: Buffer.concat(chunks).toString('utf8'),
ms: Date.now() - started,
});
});
});
}

async function main() {
const terms = [];
let jobs = Math.min(8, os.availableParallelism());
for (const arg of process.argv.slice(2)) {
const m = /^--jobs=(\d+)$/.exec(arg);
if (m) jobs = Math.max(1, Number(m[1]));
else terms.push(arg.toLowerCase());
}

let files = fs
.readdirSync(TEST_DIR)
.filter((f) => f.endsWith('.js'))
.sort();
if (terms.length) {
files = files.filter((f) => terms.some((t) => f.toLowerCase().includes(t)));
}
if (!files.length) {
console.error(`no test files match: ${terms.join(' ')}`);
process.exit(1);
}

const failures = [];
let done = 0;
const queue = files.slice();
const worker = async () => {
for (let file = queue.shift(); file; file = queue.shift()) {
const result = await runOne(file);
done += 1;
const count = `[${String(done).padStart(String(files.length).length)}/${files.length}]`;
if (result.ok) {
console.log(`${count} ok ${file} (${result.ms}ms)`);
} else {
failures.push(result);
console.log(`${count} FAIL ${file} (${result.ms}ms)`);
}
}
};
await Promise.all(Array.from({ length: Math.min(jobs, files.length) }, worker));

for (const f of failures) {
console.log(`\n=== ${f.file} ===`);
console.log(f.output.trimEnd());
}
console.log(
`\n${files.length - failures.length} passed, ${failures.length} failed` +
(failures.length ? `: ${failures.map((f) => f.file).join(', ')}` : '')
);
process.exit(failures.length ? 1 : 0);
}

main();