feat(gate-b): Gate B mask + BDFT + length axis (#21–#23) #31

Closed
grokbot_ty wants to merge 41 commits from gate-b into main
2 changed files with 125 additions and 8 deletions
Showing only changes of commit 866de43541 - Show all commits
+112
View File
@@ -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);
});
});
+13 -8
View File
@@ -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,15 +15,18 @@ 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
command: 'npx sirv build --port 18080', ? undefined
port: 18080, : {
reuseExistingServer: true, command: 'npx sirv build --port 18080',
cwd: path.resolve(__dirname, '../../'), port: 18080,
stdout: 'ignore', reuseExistingServer: true,
stderr: 'pipe', cwd: path.resolve(__dirname, '../../'),
}, stdout: 'ignore',
stderr: 'pipe',
},
projects: [ projects: [
{ {
name: 'chromium', name: 'chromium',