import { expect, test } from '@playwright/test'; import { resolve } from 'node:path'; import { writeFile } from 'node:fs/promises'; import type {} from '../physics/runner'; test('真实 WASM 3.11:LeKiwi 站稳、全向轮、臂夹爪与障碍阻挡', async ({ page }, info) => { const errors: string[] = []; page.on('pageerror', (e) => errors.push(e.message)); page.on('console', (msg) => { if (msg.type() === 'error') console.log(msg.text()); }); await page.goto('/physics/runner.html'); await page.waitForFunction(() => Boolean(window.lekiwiPhysics)); const assetBase = `/@fs${resolve(process.cwd(), 'build/lekiwi')}`; const initial = await page.evaluate((path) => window.lekiwiPhysics.boot(path), assetBase); console.log('initial', initial); expect(initial.version).toBe('3.11.0'); expect(initial.nu).toBe(9); expect(initial.njnt).toBe(46); await writeFile( info.outputPath('lekiwi-profile.xml'), await page.evaluate(() => window.lekiwiPhysics.xml()), ); const stable = await page.evaluate(() => window.lekiwiPhysics.step(30)); console.log('stable', stable); expect(stable.finite).toBe(true); expect(Math.abs(stable.roll)).toBeLessThan(0.15); expect(Math.abs(stable.pitch)).toBeLessThan(0.15); const traces: unknown[] = [{ initial, stable }]; for (const [x, y, w] of [ [0.1, 0, 0], [-0.1, 0, 0], [0, 0.1, 0], [0, -0.1, 0], [0, 0, Math.PI / 6], [0, 0, -Math.PI / 6], ]) { const start = await page.evaluate(() => window.lekiwiPhysics.reset()); const end = await page.evaluate( ([x, y, w]) => window.lekiwiPhysics.drive(x, y, w, 2), [x, y, w], ); console.log('drive', x, y, w, 'start', start.position, start.yaw, 'end', end.position, end.yaw); traces.push({ command: [x, y, w], start, end }); expect(end.finite).toBe(true); if (x || y) { expect(Math.abs(end.position[0] - start.position[0] - x * 2)).toBeLessThan(0.05); expect(Math.abs(end.position[1] - start.position[1] - y * 2)).toBeLessThan(0.05); } else expect(Math.abs(end.yaw - start.yaw - w * 2)).toBeLessThan((10 * Math.PI) / 180); } await page.evaluate(() => window.lekiwiPhysics.reset()); // Free-space tracking; negative elbow folding now has its own obstruction test. const targets = [0.1, 0.1, 0.1, 0.1, 0.1, 0.3]; const arm = await page.evaluate((values) => window.lekiwiPhysics.arm(values, 2), targets); console.log('arm', arm.arm); traces.push({ targets, arm }); arm.arm.forEach((value, i) => expect(Math.abs(value - targets[i])).toBeLessThan(0.015)); await page.evaluate(() => window.lekiwiPhysics.wall()); const blocked = await page.evaluate(() => window.lekiwiPhysics.drive(0.1, 0, 0, 5)); console.log('blocked', blocked); const contacts = await page.evaluate(() => window.lekiwiPhysics.contactPairs()); traces.push({ blocked, contacts }); expect(contacts.some((c) => c.geom1 === 'test_wall' || c.geom2 === 'test_wall')).toBe(true); expect(blocked.maxContacts).toBeGreaterThan(0); expect(blocked.position[0]).toBeLessThan(0.22); // The complete arm/accessory outline reaches the wall before the old chassis proxy. expect(blocked.position[0]).toBeGreaterThan(0.02); expect(blocked.finite).toBe(true); await writeFile(info.outputPath('physics-traces.json'), JSON.stringify(traces, null, 2)); expect(errors).toEqual([]); await page.evaluate(() => window.lekiwiPhysics.dispose()); }); test('真实 Python/WASM:控制权、旧回调隔离、安全保持、reset 与导出重载', async ({ page }) => { await page.goto('/physics/runner.html'); await page.waitForFunction(() => Boolean(window.lekiwiPhysics)); await page.evaluate( (path) => window.lekiwiPhysics.boot(path), `/@fs${resolve(process.cwd(), 'build/lekiwi')}`, ); const result = await page.evaluate(() => window.lekiwiPhysics.runtimeChecks()); expect(result.pythonTarget).toBe(0.1); expect(result.accepted.values['arm_shoulder_pan.pos']).toBe(0.3); expect(result.afterOldDispose).toBe(0.3); expect(result.manualBlocked).toBe(true); expect(result.held).toBeCloseTo(result.actual, 10); expect(result.paused?.enabled).toBe(false); expect(result.pausedOwner).toBe('manual'); expect(result.cancel).toBe('DISCONNECTED'); expect(result.reset?.modelEpoch).toBe(1); expect(result.reset?.paused).toBe(true); const reloaded = await page.evaluate(() => window.lekiwiPhysics.reimport()); expect(reloaded.nu).toBe(9); expect(reloaded.njnt).toBe(46); expect(reloaded.robot?.profileId).toBe('lekiwi-v1'); await page.evaluate(() => window.lekiwiPhysics.dispose()); });