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>
47 lines
7.1 KiB
TypeScript
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([]);
|
|
});
|