Download app/tests/render.mjs from Mike0021/MiniCPM5-2B-WebGPU-Pi-HTTP: direct link, hf CLI and curl.
- Browser
- Download file 12.1 kB
-
https://huggingface.co/spaces/Mike0021/MiniCPM5-2B-WebGPU-Pi-HTTP/resolve/main/app/tests/render.mjs
- Command line
-
hf download hf://spaces/Mike0021/MiniCPM5-2B-WebGPU-Pi-HTTP/app/tests/render.mjs
-
curl -L -o render.mjs https://huggingface.co/spaces/Mike0021/MiniCPM5-2B-WebGPU-Pi-HTTP/resolve/main/app/tests/render.mjs
12.1 kB
| import { chromium } from 'playwright'; | |
| import { visibleWidth } from '@earendil-works/pi-tui/dist/utils.js'; | |
| import fs from 'node:fs/promises'; | |
| import assert from 'node:assert/strict'; | |
| // Deterministic presentation fixtures, separate from actual-model tests. | |
| const url = process.argv[2] ?? 'http://127.0.0.1:4173/'; | |
| const label = process.argv[3] ?? 'agent-terminal-dark-render'; | |
| const result = { date: new Date().toISOString(), url, kind: 'synthetic rendering fixtures; no model inference', errors: [], checks: [], sizes: [] }; | |
| const browser = await chromium.launch({ executablePath: '/usr/bin/chromium', headless: true }); | |
| const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } }); | |
| context.setDefaultTimeout(15_000); | |
| const page = await context.newPage(); | |
| page.on('pageerror', error => result.errors.push(error.stack)); | |
| let id = 100; | |
| const user = text => ({ role: 'user', content: [{ type: 'text', text }], timestamp: id++ }); | |
| const assistant = (text, extra = {}) => ({ role: 'assistant', content: [{ type: 'text', text }], timestamp: id++, stopReason: 'stop', ...extra }); | |
| const tool = (name, args, output, isError = false) => { | |
| const toolCallId = 'render-' + id++; | |
| return [assistant('', { content: [{ type: 'toolCall', id: toolCallId, name, arguments: args }], stopReason: 'toolUse' }), | |
| { role: 'toolResult', toolCallId, toolName: name, content: [{ type: 'text', text: output }], isError, timestamp: id++ }]; | |
| }; | |
| const screen = () => page.evaluate(() => window.browserPi.terminal.readScreen()); | |
| const waitScreen = text => page.waitForFunction(text => window.browserPi.terminal.readScreen().includes(text), text); | |
| async function fixture(messages) { | |
| await page.evaluate(messages => { window.browserPi.terminal.setMessages(messages, { reset: true }); window.browserPi.terminal.focus(); }, messages); | |
| await page.keyboard.press('End'); | |
| await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))); | |
| } | |
| function luminance(value) { | |
| const n = typeof value === 'number' ? value : parseInt(value.slice(1), 16); | |
| const rgb = [n >> 16 & 255, n >> 8 & 255, n & 255].map(v => { v /= 255; return v <= .04045 ? v / 12.92 : ((v + .055) / 1.055) ** 2.4; }); | |
| return .2126 * rgb[0] + .7152 * rgb[1] + .0722 * rgb[2]; | |
| } | |
| async function geometry(name) { | |
| const measured = await page.evaluate(() => { | |
| const rect = selector => { const r = document.querySelector(selector).getBoundingClientRect(); return { x: r.x, y: r.y, width: r.width, height: r.height, right: r.right, bottom: r.bottom }; }; | |
| return { viewport: { width: innerWidth, height: innerHeight }, cols: window.browserPi.terminal.cols, rows: window.browserPi.terminal.rows, | |
| panel: rect('.terminal-panel'), terminal: rect('#pi-terminal'), screen: rect('.xterm-screen'), controls: rect('.terminal-controls'), | |
| overflow: document.documentElement.scrollWidth > innerWidth, background: getComputedStyle(document.querySelector('.terminal-panel')).backgroundColor }; | |
| }); | |
| assert.equal(measured.background, 'rgb(24, 24, 30)'); | |
| assert.equal(measured.overflow, false, name + ': page overflow'); | |
| assert.equal(measured.screen.x, measured.terminal.x, name + ': no extra CSS padding inside xterm'); | |
| assert(measured.screen.right <= measured.terminal.right + 1, name + ': terminal columns clipped'); | |
| assert(measured.terminal.right - measured.screen.right < measured.screen.width / measured.cols + 1, name + ': unused scrollbar gutter'); | |
| assert(measured.screen.bottom <= measured.terminal.bottom + 1, name + ': terminal rows clipped'); | |
| assert(measured.controls.y >= measured.screen.bottom - 1, name + ': controls overlap terminal'); | |
| assert(measured.controls.bottom <= measured.panel.bottom, name + ': controls escape panel'); | |
| assert((await screen()).includes('/workspace'), name + ': footer clipped'); | |
| result.sizes.push({ name, ...measured }); | |
| } | |
| try { | |
| await page.goto(url); await page.waitForFunction(() => window.browserPi?.terminal?.readCells); | |
| await page.evaluate(() => window.browserPi.ready); | |
| await fixture([user('USER_ONE'), assistant('ASSISTANT_ONE'), user('USER_TWO'), assistant('ASSISTANT_TWO')]); | |
| await waitScreen('ASSISTANT_TWO'); | |
| const cells = await page.evaluate(() => window.browserPi.terminal.readCells()); | |
| const rowText = cells.map(row => row.map(cell => cell?.text ?? '').join('').trimEnd()); | |
| const u1 = rowText.findIndex(row => row.includes('USER_ONE')), a1 = rowText.findIndex(row => row.includes('ASSISTANT_ONE')); | |
| const u2 = rowText.findIndex(row => row.includes('USER_TWO')); | |
| assert.equal(rowText[u1], ' USER_ONE'); | |
| assert.equal(a1 - u1, 3, 'Pi user bottom padding plus assistant separator'); | |
| assert.equal(u2 - a1, 3, 'Pi turn separator plus user top padding'); | |
| assert.equal(cells[u1 - 1][3].bg, 0x343541, 'Pi user top padded row'); | |
| assert.equal(cells[u1 + 1][3].bg, 0x343541, 'Pi user bottom padded row'); | |
| result.checks.push('Upstream Pi user/assistant indentation, inner padding and turn separators'); | |
| await fixture([user('Think about a task'), assistant('', { content: [ | |
| { type: 'thinking', thinking: 'THOUGHT_DETAIL_MARKER' }, { type: 'text', text: 'FINAL_ANSWER_MARKER' }, | |
| ] })]); | |
| await waitScreen('Thoughts · click to expand'); | |
| assert(!(await screen()).includes('THOUGHT_DETAIL_MARKER')); | |
| const thinkingPosition = await page.evaluate(() => { | |
| const t = window.browserPi.terminal; | |
| const row = t.readScreen().split('\n').findIndex(line => line.includes('Thoughts · click to expand')); | |
| const r = document.querySelector('.xterm-screen').getBoundingClientRect(); | |
| return { x: r.x + 8 * r.width / t.cols, y: r.y + (row + .5) * r.height / t.rows }; | |
| }); | |
| await page.mouse.click(thinkingPosition.x, thinkingPosition.y); | |
| await waitScreen('THOUGHT_DETAIL_MARKER'); | |
| assert((await screen()).includes('FINAL_ANSWER_MARKER')); | |
| result.checks.push('Reasoning is collapsed by default and expands through Pi’s native click target'); | |
| await fixture([user('Read todo'), ...tool('read', { path: '/workspace/todo' }, '# todo\n\n'), assistant('Verified.')]); | |
| await waitScreen('Verified.'); | |
| const toolCells = await page.evaluate(() => window.browserPi.terminal.readCells()); | |
| const toolRows = toolCells.map(row => row.map(cell => cell?.text ?? '').join('').trimEnd()); | |
| const call = toolRows.findIndex(row => row.includes('read /workspace/todo')); | |
| assert.equal(toolRows[call], ' read /workspace/todo'); | |
| assert.equal(toolRows[call + 1], ' # todo'); | |
| assert.equal(toolCells[call - 1][3].bg, 0x283228); | |
| assert.equal(toolCells[call + 2][3].bg, 0x283228); | |
| assert.equal(toolRows[call + 3], ''); | |
| assert.equal(toolRows[call + 4], ' Verified.'); | |
| result.checks.push('Upstream tool Box(1,1) padding and separator; no trailing empty output rows'); | |
| await page.screenshot({ path: `../validation/${label}-spacing.png`, fullPage: true }); | |
| const unicodePrefix = ' WIDE 世界 🦙 👩💻 e\u0301 '; | |
| await fixture([user('Unicode'), assistant(unicodePrefix.trimStart() + 'END')]); await waitScreen('END'); | |
| const unicodeCells = await page.evaluate(() => window.browserPi.terminal.readCells()); | |
| const unicodeRow = unicodeCells.find(row => row.map(cell => cell?.text ?? '').join('').includes('WIDE')); | |
| assert.equal(unicodeRow.findLastIndex(cell => cell.text === 'E'), visibleWidth(unicodePrefix)); | |
| assert.equal(unicodeRow.find(cell => cell.text === '👩💻').width, 2); | |
| assert.equal(unicodeRow.find(cell => cell.text === '🦙').width, 2); | |
| result.checks.push('CJK, emoji, ZWJ and combining accents agree with Pi cell widths'); | |
| const rich = [user('Show Markdown, file output and errors.'), assistant('# Markdown\n\nA **bold** paragraph with *italics*, `inline code`, and a [link](https://example.com/a/long/path).\n\n- First item\n- Second item\n\n> A wrapped quotation with several words and Unicode 世界.\n\n```js\nconst result = "' + 'a'.repeat(150) + '";\nconsole.log(result);\n```\n\n| Item | Value |\n| --- | --- |\n| Alpha | 123 |\n| Beta | 456 |'), | |
| ...tool('read', { path: '/workspace/deeply/nested/directory/a-long-file-name.md' }, Array.from({ length: 40 }, (_, i) => 'OUTPUT_ROW_' + i + ' value 世界').join('\n')), | |
| ...tool('read', { path: '/workspace/missing.txt' }, 'ENOENT: file does not exist', true), assistant('END_OF_TRANSCRIPT')]; | |
| await fixture(rich); await waitScreen('END_OF_TRANSCRIPT'); | |
| assert(!(await page.evaluate(() => window.browserPi.terminal.readDocument().join('\n'))).includes('OUTPUT_ROW_39')); | |
| await page.keyboard.press('Control+o'); | |
| await page.waitForFunction(() => window.browserPi.terminal.readDocument().join('\n').includes('OUTPUT_ROW_39')); | |
| await page.keyboard.press('Control+o'); | |
| result.checks.push('Long tool results collapse/expand without losing content'); | |
| for (const [width, height] of [[1440,1000],[1024,768],[851,640],[850,640],[390,844],[320,568],[390,420]]) { | |
| const name = `${width}x${height}`; | |
| await page.setViewportSize({ width, height }); | |
| await page.waitForTimeout(100); | |
| await page.keyboard.press('End'); await waitScreen('END_OF_TRANSCRIPT'); | |
| await geometry(name); | |
| const doc = await page.evaluate(() => ({ lines: window.browserPi.terminal.readDocument(), cols: window.browserPi.terminal.cols })); | |
| for (const line of doc.lines) assert(visibleWidth(line) <= doc.cols, name + ': overwide document line'); | |
| const colors = await page.evaluate(() => window.browserPi.terminal.readCells()); | |
| for (const row of colors) for (const cell of row) if (cell && /[\p{L}\p{N}]/u.test(cell.text)) { | |
| const l1 = luminance(cell.fg), l2 = luminance(cell.bg), contrast = (Math.max(l1,l2)+.05)/(Math.min(l1,l2)+.05); | |
| assert(contrast >= 4.5, `Low text contrast ${contrast.toFixed(2)} for ${JSON.stringify(cell)}`); | |
| } | |
| if ([1440,390,320].includes(width)) await page.screenshot({ path: `../validation/${label}-${name}.png`, fullPage: true }); | |
| } | |
| result.checks.push('Seven viewports: Markdown, tool cards, footer/controls, wrapping and 4.5:1 visible text contrast'); | |
| await page.setViewportSize({ width: 1440, height: 1000 }); | |
| const history = Array.from({ length: 60 }, (_, i) => [user('USER_TURN_' + i), assistant('ASSISTANT_TURN_' + i)]).flat(); | |
| await fixture(history); await waitScreen('ASSISTANT_TURN_59'); | |
| await page.keyboard.press('Home'); await page.waitForFunction(() => window.browserPi.terminal.viewportTop === 0); | |
| const more = [...history, user('Stream a response'), assistant('STREAM_FIRST', { stopReason: undefined })]; | |
| await page.evaluate(messages => window.browserPi.terminal.setMessages(messages), more); | |
| await page.waitForTimeout(80); | |
| assert.equal(await page.evaluate(() => window.browserPi.terminal.viewportTop), 0); | |
| more.at(-1).content[0].text = 'STREAM_FIRST\nSTREAM_LAST'; more.at(-1).stopReason = 'stop'; | |
| await page.evaluate(messages => window.browserPi.terminal.setMessages(messages), more); | |
| await page.keyboard.press('End'); await waitScreen('STREAM_LAST'); | |
| result.checks.push('60-turn transcript and streamed updates preserve scroll position'); | |
| await page.locator('.xterm-helper-textarea').focus(); | |
| for (let i=0;i<12;i++) { await page.keyboard.type('Draft line ' + i); await page.keyboard.press('Shift+Enter'); } | |
| await page.setViewportSize({ width: 390, height: 420 }); await page.waitForTimeout(120); | |
| await geometry('multiline-keyboard-height'); | |
| assert((await screen()).includes('Draft line 11'), 'Multiline editor cursor row must stay visible'); | |
| result.checks.push('Multiline draft remains visible at keyboard-sized height'); | |
| result.mobileLimitation = 'Viewport/keyboard-height simulation on desktop Chromium; no physical mobile device.'; | |
| assert.equal(result.errors.length, 0); | |
| result.passed = true; console.log(JSON.stringify({ passed: true, checks: result.checks })); | |
| } catch (error) { | |
| result.failure = error.stack; result.screen = await screen().catch(() => 'not initialized'); | |
| await page.screenshot({ path: `../validation/${label}-failure.png`, fullPage: true }); throw error; | |
| } finally { await fs.writeFile(`../validation/${label}.json`, JSON.stringify(result,null,2)); await browser.close(); } | |