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';
|
||||
|
||||
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({
|
||||
testDir: '.',
|
||||
@@ -13,15 +15,18 @@ export default defineConfig({
|
||||
use: {
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
baseURL: BASE_URL,
|
||||
},
|
||||
webServer: {
|
||||
command: 'npx sirv build --port 18080',
|
||||
port: 18080,
|
||||
reuseExistingServer: true,
|
||||
cwd: path.resolve(__dirname, '../../'),
|
||||
stdout: 'ignore',
|
||||
stderr: 'pipe',
|
||||
},
|
||||
webServer: BASE_URL
|
||||
? undefined
|
||||
: {
|
||||
command: 'npx sirv build --port 18080',
|
||||
port: 18080,
|
||||
reuseExistingServer: true,
|
||||
cwd: path.resolve(__dirname, '../../'),
|
||||
stdout: 'ignore',
|
||||
stderr: 'pipe',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
|
||||
Reference in New Issue
Block a user