feat(gate-b): Gate B mask + BDFT + length axis (#21–#23) #31
@@ -0,0 +1,112 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { resolve, dirname } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import * as os from 'node:os';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const TEST_IMAGE = resolve(__dirname, 'test-slab.png');
|
||||||
|
|
||||||
|
const evidenceDir = resolve(__dirname, '../../test-results/mask-ux-evidence');
|
||||||
|
|
||||||
|
function ensureDir(dir: string) {
|
||||||
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('Mask UX timing & overlay', () => {
|
||||||
|
test('overlay is translucent — photo visible under green mask', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
const fileInput = page.locator('#photo-file-input');
|
||||||
|
await fileInput.setInputFiles(TEST_IMAGE);
|
||||||
|
|
||||||
|
await expect(page.locator('h2')).toContainText('Mask & Knobs');
|
||||||
|
|
||||||
|
// Wait for the mask canvas to be drawn
|
||||||
|
const maskCanvas = page.locator('.mask-canvas');
|
||||||
|
await maskCanvas.waitFor({ state: 'visible', timeout: 10000 });
|
||||||
|
|
||||||
|
// Canvas should have actual content (not a blank white canvas)
|
||||||
|
const canvasImg = await maskCanvas.screenshot({ type: 'png' });
|
||||||
|
expect(canvasImg.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Screenshot the full mask step area for visual evidence
|
||||||
|
ensureDir(evidenceDir);
|
||||||
|
await page.locator('.mask-step').screenshot({
|
||||||
|
path: resolve(evidenceDir, 'mask-overlay-translucent.png'),
|
||||||
|
});
|
||||||
|
console.log('Saved mask overlay screenshot to', resolve(evidenceDir, 'mask-overlay-translucent.png'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sensitivity knob change is fast via programmatic dispatch', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
const fileInput = page.locator('#photo-file-input');
|
||||||
|
await fileInput.setInputFiles(TEST_IMAGE);
|
||||||
|
await expect(page.locator('h2')).toContainText('Mask & Knobs');
|
||||||
|
|
||||||
|
const sensitivitySlider = page.locator('input[type="range"]').first();
|
||||||
|
await sensitivitySlider.waitFor({ state: 'visible', timeout: 10000 });
|
||||||
|
|
||||||
|
const initialValue = await sensitivitySlider.inputValue();
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
// Dispatch input + change events programmatically (range inputs don't support fill/setInputValue)
|
||||||
|
await sensitivitySlider.evaluate((el: HTMLInputElement) => {
|
||||||
|
el.value = '0.75';
|
||||||
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
});
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
|
||||||
|
console.log(`Sensitivity knob change elapsed: ${elapsed}ms`);
|
||||||
|
expect(elapsed).toBeLessThan(300);
|
||||||
|
|
||||||
|
const newValue = await sensitivitySlider.inputValue();
|
||||||
|
expect(newValue).not.toBe(initialValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('edge offset knob change is fast via programmatic dispatch', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
const fileInput = page.locator('#photo-file-input');
|
||||||
|
await fileInput.setInputFiles(TEST_IMAGE);
|
||||||
|
await expect(page.locator('h2')).toContainText('Mask & Knobs');
|
||||||
|
|
||||||
|
const edgeOffsetSlider = page.locator('input[type="range"]').nth(1);
|
||||||
|
await edgeOffsetSlider.waitFor({ state: 'visible', timeout: 10000 });
|
||||||
|
|
||||||
|
const initialVal = await edgeOffsetSlider.inputValue();
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
await edgeOffsetSlider.evaluate((el: HTMLInputElement) => {
|
||||||
|
el.value = '5';
|
||||||
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
});
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
|
||||||
|
console.log(`Edge offset knob change elapsed: ${elapsed}ms`);
|
||||||
|
expect(elapsed).toBeLessThan(300);
|
||||||
|
|
||||||
|
const newVal = await edgeOffsetSlider.inputValue();
|
||||||
|
expect(newVal).not.toBe(initialVal);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sheet mode switch updates overlay within 1s', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
const fileInput = page.locator('#photo-file-input');
|
||||||
|
await fileInput.setInputFiles(TEST_IMAGE);
|
||||||
|
await expect(page.locator('h2')).toContainText('Mask & Knobs');
|
||||||
|
|
||||||
|
const sheetSelect = page.locator('.knob-group select');
|
||||||
|
await sheetSelect.waitFor({ state: 'visible', timeout: 10000 });
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
await sheetSelect.selectOption({ label: 'Green' });
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
console.log(`Sheet switch elapsed: ${elapsed}ms`);
|
||||||
|
expect(elapsed).toBeLessThan(1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,6 +3,8 @@ import { fileURLToPath } from 'node:url';
|
|||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
// Allow overriding via env for remote testing (e.g. against deployed instance).
|
||||||
|
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || undefined;
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: '.',
|
testDir: '.',
|
||||||
@@ -13,8 +15,11 @@ export default defineConfig({
|
|||||||
use: {
|
use: {
|
||||||
trace: 'on-first-retry',
|
trace: 'on-first-retry',
|
||||||
screenshot: 'only-on-failure',
|
screenshot: 'only-on-failure',
|
||||||
|
baseURL: BASE_URL,
|
||||||
},
|
},
|
||||||
webServer: {
|
webServer: BASE_URL
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
command: 'npx sirv build --port 18080',
|
command: 'npx sirv build --port 18080',
|
||||||
port: 18080,
|
port: 18080,
|
||||||
reuseExistingServer: true,
|
reuseExistingServer: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user