Files
workinf_Blender_Wasm/web/tests/e2e/original-packed-image-viewport.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

47 lines
7.1 KiB
TypeScript

import { expect, test } from '@playwright/test';
import path from 'node:path';
import fs from 'node:fs';
import { execFileSync } from 'node:child_process';
import { lowerPackedStorageImages, needsPackedStorageImages } from '../../app/src/workers/packed-storage-images';
import type { ComputeJob } from '../../protocol/gpu-compute';
import { installNativeGLTrace } from '../fixtures/native-gl-trace';
import { installNativeComputeCapture, readNativeComputeCapture } from '../fixtures/native-compute-capture';
import { observeNative, captureNativeWorkflowState, performNativeAction, exposeWebTools } from '../fixtures/native-ui-workflow';
import { queryNativeJSON } from '../fixtures/native-modeling-workflow';
test.afterEach(async ({ page }, info) => {
if(process.env.WBV2_COMPUTE_CAPTURE){const worker = page.workers().find(worker => worker.url().includes('web-engine.worker')); if(worker)fs.writeFileSync(info.outputPath('native-compute-capture.json'), JSON.stringify(await readNativeComputeCapture(worker)));}
fs.writeFileSync(info.outputPath('native-packed-image-state.json'), JSON.stringify(await captureNativeWorkflowState(page), null, 2)); await page.screenshot({ path: info.outputPath('native-packed-image-viewport.png') });
if(process.env.WBV2_GL_TRACE){const worker = page.workers().find(worker => worker.url().includes('web-engine.worker')); if(worker){const calls = await worker.evaluate(() => (self as any).__nativeGLTrace ?? []); fs.writeFileSync(info.outputPath('native-gl-trace.json'), JSON.stringify(calls, null, 2)); fs.writeFileSync(info.outputPath('native-gl-draw-state.json'), JSON.stringify(await worker.evaluate(() => (self as any).__nativeGLDrawState ?? null), null, 2)); expect(calls.filter((call: any) => call.name === 'getError' && call.result)).toEqual([]);}}
});
test('Original packed image attaches to the native WASM viewport', async ({ page }, info) => {
const fixture = path.resolve(import.meta.dirname, '../../../blender-5.2.0/tests/files/io_tests/blend_autosave/autosave_image_test.blend');
const baseline = "import bpy,json,hashlib\ni=bpy.data.images['PackedImage']\nprint(json.dumps({'packed':bool(i.packed_file),'size':list(i.size),'bytes':i.packed_file.size if i.packed_file else 0,'sha256':hashlib.sha256(i.packed_file.data).hexdigest() if i.packed_file else None}))";
const output = execFileSync(process.env.BLENDER_BIN!, ['--background', '--factory-startup', '--disable-autoexec', fixture, '--python-exit-code', '1', '--python-expr', baseline], { encoding: 'utf8', timeout: 60000 });
const desktop = JSON.parse(output.split('\n').find(line => line.startsWith('{"packed":'))!); expect(desktop.packed).toBe(true); fs.writeFileSync(info.outputPath('desktop-packed-image.json'), JSON.stringify(desktop));
const job: ComputeJob = { language: 'wgsl', source: '@group(0) @binding(0) var pixels: texture_storage_2d<rgba8unorm, read_write>; @compute @workgroup_size(1) fn main() { let v = textureLoad(pixels, vec2<i32>(0)); textureStore(pixels, vec2<i32>(0), v); }', entryPoint: 'main', groups: [1, 1, 1], kinds: ['storage-image'], steps: null, images: { '0': { format: 'rgba8unorm', width: desktop.size[0], height: desktop.size[1], access: 'read-write' } } };
const bytes = desktop.size[0] * desktop.size[1] * 4, lowered = lowerPackedStorageImages(job, [bytes]); expect(needsPackedStorageImages(job)).toBe(true); expect(lowered.kinds).toEqual(['storage']); expect(lowered.images).toEqual({}); expect(lowered.source).toContain('unpack4x8unorm'); expect(lowered.source).toContain('pack4x8unorm');
expect(() => lowerPackedStorageImages(job, [bytes - 4])).toThrow(/extent or byte layout mismatch/); expect(() => lowerPackedStorageImages({ ...job, images: { '0': { ...job.images!['0'], access: 'read-only' } } }, [bytes])).toThrow(/access mismatch/);
let captureReady: Promise<void> | undefined;
if(process.env.WBV2_GL_FINISH)page.on('console', message => { if(message.text().startsWith('WBV2_GL_FINISH_'))fs.appendFileSync(info.outputPath('native-gl-fence.log'), message.text() + '\n'); });
if(process.env.WBV2_GPU_STACK_DIAGNOSTIC)page.on('console', message => { if(message.text().includes('GPU compute deadline'))fs.appendFileSync(info.outputPath('gpu-deadline-live.log'), message.text() + '\n'); });
if(process.env.WBV2_COMPUTE_CAPTURE)page.on('worker', worker => { if(worker.url().includes('web-engine.worker')){captureReady = installNativeComputeCapture(worker); void captureReady.catch(() => undefined);} });
test.setTimeout(180000); await observeNative(page); await page.goto('/');
await expect(page.getByTestId('engine-status')).toHaveText('Engine: new General project', { timeout: 60000 });
if(process.env.WBV2_COMPUTE_CAPTURE){expect(captureReady).toBeDefined(); await captureReady;}
if(process.env.WBV2_GL_TRACE){const worker = page.workers().find(worker => worker.url().includes('web-engine.worker')); expect(worker).toBeDefined(); await installNativeGLTrace(worker!, Boolean(process.env.WBV2_GL_FINISH));}
await performNativeAction(page, { type: 'openBlend' }, () => page.getByTestId('blend-file-input').setInputFiles(fixture));
await expect(page.locator('main.blender-app')).toHaveAttribute('data-project-id', 'autosave_image_test', { timeout: 30000 }); await expect(page.getByTestId('engine-status')).toContainText('SceneIR');
await expect(page.locator('canvas[aria-label="Blender desktop"]')).toHaveAttribute('data-native-input-ready', 'true', { timeout: 30000 });
const packed = (await queryNativeJSON(page, baseline)).value as { packed: boolean; size: number[]; bytes: number; sha256: string };
expect((await captureNativeWorkflowState(page)).browserDiagnostics.filter((message: any) => String(message.text ?? '').includes('Blender browser WM attach:'))).toEqual([]);
expect(packed).toEqual(desktop); expect(packed.bytes).toBeGreaterThan(0); fs.writeFileSync(info.outputPath('original-packed-image.json'), JSON.stringify({ fixture, packed, externalCompanionCoverage: 'NOT_CLAIMED' }));
await exposeWebTools(page); const downloading = page.waitForEvent('download'); await page.getByRole('button', { name: '保存项目', exact: true }).click();
const saved = fs.readFileSync((await (await downloading).path())!);
await performNativeAction(page, { type: 'openBlend' }, () => page.getByTestId('blend-file-input').setInputFiles({ name: 'packed-image-saved.blend', mimeType: 'application/octet-stream', buffer: saved }));
await expect(page.locator('main.blender-app')).toHaveAttribute('data-project-id', 'packed-image-saved', { timeout: 30000 }); await expect(page.getByTestId('engine-status')).toContainText('SceneIR');
await expect(page.locator('canvas[aria-label="Blender desktop"]')).toHaveAttribute('data-native-input-ready', 'true', { timeout: 30000 });
expect((await queryNativeJSON(page, baseline)).value).toEqual(desktop); fs.writeFileSync(info.outputPath('packed-image-saved.blend'), saved);
expect((await captureNativeWorkflowState(page)).responses.filter((response: any) => String(response.errorMessage ?? '').includes('Native Main window'))).toEqual([]);
expect((await captureNativeWorkflowState(page)).browserDiagnostics.filter((message: any) => String(message.text ?? '').includes('Blender browser WM attach:'))).toEqual([]);
});