ZDDC — Zero Day Document Control. A file-naming convention plus five single-file HTML tools (archive, transmittal, classifier, mdedit, landing) and an optional Go HTTP server (zddc-server) with ACL and a virtual archive index. Self-contained, offline-capable, dependency-free. See README.md for an overview, AGENTS.md and ARCHITECTURE.md for the build/release/architecture detail, bootstrap/README.md for the two-level deployment install pattern, and zddc/README.md for the HTTP server.
174 lines
8.6 KiB
JavaScript
174 lines
8.6 KiB
JavaScript
import { test, expect } from '@playwright/test';
|
||
import { MOCK_FS_INIT_SCRIPT } from './fixtures/mock-fs-api.js';
|
||
import * as fs from 'fs';
|
||
import * as os from 'os';
|
||
import * as path from 'path';
|
||
|
||
const HTML_PATH = path.resolve('transmittal/dist/transmittal.html');
|
||
|
||
// Tab-separated clipboard data simulating a paste from a document register
|
||
const PASTED_FILE_TEXT = [
|
||
'123456-EL-SPC-2623\tMaterial Specification\tA\tIFC\tpdf',
|
||
'123456-EL-SPC-2624\tInstallation Drawing\tB\tIFR\tdwg',
|
||
].join('\n');
|
||
|
||
test.describe('Transmittal – pasted files survive a save/load cycle', () => {
|
||
test.beforeEach(async ({ page }) => {
|
||
await page.addInitScript(MOCK_FS_INIT_SCRIPT);
|
||
});
|
||
|
||
test('files pasted from clipboard appear after opening the saved draft', async ({ page }) => {
|
||
// ── 1. Load the app ──────────────────────────────────────────────────
|
||
await page.goto(`file://${HTML_PATH}`, { waitUntil: 'networkidle' });
|
||
|
||
// ── Wait for table tbody ──────────────────────────────────────────────
|
||
await page.waitForSelector('table tbody', { timeout: 30000 });
|
||
|
||
// ── 2. Inject pasted files directly into app state ───────────────────
|
||
// Simulates what "Paste Append Rows" does without needing real clipboard.
|
||
await page.evaluate((clipText) => {
|
||
const app = window.transmittalApp;
|
||
// Parse through the app's own paste parser
|
||
const result = app.modules.files._parseClipboardTextForTest
|
||
? app.modules.files._parseClipboardTextForTest(clipText)
|
||
: null;
|
||
|
||
// Direct injection: build the same structure columnsToFileRow produces
|
||
const rows = clipText.trim().split('\n').map(line => {
|
||
const cols = line.split('\t');
|
||
return {
|
||
trackingNumber: cols[0] || '',
|
||
title: cols[1] || '',
|
||
revision: cols[2] || '',
|
||
status: cols[3] || '',
|
||
extension: (cols[4] || '').toLowerCase(),
|
||
path: '', name: '', size: 0, fileSize: 0, sha256: ''
|
||
};
|
||
});
|
||
app.data.files = rows;
|
||
app.modules.files.updateFilesInJson(app.data.files);
|
||
app.modules.files.render();
|
||
}, PASTED_FILE_TEXT);
|
||
|
||
// Verify the files are visible in the table
|
||
const rowsBefore = await page.locator('table tbody tr').count();
|
||
// Self-entry row (row 0) + 2 pasted files
|
||
expect(rowsBefore).toBeGreaterThanOrEqual(3);
|
||
|
||
// ── 3. Sync UI to JSON and build the draft HTML ──────────────────────
|
||
const draftHtml = await page.evaluate(async () => {
|
||
const pub = window.transmittalApp.modules.publish;
|
||
await pub.syncUiToJson({ sign: false, computeDigest: false });
|
||
return pub.buildHtmlString();
|
||
});
|
||
|
||
expect(typeof draftHtml).toBe('string');
|
||
expect(draftHtml.length).toBeGreaterThan(1000);
|
||
|
||
// The draft HTML must contain the tracking numbers in the JSON block
|
||
expect(draftHtml).toContain('123456-EL-SPC-2623');
|
||
expect(draftHtml).toContain('123456-EL-SPC-2624');
|
||
|
||
// ── 4. Write the draft to a temp file and open it in a new page ──────
|
||
const tmp = path.join(os.tmpdir(), `transmittal-draft-test-${Date.now()}.html`);
|
||
fs.writeFileSync(tmp, draftHtml, 'utf8');
|
||
try {
|
||
const draftPage = await page.context().newPage();
|
||
await draftPage.addInitScript(MOCK_FS_INIT_SCRIPT);
|
||
await draftPage.goto(`file://${tmp}`);
|
||
await draftPage.waitForSelector('table tbody');
|
||
|
||
// ── 5. Assert files survived the round-trip ───────────────────────
|
||
const rowsAfter = await draftPage.locator('table tbody tr').count();
|
||
// Self-entry + 2 pasted files
|
||
expect(rowsAfter).toBeGreaterThanOrEqual(3);
|
||
|
||
// Check that the tracking numbers are visible in the table
|
||
await expect(draftPage.locator('table tbody')).toContainText('123456-EL-SPC-2623');
|
||
await expect(draftPage.locator('table tbody')).toContainText('123456-EL-SPC-2624');
|
||
|
||
// Check the in-memory data too
|
||
const appFiles = await draftPage.evaluate(() => window.transmittalApp.data.files);
|
||
expect(appFiles.length).toBe(2);
|
||
expect(appFiles[0].trackingNumber).toBe('123456-EL-SPC-2623');
|
||
expect(appFiles[1].trackingNumber).toBe('123456-EL-SPC-2624');
|
||
|
||
// Files must have usable names even though path/filename are empty
|
||
expect(appFiles[0].name).toBeTruthy();
|
||
expect(appFiles[1].name).toBeTruthy();
|
||
|
||
await draftPage.close();
|
||
} finally {
|
||
fs.unlinkSync(tmp);
|
||
}
|
||
});
|
||
|
||
test('files selected from filesystem survive a save/load cycle', async ({ page }) => {
|
||
// ── 1. Load the app ──────────────────────────────────────────────────
|
||
await page.goto(`file://${HTML_PATH}`, { waitUntil: 'networkidle' });
|
||
await page.waitForSelector('table tbody', { state: 'attached' });
|
||
await page.waitForTimeout(300);
|
||
|
||
// ── 2. Inject filesystem-style files (have real path + sha256) ───────
|
||
await page.evaluate(() => {
|
||
const app = window.transmittalApp;
|
||
app.data.files = [
|
||
{
|
||
trackingNumber: '123456-EL-SPC-0001', title: 'Spec One',
|
||
revision: 'A', status: 'IFC', extension: 'pdf',
|
||
path: 'project-dir/123456-EL-SPC-0001_A (IFC) - Spec One.pdf',
|
||
name: '123456-EL-SPC-0001_A (IFC) - Spec One.pdf',
|
||
size: 10000, fileSize: 10000,
|
||
sha256: 'aabbccddeeff001122334455667788990011223344556677889900aabbccddee'
|
||
},
|
||
{
|
||
trackingNumber: '123456-EL-DRW-0002', title: 'Drawing Two',
|
||
revision: 'B', status: 'IFR', extension: 'dwg',
|
||
path: 'project-dir/123456-EL-DRW-0002_B (IFR) - Drawing Two.dwg',
|
||
name: '123456-EL-DRW-0002_B (IFR) - Drawing Two.dwg',
|
||
size: 20000, fileSize: 20000,
|
||
sha256: 'ffeeddccbbaa998877665544332211ffeeddccbbaa998877665544332211ffee'
|
||
},
|
||
];
|
||
app.modules.files.updateFilesInJson(app.data.files);
|
||
app.modules.files.render();
|
||
});
|
||
await page.waitForTimeout(300);
|
||
|
||
const rowsBefore = await page.locator('table tbody tr').count();
|
||
expect(rowsBefore).toBeGreaterThanOrEqual(3);
|
||
|
||
// ── 3. Build the draft HTML ──────────────────────────────────────────
|
||
const draftHtml = await page.evaluate(async () => {
|
||
const pub = window.transmittalApp.modules.publish;
|
||
await pub.syncUiToJson({ sign: false, computeDigest: false });
|
||
return pub.buildHtmlString();
|
||
});
|
||
|
||
expect(draftHtml).toContain('123456-EL-SPC-0001');
|
||
expect(draftHtml).toContain('123456-EL-DRW-0002');
|
||
|
||
// ── 4. Open draft and verify ─────────────────────────────────────────
|
||
const tmp = path.join(os.tmpdir(), `transmittal-draft-fs-${Date.now()}.html`);
|
||
fs.writeFileSync(tmp, draftHtml, 'utf8');
|
||
try {
|
||
const draftPage = await page.context().newPage();
|
||
await draftPage.addInitScript(MOCK_FS_INIT_SCRIPT);
|
||
await draftPage.goto(`file://${tmp}`);
|
||
await draftPage.waitForSelector('table tbody');
|
||
|
||
const rowsAfter = await draftPage.locator('table tbody tr').count();
|
||
expect(rowsAfter).toBeGreaterThanOrEqual(3);
|
||
|
||
await expect(draftPage.locator('table tbody')).toContainText('123456-EL-SPC-0001');
|
||
await expect(draftPage.locator('table tbody')).toContainText('123456-EL-DRW-0002');
|
||
|
||
const appFiles = await draftPage.evaluate(() => window.transmittalApp.data.files);
|
||
expect(appFiles.length).toBe(2);
|
||
|
||
await draftPage.close();
|
||
} finally {
|
||
fs.unlinkSync(tmp);
|
||
}
|
||
});
|
||
});
|