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.
This commit is contained in:
wyt committed 2026-10-27 15:28:05 +08:00
1 parent f62b5d24c6
commit dc4abcfab2
8 files changed
+158 -23

No files matched your search

+2
View File
@@ -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',
],
});
+2 -1
View File
@@ -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 => `<script src="modules/${name}.js"></script>`).join('')}
</head><body><textarea id="composer"></textarea>
<div id="chat-history" class="chat-history" style="height:500px;width:850px;overflow:auto"></div></body></html>`);
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;
+20 -9
View File
@@ -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<any>('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<any>('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,
}) });
}
}
});
+22
View File
@@ -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);
}
}
@@ -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', `<!doctype html><style>html,body{margin:0;background:#00ff00}</style>
<script>parent.postMessage('child-ready','*')</script>`);
const entry = orkas.createWorkspaceFile('frame-check/index.html', `<!doctype html><style>html,body{margin:0;background:#ff0000}iframe{width:400px;height:400px;border:0}</style>
<output id="status">waiting</output><iframe src="child.html"></iframe><script>
addEventListener('message',event=>{if(event.data!=='child-ready')return;
let storageDenied=false,domDenied=false;
try{localStorage.setItem('probe','1')}catch{storageDenied=true}
try{domDenied=!document.querySelector('iframe').contentDocument}catch{domDenied=true}
document.getElementById('status').textContent='child-ready storage-denied:'+storageDenied+' dom-denied:'+domDenied;
if(storageDenied&&domDenied)document.body.style.background='#00ff00';
});</script>`);
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', () => {
<html><head>${blockers}<style>main { color: rgb(12, 34, 56); }</style></head>
<body><main id="ready">${'preview-ready '.repeat(8_000)}</main></body></html>`);
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 ({
+2 -1
View File
@@ -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) {
<div id="today-list"></div><div id="conversation-list"></div>
<div id="conversation-action-menu" data-cid="c2"><button data-action="to-project"></button></div>
</body></html>`);
await page.goto(pathToFileURL(file).href);
await loadRetainedFilePage(page, file);
await page.evaluate(count => {
const w = window as any;
w.metadataVisits = 0;
+10
View File
@@ -0,0 +1,10 @@
-----BEGIN CERTIFICATE-----
MIIBXTCCAQ+gAwIBAgIUHktca2EXLWH06YrObUINLcYJAF8wBQYDK2VwMCQxIjAg
BgNVBAMMGXB1YmxpYy1wcm9vZi5leGFtcGxlLnRlc3QwHhcNMjYxMDI1MTEzNDMw
WhcNMjYxMDI2MTEzNDMwWjAkMSIwIAYDVQQDDBlwdWJsaWMtcHJvb2YuZXhhbXBs
ZS50ZXN0MCowBQYDK2VwAyEArj2EJdl3KDQNCEFntd5m2NvgwoKF2MstjycEbDhp
MsejUzBRMB0GA1UdDgQWBBRcmxyBpAtE9AduPAA4zmo6GY1AmDAfBgNVHSMEGDAW
gBRcmxyBpAtE9AduPAA4zmo6GY1AmDAPBgNVHRMBAf8EBTADAQH/MAUGAytlcANB
AJA7mSRZsUBB6ZLNx8dggVBZqAwkAJUrVzaV2dYTwp2qjyquh6icykpoKYCieKLa
s+DLV4/5FQjEGM8Mq9xhTAU=
-----END CERTIFICATE-----
@@ -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();