ZDDC/tests/zddc.spec.js
2026-06-11 13:32:31 -05:00

365 lines
16 KiB
JavaScript

/**
* Tests for shared/zddc.js — canonical ZDDC naming library.
*
* Runs against the compiled dist/zddc-test.html shim (a minimal HTML page
* that loads shared/zddc.js and exposes window.zddc to the test harness).
*
* Fixtures from tests/fixtures/zddc-filenames.js define the canonical
* expected behaviour for all tools.
*/
import { test, expect } from '@playwright/test';
import { VALID_FILES, INVALID_FILES, VALID_FOLDERS, REVISION_SORT_ORDER }
from './fixtures/zddc-filenames.js';
import * as path from 'path';
const SHIM_PATH = 'file://' + path.resolve('shared/zddc-test.html');
// ── helpers ──────────────────────────────────────────────────────────────────
async function zddc(page, fn, ...args) {
return page.evaluate(
([fn, args]) => window.zddc[fn](...args),
[fn, args]
);
}
// ── setup ─────────────────────────────────────────────────────────────────────
test.beforeEach(async ({ page }) => {
await page.goto(SHIM_PATH, { waitUntil: 'load' });
// Confirm the library loaded
const ok = await page.evaluate(() => typeof window.zddc === 'object');
expect(ok).toBe(true);
});
// ── parseFilename — valid files ───────────────────────────────────────────────
for (const fixture of VALID_FILES) {
test(`parseFilename: ${fixture.filename}`, async ({ page }) => {
const result = await zddc(page, 'parseFilename', fixture.filename);
expect(result).not.toBeNull();
expect(result.valid).toBe(true);
expect(result.trackingNumber).toBe(fixture.parsed.trackingNumber);
expect(result.revision).toBe(fixture.parsed.revision);
expect(result.status).toBe(fixture.parsed.status);
expect(result.title).toBe(fixture.parsed.title);
expect(result.extension).toBe(fixture.parsed.extension);
});
}
// ── parseFilename — invalid files ─────────────────────────────────────────────
for (const fixture of INVALID_FILES) {
test(`parseFilename invalid: ${fixture.filename}`, async ({ page }) => {
const result = await zddc(page, 'parseFilename', fixture.filename);
expect(result).not.toBeNull();
expect(result.valid).toBe(false);
// Should never throw — returns degraded object
expect(typeof result.trackingNumber).toBe('string');
expect(typeof result.extension).toBe('string');
});
}
// ── parseFilename — edge cases ────────────────────────────────────────────────
test('parseFilename: null returns null', async ({ page }) => {
const result = await zddc(page, 'parseFilename', null);
expect(result).toBeNull();
});
test('parseFilename: empty string returns null', async ({ page }) => {
const result = await zddc(page, 'parseFilename', '');
expect(result).toBeNull();
});
test('parseFilename: no extension', async ({ page }) => {
const result = await zddc(page, 'parseFilename', 'just-a-name');
expect(result.valid).toBe(false);
expect(result.extension).toBe('');
});
// ── parseFolder — valid folders ───────────────────────────────────────────────
for (const fixture of VALID_FOLDERS) {
test(`parseFolder: ${fixture.foldername}`, async ({ page }) => {
const result = await zddc(page, 'parseFolder', fixture.foldername);
expect(result).not.toBeNull();
expect(result.valid).toBe(true);
expect(result.date).toBe(fixture.parsed.date);
expect(result.trackingNumber).toBe(fixture.parsed.trackingNumber);
expect(result.status).toBe(fixture.parsed.status);
expect(result.title).toBe(fixture.parsed.title);
});
}
test('parseFolder: null returns null', async ({ page }) => {
const result = await zddc(page, 'parseFolder', null);
expect(result).toBeNull();
});
test('parseFolder: non-ZDDC name returns valid=false', async ({ page }) => {
const result = await zddc(page, 'parseFolder', 'random folder name');
expect(result.valid).toBe(false);
expect(result.title).toBe('random folder name');
});
// ── formatFilename ────────────────────────────────────────────────────────────
test('formatFilename: round-trips all valid files', async ({ page }) => {
for (const fixture of VALID_FILES) {
const ext = fixture.parsed.extension;
const formatted = await zddc(page, 'formatFilename', {
trackingNumber: fixture.parsed.trackingNumber,
revision: fixture.parsed.revision,
status: fixture.parsed.status,
title: fixture.parsed.title,
extension: ext,
});
expect(formatted).toBe(fixture.filename);
}
});
test('formatFilename: empty required field returns empty string', async ({ page }) => {
expect(await zddc(page, 'formatFilename', { trackingNumber: '', revision: 'A', status: 'IFR', title: 'T', extension: 'pdf' })).toBe('');
expect(await zddc(page, 'formatFilename', { trackingNumber: '123', revision: '', status: 'IFR', title: 'T', extension: 'pdf' })).toBe('');
expect(await zddc(page, 'formatFilename', { trackingNumber: '123', revision: 'A', status: '', title: 'T', extension: 'pdf' })).toBe('');
expect(await zddc(page, 'formatFilename', { trackingNumber: '123', revision: 'A', status: 'IFR', title: '', extension: 'pdf' })).toBe('');
});
test('formatFilename: extension with leading dot is handled', async ({ page }) => {
const result = await zddc(page, 'formatFilename', {
trackingNumber: '123-EL-SPC-0001', revision: 'A', status: 'IFR',
title: 'Test Title', extension: '.pdf',
});
expect(result).toBe('123-EL-SPC-0001_A (IFR) - Test Title.pdf');
});
// ── formatFolder ──────────────────────────────────────────────────────────────
test('formatFolder: round-trips all valid folders', async ({ page }) => {
for (const fixture of VALID_FOLDERS) {
const formatted = await zddc(page, 'formatFolder', fixture.parsed);
expect(formatted).toBe(fixture.foldername);
}
});
test('formatFolder: empty required field returns empty string', async ({ page }) => {
expect(await zddc(page, 'formatFolder', { date: '', trackingNumber: '123', status: 'IFR', title: 'T' })).toBe('');
expect(await zddc(page, 'formatFolder', { date: '2025-01-01', trackingNumber: '', status: 'IFR', title: 'T' })).toBe('');
expect(await zddc(page, 'formatFolder', { date: '2025-01-01', trackingNumber: '123', status: '', title: 'T' })).toBe('');
expect(await zddc(page, 'formatFolder', { date: '2025-01-01', trackingNumber: '123', status: 'IFR', title: '' })).toBe('');
});
// ── parseRevision ─────────────────────────────────────────────────────────────
test('parseRevision: plain letter revision', async ({ page }) => {
const r = await zddc(page, 'parseRevision', 'A');
expect(r.base).toBe('A');
expect(r.isDraft).toBe(false);
expect(r.modifier).toBe('');
expect(r.modifierType).toBe('');
expect(r.modifierNumber).toBe(0);
expect(r.full).toBe('A');
});
test('parseRevision: draft revision ~A', async ({ page }) => {
const r = await zddc(page, 'parseRevision', '~A');
expect(r.base).toBe('A');
expect(r.isDraft).toBe(true);
expect(r.modifier).toBe('');
});
test('parseRevision: revision with modifier A+C1', async ({ page }) => {
const r = await zddc(page, 'parseRevision', 'A+C1');
expect(r.base).toBe('A');
expect(r.isDraft).toBe(false);
expect(r.modifier).toBe('+C1');
expect(r.modifierType).toBe('C');
expect(r.modifierNumber).toBe(1);
expect(r.modifierIsDraft).toBe(false);
});
test('parseRevision: draft modifier A+~C1', async ({ page }) => {
const r = await zddc(page, 'parseRevision', 'A+~C1');
expect(r.base).toBe('A');
expect(r.isDraft).toBe(false);
expect(r.modifier).toBe('+~C1');
expect(r.modifierType).toBe('C');
expect(r.modifierNumber).toBe(1);
expect(r.modifierIsDraft).toBe(true);
});
test('parseRevision: numeric base 0', async ({ page }) => {
const r = await zddc(page, 'parseRevision', '0');
expect(r.base).toBe('0');
expect(r.isDraft).toBe(false);
expect(r.modifier).toBe('');
});
test('parseRevision: all modifier types', async ({ page }) => {
const cases = [
['A+B1', 'B', 1],
['A+C2', 'C', 2],
['A+N1', 'N', 1],
['A+Q1', 'Q', 1],
];
for (const [rev, type, num] of cases) {
const r = await zddc(page, 'parseRevision', rev);
expect(r.modifierType).toBe(type);
expect(r.modifierNumber).toBe(num);
}
});
// ── compareRevisions — canonical REVISION_SORT_ORDER ─────────────────────────
test('compareRevisions: canonical sort order matches fixture', async ({ page }) => {
// For every adjacent pair in the expected order, a < b
for (let i = 0; i < REVISION_SORT_ORDER.length - 1; i++) {
const a = REVISION_SORT_ORDER[i];
const b = REVISION_SORT_ORDER[i + 1];
const cmp = await zddc(page, 'compareRevisions', a, b);
expect(cmp).toBeLessThan(0);
}
});
test('compareRevisions: equal revisions return 0', async ({ page }) => {
for (const rev of REVISION_SORT_ORDER) {
const cmp = await zddc(page, 'compareRevisions', rev, rev);
expect(cmp).toBe(0);
}
});
test('compareRevisions: reverse order is > 0', async ({ page }) => {
for (let i = 0; i < REVISION_SORT_ORDER.length - 1; i++) {
const a = REVISION_SORT_ORDER[i];
const b = REVISION_SORT_ORDER[i + 1];
const cmp = await zddc(page, 'compareRevisions', b, a);
expect(cmp).toBeGreaterThan(0);
}
});
test('compareRevisions: sort() produces canonical order', async ({ page }) => {
// Shuffle the order and sort with zddc.compareRevisions, expect canonical result
const shuffled = [...REVISION_SORT_ORDER].reverse();
const sorted = await page.evaluate((revisions) => {
return revisions.slice().sort((a, b) => window.zddc.compareRevisions(a, b));
}, shuffled);
expect(sorted).toEqual(REVISION_SORT_ORDER);
});
// ── isValidStatus ─────────────────────────────────────────────────────────────
test('isValidStatus: accepts all known statuses', async ({ page }) => {
const statuses = await page.evaluate(() => window.zddc.STATUSES);
for (const s of statuses) {
const ok = await zddc(page, 'isValidStatus', s);
expect(ok).toBe(true);
}
});
test('isValidStatus: rejects unknown codes', async ({ page }) => {
const bad = ['IFX', 'ABC', '', 'ifr', 'rsa'];
for (const s of bad) {
const ok = await zddc(page, 'isValidStatus', s);
expect(ok).toBe(false);
}
});
test('isValidStatus: --- is valid (unknown status)', async ({ page }) => {
const ok = await zddc(page, 'isValidStatus', '---');
expect(ok).toBe(true);
});
// ── splitExtension / joinExtension ────────────────────────────────────────────
test('splitExtension: standard filename', async ({ page }) => {
const r = await zddc(page, 'splitExtension', '123456-EL-SPC-2623_A (IFR) - Spec.pdf');
expect(r).toEqual({ name: '123456-EL-SPC-2623_A (IFR) - Spec', extension: 'pdf' });
});
test('splitExtension: no extension', async ({ page }) => {
const r = await zddc(page, 'splitExtension', 'README');
expect(r).toEqual({ name: 'README', extension: '' });
});
test('splitExtension: dotfile (leading dot is not an extension)', async ({ page }) => {
const r = await zddc(page, 'splitExtension', '.gitignore');
expect(r).toEqual({ name: '.gitignore', extension: '' });
});
test('splitExtension: multiple dots — last wins, lowercased', async ({ page }) => {
const r = await zddc(page, 'splitExtension', 'archive.tar.GZ');
expect(r).toEqual({ name: 'archive.tar', extension: 'gz' });
});
test('splitExtension: empty string', async ({ page }) => {
const r = await zddc(page, 'splitExtension', '');
expect(r).toEqual({ name: '', extension: '' });
});
test('joinExtension: with extension', async ({ page }) => {
const r = await zddc(page, 'joinExtension', 'foo', 'pdf');
expect(r).toBe('foo.pdf');
});
test('joinExtension: tolerates leading dot in extension', async ({ page }) => {
const r = await zddc(page, 'joinExtension', 'foo', '.pdf');
expect(r).toBe('foo.pdf');
});
test('joinExtension: empty extension returns name unchanged', async ({ page }) => {
const r = await zddc(page, 'joinExtension', 'foo', '');
expect(r).toBe('foo');
});
test('split / join round-trip', async ({ page }) => {
const filenames = ['foo.pdf', 'archive.tar.gz', 'README', '.gitignore', 'no-ext'];
for (const fn of filenames) {
const split = await zddc(page, 'splitExtension', fn);
const joined = await zddc(page, 'joinExtension', split.name, split.extension);
expect(joined.toLowerCase()).toBe(fn.toLowerCase());
}
});
// ── crypto helpers (shared/hash.js) ──────────────────────────────────────────
test('sha256String: known vector', async ({ page }) => {
// SHA-256 of empty string
const empty = await page.evaluate(() => window.zddc.crypto.sha256String(''));
expect(empty).toBe('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855');
// SHA-256 of "abc"
const abc = await page.evaluate(() => window.zddc.crypto.sha256String('abc'));
expect(abc).toBe('ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad');
});
test('sha256Hex: ArrayBuffer input', async ({ page }) => {
const hex = await page.evaluate(() => {
const bytes = new Uint8Array([0x61, 0x62, 0x63]); // 'abc'
return window.zddc.crypto.sha256Hex(bytes.buffer);
});
expect(hex).toBe('ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad');
});
test('sha256Hex: Uint8Array input', async ({ page }) => {
const hex = await page.evaluate(() => {
const bytes = new Uint8Array([0x61, 0x62, 0x63]); // 'abc'
return window.zddc.crypto.sha256Hex(bytes);
});
expect(hex).toBe('ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad');
});
test('sha256File: small File goes through single-shot path', async ({ page }) => {
const hex = await page.evaluate(() => {
const blob = new File(['abc'], 'test.txt');
return window.zddc.crypto.sha256File(blob);
});
expect(hex).toBe('ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad');
});
test('bytesToHex: byte array → padded hex', async ({ page }) => {
const hex = await page.evaluate(() => {
return window.zddc.crypto.bytesToHex(new Uint8Array([0x00, 0x0f, 0xff]));
});
expect(hex).toBe('000fff');
});