Skip to content
Open
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 .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
60 changes: 60 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "trackconductor",
"private": true,
"type": "module",
"description": "Static site; package.json exists only for the test tooling.",
"scripts": {
"test": "node --test test/engine.test.mjs",
"test:page": "node --test test/page.test.mjs",
"record": "node test/record-goldens.mjs"
},
"devDependencies": {
"playwright": "1.58.0"
}
}
13 changes: 13 additions & 0 deletions test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Tests

The tool's arithmetic lives in `wiring/engine.js` (no DOM). `wiring/script.js` only reads the page and writes results back.

- `npm test` replays `test/fixtures/*.json` through the engine. The fixtures were recorded from the tool itself (before the engine was extracted), so a passing run means the output is still identical to the original: every combination in `test/cases.mjs` of collecting type, loading type, landing pattern, base delay/height and two melodies, plus 312 Wiring Generator lookups.
- `npm run test:page` drives the real page in a headless browser (Playwright). It also checks that the Wiring Generator follows its own type selector rather than the Collecting Generator's.
- `npm run record` re-records the fixtures from the page. Only do this after an intentional change in behaviour, and look at the diff.

`package.json` exists for these scripts only; the site itself still has no build step.

---

`wiring/engine.js` に計算部分をまとめました(DOM 非依存)。`npm test` は元のツールから記録した結果(`test/fixtures`)と比較します。挙動を意図的に変えたときだけ `npm run record` で記録し直してください。
35 changes: 35 additions & 0 deletions test/cases.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Inputs for the golden recordings. The recorder drives the live DOM tool
* with these; the engine test replays them against wiring/engine.js.
*/
export const MELODIES = {
m1: { ticks: [480, 480, 240, 240, 480, 960, 480, 480], pitches: [0, 2, 4, 5, 7, 5, 4, 2] },
m2: { ticks: [240, 240, 240, 240, 480, 480, 720, 240, 480], pitches: [0, -3, -5, -7, -12, -7, -5, -3, 0] },
};

export const TYPES = ["wing-ground", "noWing-ground", "wing-water", "noWing-water"];

/** Column delays typed by hand when the landing pattern is "Custom" (cells 2..n). */
export const CUSTOM_COL_DELAYS = [12, 10, 11, 12, 10, 11, 12, 10, 11];

export const LOADINGS = [
{ loading: 0 },
{ loading: 1 },
{ loading: 2, slownessAve: 14.286 },
{ loading: 3, loadingInput: 1, slownessAve: 6 },
{ loading: 3, loadingInput: 2, slownessCycle: "6, 7, 6, 6, 7" },
];

export function collectingCases() {
const cases = [];
for (const type of TYPES)
for (const load of LOADINGS)
for (const landing of [0, 1])
for (const [baseDelay, baseHeight] of [[60, 5], [90, 9]])
for (const melody of Object.keys(MELODIES))
cases.push({ type, ...load, landing, baseDelay, baseHeight, melody, tickUnit: 480, quarterFrames: baseDelay === 60 ? 30 : 24 });
return cases;
}

export const WIRING_DELAYS = [30, 64, 100, 150, 200, 250];
export const WIRING_HEIGHTS = [3, 5, 8, 10];
61 changes: 61 additions & 0 deletions test/engine.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { CUSTOM_COL_DELAYS, MELODIES } from "./cases.mjs";
import {
GRADE_SYMBOLS, assetOf, collect, columnHeights, columnTimes, findWirings,
gradeSummary, landingColumnDelays, noteFrames, scrollColumnDelays,
} from "../wiring/engine.js";

const fixture = async (name) => JSON.parse(await readFile(new URL(`./fixtures/${name}.json`, import.meta.url), "utf8"));
const text = (v) => (v === null || v === undefined ? "" : String(v));

test("collecting: engine reproduces the recorded tool output", async () => {
const cases = await fixture("collecting");
for (const { input: c, output } of cases) {
const m = MELODIES[c.melody];
const n = m.ticks.length;
const frames = m.ticks.map((t) => noteFrames(String(t), { mode: "ticks", quarterFrames: c.quarterFrames, tickUnit: c.tickUnit }));
const times = columnTimes(frames);
const yDifs = columnHeights(m.pitches);
const landingName = c.landing === 0 ? null : assetOf(c.type).landings[c.landing - 1].name;
const colDelays = c.landing === 0
? [0, ...CUSTOM_COL_DELAYS.slice(0, n - 1)]
: landingColumnDelays(c.type, landingName, n);
const scrollDelays = c.loading === 3
? scrollColumnDelays(c.loadingInput === 2 ? { cycle: c.slownessCycle } : { average: c.slownessAve }, n)
: Array.from({ length: n }, (_, i) => (i === 0 ? 0 : null));

const label = JSON.stringify(c);
assert.deepEqual(times.map(text), output.time, `time ${label}`);
assert.deepEqual(yDifs.map(text), output.yDif, `yDif ${label}`);
assert.deepEqual(colDelays.map(text), output.colDelay, `colDelay ${label}`);
if (c.loading === 3) assert.deepEqual(scrollDelays.map(text), output.scrollDelay, `scrollDelay ${label}`);

const { columns, counts } = collect({
typeKey: c.type, landingName, loading: c.loading, slownessAve: c.slownessAve,
baseDelay: c.baseDelay, baseHeight: c.baseHeight, times, colDelays, scrollDelays, yDifs,
});
const pad = (arr) => [...arr, ...Array(n - arr.length).fill("")];
assert.deepEqual(pad(columns.map((col) => String(col.index))), output.order.map((v, i) => (i < columns.length ? v : String(i + 1))), `order ${label}`);
assert.deepEqual(pad(columns.map((col) => String(col.delay))), output.delay, `delay ${label}`);
assert.deepEqual(pad(columns.map((col) => String(col.height))), output.height, `height ${label}`);
assert.deepEqual(pad(columns.map((col) => GRADE_SYMBOLS[col.grade])), output.grade, `grade ${label}`);
assert.equal(gradeSummary(counts), output.evals, `evals ${label}`);
}
});

test("wiring: engine reproduces the recorded result lists", async () => {
const cases = await fixture("wiring");
for (const { input: c, output } of cases) {
const results = findWirings(c.delay, c.height, { typeKey: c.type, landingShape: c.landing });
const rendered = results.map((bucket) => bucket.map((r) => ({ str: r.str, title: `Delay: ${r.delay}, ↑: ${r.up}, ↓: ${r.down}` })));
// The page sorts by the selected order (default: ↑ ascending) before display.
const sorted = rendered.map((bucket, i) => {
const src = results[i].map((r, k) => ({ r, rendered: bucket[k] }));
src.sort((a, b) => a.r.up - b.r.up || a.r.down - b.r.down);
return src.map((x) => x.rendered);
});
assert.deepEqual(sorted, output, JSON.stringify(c));
}
});
Loading