mirror of
https://github.com/Orkas-AI/Orkas.git
synced 2026-11-07 06:16:03 -10:00
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:
1 parent
f62b5d24c6
commit
dc4abcfab2
8 files changed
+158
-23
No files matched your search
@@ -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',
|
||||
],
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}) });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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,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
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user