/** * 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'); });