import { expect, test } from '@playwright/test'; import fs from 'node:fs'; import path from 'node:path'; import { createHash } from 'node:crypto'; import { observeNative, performNativeAction, exposeWebTools, captureNativeWorkflowState } from '../fixtures/native-ui-workflow'; import { pythonReply, switchNativeWorkspace } from '../fixtures/wasm-console-workflow'; const root = path.resolve(import.meta.dirname, '../../..'), names = ['bl_blendfile_utils.py', 'bl_blendfile_io.py'], sources = names.map(name => fs.readFileSync(path.join(root, 'blender-5.2.0/tests/python', name), 'utf8')); const sourceHashes = Object.fromEntries(names.map((name, i) => [name, createHash('sha256').update(sources[i]).digest('hex')])); test.afterEach(async ({ page }, info) => { fs.writeFileSync(info.outputPath('upstream-last-state.json'), JSON.stringify(await captureNativeWorkflowState(page).catch(error => ({ error: String(error) })), null, 2)); }); test('upstream blendfile_io preserves original Blender assertions in native WASM', async ({ page }, info) => { await observeNative(page); await page.goto('/'); await expect.poll(() => page.evaluate(() => Array.isArray((window as any).__nativeWorkflowMain?.nodes)), { timeout: 60000 }).toBe(true); await expect(page.locator('main.blender-app')).toHaveAttribute('data-user-action-open-status', 'SUCCEEDED', { timeout: 60000 }); await exposeWebTools(page); await switchNativeWorkspace(page, 'Scripting'); const caseDir = `/tmp/wbv2-source/${createHash('sha256').update(`${process.env.WBV2_CASE_ATTEMPT ?? ''}:${info.outputPath('source-case')}`).digest('hex').slice(0, 24)}`; const program = [ "import os, sys, runpy, hashlib, json, bpy", `case_dir = ${JSON.stringify(caseDir)}`, "os.makedirs(case_dir, exist_ok=True)", ...names.map((name, i) => `open(os.path.join(case_dir, ${JSON.stringify(name)}), 'w', encoding='utf-8').write(${JSON.stringify(sources[i])})`), "old_argv = sys.argv; sys.argv = [os.path.join(case_dir, 'bl_blendfile_io.py'), '--', '--output-dir', os.path.join(case_dir, 'output')]", "try:\n runpy.run_path(os.path.join(case_dir, 'bl_blendfile_io.py'), run_name='__main__')\nfinally:\n sys.argv = old_argv", `print('WBV2_SOURCE_CASE_PASS', json.dumps({'platform': sys.platform, 'version': list(bpy.app.version), 'objects': sorted(obj.name for obj in bpy.context.scene.objects), 'frame': bpy.context.scene.frame_current, 'hashes': {name: hashlib.sha256(open(os.path.join(case_dir, name), 'rb').read()).hexdigest() for name in ${JSON.stringify(names)}}, 'files': sorted(name for name in os.listdir(os.path.join(case_dir, 'output')) if name.endswith('.blend'))}))`].join('\n'); const input = page.getByLabel('Python 输入', { exact: true }); await input.fill(`exec(${JSON.stringify(program)})`); await performNativeAction(page, { type: 'jsonCommand', jsonCommand: 'pythonExec' }, () => input.press('Enter')); const reply = await pythonReply(page); fs.writeFileSync(info.outputPath('upstream-blendfile-io.json'), JSON.stringify({ sourceHashes, reply }, null, 2)); expect(reply?.state, reply?.output).toBe('DONE'); expect(reply?.truncated).toBe(false); const marker = reply!.output.split('\n').findLast(line => line.startsWith('WBV2_SOURCE_CASE_PASS ')); expect(marker, reply!.output).toBeTruthy(); const payload = JSON.parse(marker!.slice('WBV2_SOURCE_CASE_PASS '.length)); const desktop = JSON.parse(fs.readFileSync(path.join(root, 'tests/golden/WBV2-P1424-001/source-cases-20261028/blendfile_io/desktop-readback.json'), 'utf8')); expect(payload.objects).toEqual(desktop.objects); expect(payload.frame).toBe(desktop.frame); expect(payload.version).toEqual(desktop.version); expect(payload.hashes['bl_blendfile_io.py']).toBe(desktop.sourceSha256); expect(payload.platform).toBe('emscripten'); expect(payload.version.slice(0, 2)).toEqual([5, 2]); expect(payload.hashes).toEqual(sourceHashes); expect(payload.files).toEqual(fs.readdirSync(path.dirname(process.env.WBV2_CASE_FIXTURE!)).filter(name => name.endsWith('.blend')).sort()); await expect(page.locator('main.blender-app')).toHaveAttribute('data-editor-workflow-status', 'AVAILABLE'); await expect.poll(() => page.evaluate(() => (window as any).__nativeWorkflowMain?.nodes.map((node: any) => node.name).sort())).toEqual(payload.objects); await expect(page.locator('main.blender-app')).toHaveAttribute('data-project-snapshot-revision', String(reply!.revision)); await expect(page.locator('main.blender-app')).toHaveAttribute('data-current-frame', String(payload.frame)); await expect(page.getByTestId('scene-stats')).not.toContainText('Cube'); await expect(page.getByTestId('scene-stats')).toContainText(`Objects ${payload.objects.length}`); await expect(page.getByTestId('engine-status')).toHaveText(`Engine: SceneIR r${reply!.revision} (${payload.objects.length} objects)`); });