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>
74 lines
8.1 KiB
TypeScript
74 lines
8.1 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { captureNativeWorkflowState, exposeWebTools, observeNative } from '../fixtures/native-ui-workflow';
|
|
import { expectWebLocation, webMain } from '../fixtures/console-main-workflow';
|
|
import { expectUILocation, openFromUI, runWasmPython, showNativeObjectProperties, switchNativeWorkspace } from '../fixtures/wasm-console-workflow';
|
|
const fixture = path.resolve(process.env.WBV2_CASE_FIXTURE ?? '../tests/files/web/basic_scene.blend');
|
|
test('WASM Console uses native Main keyboard history and save/reopen', async ({ page }, testInfo) => {
|
|
const evidence: Record<string, unknown> = {}, serverCalls: string[] = [];
|
|
const record = (stage: string, value: unknown) => { evidence[stage] = value; fs.writeFileSync(testInfo.outputPath('b1-wasm-main.json'), JSON.stringify(evidence, null, 2)); };
|
|
page.on('request', request => { if (/^\/api\/(blender-session|blender-workflow|server-operator-job)$/.test(new URL(request.url()).pathname)) serverCalls.push(request.url()); });
|
|
await observeNative(page); await page.goto('/');
|
|
await expect.poll(() => page.evaluate(() => (window as any).__nativeWorkflowMain?.nodes?.some((node: any) => node.name === 'Cube') ?? false), { timeout: 60000 }).toBe(true);
|
|
await exposeWebTools(page); await expect(page.getByTestId('open-progress')).toBeHidden();
|
|
if (process.env.WBV2_CASE_KIND === 'minimal') {
|
|
await page.getByRole('button', { name: '文件', exact: true }).click(); await page.getByRole('menuitem', { name: '新建', exact: true }).click();
|
|
await page.getByRole('menuitem', { name: '常规', exact: true }).click();
|
|
const template = page.waitForResponse(response => new URL(response.url()).pathname === '/templates/general.blend');
|
|
await openFromUI(page, () => page.getByRole('button', { name: '新建常规工程', exact: true }).click());
|
|
const response = await template, bytes = await response.body(); expect(response.status()).toBe(200); expect(Buffer.isBuffer(bytes)).toBe(true); expect(bytes.equals(fs.readFileSync(fixture))).toBe(true);
|
|
} else await openFromUI(page, () => page.getByTestId('blend-file-input').setInputFiles(fixture));
|
|
await exposeWebTools(page); const opened = await webMain(page); record('opened', opened);
|
|
const target = opened.nodes.find(node => node.type === 'MESH'); expect(target).toBeDefined();
|
|
if (process.env.WBV2_CASE_KIND === 'minimal') expect(target!.name).toBe('Cube');
|
|
await switchNativeWorkspace(page, 'Scripting');
|
|
await showNativeObjectProperties(page); record('openedUI', await expectUILocation(page, target!.name, target!.transform.translation));
|
|
await expect(page.getByLabel('Python 运行时', { exact: true })).toHaveValue('WEBENGINE');
|
|
const region = page.getByRole('region', { name: 'Python Console', exact: true }), output = region.locator('pre');
|
|
const sum = await runWasmPython(page, '1+1'); record('sum', sum); expect(sum.output.trim()).toBe('2');
|
|
expect(sum.changed).toBe(false);
|
|
await expect(output).toContainText('>>> 1+1'); await expect(output).toContainText('2');
|
|
const definition = 'def b1_native_lookup():\n return b1_native_token + sum(b1_native_state["values"])';
|
|
record('namespace', await runWasmPython(page, `import bpy; b1_native_token = 42; b1_native_state = {"values": [1, 2]}; b1_native_object = bpy.data.objects[${JSON.stringify(target!.name)}]; exec(${JSON.stringify(definition)})`));
|
|
const name = target!.name, before = target!.transform.translation, changedX = before[0] === 3 ? 4 : 3;
|
|
const changed = await runWasmPython(page, `import bpy; bpy.data.objects[${JSON.stringify(name)}].location.x = ${changedX}; print("B1_X", bpy.data.objects[${JSON.stringify(name)}].location.x)`); record('changed', changed);
|
|
expect(changed.changed).toBe(true); await expect(output).toContainText(`B1_X ${changedX}`);
|
|
const expected: [number, number, number] = [changedX, before[1], before[2]]; await expectWebLocation(page, name, expected);
|
|
record('changedUI', await expectUILocation(page, name, expected));
|
|
await expect(page.locator('main.blender-app')).toHaveAttribute('data-current-main-revision', String((await webMain(page)).revision));
|
|
await output.focus(); await page.keyboard.press('Control+z'); await expectWebLocation(page, name, before); record('undone', await webMain(page));
|
|
record('undoneUI', await expectUILocation(page, name, before));
|
|
await output.focus(); await page.keyboard.press('Control+Shift+z'); await expectWebLocation(page, name, expected); const redone = await webMain(page); record('redone', redone);
|
|
record('redoneUI', await expectUILocation(page, name, expected));
|
|
const syntaxError = await runWasmPython(page, 'def b1_native_invalid(:', 'ERROR'); record('syntaxError', syntaxError);
|
|
expect(syntaxError.changed).toBe(false); expect((await webMain(page)).nodes).toEqual(redone.nodes);
|
|
record('syntaxErrorUI', await expectUILocation(page, name, expected));
|
|
const input = page.getByLabel('Python 输入', { exact: true }), draft = 'print("B1_TOKEN", b1_native_token, b1_native_lookup())'; await input.fill(draft);
|
|
await switchNativeWorkspace(page, 'Layout'); await switchNativeWorkspace(page, 'Scripting');
|
|
await expect(input).toHaveValue(draft); const resumed = await runWasmPython(page, draft); record('workspace', resumed); expect(resumed.output.trim()).toBe('B1_TOKEN 42 45');
|
|
const aliasReply = await runWasmPython(page, `import json; print(json.dumps([C.scene.name, bpy.context.scene.name, D.objects[${JSON.stringify(name)}].location.x]))`); record('aliases', aliasReply);
|
|
const aliases = JSON.parse(aliasReply.output); expect(Array.isArray(aliases)).toBe(true); expect(aliases).toHaveLength(3);
|
|
expect(typeof aliases[0]).toBe('string'); expect(typeof aliases[1]).toBe('string'); expect(aliases[0]).toBe(aliases[1]); expect(typeof aliases[2]).toBe('number'); expect(Number.isFinite(aliases[2])).toBe(true); expect(aliases[2]).toBeCloseTo(changedX, 5);
|
|
const referenceCode = 'try:\n b1_old_name = b1_native_object.name\nexcept ReferenceError:\n b1_old_name = None\nprint(json.dumps(b1_old_name))';
|
|
const referenceReply = await runWasmPython(page, `exec(${JSON.stringify(referenceCode)})`); record('retainedReference', referenceReply);
|
|
const oldName = JSON.parse(referenceReply.output); expect(oldName === null || typeof oldName === 'string').toBe(true); if (oldName !== null) expect(oldName).toBe(name);
|
|
await expectWebLocation(page, name, expected); const download = page.waitForEvent('download'); await output.focus(); await page.keyboard.press('Control+s');
|
|
const saved = testInfo.outputPath('b1-wasm-native.blend'); await (await download).saveAs(saved);
|
|
await openFromUI(page, () => page.getByTestId('blend-file-input').setInputFiles(saved)); await expectWebLocation(page, name, expected);
|
|
await exposeWebTools(page); await switchNativeWorkspace(page, 'Scripting');
|
|
await showNativeObjectProperties(page);
|
|
record('reopenedUI', await expectUILocation(page, name, expected));
|
|
const cleared = await runWasmPython(page, 'print("B1_CLEARED", "b1_native_token" in globals())'); record('projectCleared', cleared); expect(cleared.output.trim()).toBe('B1_CLEARED False');
|
|
await runWasmPython(page, `b1_reset_token = 99; bpy.data.objects[${JSON.stringify(name)}].location.x = ${changedX + 1}`);
|
|
await output.focus(); await page.keyboard.press('Control+z'); await expectWebLocation(page, name, expected);
|
|
await page.getByRole('button', { name: '重置 Python 会话', exact: true }).click(); await expect(region.getByRole('status')).toHaveText('idle');
|
|
const reset = await runWasmPython(page, 'print("B1_RESET", "b1_reset_token" in globals())'); record('explicitResetAfterUndo', reset); expect(reset.output.trim()).toBe('B1_RESET False'); await expectWebLocation(page, name, expected);
|
|
record('reopened', await webMain(page)); record('serverCalls', serverCalls); expect(serverCalls).toEqual([]);
|
|
record('result', { status: 'PASS', kind: process.env.WBV2_CASE_KIND, name, before, expected, saved });
|
|
});
|
|
test.afterEach(async ({ page }, testInfo) => {
|
|
const last = await captureNativeWorkflowState(page).catch(error => ({ captureError: String(error) }));
|
|
fs.writeFileSync(testInfo.outputPath('b1-last-native-state.json'), JSON.stringify(last, null, 2));
|
|
});
|