File size: 9,128 Bytes
cc6b6d8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | // Embed tests: the viewer inside a cross-origin iframe on tests/embed-host.html.
// Usage: node tests/embed.mjs <viewerBase> <hostBase>
// viewerBase serves the Space (for example http://localhost:8080/), hostBase serves tests/ on another origin.
import { chromium } from 'playwright-core';
const VIEWER = process.argv[2] || 'http://localhost:8080/';
const HOST = process.argv[3] || 'http://localhost:8081/';
const browser = await chromium.launch({ executablePath: process.env.CHROME || '/usr/bin/google-chrome', args: ['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
const page = await browser.newPage({ viewport: { width: 1000, height: 800 } });
page.setDefaultTimeout(180000); // shared CI boxes can be slow; real browsers are far faster
const errors = []; page.on('pageerror', (e) => errors.push(String(e)));
const results = [];
const check = (name, pass, info) => results.push({ name, pass: !!pass, ...(pass ? {} : { info }) });
const poll = async (fn, ms = 15000) => { const t0 = Date.now(); let v; while (Date.now() - t0 < ms) { v = await fn(); if (v) return v; await page.waitForTimeout(150); } return v; };
const call = (method, ...args) => page.evaluate(([m, a]) => window.unrollEmbed.call(m, ...a), [method, args]);
const src = new URL('index.html?embed=1', VIEWER).href;
await page.goto(`${HOST}embed-host.html?src=${encodeURIComponent(src)}`, { waitUntil: 'load' });
check('host and viewer are cross-origin', new URL(HOST).origin !== new URL(VIEWER).origin);
await page.waitForFunction(() => window.unrollEmbed.ready, null, { timeout: 180000 });
const ready = await page.evaluate(() => window.unrollEmbed.ready);
check('viewer announces ready with version and methods', ready.version && ready.version.version && ready.methods.includes('setLayer'), ready);
const frame = page.frames().find((f) => f.url().startsWith(new URL(VIEWER).origin));
check('the iframe loaded the viewer', !!frame);
let st = await call('getState');
check('embed mode is on and waiting for a click', st.embed && st.embed.embedded && st.embed.framed && !st.embed.active, st.embed);
check('the gate is shown', await frame.isVisible('[data-testid="embed-activate"]'));
const box = await page.locator('#unroll').boundingBox();
const cx = box.x + box.width / 2, cy = box.y + box.height / 2;
// before activation the wheel scrolls the host page
await page.mouse.move(cx, cy);
let y0 = await page.evaluate(() => scrollY);
await page.mouse.wheel(0, 300);
await poll(async () => (await page.evaluate(() => scrollY)) > y0);
let y1 = await page.evaluate(() => scrollY);
check('wheel over an inactive embed scrolls the host page', y1 > y0, { y0, y1 });
await page.evaluate(() => scrollTo({ top: 0, behavior: 'instant' })); await poll(async () => (await page.evaluate(() => scrollY)) === 0); await page.waitForTimeout(800);
const box2 = await page.locator('#unroll').boundingBox();
const x = box2.x + box2.width / 2, y = box2.y + box2.height * 0.45;
// click to activate
await page.mouse.click(x, y); await page.waitForTimeout(300);
st = await call('getState');
check('a click activates the scene', st.embed.active, st.embed);
check('the gate is gone', !(await frame.isVisible('[data-testid="embed-activate"]')));
// drag orbits the camera inside the iframe
const c0 = (await call('camera.get')).position;
await page.mouse.move(x, y); await page.mouse.down(); await page.mouse.move(x + 120, y + 10, { steps: 8 }); await page.mouse.up(); await page.waitForTimeout(800);
const c1 = (await call('camera.get')).position;
check('dragging inside the iframe orbits the camera', JSON.stringify(c0) !== JSON.stringify(c1), { c0, c1 });
// the wheel now zooms and does not scroll the host
const d0 = (await call('camera.get')).distance; y0 = await page.evaluate(() => scrollY);
await page.mouse.move(x, y); await page.mouse.wheel(0, -400);
await poll(async () => (await call('camera.get')).distance !== d0);
await page.waitForTimeout(800);
const d1 = (await call('camera.get')).distance; y1 = await page.evaluate(() => scrollY);
check('wheel on an active embed zooms the scene', d1 !== d0, { d0, d1 });
check('and does not scroll the host page', y1 === y0, { y0, y1 });
// controls inside the iframe
await frame.click('[data-testid="layer-xray"]'); await page.waitForTimeout(400);
check('clicking a layer chip inside the iframe works', (await call('getState')).layer === 'xray');
await frame.locator('[data-testid="unroll-slider"]').fill('700'); await page.waitForTimeout(300);
check('the scrubber works inside the iframe', Math.abs((await call('getState')).target - 0.7) < 0.01);
await frame.click('[data-testid="destroy-mode"]'); await page.waitForTimeout(300);
check('Destroy works inside the iframe', (await call('getState')).destroy.on);
await frame.click('[data-testid="destroy-mode"]');
// postMessage API
let r = await call('setLayer', 'ink');
check('postMessage call returns the API result', r.ok && r.layer === 'ink', r);
await page.waitForTimeout(700);
check('subscribed events reach the host', (await page.evaluate(() => window.unrollEmbed.events)).some((e) => e.event === 'layer'));
r = await call('setUnroll', 7); check('validation errors come back over postMessage', !r.ok && r.code === 'invalid', r);
r = await call('nope.method'); check('unknown methods are refused', !r.ok, r);
r = await call('open.removeLocal', 'local-x'); check('host pages cannot touch local scrolls', !r.ok && r.code === 'forbidden', r);
r = await call('run', [['setLayer', 'xray'], ['setUnroll', 0.25]]); check('run works over postMessage', r.ok && r.completed === 2, r);
r = await call('screenshot'); check('screenshot over postMessage', r.ok && r.dataURL.startsWith('data:image/png'), r.error);
await page.click('button[data-call=\'["setLayer","ink"]\']'); await page.waitForTimeout(400);
check('host page buttons drive the viewer', (await call('getState')).layer === 'ink');
const full = await frame.getAttribute('[data-testid="embed-open-full"]', 'href');
check('Open full viewer links to the same view without embed', full && !/embed=/.test(full) && /t=/.test(full), full);
// fullscreen from inside the frame
await frame.click('[data-testid="fullscreen"]'); await page.waitForTimeout(500);
const fs = (await call('getState')).fullscreen;
check('the fullscreen button works inside the iframe', fs, fs);
if (fs) await frame.evaluate(() => document.exitFullscreen());
// responsive: the scene follows the container
await page.setViewportSize({ width: 420, height: 800 });
const sz = () => frame.evaluate(() => ({ w: innerWidth, c: document.getElementById('gl').getBoundingClientRect().width }));
await poll(async () => { const s = await sz(); return Math.abs(s.w - s.c) < 2 && s.w < 420; });
const sizes = await sz();
check('the scene resizes with its container', Math.abs(sizes.w - sizes.c) < 2 && sizes.w < 420, sizes);
// leaving: a click on the host gives the wheel back
await page.mouse.click(5, 5); await page.waitForTimeout(300);
check('clicking the host page deactivates the embed', !(await call('getState')).embed.active);
// autoplay and controls=0
await page.setViewportSize({ width: 1000, height: 800 });
const src2 = new URL('index.html?embed=1&autoplay=1&controls=0&scroll=PHerc0139&t=0.1&layer=xray', VIEWER).href;
await page.goto(`${HOST}embed-host.html?src=${encodeURIComponent(src2)}`, { waitUntil: 'load' });
await page.waitForFunction(() => window.unrollEmbed.ready, null, { timeout: 180000 });
const frame2 = page.frames().find((f) => f.url().startsWith(new URL(VIEWER).origin));
st = await call('getState');
check('autoplay plays on load', st.playing || st.target > 0.1, { playing: st.playing, target: st.target });
check('state params apply in embeds', st.layer === 'xray', st.layer);
check('controls=0 hides the controls', !(await frame2.isVisible('[data-testid="layer-ink"]')) && !(await frame2.isVisible('[data-testid="unroll-slider"]')));
// the Embed panel in the full viewer
await page.goto(VIEWER, { waitUntil: 'load' });
await page.waitForFunction(() => window.unroll && window.unroll.waitFor, null, { timeout: 120000 });
await page.evaluate(() => window.unroll.waitFor('ready', { timeout: 180000 }));
await page.click('[data-testid="embed"]');
const code = await page.inputValue('[data-testid="embed-code"]');
check('the Embed panel shows an iframe snippet', /<iframe src="https:\/\/sharpmeow-unroll\.static\.hf\.space\/index\.html\?embed=1/.test(code) && /allow="fullscreen; clipboard-write"/.test(code) && /loading="lazy"/.test(code), code);
r = await page.evaluate(() => window.unroll.embed.snippet({ autoplay: true, controls: false, state: false }));
check('embed.snippet options', r.ok && /autoplay=1/.test(r.url) && /controls=0/.test(r.url) && !/t=/.test(r.url), r);
const oe = await page.evaluate(() => fetch('oembed.json').then((x) => x.json()));
check('oembed.json is valid', oe.version === '1.0' && oe.type === 'rich' && /<iframe/.test(oe.html), oe);
await browser.close();
const failed = results.filter((x) => !x.pass);
console.log(JSON.stringify({ passed: results.length - failed.length, total: results.length, failed, errors }, null, 1));
process.exit(failed.length || errors.length ? 1 : 0);
|