Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
58 lines
5.0 KiB
TypeScript
58 lines
5.0 KiB
TypeScript
import { expect, type Page } from '@playwright/test';
|
|
import type { EmbeddedPythonResult } from '../../app/src/engine-client/WebEngineClient';
|
|
import { performNativeAction } from './native-ui-workflow';
|
|
export async function pythonReply(page: Page): Promise<(EmbeddedPythonResult & { requestId: string }) | null> {
|
|
const value = await page.evaluate(() => (window as any).__nativeWorkflowPython);
|
|
if (value === undefined) return null;
|
|
expect(value !== null && typeof value === 'object' && !Array.isArray(value)).toBe(true);
|
|
expect(typeof value.requestId).toBe('string'); expect(value.engine).toBe('CPYTHON_WEBENGINE'); expect(value.bpy).toBe(true);
|
|
expect(typeof value.state).toBe('string'); expect(['DONE', 'MORE', 'ERROR']).toContain(value.state);
|
|
expect(typeof value.output).toBe('string'); expect(typeof value.version).toBe('string'); expect(typeof value.changed).toBe('boolean'); expect(typeof value.truncated).toBe('boolean');
|
|
expect(Number.isSafeInteger(value.revision)).toBe(true); expect(value.revision).toBeGreaterThanOrEqual(0);
|
|
return value;
|
|
}
|
|
export async function runWasmPython(page: Page, source: string, state: 'DONE' | 'ERROR' = 'DONE') {
|
|
const previous = (await pythonReply(page))?.requestId ?? null;
|
|
await page.getByRole('button', { name: '清空 Python 控制台', exact: true }).click();
|
|
const input = page.getByLabel('Python 输入', { exact: true }); await input.fill(source); await input.press('Enter');
|
|
let dispatchedId: string | null = null;
|
|
await expect.poll(async () => {
|
|
const id = await page.evaluate(() => { const requests = (window as any).__nativeWorkflowRequests; if (!Array.isArray(requests)) throw new Error('Native request observation unavailable'); const request = requests.findLast((item: any) => item.type === 'jsonCommand' && item.jsonCommand === 'pythonExec'); return typeof request?.requestId === 'string' ? request.requestId : null; });
|
|
if (id !== null && id !== previous) { dispatchedId = id; return id; }
|
|
const region = page.getByRole('region', { name: 'Python Console', exact: true }), output = await region.locator('pre').textContent();
|
|
if (typeof output === 'string' && output.length > 0 && await region.getByRole('status').textContent() === 'error') throw new Error(`Console failed before native reply: ${output}`);
|
|
return previous;
|
|
}, { timeout: 10000, message: 'Console must dispatch a new native pythonExec request' }).not.toBe(previous);
|
|
await expect.poll(async () => (await pythonReply(page))?.requestId ?? null, { timeout: 60000, message: 'Native Python must reply to the dispatched request' }).toBe(dispatchedId);
|
|
const result = (await pythonReply(page))!; expect(result.state).toBe(state);
|
|
await expect(page.getByRole('region', { name: 'Python Console', exact: true }).getByRole('status')).toHaveText(state === 'ERROR' ? 'error' : 'idle');
|
|
return result;
|
|
}
|
|
export async function openFromUI(page: Page, action: () => Promise<unknown>) {
|
|
await performNativeAction(page, { type: 'openBlend' }, action, true, 60000);
|
|
await expect(page.getByTestId('open-progress')).toBeHidden();
|
|
}
|
|
export async function switchNativeWorkspace(page: Page, name: string) {
|
|
const flow = await page.evaluate(() => (window as any).__nativeWorkflowEditor);
|
|
expect(Array.isArray(flow?.workspaces)).toBe(true); expect(typeof flow?.context?.workspaceId).toBe('string');
|
|
const target = flow.workspaces.find((workspace: any) => typeof workspace.name === 'string' && workspace.name === name); expect(typeof target?.id).toBe('string');
|
|
if (flow.context.workspaceId === target.id) return;
|
|
await performNativeAction(page, { command: 'editEditorContext', editAction: 'SWITCH_WORKSPACE', workspaceId: target.id }, () => page.getByRole('button', { name, exact: true }).click());
|
|
await expect.poll(() => page.evaluate(() => (window as any).__nativeWorkflowEditor?.context?.workspaceId), { timeout: 10000 }).toBe(target.id);
|
|
}
|
|
export async function expectUILocation(page: Page, name: string, expected: [number, number, number]) {
|
|
expect(expected.every(Number.isFinite)).toBe(true);
|
|
const group = page.getByRole('group', { name: `${name}: Transform`, exact: true }), actual: number[] = [];
|
|
for (const [index, axis] of ['X', 'Y', 'Z'].entries()) {
|
|
let value = NaN;
|
|
await expect.poll(async () => { value = await group.getByRole('spinbutton', { name: `Location ${axis}`, exact: true }).evaluate(element => { if (!(element instanceof HTMLInputElement) || element.type !== 'number') throw new Error('Location must be a numeric input'); return element.valueAsNumber; }); expect(Number.isFinite(value)).toBe(true); return value; }).toBeCloseTo(expected[index], 5);
|
|
actual.push(value);
|
|
}
|
|
return actual;
|
|
}
|
|
export async function showNativeObjectProperties(page: Page) {
|
|
const context = page.getByLabel('Properties context', { exact: true });
|
|
if (await context.inputValue() !== 'OBJECT') await performNativeAction(page, { command: 'editEditorContext', editAction: 'PROPERTIES_EDIT', editOperation: 'SET_CONTEXT', context: 'OBJECT' }, () => context.selectOption('OBJECT'));
|
|
await expect(context).toHaveValue('OBJECT');
|
|
}
|