@elizaos/plugin-xr 2.0.3-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/AGENTS.md +151 -0
  2. package/CLAUDE.md +151 -0
  3. package/LICENSE +21 -0
  4. package/README.md +106 -0
  5. package/package.json +57 -0
  6. package/simulator/bun.lock +159 -0
  7. package/simulator/package.json +28 -0
  8. package/simulator/src/emulator.ts +174 -0
  9. package/simulator/src/mock-agent.ts +233 -0
  10. package/simulator/src/node.ts +9 -0
  11. package/simulator/src/playwright-fixture.ts +169 -0
  12. package/simulator/src/types.ts +51 -0
  13. package/simulator/tsconfig.json +13 -0
  14. package/simulator/vite.config.ts +25 -0
  15. package/src/__tests__/audio-pipeline.test.ts +129 -0
  16. package/src/__tests__/protocol.test.ts +53 -0
  17. package/src/__tests__/routes-e2e.test.ts +276 -0
  18. package/src/__tests__/vision-pipeline.test.ts +73 -0
  19. package/src/__tests__/xr-bundle-coverage.test.ts +303 -0
  20. package/src/__tests__/xr-feature-parity.test.ts +524 -0
  21. package/src/__tests__/xr-functional-parity.test.ts +522 -0
  22. package/src/__tests__/xr-view-host-http.test.ts +239 -0
  23. package/src/__tests__/xr-view-host.test.ts +174 -0
  24. package/src/actions/xr-query-vision.ts +64 -0
  25. package/src/actions/xr-view-actions.ts +386 -0
  26. package/src/index.ts +55 -0
  27. package/src/protocol.ts +126 -0
  28. package/src/providers/xr-context.ts +49 -0
  29. package/src/routes/xr-connect.ts +89 -0
  30. package/src/routes/xr-simulator-route.ts +37 -0
  31. package/src/routes/xr-status.ts +36 -0
  32. package/src/routes/xr-view-host.ts +359 -0
  33. package/src/routes/xr-views.ts +43 -0
  34. package/src/services/audio-pipeline.ts +120 -0
  35. package/src/services/vision-pipeline.ts +57 -0
  36. package/src/services/xr-session-service.ts +388 -0
  37. package/tsconfig.build.json +9 -0
  38. package/tsconfig.json +30 -0
  39. package/vitest.config.ts +21 -0
