@webspatial/core-sdk 1.1.0 → 1.2.0

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.
@@ -0,0 +1,1060 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+ import { composeSRT, parseCornerRadius } from './utils'
3
+ import {
4
+ CommandResultFailure,
5
+ CommandResultSuccess,
6
+ } from './platform-adapter/CommandResultUtils'
7
+ import { SpatialWebEvent } from './SpatialWebEvent'
8
+ import { createSpatialEvent } from './SpatialWebEventCreator'
9
+ import {
10
+ isValidBaseplateVisibilityType,
11
+ isValidSceneUnit,
12
+ isValidSpatialSceneType,
13
+ isValidWorldAlignmentType,
14
+ isValidWorldScalingType,
15
+ } from './types/types'
16
+
17
+ describe('utils', () => {
18
+ it('parseCornerRadius parses px and percent values', () => {
19
+ const computedStyle = {
20
+ getPropertyValue: (prop: string) => {
21
+ if (prop === 'width') return '200px'
22
+ if (prop === 'border-top-left-radius') return '10px'
23
+ if (prop === 'border-top-right-radius') return '5%'
24
+ if (prop === 'border-bottom-left-radius') return ''
25
+ if (prop === 'border-bottom-right-radius') return '20'
26
+ return ''
27
+ },
28
+ } as unknown as CSSStyleDeclaration
29
+
30
+ expect(parseCornerRadius(computedStyle)).toEqual({
31
+ topLeading: 10,
32
+ topTrailing: 10,
33
+ bottomLeading: 0,
34
+ bottomTrailing: 20,
35
+ })
36
+ })
37
+
38
+ it('composeSRT composes translate and scale in expected order', () => {
39
+ if (!(globalThis as any).DOMMatrix) {
40
+ class DOMMatrixPolyfill {
41
+ private tx = 0
42
+ private ty = 0
43
+ private tz = 0
44
+ private sx = 1
45
+ private sy = 1
46
+ private sz = 1
47
+
48
+ translate(x = 0, y = 0, z = 0) {
49
+ this.tx += x
50
+ this.ty += y
51
+ this.tz += z
52
+ return this
53
+ }
54
+
55
+ rotate() {
56
+ return this
57
+ }
58
+
59
+ scale(x = 1, y = 1, z = 1) {
60
+ this.sx *= x
61
+ this.sy *= y
62
+ this.sz *= z
63
+ return this
64
+ }
65
+
66
+ transformPoint(p: { x: number; y: number; z?: number }) {
67
+ return {
68
+ x: p.x * this.sx + this.tx,
69
+ y: p.y * this.sy + this.ty,
70
+ z: (p.z ?? 0) * this.sz + this.tz,
71
+ }
72
+ }
73
+ }
74
+ ;(globalThis as any).DOMMatrix = DOMMatrixPolyfill
75
+ }
76
+
77
+ const m = composeSRT(
78
+ { x: 10, y: 20, z: 30 },
79
+ { x: 0, y: 0, z: 0 },
80
+ { x: 2, y: 3, z: 4 },
81
+ )
82
+
83
+ const origin = m.transformPoint({ x: 0, y: 0, z: 0 })
84
+ expect(origin).toEqual({
85
+ x: 10,
86
+ y: 20,
87
+ z: 30,
88
+ })
89
+
90
+ const p = m.transformPoint({ x: 1, y: 1, z: 1 })
91
+ expect(p).toEqual({ x: 12, y: 23, z: 34 })
92
+ })
93
+ })
94
+
95
+ describe('types validators', () => {
96
+ it('validates enum-like string unions', () => {
97
+ expect(isValidBaseplateVisibilityType('automatic')).toBe(true)
98
+ expect(isValidBaseplateVisibilityType('nope')).toBe(false)
99
+
100
+ expect(isValidWorldScalingType('dynamic')).toBe(true)
101
+ expect(isValidWorldScalingType('nope')).toBe(false)
102
+
103
+ expect(isValidWorldAlignmentType('gravityAligned')).toBe(true)
104
+ expect(isValidWorldAlignmentType('nope')).toBe(false)
105
+
106
+ expect(isValidSpatialSceneType('window')).toBe(true)
107
+ expect(isValidSpatialSceneType('nope')).toBe(false)
108
+ })
109
+
110
+ it('validates scene units', () => {
111
+ expect(isValidSceneUnit(0)).toBe(true)
112
+ expect(isValidSceneUnit(-1)).toBe(false)
113
+ expect(isValidSceneUnit('10px')).toBe(true)
114
+ expect(isValidSceneUnit('10m')).toBe(true)
115
+ expect(isValidSceneUnit('10cm')).toBe(false)
116
+ expect(isValidSceneUnit('px')).toBe(true)
117
+ expect(isValidSceneUnit('apx')).toBe(false)
118
+ })
119
+ })
120
+
121
+ describe('CommandResultUtils', () => {
122
+ it('creates success and failure results', () => {
123
+ expect(CommandResultSuccess({ a: 1 })).toEqual({
124
+ success: true,
125
+ data: { a: 1 },
126
+ errorCode: '',
127
+ errorMessage: '',
128
+ })
129
+
130
+ expect(CommandResultFailure('E_TEST', 'bad')).toEqual({
131
+ success: false,
132
+ data: undefined,
133
+ errorCode: 'E_TEST',
134
+ errorMessage: 'bad',
135
+ })
136
+ })
137
+ })
138
+
139
+ describe('SpatialWebEvent', () => {
140
+ it('dispatches to registered receiver', () => {
141
+ SpatialWebEvent.init()
142
+ const cb = vi.fn()
143
+ SpatialWebEvent.addEventReceiver('id1', cb)
144
+
145
+ window.__SpatialWebEvent({ id: 'id1', data: { ok: true } })
146
+ expect(cb).toHaveBeenCalledWith({ ok: true })
147
+
148
+ SpatialWebEvent.removeEventReceiver('id1')
149
+ window.__SpatialWebEvent({ id: 'id1', data: { ok: false } })
150
+ expect(cb).toHaveBeenCalledTimes(1)
151
+ })
152
+ })
153
+
154
+ describe('SpatialWebEventCreator', () => {
155
+ it('creates a bubbling custom event with detail', () => {
156
+ const ev = createSpatialEvent('spatialmsg' as any, { x: 1 })
157
+ expect(ev.type).toBe('spatialmsg')
158
+ expect(ev.bubbles).toBe(true)
159
+ expect(ev.cancelable).toBe(false)
160
+ expect(ev.detail).toEqual({ x: 1 })
161
+ })
162
+ })
163
+
164
+ describe('SpatialObject', () => {
165
+ afterEach(() => {
166
+ vi.resetModules()
167
+ vi.clearAllMocks()
168
+ vi.unmock('./JSBCommand')
169
+ })
170
+
171
+ it('inspect returns data when command succeeds', async () => {
172
+ vi.doMock('./JSBCommand', () => {
173
+ return {
174
+ InspectCommand: vi.fn().mockImplementation(() => ({
175
+ execute: vi.fn().mockResolvedValue({
176
+ success: true,
177
+ data: { a: 1 },
178
+ errorMessage: '',
179
+ }),
180
+ })),
181
+ DestroyCommand: vi.fn().mockImplementation(() => ({
182
+ execute: vi.fn().mockResolvedValue({
183
+ success: true,
184
+ data: undefined,
185
+ errorMessage: '',
186
+ }),
187
+ })),
188
+ }
189
+ })
190
+
191
+ const { SpatialObject } = await import('./SpatialObject')
192
+ const obj = new SpatialObject('id')
193
+ await expect(obj.inspect()).resolves.toEqual({ a: 1 })
194
+ })
195
+
196
+ it('inspect throws when command fails', async () => {
197
+ vi.doMock('./JSBCommand', () => {
198
+ return {
199
+ InspectCommand: vi.fn().mockImplementation(() => ({
200
+ execute: vi.fn().mockResolvedValue({
201
+ success: false,
202
+ data: undefined,
203
+ errorMessage: 'nope',
204
+ }),
205
+ })),
206
+ DestroyCommand: vi.fn().mockImplementation(() => ({
207
+ execute: vi.fn().mockResolvedValue({
208
+ success: true,
209
+ data: undefined,
210
+ errorMessage: '',
211
+ }),
212
+ })),
213
+ }
214
+ })
215
+
216
+ const { SpatialObject } = await import('./SpatialObject')
217
+ const obj = new SpatialObject('id')
218
+ await expect(obj.inspect()).rejects.toThrow('nope')
219
+ })
220
+
221
+ it('destroy calls onDestroy once and is idempotent', async () => {
222
+ const onDestroy = vi.fn()
223
+ vi.doMock('./JSBCommand', () => {
224
+ return {
225
+ InspectCommand: vi.fn().mockImplementation(() => ({
226
+ execute: vi.fn().mockResolvedValue({
227
+ success: true,
228
+ data: undefined,
229
+ errorMessage: '',
230
+ }),
231
+ })),
232
+ DestroyCommand: vi.fn().mockImplementation(() => ({
233
+ execute: vi.fn().mockResolvedValue({
234
+ success: true,
235
+ data: { ok: true },
236
+ errorMessage: '',
237
+ }),
238
+ })),
239
+ }
240
+ })
241
+
242
+ const { SpatialObject } = await import('./SpatialObject')
243
+ class TestObject extends SpatialObject {
244
+ protected onDestroy() {
245
+ onDestroy()
246
+ }
247
+ }
248
+
249
+ const obj = new TestObject('id')
250
+ await expect(obj.destroy()).resolves.toEqual({ ok: true })
251
+ expect(obj.isDestroyed).toBe(true)
252
+ expect(onDestroy).toHaveBeenCalledTimes(1)
253
+ await expect(obj.destroy()).resolves.toBeUndefined()
254
+ expect(onDestroy).toHaveBeenCalledTimes(1)
255
+ })
256
+ })
257
+
258
+ describe('platform adapters', () => {
259
+ afterEach(() => {
260
+ vi.useRealTimers()
261
+ vi.resetModules()
262
+ vi.clearAllMocks()
263
+ vi.unmock('./JSBCommand')
264
+ })
265
+
266
+ // it('AndroidPlatform.callJSB resolves from async SpatialWebEvent callback', async () => {
267
+ // const postMessage = vi.fn((rId: string) => {
268
+ // expect(rId.startsWith('rId_')).toBe(true)
269
+ // return ''
270
+ // })
271
+ // ;(window as any).webspatialBridge = { postMessage }
272
+
273
+ // const { AndroidPlatform } = await import(
274
+ // './platform-adapter/android/AndroidPlatform'
275
+ // )
276
+ // const { SpatialWebEvent: SpatialWebEventInstance } = await import(
277
+ // './SpatialWebEvent'
278
+ // )
279
+ // const platform = new AndroidPlatform()
280
+ // const p = platform.callJSB('c', '{"a":1}')
281
+
282
+ // const rId = postMessage.mock.calls[0]?.[0] as string
283
+ // SpatialWebEventInstance.eventReceiver[rId]({
284
+ // success: true,
285
+ // data: { ok: true },
286
+ // })
287
+
288
+ // await expect(p).resolves.toEqual({
289
+ // success: true,
290
+ // data: { ok: true },
291
+ // errorCode: '',
292
+ // errorMessage: '',
293
+ // })
294
+ // })
295
+
296
+ // it('AndroidPlatform.callJSB handles sync bridge response and failures', async () => {
297
+ // ;(window as any).webspatialBridge = {
298
+ // postMessage: vi.fn(() =>
299
+ // JSON.stringify({
300
+ // success: false,
301
+ // data: { code: 'E_SYNC', message: 'bad' },
302
+ // }),
303
+ // ),
304
+ // }
305
+
306
+ // const { AndroidPlatform } = await import(
307
+ // './platform-adapter/android/AndroidPlatform'
308
+ // )
309
+ // const platform = new AndroidPlatform()
310
+ // await expect(platform.callJSB('c', '{}')).resolves.toEqual({
311
+ // success: false,
312
+ // data: undefined,
313
+ // errorCode: 'E_SYNC',
314
+ // errorMessage: 'bad',
315
+ // })
316
+ // })
317
+
318
+ // it('AndroidPlatform.callWebSpatialProtocol polls and returns injected SpatialId', async () => {
319
+ // vi.useFakeTimers()
320
+
321
+ // let canCount = 0
322
+ // vi.doMock('./JSBCommand', () => {
323
+ // return {
324
+ // CheckWebViewCanCreateCommand: vi.fn().mockImplementation(() => ({
325
+ // execute: vi.fn().mockImplementation(() => {
326
+ // canCount += 1
327
+ // return Promise.resolve({
328
+ // success: true,
329
+ // data: { can: canCount >= 2 },
330
+ // errorCode: '',
331
+ // errorMessage: '',
332
+ // })
333
+ // }),
334
+ // })),
335
+ // }
336
+ // })
337
+
338
+ // const windowProxy: any = {}
339
+ // const openFn = vi.fn()
340
+ // ;(window as any).open = vi.fn(() => windowProxy)
341
+
342
+ // setTimeout(() => {
343
+ // windowProxy.open = openFn
344
+ // }, 20)
345
+
346
+ // const { AndroidPlatform } = await import(
347
+ // './platform-adapter/android/AndroidPlatform'
348
+ // )
349
+ // const { SpatialWebEvent: SpatialWebEventInstance } = await import(
350
+ // './SpatialWebEvent'
351
+ // )
352
+ // const platform = new AndroidPlatform()
353
+ // const p = platform.callWebSpatialProtocol('open', 'x=1', '_blank', '')
354
+
355
+ // const receiverIds = Object.keys(SpatialWebEventInstance.eventReceiver)
356
+ // const createdId = receiverIds[0] as string
357
+ // const loadedId = receiverIds[1] as string
358
+ // SpatialWebEventInstance.eventReceiver[createdId]?.({ spatialId: 'temp' })
359
+ // await vi.advanceTimersByTimeAsync(100)
360
+ // SpatialWebEventInstance.eventReceiver[loadedId]?.({
361
+ // spatialId: 'spatial-1',
362
+ // })
363
+
364
+ // await vi.advanceTimersByTimeAsync(200)
365
+
366
+ // const result = await p
367
+ // expect(result.success).toBe(true)
368
+ // expect(result.data.id).toBe('spatial-1')
369
+ // expect(result.data.windowProxy).toBe(windowProxy)
370
+ // const call = openFn.mock.calls[0] as unknown as [string, string | undefined]
371
+ // expect(call?.[0]).toMatch(/^about:blank\?rid=/)
372
+ // expect(call?.[1]).toBe('_self')
373
+ // })
374
+
375
+ it('SSRPlatform returns successful no-op results', async () => {
376
+ const { SSRPlatform } = await import('./platform-adapter/ssr/SSRPlatform')
377
+ const platform = new SSRPlatform()
378
+
379
+ await expect(platform.callJSB('c', '{}')).resolves.toMatchObject({
380
+ success: true,
381
+ })
382
+ await expect(
383
+ platform.callWebSpatialProtocol('s', '', '', ''),
384
+ ).resolves.toMatchObject({
385
+ success: true,
386
+ })
387
+ expect(platform.callWebSpatialProtocolSync('s', '', '', '')).toMatchObject({
388
+ success: true,
389
+ })
390
+ })
391
+
392
+ it('VisionOSPlatform.callJSB returns success and parses failures', async () => {
393
+ ;(window as any).webkit = {
394
+ messageHandlers: {
395
+ bridge: {
396
+ postMessage: vi
397
+ .fn()
398
+ .mockResolvedValueOnce({ a: 1 })
399
+ .mockRejectedValueOnce({
400
+ message: JSON.stringify({ code: 'E_VOS', message: 'nope' }),
401
+ }),
402
+ },
403
+ },
404
+ }
405
+
406
+ const { VisionOSPlatform } = await import(
407
+ './platform-adapter/vision-os/VisionOSPlatform'
408
+ )
409
+ const platform = new VisionOSPlatform()
410
+
411
+ await expect(platform.callJSB('c', '{}')).resolves.toEqual({
412
+ success: true,
413
+ data: { a: 1 },
414
+ errorCode: '',
415
+ errorMessage: '',
416
+ })
417
+
418
+ await expect(platform.callJSB('c', '{}')).resolves.toEqual({
419
+ success: false,
420
+ data: undefined,
421
+ errorCode: 'E_VOS',
422
+ errorMessage: 'nope',
423
+ })
424
+ })
425
+
426
+ it('VisionOSPlatform parses SpatialId from userAgent', async () => {
427
+ const uuid = '12345678-1234-1234-1234-1234567890ab'
428
+ const windowProxy: any = {
429
+ navigator: { userAgent: `x ${uuid} y` },
430
+ }
431
+ ;(window as any).open = vi.fn(() => windowProxy)
432
+ ;(window as any).webkit = {
433
+ messageHandlers: { bridge: { postMessage: vi.fn() } },
434
+ }
435
+
436
+ const { VisionOSPlatform } = await import(
437
+ './platform-adapter/vision-os/VisionOSPlatform'
438
+ )
439
+ const platform = new VisionOSPlatform()
440
+ const r = await platform.callWebSpatialProtocol('open', 'a=1')
441
+
442
+ expect(r.success).toBe(true)
443
+ expect(r.data.id).toBe(uuid)
444
+ expect(r.data.windowProxy).toBe(windowProxy)
445
+ })
446
+ })
447
+
448
+ describe('geometries', () => {
449
+ it('constructs cone/cylinder/plane/sphere geometries', async () => {
450
+ const { SpatialConeGeometry } = await import(
451
+ './reality/geometry/SpatialConeGeometry'
452
+ )
453
+ const { SpatialCylinderGeometry } = await import(
454
+ './reality/geometry/SpatialCylinderGeometry'
455
+ )
456
+ const { SpatialPlaneGeometry } = await import(
457
+ './reality/geometry/SpatialPlaneGeometry'
458
+ )
459
+ const { SpatialSphereGeometry } = await import(
460
+ './reality/geometry/SpatialSphereGeometry'
461
+ )
462
+
463
+ const cone = new SpatialConeGeometry('c1', { radius: 1, height: 2 })
464
+ const cylinder = new SpatialCylinderGeometry('cy1', {
465
+ radius: 1,
466
+ height: 2,
467
+ })
468
+ const plane = new SpatialPlaneGeometry('p1', { width: 1 })
469
+ const sphere = new SpatialSphereGeometry('s1', { radius: 1 })
470
+
471
+ expect(SpatialConeGeometry.type).toBe('ConeGeometry')
472
+ expect(SpatialCylinderGeometry.type).toBe('CylinderGeometry')
473
+ expect(SpatialPlaneGeometry.type).toBe('PlaneGeometry')
474
+ expect(SpatialSphereGeometry.type).toBe('SphereGeometry')
475
+
476
+ expect(cone.id).toBe('c1')
477
+ expect(cylinder.id).toBe('cy1')
478
+ expect(plane.id).toBe('p1')
479
+ expect(sphere.id).toBe('s1')
480
+ })
481
+ })
482
+
483
+ describe('spatialWindowPolyfill', () => {
484
+ afterEach(() => {
485
+ vi.resetModules()
486
+ vi.clearAllMocks()
487
+ vi.unmock('./Spatial')
488
+ })
489
+
490
+ it('returns early when not running in SpatialWeb', async () => {
491
+ vi.doMock('./Spatial', () => {
492
+ return {
493
+ Spatial: class {
494
+ runInSpatialWeb() {
495
+ return false
496
+ }
497
+ requestSession() {
498
+ throw new Error('should not be called')
499
+ }
500
+ },
501
+ }
502
+ })
503
+
504
+ const { spatialWindowPolyfill } = await import('./spatial-window-polyfill')
505
+ await expect(spatialWindowPolyfill()).resolves.toBeUndefined()
506
+ })
507
+
508
+ it('updates scene properties and reacts to background material changes', async () => {
509
+ const updateSpatialProperties = vi.fn()
510
+ const mockSession: any = {
511
+ getSpatialScene: () => ({
512
+ updateSpatialProperties,
513
+ }),
514
+ }
515
+
516
+ vi.doMock('./Spatial', () => {
517
+ return {
518
+ Spatial: class {
519
+ runInSpatialWeb() {
520
+ return true
521
+ }
522
+ requestSession() {
523
+ return mockSession
524
+ }
525
+ },
526
+ }
527
+ })
528
+
529
+ Object.defineProperty(document, 'readyState', {
530
+ value: 'complete',
531
+ configurable: true,
532
+ })
533
+
534
+ document.documentElement.style.width = '200px'
535
+ document.documentElement.style.setProperty(
536
+ '--xr-background-material',
537
+ 'translucent',
538
+ )
539
+ document.documentElement.style.setProperty('border-top-left-radius', '10px')
540
+ document.documentElement.style.setProperty('border-top-right-radius', '5%')
541
+ document.documentElement.style.opacity = '0.5'
542
+
543
+ const { spatialWindowPolyfill } = await import('./spatial-window-polyfill')
544
+ await spatialWindowPolyfill()
545
+
546
+ expect(updateSpatialProperties).toHaveBeenCalledWith({
547
+ material: 'translucent',
548
+ })
549
+ expect(updateSpatialProperties).toHaveBeenCalledWith({
550
+ cornerRadius: {
551
+ topLeading: 10,
552
+ topTrailing: 10,
553
+ bottomLeading: 0,
554
+ bottomTrailing: 0,
555
+ },
556
+ })
557
+ expect(updateSpatialProperties).toHaveBeenCalledWith({
558
+ opacity: 0.5,
559
+ })
560
+
561
+ document.documentElement.style.setProperty(
562
+ '--xr-background-material',
563
+ 'regular',
564
+ )
565
+ expect(updateSpatialProperties).toHaveBeenCalledWith({
566
+ material: 'regular',
567
+ })
568
+
569
+ document.documentElement.style.removeProperty('--xr-background-material')
570
+ expect(updateSpatialProperties).toHaveBeenCalledWith({
571
+ material: 'none',
572
+ })
573
+ })
574
+ })
575
+
576
+ describe('SpatializedElementCreator', () => {
577
+ afterEach(() => {
578
+ vi.resetModules()
579
+ vi.clearAllMocks()
580
+ vi.unmock('./JSBCommand')
581
+ })
582
+
583
+ it('createSpatialized2DElement sets base href and returns element', async () => {
584
+ const windowProxy: any = {
585
+ document: { head: { innerHTML: '' } },
586
+ }
587
+
588
+ vi.doMock('./JSBCommand', () => {
589
+ class OkCommand {
590
+ execute() {
591
+ return Promise.resolve({
592
+ success: true,
593
+ data: undefined,
594
+ errorCode: '',
595
+ errorMessage: '',
596
+ })
597
+ }
598
+ }
599
+
600
+ return {
601
+ InspectCommand: OkCommand,
602
+ DestroyCommand: OkCommand,
603
+ UpdateSpatializedElementTransform: OkCommand,
604
+ UpdateSpatialized2DElementProperties: OkCommand,
605
+ AddSpatializedElementToSpatialized2DElement: OkCommand,
606
+ UpdateSpatializedStatic3DElementProperties: OkCommand,
607
+ UpdateSpatializedDynamic3DElementProperties: OkCommand,
608
+ SetParentForEntityCommand: OkCommand,
609
+ AddEntityToDynamic3DCommand: OkCommand,
610
+ createSpatialized2DElementCommand: vi.fn().mockImplementation(() => ({
611
+ execute: vi.fn().mockResolvedValue({
612
+ success: true,
613
+ data: { id: 'w1', windowProxy },
614
+ errorCode: '',
615
+ errorMessage: '',
616
+ }),
617
+ })),
618
+ CreateSpatializedStatic3DElementCommand: vi
619
+ .fn()
620
+ .mockImplementation(() => ({
621
+ execute: vi.fn().mockResolvedValue({
622
+ success: true,
623
+ data: { id: 's-default' },
624
+ errorCode: '',
625
+ errorMessage: '',
626
+ }),
627
+ })),
628
+ CreateSpatializedDynamic3DElementCommand: vi
629
+ .fn()
630
+ .mockImplementation(() => ({
631
+ execute: vi.fn().mockResolvedValue({
632
+ success: true,
633
+ data: { id: 'd-default' },
634
+ errorCode: '',
635
+ errorMessage: '',
636
+ }),
637
+ })),
638
+ }
639
+ })
640
+
641
+ const { createSpatialized2DElement } = await import(
642
+ './SpatializedElementCreator'
643
+ )
644
+ const el = await createSpatialized2DElement()
645
+
646
+ expect(el.id).toBe('w1')
647
+ expect(windowProxy.document.head.innerHTML).toContain('<base href="')
648
+ expect(windowProxy.document.head.innerHTML).toContain(document.baseURI)
649
+ })
650
+
651
+ it('createSpatialized2DElement throws when command fails', async () => {
652
+ vi.doMock('./JSBCommand', () => {
653
+ class OkCommand {
654
+ execute() {
655
+ return Promise.resolve({
656
+ success: true,
657
+ data: undefined,
658
+ errorCode: '',
659
+ errorMessage: '',
660
+ })
661
+ }
662
+ }
663
+
664
+ return {
665
+ InspectCommand: OkCommand,
666
+ DestroyCommand: OkCommand,
667
+ UpdateSpatializedElementTransform: OkCommand,
668
+ UpdateSpatialized2DElementProperties: OkCommand,
669
+ AddSpatializedElementToSpatialized2DElement: OkCommand,
670
+ UpdateSpatializedStatic3DElementProperties: OkCommand,
671
+ UpdateSpatializedDynamic3DElementProperties: OkCommand,
672
+ SetParentForEntityCommand: OkCommand,
673
+ AddEntityToDynamic3DCommand: OkCommand,
674
+ createSpatialized2DElementCommand: vi.fn().mockImplementation(() => ({
675
+ execute: vi.fn().mockResolvedValue({
676
+ success: false,
677
+ data: undefined,
678
+ errorCode: 'E',
679
+ errorMessage: 'bad',
680
+ }),
681
+ })),
682
+ CreateSpatializedStatic3DElementCommand: vi.fn(),
683
+ CreateSpatializedDynamic3DElementCommand: vi.fn(),
684
+ }
685
+ })
686
+
687
+ const { createSpatialized2DElement } = await import(
688
+ './SpatializedElementCreator'
689
+ )
690
+ await expect(createSpatialized2DElement()).rejects.toThrow(
691
+ 'createSpatialized2DElement failed',
692
+ )
693
+ })
694
+
695
+ it('createSpatializedStatic3DElement returns element and throws on failure', async () => {
696
+ vi.doMock('./JSBCommand', () => {
697
+ class OkCommand {
698
+ execute() {
699
+ return Promise.resolve({
700
+ success: true,
701
+ data: undefined,
702
+ errorCode: '',
703
+ errorMessage: '',
704
+ })
705
+ }
706
+ }
707
+
708
+ return {
709
+ InspectCommand: OkCommand,
710
+ DestroyCommand: OkCommand,
711
+ UpdateSpatializedElementTransform: OkCommand,
712
+ UpdateSpatialized2DElementProperties: OkCommand,
713
+ AddSpatializedElementToSpatialized2DElement: OkCommand,
714
+ UpdateSpatializedStatic3DElementProperties: OkCommand,
715
+ UpdateSpatializedDynamic3DElementProperties: OkCommand,
716
+ SetParentForEntityCommand: OkCommand,
717
+ AddEntityToDynamic3DCommand: OkCommand,
718
+ createSpatialized2DElementCommand: vi.fn(),
719
+ CreateSpatializedDynamic3DElementCommand: vi.fn(),
720
+ CreateSpatializedStatic3DElementCommand: vi
721
+ .fn()
722
+ .mockImplementationOnce(() => ({
723
+ execute: vi.fn().mockResolvedValue({
724
+ success: true,
725
+ data: { id: 's3' },
726
+ errorCode: '',
727
+ errorMessage: '',
728
+ }),
729
+ }))
730
+ .mockImplementationOnce(() => ({
731
+ execute: vi.fn().mockResolvedValue({
732
+ success: false,
733
+ data: undefined,
734
+ errorCode: 'E',
735
+ errorMessage: 'bad',
736
+ }),
737
+ })),
738
+ }
739
+ })
740
+
741
+ const { createSpatializedStatic3DElement } = await import(
742
+ './SpatializedElementCreator'
743
+ )
744
+ const ok = await createSpatializedStatic3DElement('u')
745
+ expect(ok.id).toBe('s3')
746
+
747
+ await expect(createSpatializedStatic3DElement('u')).rejects.toThrow(
748
+ 'createSpatializedStatic3DElement failed',
749
+ )
750
+ })
751
+
752
+ it('createSpatializedDynamic3DElement returns element and throws on failure', async () => {
753
+ vi.doMock('./JSBCommand', () => {
754
+ class OkCommand {
755
+ execute() {
756
+ return Promise.resolve({
757
+ success: true,
758
+ data: undefined,
759
+ errorCode: '',
760
+ errorMessage: '',
761
+ })
762
+ }
763
+ }
764
+
765
+ return {
766
+ InspectCommand: OkCommand,
767
+ DestroyCommand: OkCommand,
768
+ UpdateSpatializedElementTransform: OkCommand,
769
+ UpdateSpatialized2DElementProperties: OkCommand,
770
+ AddSpatializedElementToSpatialized2DElement: OkCommand,
771
+ UpdateSpatializedStatic3DElementProperties: OkCommand,
772
+ UpdateSpatializedDynamic3DElementProperties: OkCommand,
773
+ SetParentForEntityCommand: OkCommand,
774
+ AddEntityToDynamic3DCommand: OkCommand,
775
+ createSpatialized2DElementCommand: vi.fn(),
776
+ CreateSpatializedStatic3DElementCommand: vi.fn(),
777
+ CreateSpatializedDynamic3DElementCommand: vi
778
+ .fn()
779
+ .mockImplementationOnce(() => ({
780
+ execute: vi.fn().mockResolvedValue({
781
+ success: true,
782
+ data: { id: 'd3' },
783
+ errorCode: '',
784
+ errorMessage: '',
785
+ }),
786
+ }))
787
+ .mockImplementationOnce(() => ({
788
+ execute: vi.fn().mockResolvedValue({
789
+ success: false,
790
+ data: undefined,
791
+ errorCode: 'E',
792
+ errorMessage: 'bad',
793
+ }),
794
+ })),
795
+ }
796
+ })
797
+
798
+ const { createSpatializedDynamic3DElement } = await import(
799
+ './SpatializedElementCreator'
800
+ )
801
+ const ok = await createSpatializedDynamic3DElement()
802
+ expect(ok.id).toBe('d3')
803
+
804
+ await expect(createSpatializedDynamic3DElement()).rejects.toThrow(
805
+ 'createSpatializedDynamic3DElement failed',
806
+ )
807
+ })
808
+ })
809
+
810
+ describe('SpatializedStatic3DElement', () => {
811
+ afterEach(() => {
812
+ vi.resetModules()
813
+ vi.clearAllMocks()
814
+ vi.unmock('./JSBCommand')
815
+ })
816
+
817
+ it('resets ready when modelURL changes and resolves on load events', async () => {
818
+ const execute = vi.fn().mockResolvedValue({
819
+ success: true,
820
+ data: undefined,
821
+ errorCode: '',
822
+ errorMessage: '',
823
+ })
824
+ vi.doMock('./JSBCommand', () => {
825
+ class OkCommand {
826
+ execute() {
827
+ return Promise.resolve({
828
+ success: true,
829
+ data: undefined,
830
+ errorCode: '',
831
+ errorMessage: '',
832
+ })
833
+ }
834
+ }
835
+
836
+ return {
837
+ InspectCommand: OkCommand,
838
+ DestroyCommand: OkCommand,
839
+ UpdateSpatializedElementTransform: OkCommand,
840
+ UpdateSpatialized2DElementProperties: OkCommand,
841
+ AddSpatializedElementToSpatialized2DElement: OkCommand,
842
+ UpdateSpatializedDynamic3DElementProperties: OkCommand,
843
+ SetParentForEntityCommand: OkCommand,
844
+ AddEntityToDynamic3DCommand: OkCommand,
845
+ createSpatialized2DElementCommand: vi.fn(),
846
+ CreateSpatializedStatic3DElementCommand: vi.fn(),
847
+ CreateSpatializedDynamic3DElementCommand: vi.fn(),
848
+ UpdateSpatializedStatic3DElementProperties: vi
849
+ .fn()
850
+ .mockImplementation(() => ({ execute })),
851
+ }
852
+ })
853
+
854
+ const { SpatializedStatic3DElement } = await import(
855
+ './SpatializedStatic3DElement'
856
+ )
857
+ const { SpatialWebMsgType } = await import('./WebMsgCommand')
858
+
859
+ const el = new SpatializedStatic3DElement('m1')
860
+ const onLoad = vi.fn()
861
+ const onFail = vi.fn()
862
+ el.onLoadCallback = onLoad
863
+ el.onLoadFailureCallback = onFail
864
+
865
+ const p1 = el.ready
866
+ await el.updateProperties({ modelURL: 'a.glb' } as any)
867
+ expect(execute).toHaveBeenCalledTimes(1)
868
+ const p2 = el.ready
869
+ expect(p2).not.toBe(p1)
870
+
871
+ el.onReceiveEvent({ type: SpatialWebMsgType.modelloaded } as any)
872
+ await expect(p2).resolves.toBe(true)
873
+ expect(onLoad).toHaveBeenCalledTimes(1)
874
+
875
+ await el.updateProperties({ modelURL: 'b.glb' } as any)
876
+ const p3 = el.ready
877
+ el.onReceiveEvent({ type: SpatialWebMsgType.modelloadfailed } as any)
878
+ await expect(p3).resolves.toBe(false)
879
+ expect(onFail).toHaveBeenCalledTimes(1)
880
+ })
881
+
882
+ it('updateModelTransform passes float64 array to updateProperties', async () => {
883
+ const execute = vi.fn().mockResolvedValue({
884
+ success: true,
885
+ data: undefined,
886
+ errorCode: '',
887
+ errorMessage: '',
888
+ })
889
+ vi.doMock('./JSBCommand', () => {
890
+ class OkCommand {
891
+ execute() {
892
+ return Promise.resolve({
893
+ success: true,
894
+ data: undefined,
895
+ errorCode: '',
896
+ errorMessage: '',
897
+ })
898
+ }
899
+ }
900
+
901
+ return {
902
+ InspectCommand: OkCommand,
903
+ DestroyCommand: OkCommand,
904
+ UpdateSpatializedElementTransform: OkCommand,
905
+ UpdateSpatialized2DElementProperties: OkCommand,
906
+ AddSpatializedElementToSpatialized2DElement: OkCommand,
907
+ UpdateSpatializedDynamic3DElementProperties: OkCommand,
908
+ SetParentForEntityCommand: OkCommand,
909
+ AddEntityToDynamic3DCommand: OkCommand,
910
+ createSpatialized2DElementCommand: vi.fn(),
911
+ CreateSpatializedStatic3DElementCommand: vi.fn(),
912
+ CreateSpatializedDynamic3DElementCommand: vi.fn(),
913
+ UpdateSpatializedStatic3DElementProperties: vi
914
+ .fn()
915
+ .mockImplementation(() => ({ execute })),
916
+ }
917
+ })
918
+
919
+ const { SpatializedStatic3DElement } = await import(
920
+ './SpatializedStatic3DElement'
921
+ )
922
+
923
+ class DOMMatrixWithArray {
924
+ toFloat64Array() {
925
+ return new Float64Array([
926
+ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 10, 20, 30, 1,
927
+ ])
928
+ }
929
+ }
930
+
931
+ const el = new SpatializedStatic3DElement('m2')
932
+ el.updateModelTransform(new DOMMatrixWithArray() as any)
933
+ expect(execute).toHaveBeenCalledTimes(1)
934
+ })
935
+ })
936
+
937
+ describe('SpatializedDynamic3DElement', () => {
938
+ afterEach(() => {
939
+ vi.resetModules()
940
+ vi.clearAllMocks()
941
+ vi.unmock('./JSBCommand')
942
+ })
943
+
944
+ it('addEntity sets parent, pushes children, and calls SetParentForEntityCommand', async () => {
945
+ const execute = vi.fn().mockResolvedValue({
946
+ success: true,
947
+ data: undefined,
948
+ errorCode: '',
949
+ errorMessage: '',
950
+ })
951
+ vi.doMock('./JSBCommand', () => {
952
+ class OkCommand {
953
+ execute() {
954
+ return Promise.resolve({
955
+ success: true,
956
+ data: undefined,
957
+ errorCode: '',
958
+ errorMessage: '',
959
+ })
960
+ }
961
+ }
962
+
963
+ return {
964
+ InspectCommand: OkCommand,
965
+ DestroyCommand: OkCommand,
966
+ UpdateSpatializedElementTransform: OkCommand,
967
+ UpdateSpatialized2DElementProperties: OkCommand,
968
+ AddSpatializedElementToSpatialized2DElement: OkCommand,
969
+ UpdateSpatializedStatic3DElementProperties: OkCommand,
970
+ UpdateSpatializedDynamic3DElementProperties: OkCommand,
971
+ createSpatialized2DElementCommand: vi.fn(),
972
+ CreateSpatializedStatic3DElementCommand: vi.fn(),
973
+ CreateSpatializedDynamic3DElementCommand: vi.fn(),
974
+ AddEntityToDynamic3DCommand: OkCommand,
975
+ SetParentForEntityCommand: vi
976
+ .fn()
977
+ .mockImplementation(() => ({ execute })),
978
+ }
979
+ })
980
+
981
+ const { SpatializedDynamic3DElement } = await import(
982
+ './SpatializedDynamic3DElement'
983
+ )
984
+ const el = new SpatializedDynamic3DElement('d1')
985
+
986
+ const entity: any = { id: 'e1', parent: undefined }
987
+ await el.addEntity(entity)
988
+
989
+ expect(entity.parent).toBe(el)
990
+ expect(el.children).toContain(entity)
991
+ expect(execute).toHaveBeenCalledTimes(1)
992
+ })
993
+
994
+ it('updateProperties calls UpdateSpatializedDynamic3DElementProperties', async () => {
995
+ const execute = vi.fn().mockResolvedValue({
996
+ success: true,
997
+ data: undefined,
998
+ errorCode: '',
999
+ errorMessage: '',
1000
+ })
1001
+ vi.doMock('./JSBCommand', () => {
1002
+ class OkCommand {
1003
+ execute() {
1004
+ return Promise.resolve({
1005
+ success: true,
1006
+ data: undefined,
1007
+ errorCode: '',
1008
+ errorMessage: '',
1009
+ })
1010
+ }
1011
+ }
1012
+
1013
+ return {
1014
+ InspectCommand: OkCommand,
1015
+ DestroyCommand: OkCommand,
1016
+ UpdateSpatializedElementTransform: OkCommand,
1017
+ UpdateSpatialized2DElementProperties: OkCommand,
1018
+ AddSpatializedElementToSpatialized2DElement: OkCommand,
1019
+ UpdateSpatializedStatic3DElementProperties: OkCommand,
1020
+ SetParentForEntityCommand: OkCommand,
1021
+ AddEntityToDynamic3DCommand: OkCommand,
1022
+ createSpatialized2DElementCommand: vi.fn(),
1023
+ CreateSpatializedStatic3DElementCommand: vi.fn(),
1024
+ CreateSpatializedDynamic3DElementCommand: vi.fn(),
1025
+ UpdateSpatializedDynamic3DElementProperties: vi
1026
+ .fn()
1027
+ .mockImplementation(() => ({ execute })),
1028
+ }
1029
+ })
1030
+
1031
+ const { SpatializedDynamic3DElement } = await import(
1032
+ './SpatializedDynamic3DElement'
1033
+ )
1034
+ const el = new SpatializedDynamic3DElement('d2')
1035
+ await el.updateProperties({ visible: true } as any)
1036
+ expect(execute).toHaveBeenCalledTimes(1)
1037
+ })
1038
+ })
1039
+
1040
+ describe('ssr-polyfill', () => {
1041
+ it('isSSREnv returns false in jsdom', async () => {
1042
+ const { isSSREnv } = await import('./ssr-polyfill')
1043
+ expect(isSSREnv()).toBe(false)
1044
+ })
1045
+ })
1046
+
1047
+ describe('platform-adapter', () => {
1048
+ it('createPlatform returns SSRPlatform in SSR env', async () => {
1049
+ vi.resetModules()
1050
+ vi.doMock('./ssr-polyfill', () => {
1051
+ return { isSSREnv: () => true }
1052
+ })
1053
+
1054
+ const { createPlatform } = await import('./platform-adapter')
1055
+ const p = createPlatform() as any
1056
+ expect(typeof p.callJSB).toBe('function')
1057
+ expect(typeof p.callWebSpatialProtocol).toBe('function')
1058
+ expect(typeof p.callWebSpatialProtocolSync).toBe('function')
1059
+ })
1060
+ })