Download scripts/test-editor-ui.mjs from EHS-Studio/dssl: direct link, hf CLI and curl.
- Browser
- Download file 7.15 kB
-
https://huggingface.co/EHS-Studio/dssl/resolve/main/scripts/test-editor-ui.mjs
- Command line
-
hf download hf://EHS-Studio/dssl/scripts/test-editor-ui.mjs
-
curl -L -o test-editor-ui.mjs https://huggingface.co/EHS-Studio/dssl/resolve/main/scripts/test-editor-ui.mjs
7.15 kB
| /* test-editor-ui.mjs: drive a REAL VS Code or VSCodium like a person would. | |
| * | |
| * node scripts/test-editor-ui.mjs | |
| * | |
| * Launches the editor with the extension loaded and a debugging port, then uses Playwright over the | |
| * Chrome DevTools Protocol (the editor is Electron) to type, hover, press F1, click inside the designer | |
| * webview, and read the result back out of the open file. It needs a display and an editor, so it is not part | |
| * of test-all.mjs: run it by hand before you publish the extension, together with test-in-editor.mjs. */ | |
| import { chromium } from 'playwright-core'; | |
| import { readFileSync } from 'node:fs'; | |
| import { findEditor, scratchWorkspace, editorArgs, launchDetached } from './lib/editor.mjs'; | |
| const PORT = 9400 + Math.floor(Math.random() * 400); | |
| const editor = findEditor(); | |
| if (!editor) { console.error('No VS Code or VSCodium found. Install one, or set DSSL_EDITOR to its command.'); process.exit(2); } | |
| const { dir, ws } = scratchWorkspace('ui'); | |
| const child = launchDetached(editor, editorArgs(editor, dir, ws, [`--remote-debugging-port=${PORT}`])); | |
| child.unref(); | |
| let pass = 0, fail = 0; | |
| const ok = (n, c, x) => { if (c) { pass++; console.log(` ok ${n}`); } else { fail++; console.log(`FAIL ${n}${x !== undefined ? '; ' + x : ''}`); } }; | |
| const done = async (browser, code) => { try { await browser.close(); } catch { /* already gone */ } try { editor.kill(); child.kill(); } catch { /* ignore */ } console.log(`\n${pass} passed, ${fail} failed (clicking through a real editor)`); process.exit(code); }; | |
| let browser = null; | |
| for (let i = 0; i < 60 && !browser; i++) { | |
| try { browser = await chromium.connectOverCDP(`http://127.0.0.1:${PORT}`); } catch { await new Promise((r) => setTimeout(r, 1000)); } | |
| } | |
| if (!browser) { console.error('could not reach the editor'); editor.kill(); process.exit(1); } | |
| const page = browser.contexts()[0].pages()[0]; | |
| await page.waitForTimeout(4500); | |
| await page.click('.notification-toast .monaco-button:has-text("Never")').catch(() => {}); | |
| const editorText = () => page.evaluate(() => [...document.querySelectorAll('.monaco-editor .view-line')].map((l) => l.textContent.replace(/ /g, ' ')).join('\n')); | |
| const webview = () => page.frames().filter((x) => /vscode-webview/.test(x.url())).pop(); | |
| const palette = async (text) => { await page.keyboard.press('F1'); await page.waitForTimeout(500); await page.keyboard.type(text); await page.waitForTimeout(700); await page.keyboard.press('Enter'); await page.waitForTimeout(3500); }; | |
| try { | |
| // completions in a one-line rule, the case a real editor found broken | |
| await page.click('.monaco-editor .view-lines'); await page.keyboard.press('Control+End'); | |
| await page.keyboard.type('\n.t { texture: '); | |
| await page.keyboard.press('Control+Space'); await page.waitForTimeout(1500); | |
| const list1 = await page.locator('.suggest-widget .monaco-list-row').allTextContents(); | |
| ok('one-line rule: "texture: " offers texture words first, not pseudo-elements', list1.length > 3 && list1.slice(0, 8).every((t) => /texture (preset|setting|generator)|blend mode/.test(t)) && !list1.some((t) => /::after/.test(t)), list1.slice(0, 6).join(' | ')); | |
| await page.keyboard.type('oa'); await page.waitForTimeout(700); | |
| const list2 = await page.locator('.suggest-widget .monaco-list-row').allTextContents(); | |
| ok('typing "oa" narrows it to oak and oak-planks', list2.some((t) => /^oak\b/.test(t)) && list2.some((t) => /oak-planks/.test(t)), list2.join(' | ')); | |
| await page.keyboard.press('Escape'); | |
| await page.keyboard.press('Control+Shift+K'); await page.waitForTimeout(300); // delete the scratch line | |
| // hover | |
| await page.mouse.move(470, 348); await page.waitForTimeout(1800); | |
| const hover = await page.locator('.monaco-hover').allTextContents(); | |
| ok('hovering "texture" shows the documentation widget', hover.some((t) => /generated texture/i.test(t)), hover.join('|').slice(0, 120)); | |
| await page.mouse.move(900, 700); await page.waitForTimeout(300); | |
| // problems: the status bar counts what the squiggles say | |
| const status = (await page.locator('.statusbar').innerText()).replace(/\s+/g, ' '); | |
| ok('the status bar counts the errors and the warning', /[3-9]/.test(status) && /1/.test(status), status.slice(0, 60)); | |
| // the designer | |
| await palette('DSSL: Design a texture'); | |
| let f = webview(); | |
| ok('"DSSL: Design a texture" opens the designer on the Texture tab', f && /Textures/.test(await f.locator('body').innerText())); | |
| const tile = async (name) => { const t = webview().locator(`.tile[data-name="${name}"]`); await t.scrollIntoViewIfNeeded(); await t.click(); await page.waitForTimeout(500); }; | |
| await tile('oak-planks'); | |
| const txt = await webview().locator('body').innerText(); | |
| ok('wood settings appear for a wood preset (planks, knots, figure, mirror tile)', ['planks', 'knots', 'figure', 'mirror tile'].every((w) => txt.includes(w))); | |
| await tile('grain'); | |
| ok('and are hidden for grain', !(await webview().locator('body').innerText()).includes('knots')); | |
| await tile('oak-planks'); | |
| await webview().locator('select').filter({ has: webview().locator('option[value="both"]') }).first().selectOption('both'); await page.waitForTimeout(500); | |
| const dsslOut = await webview().locator('textarea').last().inputValue(); | |
| ok('choosing the mirror writes symmetrical into the DSSL', /^texture: oak-planks symmetrical$/.test(dsslOut.trim()), dsslOut); | |
| ok('and the panel says it compiles', /compiles/.test(await webview().locator('body').innerText())); | |
| // insert, then load it back | |
| await page.click('.monaco-editor .view-lines'); await page.keyboard.press('Control+End'); | |
| await webview().getByRole('button', { name: 'Insert at cursor' }).click(); await page.waitForTimeout(800); | |
| ok('"Insert at cursor" puts the line into the open file', /texture: oak-planks symmetrical/.test(await editorText())); | |
| await page.click('.monaco-editor .view-lines'); await page.keyboard.press('Control+End'); await page.keyboard.press('Shift+Home'); await page.waitForTimeout(200); | |
| await tile('grain'); | |
| await webview().getByRole('button', { name: 'Load from selection' }).click(); await page.waitForTimeout(800); | |
| ok('"Load from selection" reads the line back into the controls', /oak-planks symmetrical/.test(await webview().locator('textarea').last().inputValue())); | |
| // the other tabs | |
| await webview().getByRole('button', { name: 'Slider' }).first().click(); await page.waitForTimeout(800); | |
| ok('the slider tab shows a live preview slider that compiles', (await webview().locator('#pv').count()) === 1 && /compiles/.test(await webview().locator('body').innerText())); | |
| await webview().getByRole('button', { name: 'Aura' }).first().click(); await page.waitForTimeout(800); | |
| ok('the aura tab compiles too', /compiles/.test(await webview().locator('body').innerText())); | |
| await done(browser, fail ? 1 : 0); | |
| } catch (e) { | |
| fail++; console.log('FAIL the run threw: ' + (e && e.message ? e.message.split('\n')[0] : e)); | |
| await page.screenshot({ path: dir + '/failure.png' }).catch(() => {}); | |
| console.log(' a screenshot of the editor is at ' + dir + '/failure.png'); | |
| await done(browser, 1); | |
| } | |