feat: finish Stage 2B continuous sorter realism and local deploy
Unify CAD conveyor on / and /details, add RealSense D435i + contact paddle physics, keep items moving through scan, lock 7×10 drop validation, and ship Stage 2 artifacts with production on :3100 (permanent HTTPS still blocked). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,26 +1,31 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { dismissFinished, ensurePaused, ensureRunning, openPausedCase } from './helpers';
|
||||
|
||||
test.describe('controls', () => {
|
||||
test.setTimeout(90_000);
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.locator('canvas')).toBeVisible({ timeout: 60_000 });
|
||||
await expect(page.getByTestId('demo-hud')).toBeVisible();
|
||||
await openPausedCase(page, 0, '1');
|
||||
});
|
||||
|
||||
test('seek next/prev and jump cases, speed, presentation', async ({ page }) => {
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText('1/10');
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText('1/11');
|
||||
|
||||
await page.getByTestId('demo-next').click();
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText('2/10');
|
||||
await ensurePaused(page);
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText('2/11');
|
||||
|
||||
await page.getByTestId('demo-prev').click();
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText('1/10');
|
||||
await ensurePaused(page);
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText('1/11');
|
||||
|
||||
await page.getByTestId('demo-case-8').click();
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText('9/10');
|
||||
await ensurePaused(page);
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText('9/11');
|
||||
|
||||
await page.getByTestId('demo-case-0').click();
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText('1/10');
|
||||
await ensurePaused(page);
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText('1/11');
|
||||
|
||||
for (const speed of ['0.5', '1', '1.5', '2'] as const) {
|
||||
await page.getByTestId(`demo-speed-${speed}`).click();
|
||||
@@ -34,5 +39,8 @@ test.describe('controls', () => {
|
||||
await page.locator('.presentation-exit').click();
|
||||
await expect(page.getByTestId('demo-hud')).toBeVisible();
|
||||
await expect(page.getByTestId('demo-presentation')).toBeVisible();
|
||||
|
||||
await page.goto('/?debug=1');
|
||||
await ensureRunning(page);
|
||||
});
|
||||
});
|
||||
|
||||
55
e2e/helpers.ts
Normal file
55
e2e/helpers.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/** Shared e2e helpers — Stage 2B autostart means / opens already running. */
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
|
||||
/** Close the Demo Complete overlay if it is covering the controls. */
|
||||
export async function dismissFinished(page: Page) {
|
||||
const finished = page.getByTestId('demo-finished');
|
||||
if (await finished.isVisible().catch(() => false)) {
|
||||
await finished.getByRole('button', { name: /Replay/i }).click({ force: true });
|
||||
await expect(finished).toHaveCount(0, { timeout: 10_000 });
|
||||
}
|
||||
}
|
||||
|
||||
/** Ensure playback is running (noop if already on Pause button). */
|
||||
export async function ensureRunning(page: Page) {
|
||||
await dismissFinished(page);
|
||||
const pause = page.getByTestId('demo-pause');
|
||||
if (await pause.isVisible().catch(() => false)) return;
|
||||
const play = page.getByTestId('demo-play');
|
||||
await expect(play.or(pause)).toBeVisible({ timeout: 15_000 });
|
||||
if (await play.isVisible().catch(() => false)) {
|
||||
await play.click({ force: true });
|
||||
}
|
||||
await expect(page.getByTestId('demo-pause')).toBeVisible({ timeout: 10_000 });
|
||||
}
|
||||
|
||||
export async function ensurePaused(page: Page) {
|
||||
await dismissFinished(page);
|
||||
const play = page.getByTestId('demo-play');
|
||||
if (await play.isVisible().catch(() => false)) return;
|
||||
await page.getByTestId('demo-pause').click({ force: true });
|
||||
await expect(page.getByTestId('demo-play')).toBeVisible({ timeout: 10_000 });
|
||||
}
|
||||
|
||||
/** Assert either Play or Pause control is present (autostart-safe). */
|
||||
export async function expectPlaybackControl(page: Page) {
|
||||
await expect(
|
||||
page.getByTestId('demo-play').or(page.getByTestId('demo-pause')),
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Open debug demo, pause, then seek to a case WHILE paused (seek preserves
|
||||
* paused status — unlike seek from idle which auto-starts).
|
||||
*/
|
||||
export async function openPausedCase(page: Page, caseIndex: number, speed: '0.5' | '1' | '1.5' | '2' = '1') {
|
||||
await page.goto('/?debug=1');
|
||||
await expect(page.locator('canvas')).toBeVisible({ timeout: 60_000 });
|
||||
await expect(page.getByTestId('demo-hud')).toBeVisible();
|
||||
await ensurePaused(page);
|
||||
await page.getByTestId(`demo-speed-${speed}`).click();
|
||||
await page.getByTestId(`demo-case-${caseIndex}`).click();
|
||||
await dismissFinished(page);
|
||||
await ensurePaused(page);
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText(`${caseIndex + 1}/11`);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { ensureRunning } from './helpers';
|
||||
|
||||
/**
|
||||
* Stage 0 — mobile fallback & WebGL context resilience.
|
||||
@@ -67,9 +68,9 @@ test.describe('mobile fallback (390x844)', () => {
|
||||
// Auto-camera toggle must not claim hidden 3D works
|
||||
await expect(page.locator('.auto-camera-toggle')).toHaveCount(0);
|
||||
|
||||
// Play/pause drive the visible SVG scene (same playback state)
|
||||
await page.getByTestId('demo-play').click();
|
||||
await expect(page.getByTestId('demo-pause')).toBeVisible({ timeout: 10_000 });
|
||||
// Play/pause drive the visible SVG scene (same playback state).
|
||||
// Stage 2B autostart may already be running — ensureRunning is idempotent.
|
||||
await ensureRunning(page);
|
||||
await expect(page.getByTestId('demo-status')).not.toHaveText('FINISHED');
|
||||
await page.getByTestId('demo-pause').click();
|
||||
await expect(page.getByTestId('demo-play')).toBeVisible({ timeout: 10_000 });
|
||||
@@ -185,7 +186,7 @@ test.describe('stage2 mobile capability policy', () => {
|
||||
// Mobile Low mounts the real 3D canvas at low quality
|
||||
await expect(page.locator('canvas')).toBeVisible({ timeout: 60_000 });
|
||||
await expect(page.getByTestId('main-svg-fallback')).toHaveCount(0);
|
||||
await page.getByTestId('demo-play').click();
|
||||
await ensureRunning(page);
|
||||
await page.waitForTimeout(4000);
|
||||
expect(pageErrors, `pageerrors: ${pageErrors.join('; ')}`).toEqual([]);
|
||||
});
|
||||
@@ -227,7 +228,7 @@ test.describe('stage0 prototype mode', () => {
|
||||
await toggle.click();
|
||||
await expect(toggle).toContainText('Auto Cam: ON');
|
||||
|
||||
await page.getByTestId('demo-play').click();
|
||||
await ensureRunning(page);
|
||||
await page.waitForTimeout(4000);
|
||||
expect(pageErrors, `pageerrors: ${pageErrors.join('; ')}`).toEqual([]);
|
||||
expect(consoleErrors, `console errors: ${consoleErrors.join('; ')}`).toEqual([]);
|
||||
|
||||
@@ -17,7 +17,7 @@ test.describe('production smoke @production', () => {
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
await page.goto('/?debug=1');
|
||||
const html = await page.content();
|
||||
const bundle = html.match(/index-[A-Za-z0-9_-]+\.js/)?.[0] ?? '';
|
||||
expect(bundle).toMatch(/^index-/);
|
||||
@@ -50,7 +50,7 @@ test.describe('production smoke @production', () => {
|
||||
expect(webgl.ok).toBe(true);
|
||||
|
||||
await expect(page.getByTestId('demo-play').or(page.getByTestId('demo-pause'))).toBeVisible();
|
||||
await expect(page.getByTestId('demo-case-9')).toBeVisible();
|
||||
await expect(page.getByTestId('demo-case-10')).toBeVisible();
|
||||
|
||||
// Play / seek
|
||||
const pauseBtn = page.getByTestId('demo-pause');
|
||||
@@ -59,11 +59,11 @@ test.describe('production smoke @production', () => {
|
||||
}
|
||||
await page.waitForTimeout(600);
|
||||
await page.getByTestId('demo-case-0').click();
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText('1/10');
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText('1/11');
|
||||
|
||||
// Jam
|
||||
await page.getByTestId('demo-speed-2').click();
|
||||
await page.getByTestId('demo-case-8').click();
|
||||
await page.getByTestId('demo-case-9').click();
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const status = (await page.getByTestId('demo-status').textContent()) ?? '';
|
||||
@@ -73,7 +73,7 @@ test.describe('production smoke @production', () => {
|
||||
.toMatch(/FAULT|JAM/i);
|
||||
|
||||
// E-stop
|
||||
await page.getByTestId('demo-case-9').click();
|
||||
await page.getByTestId('demo-case-10').click();
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const status = (await page.getByTestId('demo-status').textContent()) ?? '';
|
||||
@@ -88,7 +88,7 @@ test.describe('production smoke @production', () => {
|
||||
await stop.click({ force: true });
|
||||
}
|
||||
await page.getByTestId('demo-case-0').click({ force: true });
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText('1/10');
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText('1/11');
|
||||
|
||||
await page.goto('/details');
|
||||
await expect(page.locator('#root')).toBeVisible();
|
||||
@@ -96,7 +96,7 @@ test.describe('production smoke @production', () => {
|
||||
await expect(page.locator('#root')).toBeVisible();
|
||||
|
||||
// Back home — version still matches
|
||||
await page.goto('/');
|
||||
await page.goto('/?debug=1');
|
||||
const version2 = await page.evaluate(async () => {
|
||||
const r = await fetch('/version.json', { cache: 'no-store' });
|
||||
return r.ok ? r.json() : null;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
* - no console errors throughout
|
||||
*/
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
import { ensureRunning } from './helpers';
|
||||
|
||||
const MODEL_RE = /\/models\/.*\.stl$/;
|
||||
|
||||
@@ -32,7 +33,7 @@ test.describe('Stage 1 real models', () => {
|
||||
|
||||
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForSelector('canvas', { timeout: 30000 });
|
||||
await page.locator('[data-testid="demo-play"]').first().click();
|
||||
await ensureRunning(page);
|
||||
|
||||
// Playlist starts with SKU-001 (box-300.stl, preloaded); let playback settle.
|
||||
await page.waitForTimeout(9000);
|
||||
@@ -49,7 +50,7 @@ test.describe('Stage 1 real models', () => {
|
||||
|
||||
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForSelector('canvas', { timeout: 30000 });
|
||||
await page.locator('[data-testid="demo-play"]').first().click();
|
||||
await ensureRunning(page);
|
||||
|
||||
// Play through several cases incl. the bottle case; scene must stay alive.
|
||||
await page.waitForTimeout(15000);
|
||||
@@ -120,7 +121,7 @@ test.describe('Stage 1 real models', () => {
|
||||
const errors = watchConsole(page);
|
||||
await page.goto('/?stage1=1&verify=real-models&sku=SKU-001', { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForSelector('canvas', { timeout: 30000 });
|
||||
await page.locator('[data-testid="demo-play"]').first().click();
|
||||
await ensureRunning(page);
|
||||
await page.waitForTimeout(8000);
|
||||
|
||||
const card = page.locator('text=STAGE1 VERIFY · SKU-001');
|
||||
@@ -135,7 +136,7 @@ test.describe('Stage 1 real models', () => {
|
||||
const errors = watchConsole(page);
|
||||
await page.goto('/?stage1=1&verify=real-models&sku=SKU-011', { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForSelector('canvas', { timeout: 30000 });
|
||||
await page.locator('[data-testid="demo-play"]').first().click();
|
||||
await ensureRunning(page);
|
||||
|
||||
// SKU-011 is the 7th playlist case — seek forward deterministically.
|
||||
for (let i = 0; i < 6; i++) {
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { ensureRunning, openPausedCase } from './helpers';
|
||||
|
||||
test.describe('safety', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.locator('canvas')).toBeVisible({ timeout: 60_000 });
|
||||
await expect(page.getByTestId('demo-hud')).toBeVisible();
|
||||
// Speed up to reach fault phases sooner
|
||||
await page.getByTestId('demo-speed-2').click();
|
||||
});
|
||||
test.setTimeout(90_000);
|
||||
|
||||
test('jam case shows FAULT in status or command', async ({ page }) => {
|
||||
await page.getByTestId('demo-case-8').click();
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText('9/10');
|
||||
await openPausedCase(page, 9, '1');
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText('10/11');
|
||||
await ensureRunning(page);
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
@@ -20,14 +16,15 @@ test.describe('safety', () => {
|
||||
const command = (await page.getByTestId('demo-command').textContent()) ?? '';
|
||||
return `${status} ${command}`;
|
||||
},
|
||||
{ timeout: 25_000 },
|
||||
{ timeout: 20_000, intervals: [50, 100, 100] },
|
||||
)
|
||||
.toMatch(/FAULT/i);
|
||||
});
|
||||
|
||||
test('emergency case shows EMERGENCY in status or command', async ({ page }) => {
|
||||
await page.getByTestId('demo-case-9').click();
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText('10/10');
|
||||
await openPausedCase(page, 10, '1');
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText('11/11');
|
||||
await ensureRunning(page);
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
@@ -36,7 +33,7 @@ test.describe('safety', () => {
|
||||
const command = (await page.getByTestId('demo-command').textContent()) ?? '';
|
||||
return `${status} ${command}`;
|
||||
},
|
||||
{ timeout: 25_000 },
|
||||
{ timeout: 20_000, intervals: [50, 100, 100] },
|
||||
)
|
||||
.toMatch(/EMERGENCY/i);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { ensureRunning, expectPlaybackControl } from './helpers';
|
||||
|
||||
test.describe('smoke', () => {
|
||||
test('home opens, canvas loads, play works', async ({ page }) => {
|
||||
@@ -13,10 +14,9 @@ test.describe('smoke', () => {
|
||||
await expect(canvas).toBeVisible({ timeout: 60_000 });
|
||||
|
||||
await expect(page.getByTestId('demo-hud')).toBeVisible();
|
||||
await expect(page.getByTestId('demo-play')).toBeVisible();
|
||||
|
||||
await page.getByTestId('demo-play').click();
|
||||
await expect(page.getByTestId('demo-pause')).toBeVisible({ timeout: 10_000 });
|
||||
// Stage 2B: demo autostarts — Pause is present immediately; Play after pause.
|
||||
await expectPlaybackControl(page);
|
||||
await ensureRunning(page);
|
||||
await expect(page.getByTestId('demo-status')).not.toHaveText('FINISHED');
|
||||
|
||||
const finished = page.getByTestId('demo-finished');
|
||||
|
||||
@@ -3,11 +3,14 @@ import { test, expect, type Page } from '@playwright/test';
|
||||
test.setTimeout(90_000);
|
||||
|
||||
async function waitScene(page: Page) {
|
||||
await page.goto('/');
|
||||
await page.goto('/?debug=1&playback=paused');
|
||||
await expect(page.locator('canvas')).toBeVisible({ timeout: 60_000 });
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
await expect(page.getByTestId('demo-hud')).toBeVisible();
|
||||
// Demo auto-starts as running — pin speed early
|
||||
// seek pins case 0 and starts running — pause again for deterministic HUD snaps
|
||||
await page.getByTestId('demo-case-0').click();
|
||||
const pause = page.getByTestId('demo-pause');
|
||||
if (await pause.isVisible().catch(() => false)) await pause.click();
|
||||
await page.getByTestId('demo-speed-2').click();
|
||||
}
|
||||
|
||||
@@ -22,7 +25,7 @@ async function dismissFinishedIfNeeded(page: Page) {
|
||||
async function jumpCase(page: Page, index: number) {
|
||||
await dismissFinishedIfNeeded(page);
|
||||
await page.getByTestId(`demo-case-${index}`).click();
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText(`${index + 1}/10`, {
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText(`${index + 1}/11`, {
|
||||
timeout: 10_000,
|
||||
});
|
||||
}
|
||||
@@ -52,7 +55,7 @@ test.describe('visual regression', () => {
|
||||
if (await pause.isVisible().catch(() => false)) {
|
||||
await pause.click();
|
||||
}
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText('1/10');
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText('1/11');
|
||||
await snapHud(page, '01-home-idle-hud.png');
|
||||
});
|
||||
|
||||
@@ -85,7 +88,7 @@ test.describe('visual regression', () => {
|
||||
|
||||
test('05 jam fault HUD', async ({ page }) => {
|
||||
await waitScene(page);
|
||||
await jumpCase(page, 8);
|
||||
await jumpCase(page, 9);
|
||||
await ensureRunning(page);
|
||||
await expect
|
||||
.poll(async () => {
|
||||
@@ -100,7 +103,7 @@ test.describe('visual regression', () => {
|
||||
|
||||
test('06 emergency stop HUD', async ({ page }) => {
|
||||
await waitScene(page);
|
||||
await jumpCase(page, 9);
|
||||
await jumpCase(page, 10);
|
||||
await ensureRunning(page);
|
||||
await expect
|
||||
.poll(async () => {
|
||||
@@ -139,7 +142,7 @@ test.describe('visual regression', () => {
|
||||
|
||||
test('09 recovery after jam stop', async ({ page }) => {
|
||||
await waitScene(page);
|
||||
await jumpCase(page, 8);
|
||||
await jumpCase(page, 9);
|
||||
await ensureRunning(page);
|
||||
await expect
|
||||
.poll(async () => {
|
||||
|
||||
Reference in New Issue
Block a user