const fs = require('node:fs/promises') const path = require('node:path') const assert = require('node:assert/strict') const { pathToFileURL } = require('node:url') if (!process.versions.electron) { ;(async () => { const directory = await fs.mkdtemp(path.resolve('.tmp/b01-native-')) const data = path.join(directory, 'data') const workspace = path.join(directory, 'workspace') await fs.mkdir(data); await fs.mkdir(workspace); await fs.mkdir(path.join(directory, 'home')) const now = Date.now() const question = { id: 'q1', question: '检查哪个本地目标? Which local target?', answers: ['本地测试目录 / Local fixture'] } const longQuestions = Array.from({ length: 24 }, (_, i) => ({ id: `long-${i}`, question: `Question ${i}: ${'长文本与换行测试 '.repeat(12)}`, answers: ['Long answer '.repeat(20)] })) const message = questions => ({ id: 'a1', role: 'assistant', content: 'Final answer after question.', createdAt: now - 9000, status: 'done', turnStartedAt: now - 10000, turnFinishedAt: now, turnDurationMs: 10000, tools: [ { id: 'q-tool', name: 'AskUserQuestion', args: { questions }, argsRaw: '{}', status: 'ok', output: 'RAW_SDK_ANSWER_SENTINEL', metadata: { questionAnswer: { questions } } }, { id: 't1', name: 'UnknownOrdinaryTool', args: {}, argsRaw: '{}', status: 'ok', output: 'ORDINARY_TOOL_OUTPUT' } ], parts: [ { type: 'reasoning', id: 'r1', text: 'First reasoning.', finished: true }, { type: 'tool', toolId: 't1' }, { type: 'tool', toolId: 'q-tool' }, { type: 'text', text: 'Final answer after question.' } ] }) const session = (id, questions) => ({ id, title: id, cwd: workspace, createdAt: now, updatedAt: now + (id === 'b01-short' ? 1 : 0), messages: [{ id: 'u1', role: 'user', content: 'Inspect a local fixture.', status: 'done', createdAt: now - 10000 }, message(questions)] }) await fs.writeFile(path.join(data, 'sessions.json'), JSON.stringify({ sessions: [session('b01-short', [question]), session('b01-long', longQuestions)] })) const env = { ...process.env, YCODE_USER_DATA_DIR: data, YCODE_SKIP_KERNEL_DATA_RESET: '1', YCODE_QA_DIRECTORY: directory, YCODE_QA_OFFSCREEN: '1', YCODE_DISABLE_GPU: '1', YCODE_DISABLE_SYSTEM_INTEGRATION: '1', B01_NATIVE_DIRECTORY: directory } delete env.ELECTRON_RUN_AS_NODE; delete env.ELECTRON_RENDERER_URL const child = require('node:child_process').spawn(require('electron'), [__filename], { env, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }) let output = '' child.stdout.on('data', bytes => { output += bytes }) child.stderr.on('data', bytes => { output += bytes }) const timer = setTimeout(() => child.kill(), 90000) child.once('exit', async code => { clearTimeout(timer); await fs.writeFile(path.join(directory, 'process.log'), output) console.log(JSON.stringify({ directory, exitCode: code })) if (code !== 0) console.error(output.slice(-6000)) process.exitCode = code === 0 ? 0 : 1 }) })().catch(error => { console.error(error); process.exitCode = 1 }) } else { const { app, BrowserWindow } = require('electron') const directory = process.env.B01_NATIVE_DIRECTORY app.disableHardwareAcceleration(); app.setPath('home', path.join(directory, 'home')) let win const errors = [] const read = code => win.webContents.executeJavaScript(code) const pause = ms => new Promise(resolve => setTimeout(resolve, ms)) const outer = '.cr-collapse--completed > .cr-collapse__header' const questionHeader = '.ask-user-question__header .cr-tool-head__main' async function wait(code) { for (let i = 0; i < 250; i++) { if (await read(code)) return; await pause(40) } throw new Error(`Timeout: ${code}`) } const click = selector => read(`document.querySelector(${JSON.stringify(selector)}).click()`) async function setTheme(theme) { await read(`(() => { localStorage.setItem("ycode:theme", ${JSON.stringify(theme)}); window.dispatchEvent(new StorageEvent("storage", {key:"ycode:theme",newValue:${JSON.stringify(theme)}})); })()`) await wait(`document.documentElement.dataset.theme === ${JSON.stringify(theme)}`) await pause(150) } async function shot(name) { await read('document.fonts.ready'); await pause(400) await fs.writeFile(path.join(directory, name + '.png'), (await win.webContents.capturePage()).toPNG()) } async function measure() { return read(`(() => { const probe = selector => { const element = document.querySelector(selector); if (!element) return null; const style = getComputedStyle(element), box = element.getBoundingClientRect(); return {selector, fontSize:style.fontSize,fontWeight:style.fontWeight,lineHeight:style.lineHeight, background:style.backgroundColor,padding:style.padding,borderRadius:style.borderRadius,border:style.border, overflow:style.overflow,maxHeight:style.maxHeight,width:box.width,height:box.height, scrollHeight:element.scrollHeight,clientHeight:element.clientHeight}; }; const section = document.querySelector(".ask-user-question--completed"); const content = section.querySelector(".ask-user-question__content"); const header = section.querySelector(".ask-user-question__header"); return { theme:document.documentElement.dataset.theme, titleOutsideContent:!content.contains(header), cardCount:document.querySelectorAll(".ask-user-question--completed").length, rawAnswerVisible:document.body.textContent.includes("RAW_SDK_ANSWER_SENTINEL"), horizontalOverflow:document.documentElement.scrollWidth > innerWidth, styles:[".ask-user-question--completed",".ask-user-question__header",".ask-user-question__content", ".question-answer-transcript__question",".question-answer-transcript__answer"].map(probe) }; })()`) } ;(async () => { await import(pathToFileURL(path.resolve('out/main/index.mjs')).href) await app.whenReady(); win = BrowserWindow.getAllWindows()[0]; win.setSize(1336, 902); win.showInactive() win.webContents.on('console-message', (_e, level, message) => { if (level >= 3) errors.push(message) }) await wait(`!!document.querySelector(${JSON.stringify(outer)})`) await click(outer); await wait(`!!document.querySelector(${JSON.stringify(questionHeader)})`) await click(questionHeader) await wait(`document.querySelector(${JSON.stringify(questionHeader)}).getAttribute("aria-expanded") === "true"`) await setTheme('light') const light = await measure(); await shot('question-light') assert.equal(light.titleOutsideContent, true); assert.equal(light.cardCount, 1); assert.equal(light.rawAnswerVisible, false) const contentStyle = light.styles[2] assert.equal(contentStyle.padding, '12px 16px'); assert.equal(contentStyle.borderRadius, '16px') assert.equal(contentStyle.maxHeight, '300px') for (const style of light.styles.slice(3)) { assert.equal(style.fontSize, '14px'); assert.equal(style.fontWeight, '400'); assert.equal(style.lineHeight, '24.5px') } await click(outer); await click(outer) await wait(`document.querySelector(${JSON.stringify(questionHeader)}).getAttribute("aria-expanded") === "true"`) const preserved = await read(`document.querySelector(${JSON.stringify(questionHeader)}).getAttribute("aria-expanded")`) await setTheme('dark') const dark = await measure(); await shot('question-dark') await setTheme('light') win.setSize(820, 902); await pause(150) const narrow = await measure(); await shot('question-narrow') assert.equal(narrow.horizontalOverflow, false) await click('[data-session-id="b01-long"]') await wait('document.querySelector(".source-chat-header")?.textContent.includes("b01-long") && !!document.querySelector(".cr-collapse--completed")') await pause(300); await click(outer) await wait(`!!document.querySelector(${JSON.stringify(questionHeader)})`); await click(questionHeader) await pause(350) const long = await measure(); await shot('question-long') assert.ok(long.styles[2].scrollHeight > long.styles[2].clientHeight) assert.ok(long.styles[2].clientHeight <= 300) assert.equal(long.horizontalOverflow, false) const report = { codeSha: '51531dcae8d8ade85db9e203bc8c0f712805b2ae', scope: 'Real isolated Windows Electron main/preload/renderer; synthetic persisted messages, no model/SDK or original app access.', light, dark, narrow, long, innerQuestionPreserved: preserved, rendererErrors: errors } await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify(report, null, 2)) assert.equal(errors.length, 0) app.exit(0) })().catch(async error => { console.error(error) await fs.writeFile(path.join(directory, 'failure.txt'), String(error)) if (win) await shot('failure').catch(() => {}) app.exit(1) }) }