From dc4abcfab25ced9bb33055905242e1ac598e3e61 Mon Sep 17 00:00:00 2001 From: wyt Date: Tue, 27 Oct 2026 15:28:05 +0800 Subject: [PATCH] Preserve public preview and retained browser test coverage Keep independent CSV/TSV layout and blocked-resource receipts, portable retained browser fixtures, existing app suite discovery, public material-proof certificate and floating video surface assertions. Preserve original actions, oracles and raw diagnostic severity. Verification: exact-input owning evidence is recorded in verified-nonnative-tests-stage-candidate.json. Earlier failed attempts and unrelated native findings remain open; skipped Windows cases are not counted as passes. Sync-source: frozen parent and supplemental ranges through6312cca60a0b59512c3fbdfc88a3a4b6783a0876. --- playwright.apps.config.ts | 2 + test/e2e/chat_e2e_streaming_markdown.spec.ts | 3 +- test/e2e/files_e2e_formats.spec.ts | 29 +++-- test/e2e/fixtures/retained-file-page.ts | 22 ++++ .../html_preview_e2e_offline_policy.spec.ts | 110 ++++++++++++++++-- test/e2e/sidebar_e2e_badges.spec.ts | 3 +- test/fixtures/public-proof-certificate.pem | 10 ++ test/renderer/video-viewer-lifecycle.test.ts | 2 + 8 files changed, 158 insertions(+), 23 deletions(-) create mode 100644 test/e2e/fixtures/retained-file-page.ts create mode 100644 test/fixtures/public-proof-certificate.pem diff --git a/playwright.apps.config.ts b/playwright.apps.config.ts index 270badc9..bd2b2a6d 100644 --- a/playwright.apps.config.ts +++ b/playwright.apps.config.ts @@ -6,9 +6,11 @@ import base from './playwright.config'; export default defineConfig(base, { testMatch: [ '**/saved_apps_e2e_lifecycle.spec.ts', + '**/artifact_revisions_e2e_lifecycle.spec.ts', '**/web_app_sdk_e2e_lifecycle.spec.ts', '**/web_app_sdk_e2e_runtime.spec.ts', '**/web_app_permissions_e2e_usage.spec.ts', '**/html_preview_e2e_generated_controls.spec.ts', + '**/resource_lifecycle_e2e_notes.spec.ts', ], }); diff --git a/test/e2e/chat_e2e_streaming_markdown.spec.ts b/test/e2e/chat_e2e_streaming_markdown.spec.ts index 939eca7d..906421e7 100644 --- a/test/e2e/chat_e2e_streaming_markdown.spec.ts +++ b/test/e2e/chat_e2e_streaming_markdown.spec.ts @@ -3,6 +3,7 @@ import path from 'node:path'; import { pathToFileURL } from 'node:url'; import { expect, test, type Page } from '@playwright/test'; import { classifyNativeCommentary } from '../../src/main/features/group_chat/message-display'; +import { loadRetainedFilePage } from './fixtures/retained-file-page'; // Real renderer, DOMPurify, local MathJax and fonts. No account or model calls. test.use({ launchOptions: { executablePath: process.env.ORKAS_E2E_BROWSER_PATH } }); @@ -80,7 +81,7 @@ async function setup(page: Page) { ${scripts.map(name => ``).join('')}
`); - await page.goto(pathToFileURL(fixture).href); + await loadRetainedFilePage(page, fixture); await page.evaluate(() => { const w = window as any; const history = document.querySelector('#chat-history') as any; diff --git a/test/e2e/files_e2e_formats.spec.ts b/test/e2e/files_e2e_formats.spec.ts index d78b5d70..55e0f3df 100644 --- a/test/e2e/files_e2e_formats.spec.ts +++ b/test/e2e/files_e2e_formats.spec.ts @@ -56,16 +56,17 @@ test('CSV and TSV use the available preview height after resizing and switching }, selector)).toBeLessThanOrEqual(1); expect(await body.locator(selector).evaluate(el => el.scrollHeight > el.clientHeight)).toBe(true); }; - // Keep enough rows and columns to overflow both viewport sizes. Repeating - // each cell adds embedding work without strengthening the layout oracle. + // Keep all 80 rows and 15 columns for vertical and horizontal overflow. + // One wide row determines column widths; repeating its padding text in every + // row adds embedding work without strengthening this layout scenario. for (const [name, delimiter] of [['height.csv', ','], ['height.tsv', '\t']]) { - const content = Array.from({ length: 30 }, (_, index) => - [String(index + 1), '中文样本', `"${'Wide cell '.repeat(4)}\nSecond line"`, - ...Array.from({ length: 12 }, (_, column) => `field-${column}`)].join(delimiter)).join('\n'); + const content = Array.from({ length: 80 }, (_, index) => + [String(index + 1), '中文样本', `"${'Wide cell '.repeat(index ? 0 : 4)}\nSecond line"`, + ...Array.from({ length: 12 }, (_, column) => index ? '' : `field-${column}`)].join(delimiter)).join('\n'); const file = orkas.createWorkspaceFile(name, content); const preview = await orkas.openPreview(() => page.evaluate(args => (window as any).openChatFileViewer(args.file, args.name), { file, name })); - await expect(preview.locator('.delimited-preview tr')).toHaveCount(30); + await expect(preview.locator('.delimited-preview tr')).toHaveCount(80); const native = await orkas.electronApp!.browserWindow(preview); const body = preview.locator('.chat-file-viewer-body'); for (const [width, height] of [[960, 720], [620, 900]]) { @@ -87,13 +88,23 @@ test('CSV and TSV use the available preview height after resizing and switching await page.locator('#contexts-btn').click(); await page.locator(`.ctx-tree-wrap[data-path="${name}"] > .skill-tree-node`).click(); const libraryBody = page.locator('#contexts-viewer-body'); - await expect(libraryBody.locator('.delimited-preview tr')).toHaveCount(30); + await expect(libraryBody.locator('.delimited-preview tr')).toHaveCount(80); await assertFilled(libraryBody, '.delimited-preview-scroll'); await libraryBody.locator('[data-delimited-toggle]').click(); await assertFilled(libraryBody, 'pre'); await expect(libraryBody.locator('pre')).toHaveText(content); - await expect.poll(async () => (await orkas.invoke('kb.status')).files.find((f: any) => f.path === name)?.status, - { timeout: 30_000 }).toBe('ready'); + let indexState: any; + const indexWaitStarted = performance.now(); + try { + await expect.poll(async () => { + indexState = (await orkas.invoke('kb.status')).files.find((f: any) => f.path === name); + return indexState?.status; + }, { timeout: 30_000 }).toBe('ready'); + } finally { + await info.attach(`indexing-${name}`, { contentType: 'application/json', body: JSON.stringify({ + bytes: Buffer.byteLength(content), waitMs: performance.now() - indexWaitStarted, state: indexState, + }) }); + } } }); diff --git a/test/e2e/fixtures/retained-file-page.ts b/test/e2e/fixtures/retained-file-page.ts new file mode 100644 index 00000000..c66184ad --- /dev/null +++ b/test/e2e/fixtures/retained-file-page.ts @@ -0,0 +1,22 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import type { Page } from '@playwright/test'; + +/** Keep report evidence at its original path, but load local HTML through a + * short path that Chromium's Windows file loader can open. The fixture's + * absolute base URL continues to own script, stylesheet and font resolution. */ +export async function loadRetainedFilePage(page: Page, retainedFile: string) { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'orkas-page-')); + const file = path.join(directory, 'index.html'); + try { + fs.copyFileSync(retainedFile, file); + await page.goto(pathToFileURL(file).href); + } finally { + // These fixtures do not reload the document after initial navigation. + // Remove only the two exact entries created above, never a recursive tree. + if (fs.existsSync(file)) fs.unlinkSync(file); + fs.rmdirSync(directory); + } +} diff --git a/test/e2e/html_preview_e2e_offline_policy.spec.ts b/test/e2e/html_preview_e2e_offline_policy.spec.ts index c88d5602..9e084881 100644 --- a/test/e2e/html_preview_e2e_offline_policy.spec.ts +++ b/test/e2e/html_preview_e2e_offline_policy.spec.ts @@ -2,7 +2,7 @@ import { createServer } from 'node:http'; import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import path from 'node:path'; import type { AddressInfo } from 'node:net'; -import type { ElectronApplication } from '@playwright/test'; +import type { ConsoleMessage, ElectronApplication, Request, Response, WebError } from '@playwright/test'; import { chatMediaLocalUrl } from '../../src/main/util/chat-media-url'; import { expect, test } from './fixtures/orkas'; @@ -87,6 +87,40 @@ test.describe('local HTML preview', () => { await expect(frame.locator('#storage')).toHaveText('SecurityError'); }); + test('renders same-folder frames with production scheme privileges while keeping opaque-origin isolation', async ({ orkas }) => { + orkas.createWorkspaceFile('frame-check/child.html', ` + `); + const entry = orkas.createWorkspaceFile('frame-check/index.html', ` + waiting`); + const observed = await orkas.electronApp!.evaluate(async (_, args) => { + const { renderResponsiveHtmlPreview } = (process as any).mainModule.require(args.modulePath); + const { nativeImage } = (process as any).mainModule.require('electron'); + const rendered = await renderResponsiveHtmlPreview(args.entry, [ + { name: 'desktop', width: 960, height: 640 }, + ], {}, { interactions: false, fileViewer: true }); + const image = nativeImage.createFromBuffer(rendered.screenshots[0]); + return { + evidence: rendered.evidence, + pixel: Array.from(image.crop({ x: 200, y: 200, width: 1, height: 1 }).toBitmap()) as number[], + isolationPixel: Array.from(image.crop({ x: 600, y: 200, width: 1, height: 1 }).toBitmap()) as number[], + }; + }, { modulePath: path.resolve(__dirname, '../../src/main/features/html_preview.ts'), entry }); + expect(observed.evidence.viewports[0].consoleErrors).toEqual([]); + // The child must paint green. The separate parent pixel turns green only + // after real postMessage delivery and both sandbox denials succeed. + for (const pixel of [observed.pixel, observed.isolationPixel]) { + expect(pixel[3]).toBe(255); + expect(pixel[1]).toBeGreaterThan(Math.max(pixel[0], pixel[2])); + } + }); + test('checks a deliverable page under the file viewer sandbox and keeps artifact checks unchanged', async ({ orkas }) => { const modulePath = path.resolve(__dirname, '../../src/main/features/html_preview.ts'); const clean = createMultiFileSite(orkas, 'check-clean', false); @@ -160,18 +194,70 @@ test.describe('local HTML preview', () => { ${blockers}
${'preview-ready '.repeat(8_000)}
`); - const startedAt = Date.now(); - const preview = await orkas.openPreview(() => appPage.evaluate((pathValue) => { - void (window as any).openChatFileViewer(pathValue, 'blocking-resource-preview.html'); - }, htmlPath)); + // The 32 absent assets are an intentional negative fixture. Record their + // real responses independently of the generic Chromium console text; any + // other response failure or renderer error still fails this case. + const context = orkas.electronApp!.context(); + const expectedUrls = Array.from({ length: 16 }, (_, index) => ['js', 'css'].map(extension => + new URL(`./blocked-${index}.${extension}`, chatMediaLocalUrl(htmlPath)).href)).flat().sort(); + const expected = new Set(expectedUrls); + const responses: Array<{ url: string; status: number }> = []; + const consoleErrors: Array<{ text: string; url: string }> = []; + const requestFailures: Array<{ url: string; error: string | null; responseUrl: string | null; status: number | null }> = []; + const pageErrors: string[] = []; + const onResponse = (response: Response) => { + const url = response.url(); + if (expected.has(url) || response.status() >= 400) responses.push({ url, status: response.status() }); + }; + const onConsole = (message: ConsoleMessage) => { + if (message.type() === 'error') consoleErrors.push({ text: message.text(), url: message.location().url }); + }; + const onRequestFailed = (request: Request) => { + // The custom-scheme missing script/style response supplies404 headers, + // then Chromium aborts its body. Pair the failure with THIS request's + // actual response, rather than accepting unrelated ERR_ABORTED events. + const response = request.existingResponse(); + requestFailures.push({ url: request.url(), error: request.failure()?.errorText ?? null, + responseUrl: response?.url() ?? null, status: response?.status() ?? null }); + }; + const onWebError = (error: WebError) => pageErrors.push(error.error().message); + context.on('weberror', onWebError); + context.on('response', onResponse); + context.on('console', onConsole); + context.on('requestfailed', onRequestFailed); + try { + const startedAt = Date.now(); + const preview = await orkas.openPreview(() => appPage.evaluate((pathValue) => { + void (window as any).openChatFileViewer(pathValue, 'blocking-resource-preview.html'); + }, htmlPath)); - const body = preview.locator('.chat-file-viewer-body'); - await expect(body).not.toHaveAttribute('aria-busy', 'true', { timeout: 5_000 }); - const elapsedMs = Date.now() - startedAt; - expect(elapsedMs).toBeLessThan(5_000); - const frame = preview.locator('.chat-file-viewer-html'); - expect(await frame.getAttribute('src')).toMatch(/^chat-media:\/\/local\//); - await expect(frame.contentFrame().locator('#ready')).toContainText('preview-ready'); + const body = preview.locator('.chat-file-viewer-body'); + await expect(body).not.toHaveAttribute('aria-busy', 'true', { timeout: 5_000 }); + const elapsedMs = Date.now() - startedAt; + expect(elapsedMs).toBeLessThan(5_000); + const frame = preview.locator('.chat-file-viewer-html'); + expect(await frame.getAttribute('src')).toMatch(/^chat-media:\/\/local\//); + await expect(frame.contentFrame().locator('#ready')).toContainText('preview-ready'); + await expect.poll(() => responses.length).toBe(32); + expect(responses.map(response => response.url).sort()).toEqual(expectedUrls); + expect(responses.every(response => response.status === 404)).toBe(true); + await expect.poll(() => consoleErrors.length).toBe(32); + expect(consoleErrors.every(error => error.text === 'Failed to load resource: the server responded with a status of 404 (Not Found)' + && (!error.url || expected.has(error.url)))).toBe(true); + await expect.poll(() => requestFailures.length).toBe(32); + expect(requestFailures.map(failure => failure.url).sort()).toEqual(expectedUrls); + expect(requestFailures.every(failure => failure.error === 'net::ERR_ABORTED' + && failure.status === 404 && failure.responseUrl === failure.url)).toBe(true); + expect(pageErrors).toEqual([]); + } finally { + context.off('weberror', onWebError); + context.off('response', onResponse); + context.off('console', onConsole); + context.off('requestfailed', onRequestFailed); + await test.info().attach('blocking-resource-receipt.json', { contentType: 'application/json', body: Buffer.from(JSON.stringify({ + expectedUrls, responses, consoleErrors, requestFailures, pageErrors, + })) }); + } }); test('keeps the file-viewer loading state visible until layout and iframe are both ready', async ({ diff --git a/test/e2e/sidebar_e2e_badges.spec.ts b/test/e2e/sidebar_e2e_badges.spec.ts index 34e939a0..fc5b9c9e 100644 --- a/test/e2e/sidebar_e2e_badges.spec.ts +++ b/test/e2e/sidebar_e2e_badges.spec.ts @@ -2,6 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; import { expect, test, type Page } from '@playwright/test'; +import { loadRetainedFilePage } from './fixtures/retained-file-page'; test.use({ launchOptions: { executablePath: process.env.ORKAS_E2E_BROWSER_PATH } }); const renderer = path.resolve(__dirname, '../../src/renderer'); @@ -35,7 +36,7 @@ async function setup(page: Page, count: number) {
`); - await page.goto(pathToFileURL(file).href); + await loadRetainedFilePage(page, file); await page.evaluate(count => { const w = window as any; w.metadataVisits = 0; diff --git a/test/fixtures/public-proof-certificate.pem b/test/fixtures/public-proof-certificate.pem new file mode 100644 index 00000000..ed9fd4f7 --- /dev/null +++ b/test/fixtures/public-proof-certificate.pem @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBXTCCAQ+gAwIBAgIUHktca2EXLWH06YrObUINLcYJAF8wBQYDK2VwMCQxIjAg +BgNVBAMMGXB1YmxpYy1wcm9vZi5leGFtcGxlLnRlc3QwHhcNMjYxMDI1MTEzNDMw +WhcNMjYxMDI2MTEzNDMwWjAkMSIwIAYDVQQDDBlwdWJsaWMtcHJvb2YuZXhhbXBs +ZS50ZXN0MCowBQYDK2VwAyEArj2EJdl3KDQNCEFntd5m2NvgwoKF2MstjycEbDhp +MsejUzBRMB0GA1UdDgQWBBRcmxyBpAtE9AduPAA4zmo6GY1AmDAfBgNVHSMEGDAW +gBRcmxyBpAtE9AduPAA4zmo6GY1AmDAPBgNVHRMBAf8EBTADAQH/MAUGAytlcANB +AJA7mSRZsUBB6ZLNx8dggVBZqAwkAJUrVzaV2dYTwp2qjyquh6icykpoKYCieKLa +s+DLV4/5FQjEGM8Mq9xhTAU= +-----END CERTIFICATE----- diff --git a/test/renderer/video-viewer-lifecycle.test.ts b/test/renderer/video-viewer-lifecycle.test.ts index 03f21b19..5148bd52 100644 --- a/test/renderer/video-viewer-lifecycle.test.ts +++ b/test/renderer/video-viewer-lifecycle.test.ts @@ -67,6 +67,7 @@ describe('video media lifecycle', () => { it('releases a pending video and ignores its late metadata/errors while the new video loads', async () => { const h = viewer(); await h.open('chat-media://local/first.mp4'); + expect(h.body.children[0].dataset).toMatchObject({ chatVideoPlaybackSurface: 'floating_player' }); const old = h.videos[0]; await h.open('chat-media://local/second.mp4'); const current = h.videos[1]; @@ -90,6 +91,7 @@ describe('video media lifecycle', () => { it('settles a failed video and can open another video without keeping its source alive', async () => { const h = viewer(); await h.openLocal(); + expect(h.body.children[0].dataset).toMatchObject({ chatVideoPlaybackSurface: 'floating_player' }); const failed = h.videos[0]; failed.dispatchEvent(new Event('error')); expect(h.body.getAttribute('aria-busy')).toBeNull();