Files
workinf_Blender_Wasm/web/tests/e2e/file-native-saveas-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
11 KiB
TypeScript

import { expect, test } from '@playwright/test';
import path from 'node:path';
import fs from 'node:fs';
import { captureNativeWorkflowState, exposeWebTools, observeNative, performNativeAction, resolveProjectReplacement } from '../fixtures/native-ui-workflow';
import { expectModelingData, focusNativeViewport, nativeKeyFrame, nativeRequest, queryNativeJSON, readModelingMain, readNativeBlend, readNativeHistory, transformNative } from '../fixtures/native-modeling-workflow';
import { expectUILocation, openFromUI, showNativeObjectProperties } from '../fixtures/wasm-console-workflow';
const fixture = path.resolve(import.meta.dirname, '../../..', process.env.WBV2_CASE_FIXTURE ?? 'tests/files/web/basic_scene.blend');
test('Native File Save As and Open preserve Main and deliver a blend', async ({ page }, testInfo) => {
const evidence: Record<string, unknown> = {}, downloads: string[] = [], record = (key: string, value: unknown) => { evidence[key] = value; fs.writeFileSync(testInfo.outputPath('b3-main.json'), JSON.stringify(evidence, null, 2)); };
const fileBrowser = () => page.evaluate(() => { const context = (window as any).__nativeWorkflowEditor?.context; return { value: context?.activeEditor === 'FILE_BROWSER' ? context : null }; });
page.on('download', download => downloads.push(download.suggestedFilename())); await observeNative(page); await page.goto('/');
await expect.poll(() => page.evaluate(() => Array.isArray((window as any).__nativeWorkflowMain?.nodes)), { timeout: 60000 }).toBe(true);
await exposeWebTools(page); await openFromUI(page, () => page.getByTestId('blend-file-input').setInputFiles(fixture));
await performNativeAction(page, { command: 'createPrimitive' }, () => page.getByRole('button', { name: '添加立方体', exact: true }).click());
const { value: name } = await queryNativeJSON(page, "import bpy, json\nprint(json.dumps(bpy.context.active_object.name))"); expect(typeof name).toBe('string');
const historyRoundTrip = async (stage: string, from: Awaited<ReturnType<typeof readModelingMain>>, to: Awaited<ReturnType<typeof readModelingMain>>) => {
record(`${stage}UndoFrame`, await nativeKeyFrame(page, 'Control+z', 90)); const undone = await readModelingMain(page, name); record(`${stage}Undone`, undone); expectModelingData(undone.value, from.value);
await exposeWebTools(page); await showNativeObjectProperties(page); record(`${stage}UndoUI`, await expectUILocation(page, name, from.value.location)); await focusNativeViewport(page);
record(`${stage}RedoFrame`, await nativeKeyFrame(page, 'Control+Shift+z', 90)); const redone = await readModelingMain(page, name); record(`${stage}Redone`, redone); expectModelingData(redone.value, to.value);
await exposeWebTools(page); await showNativeObjectProperties(page); record(`${stage}RedoUI`, await expectUILocation(page, name, to.value.location)); await focusNativeViewport(page);
};
await showNativeObjectProperties(page);
const { canvas } = await focusNativeViewport(page); const unmoved = await readModelingMain(page, name); record('unmoved', unmoved); await transformNative(page, 'g', 'x', '1');
await historyRoundTrip('beforeSave', unmoved, await readModelingMain(page, name));
const before = await readModelingMain(page, name), history = await readNativeHistory(page); record('before', before); record('historyBefore', history);
const openSaveAs = async (stage: string, menuY = 163, discardChanges = false) => {
const bounds = await canvas.boundingBox(); expect(bounds).not.toBeNull(); await performNativeAction(page, { type: 'gpuUI', gpuOperation: 'frame', mouseReleased: true }, () => page.mouse.click(bounds!.x + 44, bounds!.y + 13));
record(`${stage}MenuHistory`, await readNativeHistory(page));
await page.screenshot({ path: testInfo.outputPath(`${stage}-menu.png`) });
await performNativeAction(page, { type: 'gpuUI', gpuOperation: 'frame', mouseReleased: true }, () => page.mouse.click(bounds!.x + 90, bounds!.y + menuY));
if (discardChanges) {
expect((await fileBrowser()).value).toBeNull(); await page.screenshot({ path: testInfo.outputPath(`${stage}-discard-prompt.png`) });
await performNativeAction(page, { type: 'gpuUI', gpuOperation: 'frame', keyPressed: 49 }, () => page.keyboard.press('1'));
}
await expect.poll(async () => (await fileBrowser()).value?.activeEditor, { timeout: 15000 }).toBe('FILE_BROWSER'); record(stage, await fileBrowser());
record(`${stage}History`, await readNativeHistory(page));
await performNativeAction(page, { type: 'gpuUI', gpuOperation: 'frame', mouseReleased: true }, () => page.mouse.click(bounds!.x + bounds!.width / 2, bounds!.y + bounds!.height / 2));
record(`${stage}FocusedHistory`, await readNativeHistory(page));
};
fs.writeFileSync(testInfo.outputPath('b3-before-cancel.blend'), await readNativeBlend(page));
await openSaveAs('cancelDialog'); await page.screenshot({ path: testInfo.outputPath('b3-cancel-dialog.png') });
await performNativeAction(page, { type: 'gpuUI', gpuOperation: 'frame', keyPressed: 27 }, () => page.keyboard.press('Escape')); await expect.poll(async () => (await fileBrowser()).value).toBeNull();
const cancelled = await readModelingMain(page, name), historyAfter = await readNativeHistory(page); record('cancelled', cancelled); record('historyAfterCancel', historyAfter); expectModelingData(cancelled.value, before.value);
fs.writeFileSync(testInfo.outputPath('b3-after-cancel.blend'), await readNativeBlend(page));
expect(fs.readFileSync(testInfo.outputPath('b3-after-cancel.blend'))).toEqual(fs.readFileSync(testInfo.outputPath('b3-before-cancel.blend')));
if (historyAfter.cursor !== history.cursor) {
for (const index of [history.cursor, historyAfter.cursor - 1]) { const live = await readNativeHistory(page); const response = await nativeRequest(page, { type: 'jsonCommand', cmd: 'history', params: { baseRevision: live.revision, snapshotIndex: index } }); const bytes = response.result?.json?.snapshotBlend; expect(Array.isArray(bytes)).toBe(true); expect(bytes.every((byte: unknown) => Number.isInteger(byte) && (byte as number) >= 0 && (byte as number) <= 255)).toBe(true); fs.writeFileSync(testInfo.outputPath(`b3-stored-history-${index}.blend`), Buffer.from(bytes)); }
}
expect(historyAfter.cursor).toBe(history.cursor); expect(historyAfter.entries).toEqual(history.entries); expect(downloads).toEqual([]);
await openSaveAs('saveDialog'); const downloading = page.waitForEvent('download', { timeout: 60000 });
await performNativeAction(page, { type: 'gpuUI', gpuOperation: 'frame', keyPressed: 13 }, () => page.keyboard.press('Enter'));
const download = await downloading, saved = testInfo.outputPath('b3-native-saveas.blend'); expect(download.suggestedFilename()).toMatch(/\.blend$/i); await download.saveAs(saved); record('saved', { path: saved, name: download.suggestedFilename() });
await expect.poll(async () => (await fileBrowser()).value).toBeNull(); expect(downloads).toHaveLength(1);
const { value: savedFilepath } = await queryNativeJSON(page, "import bpy, json\nprint(json.dumps(bpy.data.filepath))"); record('nativeSavedFilepath', savedFilepath); expect(path.posix.basename(savedFilepath)).toBe(download.suggestedFilename());
const savedMain = await readModelingMain(page, name); record('savedMain', savedMain); expectModelingData(savedMain.value, before.value);
const openHistory = await readNativeHistory(page), openBaseline = await readNativeBlend(page); record('openHistoryBefore', openHistory);
await openSaveAs('nativeOpenCancelDialog', 55); await page.screenshot({ path: testInfo.outputPath('b3-open-cancel-dialog.png') });
await performNativeAction(page, { type: 'gpuUI', gpuOperation: 'frame', keyPressed: 27 }, () => page.keyboard.press('Escape')); await expect.poll(async () => (await fileBrowser()).value).toBeNull();
const openCancelled = await readModelingMain(page, name), openHistoryAfter = await readNativeHistory(page); record('nativeOpenCancelled', openCancelled); record('openHistoryAfter', openHistoryAfter);
expectModelingData(openCancelled.value, savedMain.value); expect(openHistoryAfter.cursor).toBe(openHistory.cursor); expect(openHistoryAfter.entries).toEqual(openHistory.entries); expect(await readNativeBlend(page)).toEqual(openBaseline); expect(downloads).toHaveLength(1);
await focusNativeViewport(page); await transformNative(page, 'g', 'x', '5'); const unsaved = await readModelingMain(page, name); record('unsaved', unsaved); expect(unsaved.value.location[0]).toBeCloseTo(before.value.location[0] + 5, 5);
await openSaveAs('nativeOpenDialog', 55, true); await page.screenshot({ path: testInfo.outputPath('b3-native-open-dialog.png') });
await performNativeAction(page, { type: 'gpuUI', gpuOperation: 'frame', keyPressed: 13 }, () => page.keyboard.press('Enter'));
await expect.poll(async () => (await fileBrowser()).value).toBeNull(); await expect.poll(() => page.evaluate(() => (window as any).__nativeWorkflowGPU?.modalActive)).toBe(false);
const nativeOpened = await readModelingMain(page, name); record('nativeOpened', nativeOpened); expectModelingData(nativeOpened.value, before.value); await page.screenshot({ path: testInfo.outputPath('b3-native-opened.png') });
await focusNativeViewport(page); await transformNative(page, 'g', 'x', '2'); const afterNativeOpenEdit = await readModelingMain(page, name); record('afterNativeOpenEdit', afterNativeOpenEdit); expect(afterNativeOpenEdit.value.location[0]).toBeCloseTo(before.value.location[0] + 2, 5);
await historyRoundTrip('afterNativeOpen', nativeOpened, afterNativeOpenEdit);
await exposeWebTools(page); await openFromUI(page, async () => { await page.getByTestId('blend-file-input').setInputFiles(saved); await resolveProjectReplacement(page, 'discard'); });
const reopened = await readModelingMain(page, name); record('reopened', reopened); expectModelingData(reopened.value, before.value); await showNativeObjectProperties(page);
const group = page.getByRole('group', { name: `${name}: Transform`, exact: true }); await group.scrollIntoViewIfNeeded(); for (const axis of ['X', 'Y', 'Z']) await expect(group.getByRole('spinbutton', { name: `Location ${axis}`, exact: true })).toBeInViewport({ ratio: 1 });
record('reopenedUI', await expectUILocation(page, name, before.value.location)); await page.screenshot({ path: testInfo.outputPath('b3-reopened-ui.png') }); record('result', { status: 'PASS', scope: 'native File Save As/Open/cancel/download/native and browser reopen, keyboard Undo/Redo before Save and after native Open with Main/UI readback', kind: process.env.WBV2_CASE_KIND });
});
test.afterEach(async ({ page }, testInfo) => { const state = await captureNativeWorkflowState(page).catch(error => ({ captureError: String(error) })); fs.writeFileSync(testInfo.outputPath('b3-last-native-state.json'), JSON.stringify(state, null, 2)); });