Something went wrong. Try again.
atproto Thingiverse but good
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238import { 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';const cubeGearsAssetPath = '/models/cube_gears.3mf';const sampleThingPath = '/did:plc:aaaaaaaaaaaaaaaaaaaaaaaa/thing/parametric-enclosure';
async function acquisitions(page) { return Number(await page.locator(boundary).getAttribute('data-context-acquisitions'));}
async function expectViewerReady(page) { const ready = { timeout: 15_000 }; await expect(page.locator(canvas)).toHaveCount(1, ready); await expect(page.locator(canvas)).toHaveAttribute('data-mounted', 'true', ready); await expect(page.locator(boundary)).toHaveAttribute('data-renderer-kind', 'three-d', ready); await expect(page.locator(canvas)).toHaveAttribute('data-renderer-kind', 'three-d', ready); await expect(page.locator(preview)).toHaveCount(0, ready);}
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-protocol-capabilities', expect.stringContaining('Preview'), ); await expect(page.locator(boundary)).not.toHaveAttribute('data-protocol-session-id', ''); const ready = { timeout: 15_000 }; await expect(page.locator(boundary)).toHaveAttribute('data-viewer-status', 'Viewer ready', ready); await expect(page.getByText('Viewer ready')).toBeVisible(ready); await expect(page.getByRole('button', { name: 'Body F chest v4' })).toBeVisible();});
test('a fatal worker restart replays the current model intent after renegotiation', async ({ page }) => { let abortedInitialWorker = false; await page.route('**/renderer_worker_bg.wasm', async (route) => { if (!abortedInitialWorker) { abortedInitialWorker = true; await route.abort('failed'); } else { await route.continue(); } });
await page.goto('/viewer/body-f-chest-v4'); await expect(page.locator(boundary)).toHaveAttribute('data-restart-epoch', /[1-9]/, { timeout: 15_000, }); await expectViewerReady(page); await expect(page.locator(boundary)).toHaveAttribute('data-asset-path', chestAssetPath); await expect(page.locator(boundary)).toHaveAttribute( 'data-protocol-capabilities', expect.stringContaining('CurrentFormats'), ); const pointerListenerHandled = await page.locator(canvas).evaluate((node) => { const event = new PointerEvent('pointerdown', { bubbles: true, cancelable: true }); return !node.dispatchEvent(event); }); const wheelListenerHandled = await page.locator(canvas).evaluate((node) => { const event = new WheelEvent('wheel', { bubbles: true, cancelable: true, deltaY: 24 }); return !node.dispatchEvent(event); }); expect(pointerListenerHandled).toBe(true); expect(wheelListenerHandled).toBe(true); expect(abortedInitialWorker).toBe(true);});
test('direct viewer routes select deterministic STL and 3MF assets', 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/cube-gears-3mf'); await expectViewerReady(page); await expect(page.locator(boundary)).toHaveAttribute('data-session-label', 'cube-gears-3mf#1'); await expect(page.locator(boundary)).toHaveAttribute('data-asset-path', cubeGearsAssetPath); await expect(page.getByRole('button', { name: 'Cube Gears (3MF)' })).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('logged-out Thing detail acquires the complete model bundle in one public XRPC request', async ({ page }) => { const bundleRequests = []; const resourceRequests = []; page.on('request', (request) => { const url = new URL(request.url()); if ( url.pathname === '/xrpc/space.polymodel.library.getModelLoadBundle' && request.method() === 'GET' ) { bundleRequests.push(request); } if (url.pathname.startsWith('/app/ldraw/resources/')) resourceRequests.push(request); }); const bundleResponsePromise = page.waitForResponse((response) => response.url().includes('/xrpc/space.polymodel.library.getModelLoadBundle') && response.request().method() === 'GET', );
await page.goto(sampleThingPath); const bundleResponse = await bundleResponsePromise; expect(bundleResponse.status()).toBe(200); expect(bundleResponse.headers()['content-type']).toContain('application/vnd.polymodel.load-bundle'); expect((await bundleResponse.body()).byteLength).toBeGreaterThan(0); expect(new URL(bundleResponse.url()).searchParams.get('uri')).toBe( 'at://did:plc:aaaaaaaaaaaaaaaaaaaaaaaa/space.polymodel.library.part/part-01', );
await expect(page.locator(boundary)).toHaveAttribute('data-viewer-status', 'Viewer ready', { timeout: 15_000, }); await expect(page.locator(boundary)).toHaveAttribute('data-current-identity', /.+/); await expect(page.locator(boundary)).toHaveAttribute('data-interactive-namespace', 'Interactive'); await expect(page.locator(boundary)).toHaveAttribute('data-preview-namespace', 'Preview'); await expect(page.locator(boundary)).toHaveAttribute( 'data-protocol-capabilities', expect.stringContaining('CompoundLdraw'), ); const interactiveIdentity = await page.locator(boundary).getAttribute('data-current-identity'); await expect(page.locator(boundary)).toHaveAttribute('data-compound-boundary', 'true'); expect(bundleRequests).toHaveLength(1); expect(new URL(bundleRequests[0].url()).searchParams.get('uri')).toBe( 'at://did:plc:aaaaaaaaaaaaaaaaaaaaaaaa/space.polymodel.library.part/part-01', ); expect(resourceRequests).toHaveLength(0); // Preview is a separate renderer namespace and must not replace the current // interactive identity/readiness after the complete bundle has arrived. await expect(page.locator(boundary)).toHaveAttribute('data-current-identity', interactiveIdentity); await expect(page.locator(boundary)).toHaveAttribute('data-viewer-status', 'Viewer ready');
const downloadResponsePromise = page.waitForResponse((response) => { const url = new URL(response.url()); return url.pathname === '/xrpc/space.polymodel.library.getPartFile' && response.request().method() === 'GET'; }); await expect(page.getByRole('button', { name: 'Download files' })).toBeVisible(); await page.getByRole('button', { name: 'Download files' }).click(); const downloadResponse = await downloadResponsePromise; expect(downloadResponse.status()).toBe(200); expect(downloadResponse.headers()['content-type']).toBe('application/x-ldraw'); expect(downloadResponse.headers()['content-disposition']).toBe( 'attachment; filename="Main_enclosure_body"', ); expect(new URL(downloadResponse.url()).searchParams.get('uri')).toBe( 'at://did:plc:aaaaaaaaaaaaaaaaaaaaaaaa/space.polymodel.library.part/part-01', ); expect(await downloadResponse.body()).toEqual(Buffer.from( '0 BFC CERTIFY CCW\n' + '1 16 -35 0 0 1 0 0 0 1 0 0 0 1 models/child.dat\n' + '1 16 35 0 0 0 0 -1 0 1 0 1 0 0 models/child.dat\n' + '1 16 0 0 35 1 0 0 0 1 0 0 0 1 models/companion.dat\n', ));});
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' });
// Hold the forearm asset response open so the subsequent chest selection // supersedes an in-flight worker load rather than collapsing into one render. await page.route(`**${forearmAssetPath}`, async (route) => { await new Promise((resolve) => setTimeout(resolve, 250)); await route.continue(); }); const forearmRequest = page.waitForRequest((request) => request.url().endsWith(forearmAssetPath)); await forearm.click(); await forearmRequest; await expect(page.locator(boundary)).toHaveAttribute('data-asset-path', forearmAssetPath); await chest.click();
await expectViewerReady(page); await expect(page.locator(boundary)).toHaveAttribute('data-asset-path', chestAssetPath); // Superseded loads must be cancelled while the final intent wins and the // retained context is never recreated across the switches. await expect.poll(async () => Number(await page.locator(boundary).getAttribute('data-cancelled-loads'))).toBeGreaterThan(0); 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);});