@@ -0,0 +1,159 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "@elizaos/plugin-xr-simulator",
7
+ "dependencies": {
8
+ "iwer": "^2.1.1",
9
+ "ws": "^8.18.0",
10
+ },
11
+ "devDependencies": {
12
+ "@playwright/test": "^1.48.0",
13
+ "@types/node": "^25.0.3",
14
+ "@types/ws": "^8.5.10",
15
+ "typescript": "^5.5.0",
16
+ "vite": "^5.4.0",
17
+ },
18
+ },
19
+ },
20
+ "packages": {
21
+ "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
22
+
23
+ "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
24
+
25
+ "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="],
26
+
27
+ "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="],
28
+
29
+ "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="],
30
+
31
+ "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="],
32
+
33
+ "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="],
34
+
35
+ "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="],
36
+
37
+ "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="],
38
+
39
+ "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
40
+
41
+ "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="],
42
+
43
+ "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="],
44
+
45
+ "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="],
46
+
47
+ "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="],
48
+
49
+ "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="],
50
+
51
+ "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="],
52
+
53
+ "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="],
54
+
55
+ "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="],
56
+
57
+ "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="],
58
+
59
+ "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="],
60
+
61
+ "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="],
62
+
63
+ "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
64
+
65
+ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
66
+
67
+ "@playwright/test": ["@playwright/test@1.60.0", "", { "dependencies": { "playwright": "1.60.0" }, "bin": { "playwright": "cli.js" } }, "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag=="],
68
+
69
+ "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.4", "", { "os": "android", "cpu": "arm" }, "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ=="],
70
+
71
+ "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.4", "", { "os": "android", "cpu": "arm64" }, "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw=="],
72
+
73
+ "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA=="],
74
+
75
+ "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg=="],
76
+
77
+ "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g=="],
78
+
79
+ "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw=="],
80
+
81
+ "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.4", "", { "os": "linux", "cpu": "arm" }, "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA=="],
82
+
83
+ "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.4", "", { "os": "linux", "cpu": "arm" }, "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w=="],
84
+
85
+ "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg=="],
86
+
87
+ "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A=="],
88
+
89
+ "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ=="],
90
+
91
+ "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw=="],
92
+
93
+ "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg=="],
94
+
95
+ "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A=="],
96
+
97
+ "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA=="],
98
+
99
+ "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.4", "", { "os": "linux", "cpu": "none" }, "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw=="],
100
+
101
+ "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ=="],
102
+
103
+ "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ=="],
104
+
105
+ "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg=="],
106
+
107
+ "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA=="],
108
+
109
+ "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.4", "", { "os": "none", "cpu": "arm64" }, "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg=="],
110
+
111
+ "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw=="],
112
+
113
+ "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA=="],
114
+
115
+ "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw=="],
116
+
117
+ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.4", "", { "os": "win32", "cpu": "x64" }, "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw=="],
118
+
119
+ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
120
+
121
+ "@types/node": ["@types/node@25.9.0", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ=="],
122
+
123
+ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
124
+
125
+ "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
126
+
127
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
128
+
129
+ "gl-matrix": ["gl-matrix@3.4.4", "", {}, "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ=="],
130
+
131
+ "iwer": ["iwer@2.2.1", "", { "dependencies": { "gl-matrix": "^3.4.3", "webxr-layers-polyfill": "^1.1.0" } }, "sha512-TBGgUlOpYsJpYmBCDRrW2KDHC70XZeFrlOwlDNiso+dp83wwRyCLaeN50/qo4JRIKyn08+cD6iaw5+t+D7U+uQ=="],
132
+
133
+ "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
134
+
135
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
136
+
137
+ "playwright": ["playwright@1.60.0", "", { "dependencies": { "playwright-core": "1.60.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA=="],
138
+
139
+ "playwright-core": ["playwright-core@1.60.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA=="],
140
+
141
+ "postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="],
142
+
143
+ "rollup": ["rollup@4.60.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.4", "@rollup/rollup-android-arm64": "4.60.4", "@rollup/rollup-darwin-arm64": "4.60.4", "@rollup/rollup-darwin-x64": "4.60.4", "@rollup/rollup-freebsd-arm64": "4.60.4", "@rollup/rollup-freebsd-x64": "4.60.4", "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", "@rollup/rollup-linux-arm-musleabihf": "4.60.4", "@rollup/rollup-linux-arm64-gnu": "4.60.4", "@rollup/rollup-linux-arm64-musl": "4.60.4", "@rollup/rollup-linux-loong64-gnu": "4.60.4", "@rollup/rollup-linux-loong64-musl": "4.60.4", "@rollup/rollup-linux-ppc64-gnu": "4.60.4", "@rollup/rollup-linux-ppc64-musl": "4.60.4", "@rollup/rollup-linux-riscv64-gnu": "4.60.4", "@rollup/rollup-linux-riscv64-musl": "4.60.4", "@rollup/rollup-linux-s390x-gnu": "4.60.4", "@rollup/rollup-linux-x64-gnu": "4.60.4", "@rollup/rollup-linux-x64-musl": "4.60.4", "@rollup/rollup-openbsd-x64": "4.60.4", "@rollup/rollup-openharmony-arm64": "4.60.4", "@rollup/rollup-win32-arm64-msvc": "4.60.4", "@rollup/rollup-win32-ia32-msvc": "4.60.4", "@rollup/rollup-win32-x64-gnu": "4.60.4", "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g=="],
144
+
145
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
146
+
147
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
148
+
149
+ "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
150
+
151
+ "vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
152
+
153
+ "webxr-layers-polyfill": ["webxr-layers-polyfill@1.1.0", "", { "dependencies": { "gl-matrix": "^3.4.3" } }, "sha512-GqWE6IFlut8a1Lnh9t1RPnOXud1rZ7wLPvWp7mqTDOYtgorXqlNMhEnI9EqjU33grBx0v3jm0Oc13opkAdmgMQ=="],
154
+
155
+ "ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="],
156
+
157
+ "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
158
+ }
159
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@elizaos/plugin-xr-simulator",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "WebXR device emulator and Playwright fixtures for plugin-xr development and testing",
7
+ "main": "./src/node.ts",
8
+ "exports": {
9
+ ".": "./src/node.ts",
10
+ "./emulator": "./dist/emulator.js"
11
+ },
12
+ "scripts": {
13
+ "build": "vite build",
14
+ "build:watch": "vite build --watch",
15
+ "typecheck": "tsgo --noEmit"
16
+ },
17
+ "dependencies": {
18
+ "iwer": "^2.1.1",
19
+ "ws": "^8.18.0"
20
+ },
21
+ "devDependencies": {
22
+ "@playwright/test": "^1.48.0",
23
+ "@types/node": "^25.0.3",
24
+ "@types/ws": "^8.5.10",
25
+ "typescript": "^5.5.0",
26
+ "vite": "^8.0.13"
27
+ }
28
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * XR Emulator — browser-side IIFE injected by Playwright via page.addInitScript().
3
+ *
4
+ * What it does:
5
+ * 1. Installs IWER (immersive-web-emulation-runtime) to polyfill navigator.xr
6
+ * with a controllable Quest 3 device.
7
+ * 2. Overrides navigator.mediaDevices.getUserMedia to return:
8
+ * - Video: a canvas-captureStream() that Playwright can paint frames onto.
9
+ * - Audio: a synthetic silence stream (real audio comes via __xrTestHooks).
10
+ * 3. Exposes window.__XREmulator with a programmatic control API.
11
+ *
12
+ * Fork baseline: meta-quest/immersive-web-emulator
13
+ * Additions: camera frame injection, audio stream mock, __XREmulator control API.
14
+ *
15
+ * rawCameraAccess simulation:
16
+ * The experimental WebXR rawCameraAccess path (XRWebGLBinding.getCameraImage) is
17
+ * outside IWER's current emulation surface, so app-xr automatically falls back to the getUserMedia
18
+ * video track (Path 3). Injecting frames via __XREmulator.injectCameraFrame() paints
19
+ * onto the canvas that feeds getUserMedia, making injected frames reachable by both
20
+ * the getUserMedia path and any code that reads the canvas directly.
21
+ */
22
+
23
+ import { metaQuest3, XRDevice } from "iwer";
24
+ import type { EmulatorStats, XREmulatorAPI, XRPose } from "./types.ts";
25
+
26
+ // ── Camera canvas ─────────────────────────────────────────────────────────
27
+
28
+ const cameraCanvas = document.createElement("canvas");
29
+ cameraCanvas.width = 640;
30
+ cameraCanvas.height = 480;
31
+ const cameraCtx = cameraCanvas.getContext("2d")!;
32
+
33
+ // Fill with a recognisable test pattern (grey + crosshair)
34
+ function drawTestPattern(ctx: CanvasRenderingContext2D, w: number, h: number) {
35
+ ctx.fillStyle = "#333";
36
+ ctx.fillRect(0, 0, w, h);
37
+ ctx.strokeStyle = "#0f0";
38
+ ctx.lineWidth = 2;
39
+ ctx.beginPath();
40
+ ctx.moveTo(w / 2, 0);
41
+ ctx.lineTo(w / 2, h);
42
+ ctx.moveTo(0, h / 2);
43
+ ctx.lineTo(w, h / 2);
44
+ ctx.stroke();
45
+ ctx.fillStyle = "#0f0";
46
+ ctx.font = "16px monospace";
47
+ ctx.fillText("XR SIMULATOR", 12, 24);
48
+ }
49
+ drawTestPattern(cameraCtx, 640, 480);
50
+
51
+ const cameraStream = cameraCanvas.captureStream(30); // 30 fps canvas stream
52
+
53
+ // ── Audio stream (silence) ───────────────────────────────────────────────
54
+
55
+ function createSilentAudioStream(): MediaStream {
56
+ const ctx = new AudioContext();
57
+ const dest = ctx.createMediaStreamDestination();
58
+ // Connect a silent oscillator at 0 gain to keep the stream alive
59
+ const gain = ctx.createGain();
60
+ gain.gain.value = 0;
61
+ const osc = ctx.createOscillator();
62
+ osc.connect(gain);
63
+ gain.connect(dest);
64
+ osc.start();
65
+ return dest.stream;
66
+ }
67
+
68
+ let silentAudioStream: MediaStream | null = null;
69
+
70
+ // ── getUserMedia override ─────────────────────────────────────────────────
71
+
72
+ const _originalGetUserMedia = navigator.mediaDevices.getUserMedia.bind(
73
+ navigator.mediaDevices,
74
+ );
75
+
76
+ navigator.mediaDevices.getUserMedia = async (
77
+ constraints?: MediaStreamConstraints,
78
+ ): Promise<MediaStream> => {
79
+ const hasVideo = constraints?.video;
80
+ const hasAudio = constraints?.audio;
81
+
82
+ if (hasVideo && !hasAudio) {
83
+ // Camera-only: return our canvas stream
84
+ return cameraStream;
85
+ }
86
+
87
+ if (hasAudio && !hasVideo) {
88
+ // Mic-only: return synthetic silence
89
+ if (!silentAudioStream) silentAudioStream = createSilentAudioStream();
90
+ return silentAudioStream;
91
+ }
92
+
93
+ if (hasVideo && hasAudio) {
94
+ // Combined: merge both tracks into one MediaStream
95
+ if (!silentAudioStream) silentAudioStream = createSilentAudioStream();
96
+ const combined = new MediaStream([
97
+ ...cameraStream.getVideoTracks(),
98
+ ...silentAudioStream.getAudioTracks(),
99
+ ]);
100
+ return combined;
101
+ }
102
+
103
+ // Fallback for other constraint shapes
104
+ return _originalGetUserMedia(constraints);
105
+ };
106
+
107
+ // ── IWER XR device ────────────────────────────────────────────────────────
108
+
109
+ const xrDevice = new XRDevice(metaQuest3);
110
+ xrDevice.installRuntime();
111
+
112
+ // ── State ─────────────────────────────────────────────────────────────────
113
+
114
+ let framesInjected = 0;
115
+
116
+ // ── Control API ───────────────────────────────────────────────────────────
117
+
118
+ const api: XREmulatorAPI = {
119
+ setPose(pose: Partial<XRPose>) {
120
+ if (pose.position) {
121
+ xrDevice.position.set(pose.position.x, pose.position.y, pose.position.z);
122
+ }
123
+ if (pose.orientation) {
124
+ xrDevice.quaternion.set(
125
+ pose.orientation.x,
126
+ pose.orientation.y,
127
+ pose.orientation.z,
128
+ pose.orientation.w,
129
+ );
130
+ }
131
+ },
132
+
133
+ async injectCameraFrame(jpegDataUrl: string): Promise<void> {
134
+ // createImageBitmap is more reliable than new Image() in headless contexts
135
+ const resp = await fetch(jpegDataUrl);
136
+ const blob = await resp.blob();
137
+ const bmp = await createImageBitmap(blob);
138
+ cameraCtx.drawImage(bmp, 0, 0, cameraCanvas.width, cameraCanvas.height);
139
+ bmp.close();
140
+ framesInjected++;
141
+ },
142
+
143
+ getStats(): EmulatorStats {
144
+ const wsConnected =
145
+ typeof window.__xrTestHooks !== "undefined" &&
146
+ window.__xrTestHooks.getSocketState() === "OPEN";
147
+ return {
148
+ sessionActive: false, // updated below once session is active
149
+ framesInjected,
150
+ cameraStreamActive: cameraStream.active,
151
+ wsConnected,
152
+ };
153
+ },
154
+
155
+ simulateDisconnect() {
156
+ // Force-close the WebSocket so the reconnect logic kicks in
157
+ // The app exposes the socket via __xrTestHooks
158
+ if (window.__xrTestHooks) {
159
+ (
160
+ window as { __xrForceDisconnect?: () => void }
161
+ ).__xrForceDisconnect?.();
162
+ }
163
+ },
164
+
165
+ simulateReconnect() {
166
+ (
167
+ window as { __xrForceReconnect?: () => void }
168
+ ).__xrForceReconnect?.();
169
+ },
170
+ };
171
+
172
+ window.__XREmulator = api;
173
+
174
+ console.info("[XR Emulator] installed — navigator.xr:", !!navigator.xr);
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Lightweight WebSocket server that simulates plugin-xr's XRSessionService.
3
+ * Used in Playwright tests — starts a real ws server on a configurable port,
4
+ * records all received binary frames, and lets tests script responses.
5
+ */
6
+
7
+ import { WebSocket, WebSocketServer } from "ws";
8
+ import {
9
+ decodeBinaryFrame,
10
+ encodeBinaryFrame,
11
+ type XRBinaryHeader,
12
+ type XRClientControl,
13
+ type XRTTSAudioHeader,
14
+ } from "../../src/protocol.ts";
15
+
16
+ export interface ReceivedFrame {
17
+ header: XRBinaryHeader | XRTTSAudioHeader;
18
+ payload: Buffer;
19
+ receivedAt: number;
20
+ }
21
+
22
+ export interface ReceivedControl {
23
+ message: XRClientControl;
24
+ receivedAt: number;
25
+ }
26
+
27
+ export interface MockAgentServerOptions {
28
+ port?: number;
29
+ /** Automatically send a transcript response after receiving N audio frames */
30
+ autoTranscriptAfterFrames?: number;
31
+ autoTranscriptText?: string;
32
+ }
33
+
34
+ export class MockAgentServer {
35
+ private wss: WebSocketServer | null = null;
36
+ private client: WebSocket | null = null;
37
+
38
+ readonly receivedFrames: ReceivedFrame[] = [];
39
+ readonly receivedControls: ReceivedControl[] = [];
40
+ private waiters = new Map<string, Array<() => void>>();
41
+
42
+ constructor(private readonly options: MockAgentServerOptions = {}) {}
43
+
44
+ get port(): number {
45
+ return this.options.port ?? 31338;
46
+ }
47
+
48
+ async start(): Promise<void> {
49
+ return new Promise((resolve, reject) => {
50
+ this.wss = new WebSocketServer({ port: this.port });
51
+ this.wss.on("listening", resolve);
52
+ this.wss.on("error", reject);
53
+ this.wss.on("connection", (ws) => this.onClient(ws));
54
+ });
55
+ }
56
+
57
+ async stop(): Promise<void> {
58
+ this.client?.close();
59
+ this.client = null;
60
+ return new Promise((resolve) => this.wss?.close(() => resolve()));
61
+ }
62
+
63
+ // ── Sending responses ──────────────────────────────────────────────────
64
+
65
+ sendTranscript(text: string, final = true): void {
66
+ this.sendText({ type: "transcript", text, final });
67
+ }
68
+
69
+ sendAgentText(text: string): void {
70
+ this.sendText({ type: "agent_text", text });
71
+ }
72
+
73
+ sendTTSAudio(audio: Buffer, sampleRate = 24000): void {
74
+ if (!this.client || this.client.readyState !== WebSocket.OPEN) return;
75
+ const header: XRTTSAudioHeader = {
76
+ type: "tts_audio",
77
+ sampleRate,
78
+ channels: 1,
79
+ encoding: "mp3",
80
+ };
81
+ this.client.send(encodeBinaryFrame(header, audio), { binary: true });
82
+ }
83
+
84
+ // ── Waiting helpers ────────────────────────────────────────────────────
85
+
86
+ /** Resolves when the device has connected and sent a 'hello' message.
87
+ * Resolves immediately if 'hello' was already received (handles the race
88
+ * where the connection fires before the waiter is registered). */
89
+ waitForConnection(timeoutMs = 5000): Promise<void> {
90
+ if (this.receivedControls.some((c) => c.message.type === "hello")) {
91
+ return Promise.resolve();
92
+ }
93
+ return this.waitFor("connected", timeoutMs);
94
+ }
95
+
96
+ /** Resolves when at least one audio binary frame has been received. */
97
+ waitForAudioFrame(timeoutMs = 10000): Promise<ReceivedFrame> {
98
+ return this.waitForFrame("audio", timeoutMs);
99
+ }
100
+
101
+ /** Resolves when at least one camera binary frame has been received. */
102
+ waitForCameraFrame(timeoutMs = 10000): Promise<ReceivedFrame> {
103
+ return this.waitForFrame("frame", timeoutMs);
104
+ }
105
+
106
+ audioFrames(): ReceivedFrame[] {
107
+ return this.receivedFrames.filter((f) => f.header.type === "audio");
108
+ }
109
+
110
+ cameraFrames(): ReceivedFrame[] {
111
+ return this.receivedFrames.filter((f) => f.header.type === "frame");
112
+ }
113
+
114
+ reset(): void {
115
+ this.receivedFrames.length = 0;
116
+ this.receivedControls.length = 0;
117
+ }
118
+
119
+ // ── Private ────────────────────────────────────────────────────────────
120
+
121
+ private onClient(ws: WebSocket): void {
122
+ this.client = ws;
123
+
124
+ ws.on("message", (data, isBinary) => {
125
+ if (isBinary) {
126
+ this.handleBinary(data as Buffer);
127
+ } else {
128
+ this.handleText(ws, data.toString("utf8"));
129
+ }
130
+ });
131
+
132
+ ws.on("close", () => {
133
+ if (this.client === ws) this.client = null;
134
+ });
135
+ }
136
+
137
+ private handleText(ws: WebSocket, raw: string): void {
138
+ try {
139
+ const msg = JSON.parse(raw) as XRClientControl;
140
+ this.receivedControls.push({ message: msg, receivedAt: Date.now() });
141
+
142
+ if (msg.type === "hello") {
143
+ // Send ready
144
+ ws.send(JSON.stringify({ type: "ready", sessionId: "mock-session" }));
145
+ this.notify("connected");
146
+ }
147
+ if (msg.type === "ping") {
148
+ ws.send(JSON.stringify({ type: "pong" }));
149
+ }
150
+ } catch {}
151
+ }
152
+
153
+ private handleBinary(data: Buffer): void {
154
+ try {
155
+ const { header, payload } = decodeBinaryFrame(data);
156
+ const frame: ReceivedFrame = { header, payload, receivedAt: Date.now() };
157
+ this.receivedFrames.push(frame);
158
+ this.notify(`frame:${header.type}`);
159
+
160
+ // Auto-transcript after N audio frames
161
+ const { autoTranscriptAfterFrames, autoTranscriptText } = this.options;
162
+ if (
163
+ header.type === "audio" &&
164
+ autoTranscriptAfterFrames !== undefined &&
165
+ this.audioFrames().length >= autoTranscriptAfterFrames
166
+ ) {
167
+ const text = autoTranscriptText ?? "test transcript";
168
+ setTimeout(() => {
169
+ this.sendTranscript(text);
170
+ this.sendAgentText(`Agent response to: ${text}`);
171
+ }, 50);
172
+ }
173
+ } catch {}
174
+ }
175
+
176
+ private sendText(msg: object): void {
177
+ if (!this.client || this.client.readyState !== WebSocket.OPEN) return;
178
+ this.client.send(JSON.stringify(msg));
179
+ }
180
+
181
+ private waitFor(event: string, timeoutMs: number): Promise<void> {
182
+ return new Promise((resolve, reject) => {
183
+ const timer = setTimeout(
184
+ () =>
185
+ reject(new Error(`MockAgentServer: timeout waiting for "${event}"`)),
186
+ timeoutMs,
187
+ );
188
+ const list = this.waiters.get(event) ?? [];
189
+ list.push(() => {
190
+ clearTimeout(timer);
191
+ resolve();
192
+ });
193
+ this.waiters.set(event, list);
194
+ });
195
+ }
196
+
197
+ private waitForFrame(
198
+ type: string,
199
+ timeoutMs: number,
200
+ ): Promise<ReceivedFrame> {
201
+ // Check if already received
202
+ const existing = this.receivedFrames.find((f) => f.header.type === type);
203
+ if (existing) return Promise.resolve(existing);
204
+
205
+ return new Promise((resolve, reject) => {
206
+ const timer = setTimeout(
207
+ () =>
208
+ reject(
209
+ new Error(
210
+ `MockAgentServer: timeout waiting for frame type "${type}"`,
211
+ ),
212
+ ),
213
+ timeoutMs,
214
+ );
215
+ const event = `frame:${type}`;
216
+ const list = this.waiters.get(event) ?? [];
217
+ list.push(() => {
218
+ clearTimeout(timer);
219
+ const frame = [...this.receivedFrames].reverse().find(
220
+ (f: ReceivedFrame) => f.header.type === type,
221
+ )!;
222
+ resolve(frame);
223
+ });
224
+ this.waiters.set(event, list);
225
+ });
226
+ }
227
+
228
+ private notify(event: string): void {
229
+ const list = this.waiters.get(event) ?? [];
230
+ this.waiters.delete(event);
231
+ for (const cb of list) cb();
232
+ }
233
+ }
@@ -0,0 +1,9 @@
1
+ // Node-side entry point — exports the Playwright fixture and mock server.
2
+ // Do not import this from browser code.
3
+ export {
4
+ expect,
5
+ MockAgentServer,
6
+ test,
7
+ XREmulatorPage,
8
+ } from "./playwright-fixture.ts";
9
+ export type { EmulatorStats, XRPose } from "./types.ts";