import { expect, test } from '@playwright/test'; const boundary = '#polymodel-viewer-boundary'; const canvas = '#polymodel-viewer-canvas'; const preview = '#polymodel-viewer-preview'; const chestAssetPath = '/models/body_f_chest-v4.stl'; const forearmAssetPath = '/models/body_f_forearm_2x-v9.stl'; async function acquisitions(page) { return Number(await page.locator(boundary).getAttribute('data-context-acquisitions')); } async function expectViewerReady(page) { await expect(page.locator(canvas)).toHaveCount(1); await expect(page.locator(canvas)).toHaveAttribute('data-mounted', 'true'); await expect(page.locator(boundary)).toHaveAttribute('data-renderer-kind', 'three-d'); await expect(page.locator(canvas)).toHaveAttribute('data-renderer-kind', 'three-d'); await expect(page.locator(preview)).toHaveCount(0); } test('the demo viewer route displays the committed STL asset inside the shell', async ({ page }) => { await page.goto('/viewer/body-f-chest-v4'); await expect(page).toHaveURL(/\/viewer\/body-f-chest-v4$/); await expectViewerReady(page); // The product shell wraps the demo viewer route (reachable by direct URL only). await expect(page.getByRole('link', { name: 'Polymodel home' })).toBeVisible(); // The selected model and ready status surface on the boundary's data attributes // and in the visible header chrome (status pill + active model control). await expect(page.locator(boundary)).toHaveAttribute('data-session-label', 'body-f-chest-v4#1'); await expect(page.locator(boundary)).toHaveAttribute('data-asset-path', chestAssetPath); await expect(page.locator(boundary)).toHaveAttribute('data-viewer-status', 'Viewer ready'); await expect(page.getByText('Viewer ready')).toBeVisible(); await expect(page.getByRole('button', { name: 'Body F chest v4' })).toBeVisible(); }); test('direct viewer routes select deterministic asset-backed models', async ({ page }) => { await page.goto('/viewer/body-f-chest-v4'); await expectViewerReady(page); await expect(page.locator(boundary)).toHaveAttribute('data-session-label', 'body-f-chest-v4#1'); await expect(page.locator(boundary)).toHaveAttribute('data-asset-path', chestAssetPath); await expect(page.getByRole('button', { name: 'Body F chest v4' })).toBeVisible(); await page.goto('/viewer/body-f-forearm-2x-v9'); await expectViewerReady(page); await expect(page.locator(boundary)).toHaveAttribute('data-session-label', 'body-f-forearm-2x-v9#1'); await expect(page.locator(boundary)).toHaveAttribute('data-asset-path', forearmAssetPath); await expect(page.getByRole('button', { name: 'Body F forearm 2x v9' })).toBeVisible(); }); test('model switching keeps one WebGL context and updates renderer session metadata', async ({ page }) => { await page.goto('/viewer/body-f-chest-v4'); await expectViewerReady(page); await expect(page.locator(canvas)).toHaveCount(1); // The WebGL context is acquired exactly once for the initial mount. await expect.poll(acquisitions.bind(null, page)).toBe(1); await page.getByRole('button', { name: 'Body F forearm 2x v9' }).click(); await expectViewerReady(page); await expect(page.locator(canvas)).toHaveCount(1); await expect(page.locator(boundary)).toHaveAttribute('data-session-label', 'body-f-forearm-2x-v9#1'); await expect(page.locator(boundary)).toHaveAttribute('data-asset-path', forearmAssetPath); // Switching models must NOT acquire a second WebGL context (in-place replace). await expect.poll(acquisitions.bind(null, page)).toBe(1); await page.getByRole('button', { name: 'Body F chest v4' }).click(); await expectViewerReady(page); await expect(page.locator(canvas)).toHaveCount(1); await expect(page.locator(boundary)).toHaveAttribute('data-session-label', 'body-f-chest-v4#1'); await expect.poll(acquisitions.bind(null, page)).toBe(1); }); test('rapid model switching through loading windows keeps a single WebGL context', async ({ page }) => { await page.goto('/viewer/body-f-chest-v4'); await expectViewerReady(page); const chest = page.getByRole('button', { name: 'Body F chest v4' }); const forearm = page.getByRole('button', { name: 'Body F forearm 2x v9' }); // Switch rapidly without waiting for ready between clicks, exercising the // async Loading window and stale-fetch cancellation from use_resource. await forearm.click(); await chest.click(); await forearm.click(); await chest.click(); await expectViewerReady(page); await expect(page.locator(boundary)).toHaveAttribute('data-asset-path', chestAssetPath); // The retained context is never recreated across the switches. await expect.poll(acquisitions.bind(null, page)).toBe(1); }); test('resize callback remains single after model transitions', async ({ page }) => { await page.goto('/viewer/body-f-chest-v4'); const currentResizeCount = async () => Number(await page.locator(boundary).getAttribute('data-resize-count')); await expectViewerReady(page); const before = await currentResizeCount(); await page.setViewportSize({ width: 1100, height: 760 }); await expect.poll(currentResizeCount).toBe(before + 1); await page.getByRole('button', { name: 'Body F forearm 2x v9' }).click(); await expectViewerReady(page); await expect(page.locator(boundary)).toHaveAttribute('data-asset-path', forearmAssetPath); const afterTransition = await currentResizeCount(); await page.setViewportSize({ width: 1000, height: 700 }); await expect.poll(currentResizeCount).toBe(afterTransition + 1); });