Files
workinf_Blender_Wasm/web/tests/e2e/console-wasm-keyboard-production.spec.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

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