Compare commits

...

2 commits

Author SHA1 Message Date
86d667309d fix(classifier): drop vestigial Save-All delays that masked a fixed await bug
saveAllFiles() carried a 200ms inter-operation sleep "to prevent race
conditions" plus a 300ms post-error "settle" sleep. But saveFile() is fully
awaited and each iteration renames a distinct file, so the saves are already
serialized — the sleeps were the band-aid for an earlier missing-await bug
(the "ensure properly awaited" comment marks where that got fixed). Remove
both: correctness comes from the awaits, the operations are independent, and
Save All no longer pays 200ms per file (≈10s on a 50-file batch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:07:28 -05:00
d91a37f356 test(transmittal): cover the publish-time validation gate
transmittal/js/validation.js had no in-browser coverage. Add a spec for
both halves: the live #tracking-number aria-invalid binding (whitespace /
underscore) and validateBeforePublish() — a clean transmittal passes; a
tracking number with spaces/underscores fails and focuses the field; a
per-file bad tracking number or revision is flagged by row.

(The earlier audit's "transmittal is untested" was inaccurate — it already
has paste/FS round-trip, drag-drop, and init-state specs; this fills the
validation gap none of them covered.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:49:23 -05:00
3 changed files with 92 additions and 6 deletions

View file

@ -755,10 +755,10 @@
const { file, index } = modifiedFiles[i];
try {
// Add small delay between operations to prevent race conditions
if (i > 0) {
await new Promise(resolve => setTimeout(resolve, 200));
}
// No inter-operation delay: saveFile() is fully awaited and
// each iteration renames a distinct file, so the saves are
// already serialized. (The old 200ms sleep papered over an
// earlier missing-await bug, since fixed.)
// Validate before saving
const newFilename = computeNewFilename(file, index);
@ -801,8 +801,8 @@
errorCount++;
errors.push(`${zddc.joinExtension(file.originalFilename, file.extension)}: ${saveErr.message}`);
// Add delay after errors to let filesystem stabilize
await new Promise(resolve => setTimeout(resolve, 300));
// No post-error delay: each file is independent, so an
// error on one doesn't require "settling" before the next.
}
} catch (err) {
console.error(`Error processing file ${index}:`, err);

View file

@ -43,6 +43,10 @@ export default defineConfig({
name: 'transmittal-drag-drop',
testMatch: 'transmittal-drag-drop.spec.js',
},
{
name: 'transmittal-validation',
testMatch: 'transmittal-validation.spec.js',
},
{
name: 'classifier',
testMatch: 'classifier.spec.js',

View file

@ -0,0 +1,82 @@
import { test, expect } from '@playwright/test';
import { MOCK_FS_INIT_SCRIPT } from './fixtures/mock-fs-api.js';
import * as path from 'path';
const HTML_PATH = path.resolve('transmittal/dist/transmittal.html');
// Covers the transmittal validation module (transmittal/js/validation.js),
// which had no in-browser coverage:
// - the live #tracking-number aria-invalid binding (whitespace / underscore)
// - validateBeforePublish(): the publish-time gate that rejects a tracking
// number or a per-file tracking number / revision containing spaces or
// underscores.
test.describe('Transmittal validation', () => {
test.beforeEach(async ({ page }) => {
await page.addInitScript(MOCK_FS_INIT_SCRIPT);
await page.goto(`file://${HTML_PATH}`, { waitUntil: 'networkidle' });
await page.waitForSelector('#tracking-number');
});
test('tracking-number field flags whitespace and underscores live', async ({ page }) => {
const input = page.locator('#tracking-number');
await input.fill('123456-EL-TRX-0001');
await expect(input).toHaveAttribute('aria-invalid', 'false');
await input.fill('123456 EL TRX'); // space
await expect(input).toHaveAttribute('aria-invalid', 'true');
await expect(input).toHaveClass(/border-red-500/);
await input.fill('123456_EL_TRX'); // underscore
await expect(input).toHaveAttribute('aria-invalid', 'true');
await input.fill('123456-EL-TRX-0002'); // clean again — clears the flag
await expect(input).toHaveAttribute('aria-invalid', 'false');
await expect(input).not.toHaveClass(/border-red-500/);
});
test('validateBeforePublish passes for a clean transmittal', async ({ page }) => {
const result = await page.evaluate(() => {
const app = window.transmittalApp;
app.dom.qs('#tracking-number').value = '123456-EL-TRX-0001';
app.data.files = [{
trackingNumber: '123456-EL-SPC-2623', revision: 'A',
status: 'IFC', extension: 'pdf', title: 'Spec',
name: 'x', path: '', size: 0, fileSize: 0, sha256: '',
}];
return app.modules.validation.validateBeforePublish();
});
expect(result.ok).toBe(true);
expect(result.message).toBe('');
});
test('validateBeforePublish rejects a tracking number with spaces/underscores and focuses it', async ({ page }) => {
const result = await page.evaluate(() => {
const app = window.transmittalApp;
app.dom.qs('#tracking-number').value = 'BAD NUMBER_01';
app.data.files = [];
const r = app.modules.validation.validateBeforePublish();
return { ok: r.ok, message: r.message, focusId: r.focusEl ? r.focusEl.id : null };
});
expect(result.ok).toBe(false);
expect(result.message).toContain('spaces or underscores');
expect(result.focusId).toBe('tracking-number'); // gate points the user at the bad field
});
test('validateBeforePublish flags per-file bad tracking numbers and revisions by row', async ({ page }) => {
const result = await page.evaluate(() => {
const app = window.transmittalApp;
app.dom.qs('#tracking-number').value = '123456-EL-TRX-0001'; // header is clean
app.data.files = [
{ trackingNumber: '123456 EL SPC', revision: 'A' }, // row 1: space in tracking
{ trackingNumber: '123456-EL-DRW', revision: 'B 1' }, // row 2: space in revision
];
return app.modules.validation.validateBeforePublish();
});
expect(result.ok).toBe(false);
expect(result.message).toContain('Row 1');
expect(result.message).toContain('Row 2');
expect(result.message).toMatch(/tracking number must not contain spaces or underscores/i);
expect(result.message).toMatch(/revision must not contain spaces/i);
});
});