Download app/tests/cat-interactions.mjs from Mike0021/MiniCPM5-2B-WebGPU-Pi-HTTP: direct link, hf CLI and curl.
- Browser
- Download file 8.56 kB
-
https://huggingface.co/spaces/Mike0021/MiniCPM5-2B-WebGPU-Pi-HTTP/resolve/main/app/tests/cat-interactions.mjs
- Command line
-
hf download hf://spaces/Mike0021/MiniCPM5-2B-WebGPU-Pi-HTTP/app/tests/cat-interactions.mjs
-
curl -L -o cat-interactions.mjs https://huggingface.co/spaces/Mike0021/MiniCPM5-2B-WebGPU-Pi-HTTP/resolve/main/app/tests/cat-interactions.mjs
8.56 kB
| import { chromium, webkit } from 'playwright'; | |
| import assert from 'node:assert/strict'; | |
| import fs from 'node:fs/promises'; | |
| const url = process.argv[2] ?? 'http://127.0.0.1:4173/'; | |
| const result = { url, checks: [], errors: [] }; | |
| const webkitRoot = process.env.WEBKIT_MINIBROWSER_ROOT; | |
| const cases = [ | |
| { name: 'chromium-desktop', engine: chromium, mobile: false }, | |
| { name: 'chromium-touch', engine: chromium, mobile: true }, | |
| ...(process.env.TEST_WEBKIT ? [{ name: 'webkit-touch', engine: webkit, mobile: true }] : []), | |
| ]; | |
| for (const { name, engine, mobile } of cases.filter(item => !process.env.CAT_TEST_CASE || item.name === process.env.CAT_TEST_CASE)) { | |
| console.log(`Checking ${name}`); | |
| const browser = await engine.launch({ headless: true, ...(engine === chromium ? { executablePath: '/usr/bin/chromium' } | |
| : webkitRoot ? { executablePath: webkitRoot + '/bin/MiniBrowser', env: { ...process.env, | |
| WEBKIT_EXEC_PATH: webkitRoot + '/bin', WEBKIT_INJECTED_BUNDLE_PATH: webkitRoot + '/lib', | |
| WEBKIT_INSPECTOR_RESOURCES_PATH: webkitRoot + '/share', | |
| LD_LIBRARY_PATH: webkitRoot + '/lib:' + webkitRoot + '/sys/lib:' + (process.env.WEBKIT_EXTRA_LIBS ?? ''), | |
| } } : {}) }); | |
| const page = await browser.newPage({ viewport: mobile ? { width: 390, height: 844 } : { width: 1440, height: 1000 }, | |
| isMobile: mobile, hasTouch: mobile }); | |
| page.setDefaultTimeout(20000); | |
| page.on('pageerror', error => result.errors.push(`${name}: ${error.message}`)); | |
| // Observe real HTMLAudioElement playback; do not mock play() or decoding. | |
| await page.addInitScript(() => { | |
| const NativeAudio = window.Audio; | |
| window.catAudioInstances = []; | |
| window.Audio = function (...args) { | |
| const audio = new NativeAudio(...args); | |
| if (String(args[0]).includes('cat-meow')) window.catAudioInstances.push(audio); | |
| return audio; | |
| }; | |
| window.Audio.prototype = NativeAudio.prototype; | |
| }); | |
| const activate = selector => mobile ? page.locator(selector).tap() : page.locator(selector).click(); | |
| const heard = () => page.waitForFunction(() => { | |
| const audio = window.catAudioInstances[0]; | |
| return !audio.paused && audio.currentTime > .025 && !audio.muted && audio.volume > 0; | |
| }); | |
| const ended = () => page.waitForFunction(() => window.catAudioInstances[0].ended); | |
| async function unfocusable(selector) { | |
| const states = await page.locator(selector).evaluate(figure => { | |
| const focused = document.activeElement; | |
| const nodes = [figure, ...figure.querySelectorAll('*')]; | |
| const states = nodes.map(node => { | |
| node.focus(); | |
| const style = getComputedStyle(node); | |
| return { keepsFocus: document.activeElement === focused, tabIndex: node.tabIndex, | |
| selection: style.userSelect, drag: style.webkitUserDrag, outline: style.outlineStyle }; | |
| }); | |
| for (const type of ['pointerdown', 'mousedown']) { | |
| const event = type === 'pointerdown' | |
| ? new PointerEvent(type, { bubbles: true, cancelable: true, isPrimary: true }) | |
| : new MouseEvent(type, { bubbles: true, cancelable: true }); | |
| figure.querySelector('img').dispatchEvent(event); | |
| states.push({ type, prevented: event.defaultPrevented }); | |
| } | |
| return states; | |
| }); | |
| for (const state of states) { | |
| if (state.type) assert(state.prevented, `${selector}: ${state.type} must preserve focus`); | |
| else { | |
| assert(state.keepsFocus && state.tabIndex < 0, `${selector}: cat layer took focus`); | |
| assert.equal(state.selection, 'none'); assert.equal(state.drag, 'none'); assert.equal(state.outline, 'none'); | |
| } | |
| } | |
| } | |
| async function tabPastCat(start) { | |
| await page.locator(start).focus(); | |
| for (let i = 0; i < 14; i++) { | |
| await page.keyboard.press('Tab'); | |
| assert(!await page.evaluate(() => document.activeElement.closest('.mascot-cat, .setup-cat'))); | |
| } | |
| await page.locator(start).focus(); | |
| } | |
| try { | |
| console.log(`${name}: opening app`); | |
| await page.goto(url); | |
| await page.waitForFunction(() => window.browserPi?.terminal); | |
| await page.evaluate(() => window.browserPi.ready); | |
| await page.locator('.mascot-cat').waitFor({ state: 'visible' }); | |
| console.log(`${name}: preloading audio`); | |
| await page.waitForFunction(() => window.catAudioInstances[0]?.readyState >= 2); | |
| await page.waitForTimeout(2200); // Includes the automatic greeting hop. | |
| assert.deepEqual(await page.evaluate(() => window.catAudioInstances.map(a => ({ paused: a.paused, time: a.currentTime }))), [{ paused: true, time: 0 }]); | |
| await page.locator('.xterm-helper-textarea').focus(); | |
| await unfocusable('.mascot-cat'); | |
| await tabPastCat('.xterm-helper-textarea'); | |
| console.log(`${name}: playing empty-state meow`); | |
| await activate('.mascot-cat'); await heard(); | |
| assert(await page.locator('.xterm-helper-textarea').evaluate(el => el === document.activeElement)); | |
| await ended(); | |
| await page.emulateMedia({ reducedMotion: 'reduce' }); | |
| await activate('.mascot-cat'); await heard(); | |
| assert.equal(await page.locator('.mascot-cat').evaluate(el => el.getAnimations().length), 0); | |
| await ended(); | |
| await page.emulateMedia({ reducedMotion: 'no-preference' }); | |
| await page.locator('.xterm-helper-textarea').focus(); | |
| await page.keyboard.type('Hello cat'); await page.locator('#send').click(); | |
| await page.locator('#model-dialog[open]').waitFor(); | |
| await page.locator('.setup-cat.is-ready').waitFor(); | |
| await unfocusable('.setup-cat'); | |
| await tabPastCat('#model-confirm'); | |
| console.log(`${name}: playing modal meow and swish`); | |
| await page.locator('.setup-cat').evaluate(el => el.focus()); | |
| assert(await page.locator('#model-confirm').evaluate(el => el === document.activeElement)); | |
| const bodyPose = await page.locator('.setup-cat img').evaluate(el => getComputedStyle(el).transform); | |
| await activate('.setup-cat'); await heard(); | |
| assert(await page.locator('#model-confirm').evaluate(el => el === document.activeElement)); | |
| assert(await page.locator('.setup-cat-tail').evaluate(el => el.getAnimations().some(a => a.id === 'cat-tail-reaction'))); | |
| const pose = await page.locator('.setup-cat-tail').evaluate(el => getComputedStyle(el).transform); | |
| await page.waitForTimeout(250); | |
| assert.notEqual(await page.locator('.setup-cat-tail').evaluate(el => getComputedStyle(el).transform), pose); | |
| assert.equal(await page.locator('.setup-cat img').evaluate(el => getComputedStyle(el).transform), bodyPose); | |
| await activate('.setup-cat'); await activate('.setup-cat'); await heard(); | |
| assert.equal(await page.locator('.setup-cat-tail').evaluate(el => el.getAnimations().filter(a => a.id === 'cat-tail-reaction').length), 1); | |
| assert.equal(await page.evaluate(() => window.catAudioInstances.length), 1); | |
| await ended(); | |
| await page.emulateMedia({ reducedMotion: 'reduce' }); | |
| await page.waitForFunction(() => document.querySelector('.setup-cat-tail').getAnimations().length === 0); | |
| await activate('.setup-cat'); await heard(); await ended(); | |
| assert.equal(await page.locator('.setup-cat-tail').evaluate(el => el.getAnimations().length), 0); | |
| await page.emulateMedia({ reducedMotion: 'no-preference' }); | |
| await activate('.setup-cat'); await heard(); | |
| await page.locator('#model-cancel').click(); | |
| assert.equal(await page.locator('.setup-cat-tail').evaluate(el => el.getAnimations().length), 0); | |
| assert.equal(await page.evaluate(() => window.browserPi.terminal.text), 'Hello cat'); | |
| await ended(); | |
| // A blocked playback promise is swallowed; the UI reaction still works. | |
| await page.evaluate(() => { window.catAudioInstances[0].play = () => Promise.reject(new DOMException('Blocked', 'NotAllowedError')); }); | |
| await page.locator('#send').click(); | |
| await activate('.setup-cat'); | |
| assert(await page.locator('.setup-cat-tail').evaluate(el => el.getAnimations().some(a => a.id === 'cat-tail-reaction'))); | |
| await page.screenshot({ path: `../validation/cat-interactions-${name}.png` }); | |
| result.checks.push(`${name}: both cats and their layers reject focus, Tab skips cats, pointer/mouse preserve focus, no selection/drag/outline; real meow playback, repeated taps, silent idle, one shared clip, tail-only motion, reduced motion, close cleanup, blocked-audio fallback`); | |
| } finally { await browser.close(); } | |
| } | |
| assert.deepEqual(result.errors, []); | |
| result.passed = true; | |
| await fs.writeFile('../validation/cat-interactions.json', JSON.stringify(result, null, 2) + '\n'); | |
| console.log(JSON.stringify(result)); | |