Download tests/embed.mjs from SharpMeow/unroll: direct link, hf CLI and curl.
- Browser
- Download file 9.13 kB
-
https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/tests/embed.mjs
- Command line
-
hf download hf://spaces/SharpMeow/unroll/tests/embed.mjs
-
curl -L -o embed.mjs https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/tests/embed.mjs
9.13 kB
| // 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); | |