Files
workinf_Blender_Wasm/web/tests/fixtures/wasm-console-workflow.ts
mes123456 f3e21c6b98
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
chore: checkpoint web Blender migration source, tools and docs
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>
2026-11-10 11:18:01 +08:00

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');
}