Make the Playwright e2e + kicad suites deterministic so screenshot flake stops tracing to timing races. - Blind page.waitForTimeout -> condition waits (expect.poll, web-first assertions, waitUntil) + readiness helpers (waitForWxApp, waitForCanvasApp). Remaining sleeps are documented interaction dwells (annotated). - Defensive "if element exists" branches -> loud asserts; label-fallback chains -> normalized clickMenuItemByText. First-run wizard for/if loops removed by seeding calculator/gerbview/pcbnew HTMLs. - Screenshots: new stableShot(page, name) settles the render in-page (canvas hash over rAF) then writes a raw PNG to test-results/ for the existing offline gate (tools/screenshots vs baseline-screenshots). Replaces toHaveScreenshot, which did inline compare + its own baselines and had decoupled the specs from the real gate. scale:'css' pinned. - retries: 0 in both configs. - Guard: tests/tools/lint-determinism.ts (npm run lint:determinism) bans blind sleeps / toHaveScreenshot / inline retries / swallowed catches in specs; documented exceptions carry a marker. Rules in tests/TESTING.md. Assertions, coverage, and renders unchanged (semantic-equivalence reviewed; captures pixel-identical modulo inherent timer/timestamp/3d-raytrace variance). Both suites green at retries:0 (e2e 340, kicad 92); ~35-61% faster. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVX1pHMvRPYHdp6ZfEawrk
292 lines
10 KiB
TypeScript
292 lines
10 KiB
TypeScript
// wxDragDrop Tests - HTML5 file drop support for KiCad
|
|
// Tests external file drops via HTML5 drag and drop API
|
|
import { test, expect, getCanvasBox, waitForCanvasApp } from './utils/fixtures';
|
|
import * as path from 'path';
|
|
import * as fs from 'fs';
|
|
import { stableShot } from './utils/element-tracker';
|
|
|
|
test.describe('wxDragDrop Tests', () => {
|
|
|
|
test('DnD test app loads successfully', async ({ page, testLogger }) => {
|
|
await page.goto('/standalone/dnd/dnd_test.html');
|
|
await waitForCanvasApp(page);
|
|
|
|
await stableShot(page, 'dnd-01-loaded.png', { fullPage: true });
|
|
|
|
const hasStartup = testLogger.consoleLogs.some(l => l.includes('DND_TEST'));
|
|
|
|
expect(testLogger.errors.filter(e => !e.includes('favicon'))).toHaveLength(0);
|
|
});
|
|
|
|
test('DnD handlers are registered', async ({ page, testLogger }) => {
|
|
await page.goto('/standalone/dnd/dnd_test.html');
|
|
await waitForCanvasApp(page);
|
|
|
|
await expect.poll(
|
|
() => testLogger.consoleLogs.some(l => l.includes('[DND] Drag and drop handlers registered')),
|
|
{ message: 'DnD handlers should be registered' }
|
|
).toBe(true);
|
|
|
|
await stableShot(page, 'dnd-02-handlers.png', { fullPage: true });
|
|
});
|
|
|
|
test('DragEnter event is detected', async ({ page, testLogger }) => {
|
|
await page.goto('/standalone/dnd/dnd_test.html');
|
|
await waitForCanvasApp(page);
|
|
|
|
const canvas = page.locator('#canvas');
|
|
const box = await canvas.boundingBox();
|
|
expect(box, 'Canvas should have bounding box').not.toBeNull();
|
|
|
|
// Simulate dragenter event
|
|
await page.evaluate(({ x, y }) => {
|
|
const canvas = document.getElementById('canvas');
|
|
if (canvas) {
|
|
const event = new DragEvent('dragenter', {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
clientX: x,
|
|
clientY: y,
|
|
dataTransfer: new DataTransfer()
|
|
});
|
|
canvas.dispatchEvent(event);
|
|
}
|
|
}, { x: box.x + 400, y: box.y + 200 });
|
|
|
|
await expect.poll(
|
|
() => testLogger.consoleLogs.some(l => l.includes('[DND] dragenter')),
|
|
{ message: 'DragEnter event should be logged' }
|
|
).toBe(true);
|
|
|
|
await stableShot(page, 'dnd-03-dragenter.png', { fullPage: true });
|
|
});
|
|
|
|
test('DragLeave event is detected', async ({ page, testLogger }) => {
|
|
await page.goto('/standalone/dnd/dnd_test.html');
|
|
await waitForCanvasApp(page);
|
|
|
|
const canvas = page.locator('#canvas');
|
|
const box = await canvas.boundingBox();
|
|
expect(box, 'Canvas should have bounding box').not.toBeNull();
|
|
|
|
// Simulate dragenter then dragleave
|
|
await page.evaluate(({ x, y }) => {
|
|
const canvas = document.getElementById('canvas');
|
|
if (canvas) {
|
|
const enterEvent = new DragEvent('dragenter', {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
clientX: x,
|
|
clientY: y,
|
|
dataTransfer: new DataTransfer()
|
|
});
|
|
canvas.dispatchEvent(enterEvent);
|
|
|
|
const leaveEvent = new DragEvent('dragleave', {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
dataTransfer: new DataTransfer()
|
|
});
|
|
canvas.dispatchEvent(leaveEvent);
|
|
}
|
|
}, { x: box.x + 400, y: box.y + 200 });
|
|
|
|
await expect.poll(
|
|
() => testLogger.consoleLogs.some(l => l.includes('[DND] dragleave')),
|
|
{ message: 'DragLeave event should be logged' }
|
|
).toBe(true);
|
|
|
|
await stableShot(page, 'dnd-04-dragleave.png', { fullPage: true });
|
|
});
|
|
|
|
test('Drop event triggers file processing', async ({ page, testLogger }) => {
|
|
await page.goto('/standalone/dnd/dnd_test.html');
|
|
await waitForCanvasApp(page);
|
|
|
|
const canvas = page.locator('#canvas');
|
|
const box = await canvas.boundingBox();
|
|
expect(box, 'Canvas should have bounding box').not.toBeNull();
|
|
|
|
// Create a test file and simulate drop
|
|
const testContent = 'Test file content for DnD';
|
|
const testFileName = 'test-drop-file.txt';
|
|
|
|
await page.evaluate(({ x, y, fileName, content }) => {
|
|
const canvas = document.getElementById('canvas');
|
|
if (canvas) {
|
|
const dataTransfer = new DataTransfer();
|
|
const file = new File([content], fileName, { type: 'text/plain' });
|
|
dataTransfer.items.add(file);
|
|
|
|
const event = new DragEvent('drop', {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
clientX: x,
|
|
clientY: y,
|
|
dataTransfer: dataTransfer
|
|
});
|
|
canvas.dispatchEvent(event);
|
|
}
|
|
}, { x: box.x + 400, y: box.y + 200, fileName: testFileName, content: testContent });
|
|
|
|
// Wait for async file processing (deterministic: poll for the drop log)
|
|
await expect.poll(
|
|
() => testLogger.consoleLogs.some(l => l.includes('[DND] drop')),
|
|
{ message: 'Drop event should be logged' }
|
|
).toBe(true);
|
|
|
|
await stableShot(page, 'dnd-05-drop.png', { fullPage: true });
|
|
});
|
|
|
|
test('Dropped file is written to WASM filesystem', async ({ page, testLogger }) => {
|
|
await page.goto('/standalone/dnd/dnd_test.html');
|
|
await waitForCanvasApp(page);
|
|
|
|
const canvas = page.locator('#canvas');
|
|
const box = await canvas.boundingBox();
|
|
expect(box, 'Canvas should have bounding box').not.toBeNull();
|
|
|
|
const testFileName = 'wasm-test-file.txt';
|
|
const testContent = 'Content written via DnD';
|
|
|
|
await page.evaluate(({ x, y, fileName, content }) => {
|
|
const canvas = document.getElementById('canvas');
|
|
if (canvas) {
|
|
const dataTransfer = new DataTransfer();
|
|
const file = new File([content], fileName, { type: 'text/plain' });
|
|
dataTransfer.items.add(file);
|
|
|
|
const event = new DragEvent('drop', {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
clientX: x,
|
|
clientY: y,
|
|
dataTransfer: dataTransfer
|
|
});
|
|
canvas.dispatchEvent(event);
|
|
}
|
|
}, { x: box.x + 400, y: box.y + 200, fileName: testFileName, content: testContent });
|
|
|
|
// Wait for file to be written (deterministic: poll for the write log)
|
|
await expect.poll(
|
|
() => testLogger.consoleLogs.some(l =>
|
|
l.includes('[DND] Wrote file:') && l.includes(testFileName)),
|
|
{ message: 'File should be written to WASM filesystem' }
|
|
).toBe(true);
|
|
|
|
await stableShot(page, 'dnd-06-file-written.png', { fullPage: true });
|
|
});
|
|
|
|
test('wxDropFilesEvent is fired after drop', async ({ page, testLogger }) => {
|
|
await page.goto('/standalone/dnd/dnd_test.html');
|
|
await waitForCanvasApp(page);
|
|
|
|
const canvas = page.locator('#canvas');
|
|
const box = await canvas.boundingBox();
|
|
expect(box, 'Canvas should have bounding box').not.toBeNull();
|
|
|
|
const testFileName = 'event-test.kicad_pcb';
|
|
const testContent = '(kicad_pcb (version 20230121))';
|
|
|
|
await page.evaluate(({ x, y, fileName, content }) => {
|
|
const canvas = document.getElementById('canvas');
|
|
if (canvas) {
|
|
const dataTransfer = new DataTransfer();
|
|
const file = new File([content], fileName, { type: 'application/octet-stream' });
|
|
dataTransfer.items.add(file);
|
|
|
|
const event = new DragEvent('drop', {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
clientX: x,
|
|
clientY: y,
|
|
dataTransfer: dataTransfer
|
|
});
|
|
canvas.dispatchEvent(event);
|
|
}
|
|
}, { x: box.x + 400, y: box.y + 200, fileName: testFileName, content: testContent });
|
|
|
|
// Wait for wxDropFilesEvent processing (deterministic: poll for the [DND_EVENT] log).
|
|
// The app logs "=== wxDropFilesEvent received! ===" which includes DND_EVENT prefix.
|
|
await expect.poll(
|
|
() => testLogger.consoleLogs.some(l => l.includes('[DND_EVENT]')),
|
|
{ message: 'wxDropFilesEvent should be fired' }
|
|
).toBe(true);
|
|
|
|
await stableShot(page, 'dnd-07-event-fired.png', { fullPage: true });
|
|
});
|
|
|
|
test('Multiple files can be dropped', async ({ page, testLogger }) => {
|
|
await page.goto('/standalone/dnd/dnd_test.html');
|
|
await waitForCanvasApp(page);
|
|
|
|
const canvas = page.locator('#canvas');
|
|
const box = await canvas.boundingBox();
|
|
expect(box, 'Canvas should have bounding box').not.toBeNull();
|
|
|
|
await page.evaluate(({ x, y }) => {
|
|
const canvas = document.getElementById('canvas');
|
|
if (canvas) {
|
|
const dataTransfer = new DataTransfer();
|
|
const file1 = new File(['content1'], 'file1.txt', { type: 'text/plain' });
|
|
const file2 = new File(['content2'], 'file2.txt', { type: 'text/plain' });
|
|
const file3 = new File(['content3'], 'file3.txt', { type: 'text/plain' });
|
|
dataTransfer.items.add(file1);
|
|
dataTransfer.items.add(file2);
|
|
dataTransfer.items.add(file3);
|
|
|
|
const event = new DragEvent('drop', {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
clientX: x,
|
|
clientY: y,
|
|
dataTransfer: dataTransfer
|
|
});
|
|
canvas.dispatchEvent(event);
|
|
}
|
|
}, { x: box.x + 400, y: box.y + 200 });
|
|
|
|
await expect.poll(
|
|
() => testLogger.consoleLogs.some(l => l.includes('[DND] drop: 3 files')),
|
|
{ message: 'Multiple files should be detected' }
|
|
).toBe(true);
|
|
|
|
await stableShot(page, 'dnd-08-multiple-files.png', { fullPage: true });
|
|
});
|
|
|
|
test('Clear files button exists in UI', async ({ page, testLogger }) => {
|
|
await page.goto('/standalone/dnd/dnd_test.html');
|
|
await waitForCanvasApp(page);
|
|
|
|
// First drop a file to verify drop works
|
|
const canvas = page.locator('#canvas');
|
|
const box = await canvas.boundingBox();
|
|
expect(box, 'Canvas should have bounding box').not.toBeNull();
|
|
|
|
await page.evaluate(({ x, y }) => {
|
|
const canvas = document.getElementById('canvas');
|
|
if (canvas) {
|
|
const dataTransfer = new DataTransfer();
|
|
const file = new File(['test'], 'test.txt', { type: 'text/plain' });
|
|
dataTransfer.items.add(file);
|
|
|
|
const event = new DragEvent('drop', {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
clientX: x,
|
|
clientY: y,
|
|
dataTransfer: dataTransfer
|
|
});
|
|
canvas.dispatchEvent(event);
|
|
}
|
|
}, { x: box.x + 400, y: box.y + 200 });
|
|
|
|
// Verify file was dropped (from JS side) - deterministic poll for the write log
|
|
await expect.poll(
|
|
() => testLogger.consoleLogs.some(l => l.includes('[DND] Wrote file:')),
|
|
{ message: 'File should be dropped and logged' }
|
|
).toBe(true);
|
|
|
|
await stableShot(page, 'dnd-09-with-file.png', { fullPage: true });
|
|
});
|
|
});
|