@react-three-dom/playwright 0.2.0 → 0.3.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.
package/dist/index.cjs CHANGED
@@ -4,13 +4,66 @@ var test$1 = require('@playwright/test');
4
4
 
5
5
  // src/fixtures.ts
6
6
 
7
+ // src/diffSnapshots.ts
8
+ var FIELDS_TO_COMPARE = [
9
+ "name",
10
+ "type",
11
+ "testId",
12
+ "visible",
13
+ "position",
14
+ "rotation",
15
+ "scale"
16
+ ];
17
+ function flattenTree(node) {
18
+ const map = /* @__PURE__ */ new Map();
19
+ function walk(n) {
20
+ map.set(n.uuid, n);
21
+ n.children.forEach(walk);
22
+ }
23
+ walk(node);
24
+ return map;
25
+ }
26
+ function valueEqual(a, b) {
27
+ if (a === b) return true;
28
+ if (Array.isArray(a) && Array.isArray(b)) {
29
+ if (a.length !== b.length) return false;
30
+ return a.every((v, i) => valueEqual(v, b[i]));
31
+ }
32
+ return false;
33
+ }
34
+ function diffSnapshots(before, after) {
35
+ const beforeMap = flattenTree(before.tree);
36
+ const afterMap = flattenTree(after.tree);
37
+ const added = [];
38
+ const removed = [];
39
+ const changed = [];
40
+ for (const [uuid, node] of afterMap) {
41
+ if (!beforeMap.has(uuid)) added.push(node);
42
+ }
43
+ for (const [uuid, node] of beforeMap) {
44
+ if (!afterMap.has(uuid)) removed.push(node);
45
+ }
46
+ for (const [uuid, afterNode] of afterMap) {
47
+ const beforeNode = beforeMap.get(uuid);
48
+ if (!beforeNode) continue;
49
+ for (const field of FIELDS_TO_COMPARE) {
50
+ const from = beforeNode[field];
51
+ const to = afterNode[field];
52
+ if (!valueEqual(from, to)) {
53
+ changed.push({ uuid, field, from, to });
54
+ }
55
+ }
56
+ }
57
+ return { added, removed, changed };
58
+ }
59
+
7
60
  // src/waiters.ts
8
- async function waitForReadyBridge(page, timeout) {
61
+ async function waitForReadyBridge(page, timeout, canvasId) {
9
62
  const deadline = Date.now() + timeout;
10
63
  const pollMs = 100;
11
64
  while (Date.now() < deadline) {
12
- const state = await page.evaluate(() => {
13
- const api = window.__R3F_DOM__;
65
+ const state = await page.evaluate((cid) => {
66
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
14
67
  if (!api) return { exists: false };
15
68
  return {
16
69
  exists: true,
@@ -18,7 +71,7 @@ async function waitForReadyBridge(page, timeout) {
18
71
  error: api._error ?? null,
19
72
  count: api.getCount()
20
73
  };
21
- });
74
+ }, canvasId ?? null);
22
75
  if (state.exists && state.ready) {
23
76
  return;
24
77
  }
@@ -30,11 +83,11 @@ The <ThreeDom> component mounted but threw during setup. Check the browser conso
30
83
  }
31
84
  await page.waitForTimeout(pollMs);
32
85
  }
33
- const finalState = await page.evaluate(() => {
34
- const api = window.__R3F_DOM__;
86
+ const finalState = await page.evaluate((cid) => {
87
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
35
88
  if (!api) return { exists: false, ready: false, error: null };
36
89
  return { exists: true, ready: api._ready, error: api._error ?? null };
37
- });
90
+ }, canvasId ?? null);
38
91
  if (finalState.exists && finalState.error) {
39
92
  throw new Error(
40
93
  `[react-three-dom] Bridge initialization failed: ${finalState.error}
@@ -51,14 +104,18 @@ async function waitForSceneReady(page, options = {}) {
51
104
  const {
52
105
  stableChecks = 3,
53
106
  pollIntervalMs = 100,
54
- timeout = 1e4
107
+ timeout = 1e4,
108
+ canvasId
55
109
  } = options;
56
110
  const deadline = Date.now() + timeout;
57
- await waitForReadyBridge(page, timeout);
111
+ await waitForReadyBridge(page, timeout, canvasId);
58
112
  let lastCount = -1;
59
113
  let stableRuns = 0;
60
114
  while (Date.now() < deadline) {
61
- const count = await page.evaluate(() => window.__R3F_DOM__.getCount());
115
+ const count = await page.evaluate((cid) => {
116
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
117
+ return api.getCount();
118
+ }, canvasId ?? null);
62
119
  if (count === lastCount && count > 0) {
63
120
  stableRuns++;
64
121
  if (stableRuns >= stableChecks) return;
@@ -68,52 +125,76 @@ async function waitForSceneReady(page, options = {}) {
68
125
  lastCount = count;
69
126
  await page.waitForTimeout(pollIntervalMs);
70
127
  }
128
+ const state = await page.evaluate((cid) => {
129
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
130
+ if (!api)
131
+ return { bridgeFound: false, ready: false, error: null, objectCount: 0 };
132
+ return {
133
+ bridgeFound: true,
134
+ ready: api._ready,
135
+ error: api._error ?? null,
136
+ objectCount: api.getCount()
137
+ };
138
+ }, canvasId ?? null);
139
+ const stateLine = state.bridgeFound ? `Bridge found: yes, _ready: ${state.ready}, _error: ${state.error ?? "none"}, objectCount: ${state.objectCount}` : "Bridge found: no (ensure <ThreeDom /> is mounted inside <Canvas> and refresh)";
71
140
  throw new Error(
72
- `waitForSceneReady timed out after ${timeout}ms. Last count: ${lastCount}, stable runs: ${stableRuns}/${stableChecks}`
141
+ `waitForSceneReady timed out after ${timeout}ms. ${stateLine}. Last count: ${lastCount}, stable runs: ${stableRuns}/${stableChecks}.`
73
142
  );
74
143
  }
75
144
  async function waitForObject(page, idOrUuid, options = {}) {
76
145
  const {
77
146
  bridgeTimeout = 3e4,
78
147
  objectTimeout = 4e4,
79
- pollIntervalMs = 200
148
+ pollIntervalMs = 200,
149
+ canvasId
80
150
  } = options;
81
- await waitForReadyBridge(page, bridgeTimeout);
151
+ await waitForReadyBridge(page, bridgeTimeout, canvasId);
82
152
  const deadline = Date.now() + objectTimeout;
83
153
  while (Date.now() < deadline) {
84
154
  const found = await page.evaluate(
85
- (id) => {
86
- const api = window.__R3F_DOM__;
155
+ ([id, cid]) => {
156
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
87
157
  if (!api || !api._ready) return false;
88
158
  return (api.getByTestId(id) ?? api.getByUuid(id)) !== null;
89
159
  },
90
- idOrUuid
160
+ [idOrUuid, canvasId ?? null]
91
161
  );
92
162
  if (found) return;
93
163
  await page.waitForTimeout(pollIntervalMs);
94
164
  }
95
- const diagnostics = await page.evaluate(() => {
96
- const api = window.__R3F_DOM__;
97
- if (!api) return { bridgeExists: false, ready: false, count: 0, error: null };
98
- return {
99
- bridgeExists: true,
100
- ready: api._ready,
101
- count: api.getCount(),
102
- error: api._error ?? null
103
- };
104
- });
105
- throw new Error(
106
- `waitForObject("${idOrUuid}") timed out after ${objectTimeout}ms. Bridge: ${diagnostics.bridgeExists ? "exists" : "missing"}, ready: ${diagnostics.ready}, objectCount: ${diagnostics.count}` + (diagnostics.error ? `, error: ${diagnostics.error}` : "") + `. Is the object rendered with userData.testId="${idOrUuid}" or uuid="${idOrUuid}"?`
165
+ const diagnostics = await page.evaluate(
166
+ ([id, cid]) => {
167
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
168
+ if (!api) return { bridgeExists: false, ready: false, count: 0, error: null, suggestions: [] };
169
+ const suggestions = typeof api.fuzzyFind === "function" ? api.fuzzyFind(id, 5).map((m) => ({ testId: m.testId, name: m.name, uuid: m.uuid })) : [];
170
+ return {
171
+ bridgeExists: true,
172
+ ready: api._ready,
173
+ count: api.getCount(),
174
+ error: api._error ?? null,
175
+ suggestions
176
+ };
177
+ },
178
+ [idOrUuid, canvasId ?? null]
107
179
  );
180
+ let msg = `waitForObject("${idOrUuid}") timed out after ${objectTimeout}ms. Bridge: ${diagnostics.bridgeExists ? "exists" : "missing"}, ready: ${diagnostics.ready}, objectCount: ${diagnostics.count}` + (diagnostics.error ? `, error: ${diagnostics.error}` : "") + `. Is the object rendered with userData.testId="${idOrUuid}" or uuid="${idOrUuid}"?`;
181
+ if (diagnostics.suggestions.length > 0) {
182
+ msg += "\nDid you mean:\n" + diagnostics.suggestions.map((s) => {
183
+ const id = s.testId ? `testId="${s.testId}"` : `uuid="${s.uuid}"`;
184
+ return ` \u2192 ${s.name || "(unnamed)"} [${id}]`;
185
+ }).join("\n");
186
+ }
187
+ throw new Error(msg);
108
188
  }
109
189
  async function waitForIdle(page, options = {}) {
110
190
  const {
111
191
  idleFrames = 10,
112
- timeout = 1e4
192
+ timeout = 1e4,
193
+ canvasId
113
194
  } = options;
114
- await waitForReadyBridge(page, timeout);
195
+ await waitForReadyBridge(page, timeout, canvasId);
115
196
  const settled = await page.evaluate(
116
- ([frames, timeoutMs]) => {
197
+ ([frames, timeoutMs, cid]) => {
117
198
  return new Promise((resolve) => {
118
199
  const deadline = Date.now() + timeoutMs;
119
200
  let lastJson = "";
@@ -123,7 +204,7 @@ async function waitForIdle(page, options = {}) {
123
204
  resolve(false);
124
205
  return;
125
206
  }
126
- const api = window.__R3F_DOM__;
207
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
127
208
  if (!api || !api._ready) {
128
209
  if (api && api._error) {
129
210
  resolve(`Bridge error: ${api._error}`);
@@ -149,7 +230,7 @@ async function waitForIdle(page, options = {}) {
149
230
  requestAnimationFrame(check);
150
231
  });
151
232
  },
152
- [idleFrames, timeout]
233
+ [idleFrames, timeout, canvasId ?? null]
153
234
  );
154
235
  if (typeof settled === "string") {
155
236
  throw new Error(`waitForIdle failed: ${settled}`);
@@ -163,10 +244,11 @@ async function waitForNewObject(page, options = {}) {
163
244
  type,
164
245
  nameContains,
165
246
  pollIntervalMs = 100,
166
- timeout = 1e4
247
+ timeout = 1e4,
248
+ canvasId
167
249
  } = options;
168
- const baselineUuids = await page.evaluate(() => {
169
- const api = window.__R3F_DOM__;
250
+ const baselineUuids = await page.evaluate((cid) => {
251
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
170
252
  if (!api) return [];
171
253
  const snap = api.snapshot();
172
254
  const uuids = [];
@@ -178,13 +260,13 @@ async function waitForNewObject(page, options = {}) {
178
260
  }
179
261
  collect(snap.tree);
180
262
  return uuids;
181
- });
263
+ }, canvasId ?? null);
182
264
  const deadline = Date.now() + timeout;
183
265
  while (Date.now() < deadline) {
184
266
  await page.waitForTimeout(pollIntervalMs);
185
267
  const result = await page.evaluate(
186
- ([filterType, filterName, knownUuids]) => {
187
- const api = window.__R3F_DOM__;
268
+ ([filterType, filterName, knownUuids, cid]) => {
269
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
188
270
  if (!api) return null;
189
271
  const snap = api.snapshot();
190
272
  const known = new Set(knownUuids);
@@ -218,7 +300,7 @@ async function waitForNewObject(page, options = {}) {
218
300
  count: newObjects.length
219
301
  };
220
302
  },
221
- [type ?? null, nameContains ?? null, baselineUuids]
303
+ [type ?? null, nameContains ?? null, baselineUuids, canvasId ?? null]
222
304
  );
223
305
  if (result) {
224
306
  return result;
@@ -232,16 +314,39 @@ async function waitForNewObject(page, options = {}) {
232
314
  `waitForNewObject timed out after ${timeout}ms. No new objects appeared${filterDesc ? ` matching ${filterDesc}` : ""}. Baseline had ${baselineUuids.length} objects.`
233
315
  );
234
316
  }
317
+ async function waitForObjectRemoved(page, idOrUuid, options = {}) {
318
+ const {
319
+ bridgeTimeout = 3e4,
320
+ pollIntervalMs = 100,
321
+ timeout = 1e4,
322
+ canvasId
323
+ } = options;
324
+ await waitForReadyBridge(page, bridgeTimeout, canvasId);
325
+ const deadline = Date.now() + timeout;
326
+ while (Date.now() < deadline) {
327
+ const stillPresent = await page.evaluate(([id, cid]) => {
328
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
329
+ if (!api) return true;
330
+ const meta = api.getByTestId(id) ?? api.getByUuid(id);
331
+ return meta !== null;
332
+ }, [idOrUuid, canvasId ?? null]);
333
+ if (!stillPresent) return;
334
+ await page.waitForTimeout(pollIntervalMs);
335
+ }
336
+ throw new Error(
337
+ `waitForObjectRemoved timed out after ${timeout}ms. Object "${idOrUuid}" is still in the scene.`
338
+ );
339
+ }
235
340
 
236
341
  // src/interactions.ts
237
342
  var DEFAULT_AUTO_WAIT_TIMEOUT = 5e3;
238
343
  var AUTO_WAIT_POLL_MS = 100;
239
- async function autoWaitForObject(page, idOrUuid, timeout = DEFAULT_AUTO_WAIT_TIMEOUT) {
344
+ async function autoWaitForObject(page, idOrUuid, timeout = DEFAULT_AUTO_WAIT_TIMEOUT, canvasId) {
240
345
  const deadline = Date.now() + timeout;
241
346
  while (Date.now() < deadline) {
242
347
  const state = await page.evaluate(
243
- (id) => {
244
- const api = window.__R3F_DOM__;
348
+ ([id, cid]) => {
349
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
245
350
  if (!api) return { bridge: "missing" };
246
351
  if (!api._ready) {
247
352
  return {
@@ -252,7 +357,7 @@ async function autoWaitForObject(page, idOrUuid, timeout = DEFAULT_AUTO_WAIT_TIM
252
357
  const found = (api.getByTestId(id) ?? api.getByUuid(id)) !== null;
253
358
  return { bridge: "ready", found };
254
359
  },
255
- idOrUuid
360
+ [idOrUuid, canvasId ?? null]
256
361
  );
257
362
  if (state.bridge === "ready" && state.found) {
258
363
  return;
@@ -260,49 +365,56 @@ async function autoWaitForObject(page, idOrUuid, timeout = DEFAULT_AUTO_WAIT_TIM
260
365
  if (state.bridge === "not-ready" && state.error) {
261
366
  throw new Error(
262
367
  `[react-three-dom] Bridge initialization failed: ${state.error}
263
- Cannot perform interaction on "${idOrUuid}".`
368
+ Cannot perform interaction on "${idOrUuid}"${canvasId ? ` (canvas: "${canvasId}")` : ""}.`
264
369
  );
265
370
  }
266
371
  await page.waitForTimeout(AUTO_WAIT_POLL_MS);
267
372
  }
268
373
  const finalState = await page.evaluate(
269
- (id) => {
270
- const api = window.__R3F_DOM__;
271
- if (!api) return { bridge: false, ready: false, count: 0, error: null, found: false };
374
+ ([id, cid]) => {
375
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
376
+ if (!api) return { bridge: false, ready: false, count: 0, error: null, found: false, suggestions: [] };
377
+ const suggestions = typeof api.fuzzyFind === "function" ? api.fuzzyFind(id, 5).map((m) => ({ testId: m.testId, name: m.name, uuid: m.uuid })) : [];
272
378
  return {
273
379
  bridge: true,
274
380
  ready: api._ready,
275
381
  count: api.getCount(),
276
382
  error: api._error ?? null,
277
- found: (api.getByTestId(id) ?? api.getByUuid(id)) !== null
383
+ found: (api.getByTestId(id) ?? api.getByUuid(id)) !== null,
384
+ suggestions
278
385
  };
279
386
  },
280
- idOrUuid
387
+ [idOrUuid, canvasId ?? null]
281
388
  );
282
389
  if (!finalState.bridge) {
283
390
  throw new Error(
284
- `[react-three-dom] Auto-wait timed out after ${timeout}ms: bridge not found.
285
- Ensure <ThreeDom> is mounted inside your <Canvas> component.`
391
+ `[react-three-dom] Auto-wait timed out after ${timeout}ms: bridge not found${canvasId ? ` (canvas: "${canvasId}")` : ""}.
392
+ Ensure <ThreeDom${canvasId ? ` canvasId="${canvasId}"` : ""}> is mounted inside your <Canvas> component.`
286
393
  );
287
394
  }
288
- throw new Error(
289
- `[react-three-dom] Auto-wait timed out after ${timeout}ms: object "${idOrUuid}" not found.
395
+ let msg = `[react-three-dom] Auto-wait timed out after ${timeout}ms: object "${idOrUuid}" not found${canvasId ? ` (canvas: "${canvasId}")` : ""}.
290
396
  Bridge: ready=${finalState.ready}, objectCount=${finalState.count}` + (finalState.error ? `, error=${finalState.error}` : "") + `.
291
- Ensure the object has userData.testId="${idOrUuid}" or uuid="${idOrUuid}".`
292
- );
397
+ Ensure the object has userData.testId="${idOrUuid}" or uuid="${idOrUuid}".`;
398
+ if (finalState.suggestions.length > 0) {
399
+ msg += "\nDid you mean:\n" + finalState.suggestions.map((s) => {
400
+ const id = s.testId ? `testId="${s.testId}"` : `uuid="${s.uuid}"`;
401
+ return ` \u2192 ${s.name || "(unnamed)"} [${id}]`;
402
+ }).join("\n");
403
+ }
404
+ throw new Error(msg);
293
405
  }
294
- async function autoWaitForBridge(page, timeout = DEFAULT_AUTO_WAIT_TIMEOUT) {
406
+ async function autoWaitForBridge(page, timeout = DEFAULT_AUTO_WAIT_TIMEOUT, canvasId) {
295
407
  const deadline = Date.now() + timeout;
296
408
  while (Date.now() < deadline) {
297
- const state = await page.evaluate(() => {
298
- const api = window.__R3F_DOM__;
409
+ const state = await page.evaluate((cid) => {
410
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
299
411
  if (!api) return { exists: false };
300
412
  return {
301
413
  exists: true,
302
414
  ready: api._ready,
303
415
  error: api._error ?? null
304
416
  };
305
- });
417
+ }, canvasId ?? null);
306
418
  if (state.exists && state.ready) return;
307
419
  if (state.exists && !state.ready && state.error) {
308
420
  throw new Error(
@@ -312,73 +424,245 @@ async function autoWaitForBridge(page, timeout = DEFAULT_AUTO_WAIT_TIMEOUT) {
312
424
  await page.waitForTimeout(AUTO_WAIT_POLL_MS);
313
425
  }
314
426
  throw new Error(
315
- `[react-three-dom] Auto-wait timed out after ${timeout}ms: bridge not ready.
316
- Ensure <ThreeDom> is mounted inside your <Canvas> component.`
427
+ `[react-three-dom] Auto-wait timed out after ${timeout}ms: bridge not ready${canvasId ? ` (canvas: "${canvasId}")` : ""}.
428
+ Ensure <ThreeDom${canvasId ? ` canvasId="${canvasId}"` : ""}> is mounted inside your <Canvas> component.`
317
429
  );
318
430
  }
319
- async function click(page, idOrUuid, timeout) {
320
- await autoWaitForObject(page, idOrUuid, timeout);
321
- await page.evaluate((id) => {
322
- window.__R3F_DOM__.click(id);
323
- }, idOrUuid);
431
+ async function click(page, idOrUuid, timeout, canvasId) {
432
+ await autoWaitForObject(page, idOrUuid, timeout, canvasId);
433
+ await page.evaluate(([id, cid]) => {
434
+ const api = cid ? window.__R3F_DOM_INSTANCES__[cid] : window.__R3F_DOM__;
435
+ api.click(id);
436
+ }, [idOrUuid, canvasId ?? null]);
437
+ }
438
+ async function doubleClick(page, idOrUuid, timeout, canvasId) {
439
+ await autoWaitForObject(page, idOrUuid, timeout, canvasId);
440
+ await page.evaluate(([id, cid]) => {
441
+ const api = cid ? window.__R3F_DOM_INSTANCES__[cid] : window.__R3F_DOM__;
442
+ api.doubleClick(id);
443
+ }, [idOrUuid, canvasId ?? null]);
324
444
  }
325
- async function doubleClick(page, idOrUuid, timeout) {
326
- await autoWaitForObject(page, idOrUuid, timeout);
327
- await page.evaluate((id) => {
328
- window.__R3F_DOM__.doubleClick(id);
329
- }, idOrUuid);
445
+ async function contextMenu(page, idOrUuid, timeout, canvasId) {
446
+ await autoWaitForObject(page, idOrUuid, timeout, canvasId);
447
+ await page.evaluate(([id, cid]) => {
448
+ const api = cid ? window.__R3F_DOM_INSTANCES__[cid] : window.__R3F_DOM__;
449
+ api.contextMenu(id);
450
+ }, [idOrUuid, canvasId ?? null]);
330
451
  }
331
- async function contextMenu(page, idOrUuid, timeout) {
332
- await autoWaitForObject(page, idOrUuid, timeout);
333
- await page.evaluate((id) => {
334
- window.__R3F_DOM__.contextMenu(id);
335
- }, idOrUuid);
452
+ async function hover(page, idOrUuid, timeout, canvasId) {
453
+ await autoWaitForObject(page, idOrUuid, timeout, canvasId);
454
+ await page.evaluate(([id, cid]) => {
455
+ const api = cid ? window.__R3F_DOM_INSTANCES__[cid] : window.__R3F_DOM__;
456
+ api.hover(id);
457
+ }, [idOrUuid, canvasId ?? null]);
336
458
  }
337
- async function hover(page, idOrUuid, timeout) {
338
- await autoWaitForObject(page, idOrUuid, timeout);
339
- await page.evaluate((id) => {
340
- window.__R3F_DOM__.hover(id);
341
- }, idOrUuid);
459
+ async function unhover(page, timeout, canvasId) {
460
+ await autoWaitForBridge(page, timeout, canvasId);
461
+ await page.evaluate((cid) => {
462
+ const api = cid ? window.__R3F_DOM_INSTANCES__[cid] : window.__R3F_DOM__;
463
+ api.unhover();
464
+ }, canvasId ?? null);
342
465
  }
343
- async function drag(page, idOrUuid, delta, timeout) {
344
- await autoWaitForObject(page, idOrUuid, timeout);
466
+ async function drag(page, idOrUuid, delta, timeout, canvasId) {
467
+ await autoWaitForObject(page, idOrUuid, timeout, canvasId);
345
468
  await page.evaluate(
346
- async ([id, d]) => {
347
- await window.__R3F_DOM__.drag(id, d);
469
+ async ([id, d, cid]) => {
470
+ const api = cid ? window.__R3F_DOM_INSTANCES__[cid] : window.__R3F_DOM__;
471
+ await api.drag(id, d);
348
472
  },
349
- [idOrUuid, delta]
473
+ [idOrUuid, delta, canvasId ?? null]
350
474
  );
351
475
  }
352
- async function wheel(page, idOrUuid, options, timeout) {
353
- await autoWaitForObject(page, idOrUuid, timeout);
476
+ async function wheel(page, idOrUuid, options, timeout, canvasId) {
477
+ await autoWaitForObject(page, idOrUuid, timeout, canvasId);
354
478
  await page.evaluate(
355
- ([id, opts]) => {
356
- window.__R3F_DOM__.wheel(id, opts);
479
+ ([id, opts, cid]) => {
480
+ const api = cid ? window.__R3F_DOM_INSTANCES__[cid] : window.__R3F_DOM__;
481
+ api.wheel(id, opts);
357
482
  },
358
- [idOrUuid, options]
483
+ [idOrUuid, options, canvasId ?? null]
359
484
  );
360
485
  }
361
- async function pointerMiss(page, timeout) {
362
- await autoWaitForBridge(page, timeout);
363
- await page.evaluate(() => {
364
- window.__R3F_DOM__.pointerMiss();
365
- });
486
+ async function pointerMiss(page, timeout, canvasId) {
487
+ await autoWaitForBridge(page, timeout, canvasId);
488
+ await page.evaluate((cid) => {
489
+ const api = cid ? window.__R3F_DOM_INSTANCES__[cid] : window.__R3F_DOM__;
490
+ api.pointerMiss();
491
+ }, canvasId ?? null);
366
492
  }
367
- async function drawPathOnCanvas(page, points, options, timeout) {
368
- await autoWaitForBridge(page, timeout);
493
+ async function drawPathOnCanvas(page, points, options, timeout, canvasId) {
494
+ await autoWaitForBridge(page, timeout, canvasId);
369
495
  return page.evaluate(
370
- async ([pts, opts]) => {
371
- return window.__R3F_DOM__.drawPath(pts, opts ?? void 0);
496
+ async ([pts, opts, cid]) => {
497
+ const api = cid ? window.__R3F_DOM_INSTANCES__[cid] : window.__R3F_DOM__;
498
+ return api.drawPath(pts, opts ?? void 0);
372
499
  },
373
- [points, options ?? null]
500
+ [points, options ?? null, canvasId ?? null]
374
501
  );
375
502
  }
503
+ async function getCameraState(page, timeout, canvasId) {
504
+ await autoWaitForBridge(page, timeout, canvasId);
505
+ return page.evaluate((cid) => {
506
+ const api = cid ? window.__R3F_DOM_INSTANCES__[cid] : window.__R3F_DOM__;
507
+ return api.getCameraState();
508
+ }, canvasId ?? null);
509
+ }
510
+
511
+ // src/reporter.ts
512
+ var RESET = "\x1B[0m";
513
+ var BOLD = "\x1B[1m";
514
+ var DIM = "\x1B[2m";
515
+ var GREEN = "\x1B[32m";
516
+ var RED = "\x1B[31m";
517
+ var YELLOW = "\x1B[33m";
518
+ var CYAN = "\x1B[36m";
519
+ var MAGENTA = "\x1B[35m";
520
+ var TAG = `${CYAN}[r3f-dom]${RESET}`;
521
+ function ok(msg) {
522
+ return `${TAG} ${GREEN}\u2713${RESET} ${msg}`;
523
+ }
524
+ function fail(msg) {
525
+ return `${TAG} ${RED}\u2717${RESET} ${msg}`;
526
+ }
527
+ function warn(msg) {
528
+ return `${TAG} ${YELLOW}\u26A0${RESET} ${msg}`;
529
+ }
530
+ function info(msg) {
531
+ return `${TAG} ${DIM}${msg}${RESET}`;
532
+ }
533
+ function heading(msg) {
534
+ return `
535
+ ${TAG} ${BOLD}${MAGENTA}${msg}${RESET}`;
536
+ }
537
+ var R3FReporter = class {
538
+ constructor(_page, enabled = true, canvasId) {
539
+ this._page = _page;
540
+ this._enabled = true;
541
+ this._enabled = enabled;
542
+ this._canvasId = canvasId;
543
+ }
544
+ // -----------------------------------------------------------------------
545
+ // Lifecycle events
546
+ // -----------------------------------------------------------------------
547
+ logBridgeWaiting() {
548
+ if (!this._enabled) return;
549
+ console.log(info("Waiting for bridge (window.__R3F_DOM__)..."));
550
+ }
551
+ logBridgeConnected(diag) {
552
+ if (!this._enabled) return;
553
+ if (diag) {
554
+ console.log(ok(`Bridge connected \u2014 v${diag.version}, ${diag.objectCount} objects, ${diag.meshCount} meshes`));
555
+ console.log(info(` Canvas: ${diag.canvasWidth}\xD7${diag.canvasHeight} GPU: ${diag.webglRenderer}`));
556
+ console.log(info(` DOM nodes: ${diag.materializedDomNodes}/${diag.maxDomNodes} Dirty queue: ${diag.dirtyQueueSize}`));
557
+ } else {
558
+ console.log(ok("Bridge connected"));
559
+ }
560
+ }
561
+ logBridgeError(error) {
562
+ if (!this._enabled) return;
563
+ console.log(fail(`Bridge error: ${error}`));
564
+ }
565
+ logSceneReady(objectCount) {
566
+ if (!this._enabled) return;
567
+ console.log(ok(`Scene ready \u2014 ${objectCount} objects stabilized`));
568
+ }
569
+ logObjectFound(idOrUuid, type, name) {
570
+ if (!this._enabled) return;
571
+ const label = name ? `"${name}" (${type})` : type;
572
+ console.log(ok(`Object found: "${idOrUuid}" \u2192 ${label}`));
573
+ }
574
+ logObjectNotFound(idOrUuid, suggestions) {
575
+ if (!this._enabled) return;
576
+ console.log(fail(`Object not found: "${idOrUuid}"`));
577
+ if (suggestions && suggestions.length > 0) {
578
+ console.log(warn("Did you mean:"));
579
+ for (const s of suggestions.slice(0, 5)) {
580
+ const id = s.testId ? `testId="${s.testId}"` : `uuid="${s.uuid}"`;
581
+ console.log(info(` \u2192 ${s.name || "(unnamed)"} [${id}]`));
582
+ }
583
+ }
584
+ }
585
+ // -----------------------------------------------------------------------
586
+ // Interaction events
587
+ // -----------------------------------------------------------------------
588
+ logInteraction(action, idOrUuid, extra) {
589
+ if (!this._enabled) return;
590
+ const suffix = extra ? ` ${DIM}${extra}${RESET}` : "";
591
+ console.log(info(`${action}("${idOrUuid}")${suffix}`));
592
+ }
593
+ logInteractionDone(action, idOrUuid, durationMs) {
594
+ if (!this._enabled) return;
595
+ console.log(ok(`${action}("${idOrUuid}") \u2014 ${durationMs}ms`));
596
+ }
597
+ // -----------------------------------------------------------------------
598
+ // Assertion context
599
+ // -----------------------------------------------------------------------
600
+ logAssertionFailure(matcherName, id, detail, diag) {
601
+ if (!this._enabled) return;
602
+ console.log(heading(`Assertion failed: ${matcherName}("${id}")`));
603
+ console.log(fail(detail));
604
+ if (diag) {
605
+ this._printDiagnosticsSummary(diag);
606
+ }
607
+ }
608
+ // -----------------------------------------------------------------------
609
+ // Full diagnostics dump
610
+ // -----------------------------------------------------------------------
611
+ async logDiagnostics() {
612
+ if (!this._enabled) return;
613
+ const diag = await this.fetchDiagnostics();
614
+ if (!diag) {
615
+ console.log(fail("Cannot fetch diagnostics \u2014 bridge not available"));
616
+ return;
617
+ }
618
+ console.log(heading("Bridge Diagnostics"));
619
+ this._printDiagnosticsFull(diag);
620
+ }
621
+ async fetchDiagnostics() {
622
+ return this._page.evaluate((cid) => {
623
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
624
+ if (!api || typeof api.getDiagnostics !== "function") return null;
625
+ return api.getDiagnostics();
626
+ }, this._canvasId ?? null);
627
+ }
628
+ async fetchFuzzyMatches(query, limit = 5) {
629
+ return this._page.evaluate(
630
+ ({ q, lim, cid }) => {
631
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
632
+ if (!api || typeof api.fuzzyFind !== "function") return [];
633
+ return api.fuzzyFind(q, lim);
634
+ },
635
+ { q: query, lim: limit, cid: this._canvasId ?? null }
636
+ );
637
+ }
638
+ // -----------------------------------------------------------------------
639
+ // Private formatting
640
+ // -----------------------------------------------------------------------
641
+ _printDiagnosticsSummary(d) {
642
+ console.log(info(` Bridge: v${d.version} ready=${d.ready}${d.error ? ` error="${d.error}"` : ""}`));
643
+ console.log(info(` Scene: ${d.objectCount} objects (${d.meshCount} meshes, ${d.groupCount} groups, ${d.lightCount} lights)`));
644
+ console.log(info(` Canvas: ${d.canvasWidth}\xD7${d.canvasHeight} GPU: ${d.webglRenderer}`));
645
+ }
646
+ _printDiagnosticsFull(d) {
647
+ const status = d.ready ? `${GREEN}READY${RESET}` : `${RED}NOT READY${RESET}`;
648
+ console.log(` ${BOLD}Status:${RESET} ${status} v${d.version}`);
649
+ if (d.error) console.log(` ${BOLD}Error:${RESET} ${RED}${d.error}${RESET}`);
650
+ console.log(` ${BOLD}Objects:${RESET} ${d.objectCount} total`);
651
+ console.log(` ${d.meshCount} meshes, ${d.groupCount} groups, ${d.lightCount} lights, ${d.cameraCount} cameras`);
652
+ console.log(` ${BOLD}DOM:${RESET} ${d.materializedDomNodes}/${d.maxDomNodes} materialized`);
653
+ console.log(` ${BOLD}Canvas:${RESET} ${d.canvasWidth}\xD7${d.canvasHeight}`);
654
+ console.log(` ${BOLD}GPU:${RESET} ${d.webglRenderer}`);
655
+ console.log(` ${BOLD}Dirty:${RESET} ${d.dirtyQueueSize} queued updates`);
656
+ }
657
+ };
376
658
 
377
659
  // src/fixtures.ts
378
- var R3FFixture = class {
660
+ var R3FFixture = class _R3FFixture {
379
661
  constructor(_page, opts) {
380
662
  this._page = _page;
381
663
  this._debugListenerAttached = false;
664
+ this.canvasId = opts?.canvasId;
665
+ this._reporter = new R3FReporter(_page, opts?.report !== false, this.canvasId);
382
666
  if (opts?.debug) {
383
667
  this._attachDebugListener();
384
668
  }
@@ -387,6 +671,40 @@ var R3FFixture = class {
387
671
  get page() {
388
672
  return this._page;
389
673
  }
674
+ /** Access the reporter for custom diagnostic logging. */
675
+ get reporter() {
676
+ return this._reporter;
677
+ }
678
+ /**
679
+ * Create a scoped fixture targeting a specific canvas instance.
680
+ * All queries, interactions, and assertions on the returned fixture
681
+ * will use `window.__R3F_DOM_INSTANCES__[canvasId]` instead of
682
+ * `window.__R3F_DOM__`.
683
+ *
684
+ * @example
685
+ * ```typescript
686
+ * const mainR3f = r3f.forCanvas('main-viewport');
687
+ * const minimapR3f = r3f.forCanvas('minimap');
688
+ * await mainR3f.click('building-42');
689
+ * await expect(minimapR3f).toExist('building-42-marker');
690
+ * ```
691
+ */
692
+ forCanvas(canvasId) {
693
+ return new _R3FFixture(this._page, {
694
+ canvasId,
695
+ report: this._reporter !== null
696
+ });
697
+ }
698
+ /**
699
+ * List all active canvas IDs registered on the page.
700
+ * Returns an empty array if only the default (unnamed) bridge is active.
701
+ */
702
+ async getCanvasIds() {
703
+ return this._page.evaluate(() => {
704
+ const instances = window.__R3F_DOM_INSTANCES__;
705
+ return instances ? Object.keys(instances) : [];
706
+ });
707
+ }
390
708
  // -----------------------------------------------------------------------
391
709
  // Debug logging
392
710
  // -----------------------------------------------------------------------
@@ -418,89 +736,177 @@ var R3FFixture = class {
418
736
  // -----------------------------------------------------------------------
419
737
  /** Get object metadata by testId or uuid. Returns null if not found. */
420
738
  async getObject(idOrUuid) {
421
- return this._page.evaluate((id) => {
422
- const api = window.__R3F_DOM__;
739
+ return this._page.evaluate(([id, cid]) => {
740
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
423
741
  if (!api) return null;
424
742
  return api.getByTestId(id) ?? api.getByUuid(id) ?? null;
425
- }, idOrUuid);
743
+ }, [idOrUuid, this.canvasId ?? null]);
744
+ }
745
+ /** Get object metadata by testId (userData.testId). Returns null if not found. */
746
+ async getByTestId(testId) {
747
+ return this._page.evaluate(([id, cid]) => {
748
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
749
+ return api ? api.getByTestId(id) : null;
750
+ }, [testId, this.canvasId ?? null]);
751
+ }
752
+ /** Get object metadata by UUID. Returns null if not found. */
753
+ async getByUuid(uuid) {
754
+ return this._page.evaluate(([u, cid]) => {
755
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
756
+ return api ? api.getByUuid(u) : null;
757
+ }, [uuid, this.canvasId ?? null]);
758
+ }
759
+ /** Get all objects with the given name (names are not unique in Three.js). */
760
+ async getByName(name) {
761
+ return this._page.evaluate(([n, cid]) => {
762
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
763
+ return api ? api.getByName(n) : [];
764
+ }, [name, this.canvasId ?? null]);
765
+ }
766
+ /** Get direct children of an object by testId or uuid. */
767
+ async getChildren(idOrUuid) {
768
+ return this._page.evaluate(([id, cid]) => {
769
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
770
+ return api ? api.getChildren(id) : [];
771
+ }, [idOrUuid, this.canvasId ?? null]);
772
+ }
773
+ /** Get parent of an object by testId or uuid. Returns null if root or not found. */
774
+ async getParent(idOrUuid) {
775
+ return this._page.evaluate(([id, cid]) => {
776
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
777
+ return api ? api.getParent(id) : null;
778
+ }, [idOrUuid, this.canvasId ?? null]);
426
779
  }
427
- /** Get heavy inspection data (Tier 2) by testId or uuid. */
428
- async inspect(idOrUuid) {
429
- return this._page.evaluate((id) => {
430
- const api = window.__R3F_DOM__;
780
+ /** Get heavy inspection data (Tier 2) by testId or uuid. Pass { includeGeometryData: true } to include vertex positions and triangle indices. */
781
+ async inspect(idOrUuid, options) {
782
+ return this._page.evaluate(
783
+ ({ id, opts, cid }) => {
784
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
785
+ if (!api) return null;
786
+ return api.inspect(id, opts);
787
+ },
788
+ { id: idOrUuid, opts: options, cid: this.canvasId ?? null }
789
+ );
790
+ }
791
+ /**
792
+ * Get world-space position [x, y, z] of an object (from its world matrix).
793
+ * Use for nested objects where local position differs from world position.
794
+ */
795
+ async getWorldPosition(idOrUuid) {
796
+ return this._page.evaluate(([id, cid]) => {
797
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
431
798
  if (!api) return null;
432
- return api.inspect(id);
433
- }, idOrUuid);
799
+ const insp = api.inspect(id);
800
+ if (!insp?.worldMatrix || insp.worldMatrix.length < 15) return null;
801
+ const m = insp.worldMatrix;
802
+ return [m[12], m[13], m[14]];
803
+ }, [idOrUuid, this.canvasId ?? null]);
434
804
  }
435
805
  /** Take a full scene snapshot. */
436
806
  async snapshot() {
437
- return this._page.evaluate(() => {
438
- const api = window.__R3F_DOM__;
807
+ return this._page.evaluate((cid) => {
808
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
439
809
  return api ? api.snapshot() : null;
440
- });
810
+ }, this.canvasId ?? null);
811
+ }
812
+ /**
813
+ * Compare two scene snapshots: returns added nodes, removed nodes, and
814
+ * property changes (name, type, testId, visible, position, rotation, scale).
815
+ * Use after taking snapshots before/after an action to assert on scene changes.
816
+ */
817
+ diffSnapshots(before, after) {
818
+ return diffSnapshots(before, after);
819
+ }
820
+ /**
821
+ * Run an async action and return how many objects were added and removed
822
+ * compared to before the action. Uses snapshots before/after so add and
823
+ * remove are both counted correctly when both happen.
824
+ */
825
+ async trackObjectCount(action) {
826
+ const before = await this.snapshot();
827
+ if (!before) throw new Error("trackObjectCount: no snapshot before (bridge not ready?)");
828
+ await action();
829
+ const after = await this.snapshot();
830
+ if (!after) throw new Error("trackObjectCount: no snapshot after (bridge not ready?)");
831
+ const diff = diffSnapshots(before, after);
832
+ return { added: diff.added.length, removed: diff.removed.length };
441
833
  }
442
834
  /** Get the total number of tracked objects. */
443
835
  async getCount() {
444
- return this._page.evaluate(() => {
445
- const api = window.__R3F_DOM__;
836
+ return this._page.evaluate((cid) => {
837
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
446
838
  return api ? api.getCount() : 0;
447
- });
839
+ }, this.canvasId ?? null);
840
+ }
841
+ /**
842
+ * Return a Playwright locator for the R3F canvas element the bridge is attached to.
843
+ * The canvas has `data-r3f-canvas` set by the bridge (value is the canvasId or "true").
844
+ */
845
+ getCanvasLocator() {
846
+ if (this.canvasId) {
847
+ return this._page.locator(`[data-r3f-canvas="${this.canvasId}"]`);
848
+ }
849
+ return this._page.locator("[data-r3f-canvas]");
448
850
  }
449
851
  /**
450
852
  * Get all objects of a given Three.js type (e.g. "Mesh", "Group", "Line").
451
- * Useful for BIM/CAD apps to find all walls, doors, etc. by object type.
452
853
  */
453
854
  async getByType(type) {
454
- return this._page.evaluate((t) => {
455
- const api = window.__R3F_DOM__;
855
+ return this._page.evaluate(([t, cid]) => {
856
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
456
857
  return api ? api.getByType(t) : [];
457
- }, type);
858
+ }, [type, this.canvasId ?? null]);
859
+ }
860
+ /**
861
+ * Get all objects with a given geometry type (e.g. "BoxGeometry", "BufferGeometry").
862
+ */
863
+ async getByGeometryType(type) {
864
+ return this._page.evaluate(([t, cid]) => {
865
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
866
+ return api ? api.getByGeometryType(t) : [];
867
+ }, [type, this.canvasId ?? null]);
868
+ }
869
+ /**
870
+ * Get all objects with a given material type (e.g. "MeshStandardMaterial").
871
+ */
872
+ async getByMaterialType(type) {
873
+ return this._page.evaluate(([t, cid]) => {
874
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
875
+ return api ? api.getByMaterialType(t) : [];
876
+ }, [type, this.canvasId ?? null]);
458
877
  }
459
878
  /**
460
879
  * Get objects that have a specific userData key (and optionally matching value).
461
- * Useful for BIM/CAD apps where objects are tagged with metadata like
462
- * `userData.category = "wall"` or `userData.floorId = 2`.
463
880
  */
464
881
  async getByUserData(key, value) {
465
- return this._page.evaluate(({ k, v }) => {
466
- const api = window.__R3F_DOM__;
882
+ return this._page.evaluate(({ k, v, cid }) => {
883
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
467
884
  return api ? api.getByUserData(k, v) : [];
468
- }, { k: key, v: value });
885
+ }, { k: key, v: value, cid: this.canvasId ?? null });
469
886
  }
470
887
  /**
471
888
  * Count objects of a given Three.js type.
472
- * More efficient than `getByType(type).then(arr => arr.length)`.
473
889
  */
474
890
  async getCountByType(type) {
475
- return this._page.evaluate((t) => {
476
- const api = window.__R3F_DOM__;
891
+ return this._page.evaluate(([t, cid]) => {
892
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
477
893
  return api ? api.getCountByType(t) : 0;
478
- }, type);
894
+ }, [type, this.canvasId ?? null]);
479
895
  }
480
896
  /**
481
897
  * Batch lookup: get metadata for multiple objects by testId or uuid in a
482
- * single browser round-trip. Returns a record from id to metadata (or null).
483
- *
484
- * Much more efficient than calling `getObject()` in a loop for BIM/CAD
485
- * scenes with many objects.
486
- *
487
- * @example
488
- * ```typescript
489
- * const results = await r3f.getObjects(['wall-1', 'door-2', 'window-3']);
490
- * expect(results['wall-1']).not.toBeNull();
491
- * expect(results['door-2']?.type).toBe('Mesh');
492
- * ```
898
+ * single browser round-trip.
493
899
  */
494
900
  async getObjects(ids) {
495
- return this._page.evaluate((idList) => {
496
- const api = window.__R3F_DOM__;
901
+ return this._page.evaluate(([idList, cid]) => {
902
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
497
903
  if (!api) {
498
904
  const result = {};
499
905
  for (const id of idList) result[id] = null;
500
906
  return result;
501
907
  }
502
908
  return api.getObjects(idList);
503
- }, ids);
909
+ }, [ids, this.canvasId ?? null]);
504
910
  }
505
911
  /**
506
912
  * Log the scene tree to the test terminal for debugging.
@@ -537,49 +943,56 @@ ${lines}
537
943
  * @param timeout Optional auto-wait timeout in ms. Default: 5000
538
944
  */
539
945
  async click(idOrUuid, timeout) {
540
- return click(this._page, idOrUuid, timeout);
946
+ return click(this._page, idOrUuid, timeout, this.canvasId);
541
947
  }
542
948
  /**
543
949
  * Double-click a 3D object by testId or uuid.
544
950
  * Auto-waits for the object to exist.
545
951
  */
546
952
  async doubleClick(idOrUuid, timeout) {
547
- return doubleClick(this._page, idOrUuid, timeout);
953
+ return doubleClick(this._page, idOrUuid, timeout, this.canvasId);
548
954
  }
549
955
  /**
550
956
  * Right-click / context-menu a 3D object by testId or uuid.
551
957
  * Auto-waits for the object to exist.
552
958
  */
553
959
  async contextMenu(idOrUuid, timeout) {
554
- return contextMenu(this._page, idOrUuid, timeout);
960
+ return contextMenu(this._page, idOrUuid, timeout, this.canvasId);
555
961
  }
556
962
  /**
557
963
  * Hover over a 3D object by testId or uuid.
558
964
  * Auto-waits for the object to exist.
559
965
  */
560
966
  async hover(idOrUuid, timeout) {
561
- return hover(this._page, idOrUuid, timeout);
967
+ return hover(this._page, idOrUuid, timeout, this.canvasId);
968
+ }
969
+ /**
970
+ * Unhover / pointer-leave — resets hover state by moving pointer off-canvas.
971
+ * Auto-waits for the bridge to be ready.
972
+ */
973
+ async unhover(timeout) {
974
+ return unhover(this._page, timeout, this.canvasId);
562
975
  }
563
976
  /**
564
977
  * Drag a 3D object with a world-space delta vector.
565
978
  * Auto-waits for the object to exist.
566
979
  */
567
980
  async drag(idOrUuid, delta, timeout) {
568
- return drag(this._page, idOrUuid, delta, timeout);
981
+ return drag(this._page, idOrUuid, delta, timeout, this.canvasId);
569
982
  }
570
983
  /**
571
984
  * Dispatch a wheel/scroll event on a 3D object.
572
985
  * Auto-waits for the object to exist.
573
986
  */
574
987
  async wheel(idOrUuid, options, timeout) {
575
- return wheel(this._page, idOrUuid, options, timeout);
988
+ return wheel(this._page, idOrUuid, options, timeout, this.canvasId);
576
989
  }
577
990
  /**
578
991
  * Click empty space to trigger onPointerMissed handlers.
579
992
  * Auto-waits for the bridge to be ready.
580
993
  */
581
994
  async pointerMiss(timeout) {
582
- return pointerMiss(this._page, timeout);
995
+ return pointerMiss(this._page, timeout, this.canvasId);
583
996
  }
584
997
  /**
585
998
  * Draw a freeform path on the canvas. Dispatches pointerdown → N × pointermove → pointerup.
@@ -592,7 +1005,17 @@ ${lines}
592
1005
  * @returns { eventCount, pointCount }
593
1006
  */
594
1007
  async drawPath(points, options, timeout) {
595
- return drawPathOnCanvas(this._page, points, options, timeout);
1008
+ return drawPathOnCanvas(this._page, points, options, timeout, this.canvasId);
1009
+ }
1010
+ // -----------------------------------------------------------------------
1011
+ // Camera
1012
+ // -----------------------------------------------------------------------
1013
+ /**
1014
+ * Get the current camera state (position, rotation, fov, near, far, zoom, target).
1015
+ * Auto-waits for the bridge to be ready.
1016
+ */
1017
+ async getCameraState(timeout) {
1018
+ return getCameraState(this._page, timeout, this.canvasId);
596
1019
  }
597
1020
  // -----------------------------------------------------------------------
598
1021
  // Waiters
@@ -600,9 +1023,22 @@ ${lines}
600
1023
  /**
601
1024
  * Wait until the scene is ready — `window.__R3F_DOM__` is available and
602
1025
  * the object count has stabilised across several consecutive checks.
1026
+ * Logs bridge connection and scene readiness to the terminal.
603
1027
  */
604
1028
  async waitForSceneReady(options) {
605
- return waitForSceneReady(this._page, options);
1029
+ this._reporter.logBridgeWaiting();
1030
+ try {
1031
+ await waitForSceneReady(this._page, { ...options, canvasId: this.canvasId });
1032
+ const diag = await this._reporter.fetchDiagnostics();
1033
+ if (diag) {
1034
+ this._reporter.logBridgeConnected(diag);
1035
+ this._reporter.logSceneReady(diag.objectCount);
1036
+ }
1037
+ } catch (e) {
1038
+ const diag = await this._reporter.fetchDiagnostics();
1039
+ if (diag?.error) this._reporter.logBridgeError(diag.error);
1040
+ throw e;
1041
+ }
606
1042
  }
607
1043
  /**
608
1044
  * Wait until the bridge is available and an object with the given testId or
@@ -610,14 +1046,25 @@ ${lines}
610
1046
  * never stabilizes (e.g. async model loading, continuous animations).
611
1047
  */
612
1048
  async waitForObject(idOrUuid, options) {
613
- return waitForObject(this._page, idOrUuid, options);
1049
+ this._reporter.logBridgeWaiting();
1050
+ try {
1051
+ await waitForObject(this._page, idOrUuid, { ...options, canvasId: this.canvasId });
1052
+ const meta = await this.getObject(idOrUuid);
1053
+ if (meta) {
1054
+ this._reporter.logObjectFound(idOrUuid, meta.type, meta.name || void 0);
1055
+ }
1056
+ } catch (e) {
1057
+ const suggestions = await this._reporter.fetchFuzzyMatches(idOrUuid);
1058
+ this._reporter.logObjectNotFound(idOrUuid, suggestions);
1059
+ throw e;
1060
+ }
614
1061
  }
615
1062
  /**
616
1063
  * Wait until no object properties have changed for a number of consecutive
617
1064
  * animation frames. Useful after triggering interactions or animations.
618
1065
  */
619
1066
  async waitForIdle(options) {
620
- return waitForIdle(this._page, options);
1067
+ return waitForIdle(this._page, { ...options, canvasId: this.canvasId });
621
1068
  }
622
1069
  /**
623
1070
  * Wait until one or more new objects appear in the scene that were not
@@ -628,25 +1075,49 @@ ${lines}
628
1075
  * @returns Metadata of the newly added object(s)
629
1076
  */
630
1077
  async waitForNewObject(options) {
631
- return waitForNewObject(this._page, options);
1078
+ return waitForNewObject(this._page, { ...options, canvasId: this.canvasId });
1079
+ }
1080
+ /**
1081
+ * Wait until an object (by testId or uuid) is no longer in the scene.
1082
+ * Use for delete flows: trigger removal, then wait until the object is gone.
1083
+ */
1084
+ async waitForObjectRemoved(idOrUuid, options) {
1085
+ return waitForObjectRemoved(this._page, idOrUuid, { ...options, canvasId: this.canvasId });
632
1086
  }
633
1087
  // -----------------------------------------------------------------------
634
1088
  // Selection (for inspector integration)
635
1089
  // -----------------------------------------------------------------------
636
1090
  /** Select a 3D object by testId or uuid (highlights in scene). */
637
1091
  async select(idOrUuid) {
638
- await this._page.evaluate((id) => {
639
- const api = window.__R3F_DOM__;
1092
+ await this._page.evaluate(([id, cid]) => {
1093
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
640
1094
  if (!api) throw new Error("react-three-dom bridge not found");
641
1095
  api.select(id);
642
- }, idOrUuid);
1096
+ }, [idOrUuid, this.canvasId ?? null]);
643
1097
  }
644
1098
  /** Clear the current selection. */
645
1099
  async clearSelection() {
646
- await this._page.evaluate(() => {
647
- const api = window.__R3F_DOM__;
1100
+ await this._page.evaluate((cid) => {
1101
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
648
1102
  if (api) api.clearSelection();
649
- });
1103
+ }, this.canvasId ?? null);
1104
+ }
1105
+ // -----------------------------------------------------------------------
1106
+ // Diagnostics
1107
+ // -----------------------------------------------------------------------
1108
+ /**
1109
+ * Fetch full bridge diagnostics (version, object counts, GPU info, etc.).
1110
+ * Returns null if the bridge is not available.
1111
+ */
1112
+ async getDiagnostics() {
1113
+ return this._reporter.fetchDiagnostics();
1114
+ }
1115
+ /**
1116
+ * Print a full diagnostics report to the terminal.
1117
+ * Useful at the start of a test suite or when debugging failures.
1118
+ */
1119
+ async logDiagnostics() {
1120
+ return this._reporter.logDiagnostics();
650
1121
  }
651
1122
  };
652
1123
  function formatSceneTree(node, prefix = "", isLast = true) {
@@ -680,21 +1151,21 @@ function createR3FTest(options) {
680
1151
  }
681
1152
  var DEFAULT_TIMEOUT = 5e3;
682
1153
  var DEFAULT_INTERVAL = 100;
683
- async function fetchSceneCount(page) {
684
- return page.evaluate(() => {
685
- const api = window.__R3F_DOM__;
1154
+ async function fetchSceneCount(page, canvasId) {
1155
+ return page.evaluate((cid) => {
1156
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
686
1157
  return api ? api.getCount() : 0;
687
- });
1158
+ }, canvasId ?? null);
688
1159
  }
689
- async function fetchCountByType(page, type) {
690
- return page.evaluate((t) => {
691
- const api = window.__R3F_DOM__;
1160
+ async function fetchCountByType(page, type, canvasId) {
1161
+ return page.evaluate(([t, cid]) => {
1162
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
692
1163
  return api ? api.getCountByType(t) : 0;
693
- }, type);
1164
+ }, [type, canvasId ?? null]);
694
1165
  }
695
- async function fetchTotalTriangles(page) {
696
- return page.evaluate(() => {
697
- const api = window.__R3F_DOM__;
1166
+ async function fetchTotalTriangles(page, canvasId) {
1167
+ return page.evaluate((cid) => {
1168
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
698
1169
  if (!api) return 0;
699
1170
  const bridge = api;
700
1171
  const snap = bridge.snapshot();
@@ -708,21 +1179,31 @@ async function fetchTotalTriangles(page) {
708
1179
  }
709
1180
  walk(snap.tree);
710
1181
  return total;
711
- });
1182
+ }, canvasId ?? null);
712
1183
  }
713
- async function fetchMeta(page, id) {
714
- return page.evaluate((i) => {
715
- const api = window.__R3F_DOM__;
1184
+ async function fetchMeta(page, id, canvasId) {
1185
+ return page.evaluate(([i, cid]) => {
1186
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
716
1187
  if (!api) return null;
717
1188
  return api.getByTestId(i) ?? api.getByUuid(i) ?? null;
718
- }, id);
1189
+ }, [id, canvasId ?? null]);
719
1190
  }
720
- async function fetchInsp(page, id) {
721
- return page.evaluate((i) => {
722
- const api = window.__R3F_DOM__;
1191
+ async function fetchInsp(page, id, canvasId) {
1192
+ return page.evaluate(([i, cid]) => {
1193
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
723
1194
  if (!api) return null;
724
1195
  return api.inspect(i);
725
- }, id);
1196
+ }, [id, canvasId ?? null]);
1197
+ }
1198
+ async function fetchWorldPosition(page, id, canvasId) {
1199
+ return page.evaluate(([i, cid]) => {
1200
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
1201
+ if (!api) return null;
1202
+ const insp = api.inspect(i);
1203
+ if (!insp || !insp.worldMatrix || insp.worldMatrix.length < 15) return null;
1204
+ const m = insp.worldMatrix;
1205
+ return [m[12], m[13], m[14]];
1206
+ }, [id, canvasId ?? null]);
726
1207
  }
727
1208
  function parseTol(v, def) {
728
1209
  const o = typeof v === "number" ? { tolerance: v } : v ?? {};
@@ -732,14 +1213,55 @@ function parseTol(v, def) {
732
1213
  tolerance: o.tolerance ?? def
733
1214
  };
734
1215
  }
735
- function notFound(name, id, detail, timeout) {
1216
+ async function fetchFuzzyHints(page, query, canvasId) {
1217
+ try {
1218
+ const suggestions = await page.evaluate(
1219
+ ({ q, cid }) => {
1220
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
1221
+ if (!api || typeof api.fuzzyFind !== "function") return [];
1222
+ return api.fuzzyFind(q, 5).map((m) => ({
1223
+ testId: m.testId,
1224
+ name: m.name,
1225
+ uuid: m.uuid
1226
+ }));
1227
+ },
1228
+ { q: query, cid: canvasId ?? null }
1229
+ );
1230
+ if (suggestions.length === 0) return "";
1231
+ return "\nDid you mean:\n" + suggestions.map((s) => {
1232
+ const id = s.testId ? `testId="${s.testId}"` : `uuid="${s.uuid}"`;
1233
+ return ` \u2192 ${s.name || "(unnamed)"} [${id}]`;
1234
+ }).join("\n");
1235
+ } catch {
1236
+ return "";
1237
+ }
1238
+ }
1239
+ async function fetchDiagnosticHint(page, canvasId) {
1240
+ try {
1241
+ const diag = await page.evaluate((cid) => {
1242
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
1243
+ if (!api || typeof api.getDiagnostics !== "function") return null;
1244
+ return api.getDiagnostics();
1245
+ }, canvasId ?? null);
1246
+ if (!diag) return "";
1247
+ return `
1248
+ Bridge: v${diag.version} ready=${diag.ready}, ${diag.objectCount} objects (${diag.meshCount} meshes)`;
1249
+ } catch {
1250
+ return "";
1251
+ }
1252
+ }
1253
+ async function notFoundAsync(page, name, id, detail, timeout, canvasId) {
1254
+ const [fuzzy, diag] = await Promise.all([
1255
+ fetchFuzzyHints(page, id, canvasId),
1256
+ fetchDiagnosticHint(page, canvasId)
1257
+ ]);
736
1258
  return {
737
1259
  pass: false,
738
- message: () => `Expected object "${id}" ${detail}, but it was not found (waited ${timeout}ms)`,
1260
+ message: () => `Expected object "${id}" ${detail}, but it was not found (waited ${timeout}ms)${diag}${fuzzy}`,
739
1261
  name
740
1262
  };
741
1263
  }
742
- var expect = test$1.expect.extend({
1264
+ var r3fMatchers = {
743
1265
  // ========================= TIER 1 — Metadata ============================
744
1266
  // --- toExist ---
745
1267
  async toExist(r3f, id, opts) {
@@ -749,7 +1271,7 @@ var expect = test$1.expect.extend({
749
1271
  let meta = null;
750
1272
  try {
751
1273
  await test$1.expect.poll(async () => {
752
- meta = await fetchMeta(r3f.page, id);
1274
+ meta = await fetchMeta(r3f.page, id, r3f.canvasId);
753
1275
  return meta !== null;
754
1276
  }, { timeout, intervals: [interval] }).toBe(!isNot);
755
1277
  } catch {
@@ -771,12 +1293,12 @@ var expect = test$1.expect.extend({
771
1293
  let meta = null;
772
1294
  try {
773
1295
  await test$1.expect.poll(async () => {
774
- meta = await fetchMeta(r3f.page, id);
1296
+ meta = await fetchMeta(r3f.page, id, r3f.canvasId);
775
1297
  return meta?.visible ?? false;
776
1298
  }, { timeout, intervals: [interval] }).toBe(!isNot);
777
1299
  } catch {
778
1300
  }
779
- if (!meta) return notFound("toBeVisible", id, "to be visible", timeout);
1301
+ if (!meta) return notFoundAsync(r3f.page, "toBeVisible", id, "to be visible", timeout, r3f.canvasId);
780
1302
  const m = meta;
781
1303
  return {
782
1304
  pass: m.visible,
@@ -795,7 +1317,7 @@ var expect = test$1.expect.extend({
795
1317
  let delta = [0, 0, 0];
796
1318
  try {
797
1319
  await test$1.expect.poll(async () => {
798
- meta = await fetchMeta(r3f.page, id);
1320
+ meta = await fetchMeta(r3f.page, id, r3f.canvasId);
799
1321
  if (!meta) return false;
800
1322
  delta = [Math.abs(meta.position[0] - expected[0]), Math.abs(meta.position[1] - expected[1]), Math.abs(meta.position[2] - expected[2])];
801
1323
  pass = delta.every((d) => d <= tolerance);
@@ -803,7 +1325,7 @@ var expect = test$1.expect.extend({
803
1325
  }, { timeout, intervals: [interval] }).toBe(!isNot);
804
1326
  } catch {
805
1327
  }
806
- if (!meta) return notFound("toHavePosition", id, `to have position [${expected}]`, timeout);
1328
+ if (!meta) return notFoundAsync(r3f.page, "toHavePosition", id, `to have position [${expected}]`, timeout, r3f.canvasId);
807
1329
  const m = meta;
808
1330
  return {
809
1331
  pass,
@@ -813,6 +1335,33 @@ var expect = test$1.expect.extend({
813
1335
  actual: m.position
814
1336
  };
815
1337
  },
1338
+ // --- toHaveWorldPosition ---
1339
+ async toHaveWorldPosition(r3f, id, expected, tolOpts) {
1340
+ const { timeout, interval, tolerance } = parseTol(tolOpts, 0.01);
1341
+ const isNot = this.isNot;
1342
+ let worldPos = null;
1343
+ let pass = false;
1344
+ let delta = [0, 0, 0];
1345
+ try {
1346
+ await test$1.expect.poll(async () => {
1347
+ worldPos = await fetchWorldPosition(r3f.page, id, r3f.canvasId);
1348
+ if (!worldPos) return false;
1349
+ delta = [Math.abs(worldPos[0] - expected[0]), Math.abs(worldPos[1] - expected[1]), Math.abs(worldPos[2] - expected[2])];
1350
+ pass = delta.every((d) => d <= tolerance);
1351
+ return pass;
1352
+ }, { timeout, intervals: [interval] }).toBe(!isNot);
1353
+ } catch {
1354
+ }
1355
+ if (!worldPos) return notFoundAsync(r3f.page, "toHaveWorldPosition", id, `to have world position [${expected}]`, timeout, r3f.canvasId);
1356
+ const actualWorldPos = worldPos;
1357
+ return {
1358
+ pass,
1359
+ message: () => pass ? `Expected "${id}" to NOT have world position [${expected}] (\xB1${tolerance})` : `Expected "${id}" world position [${expected}] (\xB1${tolerance}), got [${actualWorldPos[0].toFixed(4)}, ${actualWorldPos[1].toFixed(4)}, ${actualWorldPos[2].toFixed(4)}] (\u0394 [${delta.map((d) => d.toFixed(4))}]) (waited ${timeout}ms)`,
1360
+ name: "toHaveWorldPosition",
1361
+ expected,
1362
+ actual: actualWorldPos
1363
+ };
1364
+ },
816
1365
  // --- toHaveRotation ---
817
1366
  async toHaveRotation(r3f, id, expected, tolOpts) {
818
1367
  const { timeout, interval, tolerance } = parseTol(tolOpts, 0.01);
@@ -822,7 +1371,7 @@ var expect = test$1.expect.extend({
822
1371
  let delta = [0, 0, 0];
823
1372
  try {
824
1373
  await test$1.expect.poll(async () => {
825
- meta = await fetchMeta(r3f.page, id);
1374
+ meta = await fetchMeta(r3f.page, id, r3f.canvasId);
826
1375
  if (!meta) return false;
827
1376
  delta = [Math.abs(meta.rotation[0] - expected[0]), Math.abs(meta.rotation[1] - expected[1]), Math.abs(meta.rotation[2] - expected[2])];
828
1377
  pass = delta.every((d) => d <= tolerance);
@@ -830,7 +1379,7 @@ var expect = test$1.expect.extend({
830
1379
  }, { timeout, intervals: [interval] }).toBe(!isNot);
831
1380
  } catch {
832
1381
  }
833
- if (!meta) return notFound("toHaveRotation", id, `to have rotation [${expected}]`, timeout);
1382
+ if (!meta) return notFoundAsync(r3f.page, "toHaveRotation", id, `to have rotation [${expected}]`, timeout, r3f.canvasId);
834
1383
  const m = meta;
835
1384
  return {
836
1385
  pass,
@@ -849,7 +1398,7 @@ var expect = test$1.expect.extend({
849
1398
  let delta = [0, 0, 0];
850
1399
  try {
851
1400
  await test$1.expect.poll(async () => {
852
- meta = await fetchMeta(r3f.page, id);
1401
+ meta = await fetchMeta(r3f.page, id, r3f.canvasId);
853
1402
  if (!meta) return false;
854
1403
  delta = [Math.abs(meta.scale[0] - expected[0]), Math.abs(meta.scale[1] - expected[1]), Math.abs(meta.scale[2] - expected[2])];
855
1404
  pass = delta.every((d) => d <= tolerance);
@@ -857,7 +1406,7 @@ var expect = test$1.expect.extend({
857
1406
  }, { timeout, intervals: [interval] }).toBe(!isNot);
858
1407
  } catch {
859
1408
  }
860
- if (!meta) return notFound("toHaveScale", id, `to have scale [${expected}]`, timeout);
1409
+ if (!meta) return notFoundAsync(r3f.page, "toHaveScale", id, `to have scale [${expected}]`, timeout, r3f.canvasId);
861
1410
  const m = meta;
862
1411
  return {
863
1412
  pass,
@@ -876,14 +1425,14 @@ var expect = test$1.expect.extend({
876
1425
  let pass = false;
877
1426
  try {
878
1427
  await test$1.expect.poll(async () => {
879
- meta = await fetchMeta(r3f.page, id);
1428
+ meta = await fetchMeta(r3f.page, id, r3f.canvasId);
880
1429
  if (!meta) return false;
881
1430
  pass = meta.type === expectedType;
882
1431
  return pass;
883
1432
  }, { timeout, intervals: [interval] }).toBe(!isNot);
884
1433
  } catch {
885
1434
  }
886
- if (!meta) return notFound("toHaveType", id, `to have type "${expectedType}"`, timeout);
1435
+ if (!meta) return notFoundAsync(r3f.page, "toHaveType", id, `to have type "${expectedType}"`, timeout, r3f.canvasId);
887
1436
  const m = meta;
888
1437
  return {
889
1438
  pass,
@@ -902,14 +1451,14 @@ var expect = test$1.expect.extend({
902
1451
  let pass = false;
903
1452
  try {
904
1453
  await test$1.expect.poll(async () => {
905
- meta = await fetchMeta(r3f.page, id);
1454
+ meta = await fetchMeta(r3f.page, id, r3f.canvasId);
906
1455
  if (!meta) return false;
907
1456
  pass = meta.name === expectedName;
908
1457
  return pass;
909
1458
  }, { timeout, intervals: [interval] }).toBe(!isNot);
910
1459
  } catch {
911
1460
  }
912
- if (!meta) return notFound("toHaveName", id, `to have name "${expectedName}"`, timeout);
1461
+ if (!meta) return notFoundAsync(r3f.page, "toHaveName", id, `to have name "${expectedName}"`, timeout, r3f.canvasId);
913
1462
  const m = meta;
914
1463
  return {
915
1464
  pass,
@@ -928,14 +1477,14 @@ var expect = test$1.expect.extend({
928
1477
  let pass = false;
929
1478
  try {
930
1479
  await test$1.expect.poll(async () => {
931
- meta = await fetchMeta(r3f.page, id);
1480
+ meta = await fetchMeta(r3f.page, id, r3f.canvasId);
932
1481
  if (!meta) return false;
933
1482
  pass = meta.geometryType === expectedGeo;
934
1483
  return pass;
935
1484
  }, { timeout, intervals: [interval] }).toBe(!isNot);
936
1485
  } catch {
937
1486
  }
938
- if (!meta) return notFound("toHaveGeometryType", id, `to have geometry "${expectedGeo}"`, timeout);
1487
+ if (!meta) return notFoundAsync(r3f.page, "toHaveGeometryType", id, `to have geometry "${expectedGeo}"`, timeout, r3f.canvasId);
939
1488
  const m = meta;
940
1489
  return {
941
1490
  pass,
@@ -954,14 +1503,14 @@ var expect = test$1.expect.extend({
954
1503
  let pass = false;
955
1504
  try {
956
1505
  await test$1.expect.poll(async () => {
957
- meta = await fetchMeta(r3f.page, id);
1506
+ meta = await fetchMeta(r3f.page, id, r3f.canvasId);
958
1507
  if (!meta) return false;
959
1508
  pass = meta.materialType === expectedMat;
960
1509
  return pass;
961
1510
  }, { timeout, intervals: [interval] }).toBe(!isNot);
962
1511
  } catch {
963
1512
  }
964
- if (!meta) return notFound("toHaveMaterialType", id, `to have material "${expectedMat}"`, timeout);
1513
+ if (!meta) return notFoundAsync(r3f.page, "toHaveMaterialType", id, `to have material "${expectedMat}"`, timeout, r3f.canvasId);
965
1514
  const m = meta;
966
1515
  return {
967
1516
  pass,
@@ -980,14 +1529,14 @@ var expect = test$1.expect.extend({
980
1529
  let actual = 0;
981
1530
  try {
982
1531
  await test$1.expect.poll(async () => {
983
- meta = await fetchMeta(r3f.page, id);
1532
+ meta = await fetchMeta(r3f.page, id, r3f.canvasId);
984
1533
  if (!meta) return false;
985
1534
  actual = meta.childrenUuids.length;
986
1535
  return actual === expectedCount;
987
1536
  }, { timeout, intervals: [interval] }).toBe(!isNot);
988
1537
  } catch {
989
1538
  }
990
- if (!meta) return notFound("toHaveChildCount", id, `to have ${expectedCount} children`, timeout);
1539
+ if (!meta) return notFoundAsync(r3f.page, "toHaveChildCount", id, `to have ${expectedCount} children`, timeout, r3f.canvasId);
991
1540
  const pass = actual === expectedCount;
992
1541
  return {
993
1542
  pass,
@@ -1007,16 +1556,16 @@ var expect = test$1.expect.extend({
1007
1556
  let pass = false;
1008
1557
  try {
1009
1558
  await test$1.expect.poll(async () => {
1010
- meta = await fetchMeta(r3f.page, id);
1559
+ meta = await fetchMeta(r3f.page, id, r3f.canvasId);
1011
1560
  if (!meta?.parentUuid) return false;
1012
- parentMeta = await fetchMeta(r3f.page, meta.parentUuid);
1561
+ parentMeta = await fetchMeta(r3f.page, meta.parentUuid, r3f.canvasId);
1013
1562
  if (!parentMeta) return false;
1014
1563
  pass = parentMeta.uuid === expectedParent || parentMeta.testId === expectedParent || parentMeta.name === expectedParent;
1015
1564
  return pass;
1016
1565
  }, { timeout, intervals: [interval] }).toBe(!isNot);
1017
1566
  } catch {
1018
1567
  }
1019
- if (!meta) return notFound("toHaveParent", id, `to have parent "${expectedParent}"`, timeout);
1568
+ if (!meta) return notFoundAsync(r3f.page, "toHaveParent", id, `to have parent "${expectedParent}"`, timeout, r3f.canvasId);
1020
1569
  const m = meta;
1021
1570
  const pm = parentMeta;
1022
1571
  const parentLabel = pm?.testId ?? pm?.name ?? m.parentUuid;
@@ -1037,13 +1586,13 @@ var expect = test$1.expect.extend({
1037
1586
  let actual = 0;
1038
1587
  try {
1039
1588
  await test$1.expect.poll(async () => {
1040
- meta = await fetchMeta(r3f.page, id);
1589
+ meta = await fetchMeta(r3f.page, id, r3f.canvasId);
1041
1590
  actual = meta?.instanceCount ?? 0;
1042
1591
  return actual === expectedCount;
1043
1592
  }, { timeout, intervals: [interval] }).toBe(!isNot);
1044
1593
  } catch {
1045
1594
  }
1046
- if (!meta) return notFound("toHaveInstanceCount", id, `to have instance count ${expectedCount}`, timeout);
1595
+ if (!meta) return notFoundAsync(r3f.page, "toHaveInstanceCount", id, `to have instance count ${expectedCount}`, timeout, r3f.canvasId);
1047
1596
  const pass = actual === expectedCount;
1048
1597
  return {
1049
1598
  pass,
@@ -1063,7 +1612,7 @@ var expect = test$1.expect.extend({
1063
1612
  let pass = false;
1064
1613
  try {
1065
1614
  await test$1.expect.poll(async () => {
1066
- insp = await fetchInsp(r3f.page, id);
1615
+ insp = await fetchInsp(r3f.page, id, r3f.canvasId);
1067
1616
  if (!insp) return false;
1068
1617
  const fin = (v) => v.every(Number.isFinite);
1069
1618
  pass = fin(insp.bounds.min) && fin(insp.bounds.max);
@@ -1071,7 +1620,7 @@ var expect = test$1.expect.extend({
1071
1620
  }, { timeout, intervals: [interval] }).toBe(!isNot);
1072
1621
  } catch {
1073
1622
  }
1074
- if (!insp) return notFound("toBeInFrustum", id, "to be in frustum", timeout);
1623
+ if (!insp) return notFoundAsync(r3f.page, "toBeInFrustum", id, "to be in frustum", timeout, r3f.canvasId);
1075
1624
  const i = insp;
1076
1625
  return {
1077
1626
  pass,
@@ -1089,7 +1638,7 @@ var expect = test$1.expect.extend({
1089
1638
  let pass = false;
1090
1639
  try {
1091
1640
  await test$1.expect.poll(async () => {
1092
- insp = await fetchInsp(r3f.page, id);
1641
+ insp = await fetchInsp(r3f.page, id, r3f.canvasId);
1093
1642
  if (!insp) return false;
1094
1643
  const w = (a, b) => a.every((v, j) => Math.abs(v - b[j]) <= tolerance);
1095
1644
  pass = w(insp.bounds.min, expected.min) && w(insp.bounds.max, expected.max);
@@ -1097,7 +1646,7 @@ var expect = test$1.expect.extend({
1097
1646
  }, { timeout, intervals: [interval] }).toBe(!isNot);
1098
1647
  } catch {
1099
1648
  }
1100
- if (!insp) return notFound("toHaveBounds", id, "to have specific bounds", timeout);
1649
+ if (!insp) return notFoundAsync(r3f.page, "toHaveBounds", id, "to have specific bounds", timeout, r3f.canvasId);
1101
1650
  const i = insp;
1102
1651
  return {
1103
1652
  pass,
@@ -1118,7 +1667,7 @@ var expect = test$1.expect.extend({
1118
1667
  let pass = false;
1119
1668
  try {
1120
1669
  await test$1.expect.poll(async () => {
1121
- insp = await fetchInsp(r3f.page, id);
1670
+ insp = await fetchInsp(r3f.page, id, r3f.canvasId);
1122
1671
  if (!insp?.material?.color) return false;
1123
1672
  actual = insp.material.color.toLowerCase();
1124
1673
  pass = actual === norm;
@@ -1126,7 +1675,7 @@ var expect = test$1.expect.extend({
1126
1675
  }, { timeout, intervals: [interval] }).toBe(!isNot);
1127
1676
  } catch {
1128
1677
  }
1129
- if (!insp) return notFound("toHaveColor", id, `to have color "${norm}"`, timeout);
1678
+ if (!insp) return notFoundAsync(r3f.page, "toHaveColor", id, `to have color "${norm}"`, timeout, r3f.canvasId);
1130
1679
  return {
1131
1680
  pass,
1132
1681
  message: () => pass ? `Expected "${id}" to NOT have color "${norm}"` : `Expected "${id}" color "${norm}", got "${actual ?? "no color"}" (waited ${timeout}ms)`,
@@ -1144,7 +1693,7 @@ var expect = test$1.expect.extend({
1144
1693
  let pass = false;
1145
1694
  try {
1146
1695
  await test$1.expect.poll(async () => {
1147
- insp = await fetchInsp(r3f.page, id);
1696
+ insp = await fetchInsp(r3f.page, id, r3f.canvasId);
1148
1697
  if (!insp?.material) return false;
1149
1698
  actual = insp.material.opacity;
1150
1699
  pass = actual !== void 0 && Math.abs(actual - expectedOpacity) <= tolerance;
@@ -1152,7 +1701,7 @@ var expect = test$1.expect.extend({
1152
1701
  }, { timeout, intervals: [interval] }).toBe(!isNot);
1153
1702
  } catch {
1154
1703
  }
1155
- if (!insp) return notFound("toHaveOpacity", id, `to have opacity ${expectedOpacity}`, timeout);
1704
+ if (!insp) return notFoundAsync(r3f.page, "toHaveOpacity", id, `to have opacity ${expectedOpacity}`, timeout, r3f.canvasId);
1156
1705
  return {
1157
1706
  pass,
1158
1707
  message: () => pass ? `Expected "${id}" to NOT have opacity ${expectedOpacity} (\xB1${tolerance})` : `Expected "${id}" opacity ${expectedOpacity} (\xB1${tolerance}), got ${actual ?? "no material"} (waited ${timeout}ms)`,
@@ -1170,14 +1719,14 @@ var expect = test$1.expect.extend({
1170
1719
  let pass = false;
1171
1720
  try {
1172
1721
  await test$1.expect.poll(async () => {
1173
- insp = await fetchInsp(r3f.page, id);
1722
+ insp = await fetchInsp(r3f.page, id, r3f.canvasId);
1174
1723
  if (!insp?.material) return false;
1175
1724
  pass = insp.material.transparent === true;
1176
1725
  return pass;
1177
1726
  }, { timeout, intervals: [interval] }).toBe(!isNot);
1178
1727
  } catch {
1179
1728
  }
1180
- if (!insp) return notFound("toBeTransparent", id, "to be transparent", timeout);
1729
+ if (!insp) return notFoundAsync(r3f.page, "toBeTransparent", id, "to be transparent", timeout, r3f.canvasId);
1181
1730
  const i = insp;
1182
1731
  return {
1183
1732
  pass,
@@ -1196,13 +1745,13 @@ var expect = test$1.expect.extend({
1196
1745
  let actual = 0;
1197
1746
  try {
1198
1747
  await test$1.expect.poll(async () => {
1199
- meta = await fetchMeta(r3f.page, id);
1748
+ meta = await fetchMeta(r3f.page, id, r3f.canvasId);
1200
1749
  actual = meta?.vertexCount ?? 0;
1201
1750
  return actual === expectedCount;
1202
1751
  }, { timeout, intervals: [interval] }).toBe(!isNot);
1203
1752
  } catch {
1204
1753
  }
1205
- if (!meta) return notFound("toHaveVertexCount", id, `to have ${expectedCount} vertices`, timeout);
1754
+ if (!meta) return notFoundAsync(r3f.page, "toHaveVertexCount", id, `to have ${expectedCount} vertices`, timeout, r3f.canvasId);
1206
1755
  const pass = actual === expectedCount;
1207
1756
  return {
1208
1757
  pass,
@@ -1221,13 +1770,13 @@ var expect = test$1.expect.extend({
1221
1770
  let actual = 0;
1222
1771
  try {
1223
1772
  await test$1.expect.poll(async () => {
1224
- meta = await fetchMeta(r3f.page, id);
1773
+ meta = await fetchMeta(r3f.page, id, r3f.canvasId);
1225
1774
  actual = meta?.triangleCount ?? 0;
1226
1775
  return actual === expectedCount;
1227
1776
  }, { timeout, intervals: [interval] }).toBe(!isNot);
1228
1777
  } catch {
1229
1778
  }
1230
- if (!meta) return notFound("toHaveTriangleCount", id, `to have ${expectedCount} triangles`, timeout);
1779
+ if (!meta) return notFoundAsync(r3f.page, "toHaveTriangleCount", id, `to have ${expectedCount} triangles`, timeout, r3f.canvasId);
1231
1780
  const pass = actual === expectedCount;
1232
1781
  return {
1233
1782
  pass,
@@ -1247,7 +1796,7 @@ var expect = test$1.expect.extend({
1247
1796
  let pass = false;
1248
1797
  try {
1249
1798
  await test$1.expect.poll(async () => {
1250
- insp = await fetchInsp(r3f.page, id);
1799
+ insp = await fetchInsp(r3f.page, id, r3f.canvasId);
1251
1800
  if (!insp) return false;
1252
1801
  if (!(key in insp.userData)) return false;
1253
1802
  if (expectedValue === void 0) {
@@ -1260,7 +1809,7 @@ var expect = test$1.expect.extend({
1260
1809
  }, { timeout, intervals: [interval] }).toBe(!isNot);
1261
1810
  } catch {
1262
1811
  }
1263
- if (!insp) return notFound("toHaveUserData", id, `to have userData.${key}`, timeout);
1812
+ if (!insp) return notFoundAsync(r3f.page, "toHaveUserData", id, `to have userData.${key}`, timeout, r3f.canvasId);
1264
1813
  return {
1265
1814
  pass,
1266
1815
  message: () => {
@@ -1284,7 +1833,7 @@ var expect = test$1.expect.extend({
1284
1833
  let pass = false;
1285
1834
  try {
1286
1835
  await test$1.expect.poll(async () => {
1287
- insp = await fetchInsp(r3f.page, id);
1836
+ insp = await fetchInsp(r3f.page, id, r3f.canvasId);
1288
1837
  if (!insp?.material?.map) return false;
1289
1838
  actual = insp.material.map;
1290
1839
  if (!expectedName) {
@@ -1296,7 +1845,7 @@ var expect = test$1.expect.extend({
1296
1845
  }, { timeout, intervals: [interval] }).toBe(!isNot);
1297
1846
  } catch {
1298
1847
  }
1299
- if (!insp) return notFound("toHaveMapTexture", id, "to have a map texture", timeout);
1848
+ if (!insp) return notFoundAsync(r3f.page, "toHaveMapTexture", id, "to have a map texture", timeout, r3f.canvasId);
1300
1849
  return {
1301
1850
  pass,
1302
1851
  message: () => {
@@ -1328,7 +1877,7 @@ var expect = test$1.expect.extend({
1328
1877
  let pass = false;
1329
1878
  try {
1330
1879
  await test$1.expect.poll(async () => {
1331
- actual = await fetchSceneCount(r3f.page);
1880
+ actual = await fetchSceneCount(r3f.page, r3f.canvasId);
1332
1881
  pass = actual === expected;
1333
1882
  return pass;
1334
1883
  }, { timeout, intervals: [interval] }).toBe(!isNot);
@@ -1356,7 +1905,7 @@ var expect = test$1.expect.extend({
1356
1905
  let pass = false;
1357
1906
  try {
1358
1907
  await test$1.expect.poll(async () => {
1359
- actual = await fetchSceneCount(r3f.page);
1908
+ actual = await fetchSceneCount(r3f.page, r3f.canvasId);
1360
1909
  pass = actual > min;
1361
1910
  return pass;
1362
1911
  }, { timeout, intervals: [interval] }).toBe(!isNot);
@@ -1385,7 +1934,7 @@ var expect = test$1.expect.extend({
1385
1934
  let pass = false;
1386
1935
  try {
1387
1936
  await test$1.expect.poll(async () => {
1388
- actual = await fetchCountByType(r3f.page, type);
1937
+ actual = await fetchCountByType(r3f.page, type, r3f.canvasId);
1389
1938
  pass = actual === expected;
1390
1939
  return pass;
1391
1940
  }, { timeout, intervals: [interval] }).toBe(!isNot);
@@ -1414,7 +1963,7 @@ var expect = test$1.expect.extend({
1414
1963
  let pass = false;
1415
1964
  try {
1416
1965
  await test$1.expect.poll(async () => {
1417
- actual = await fetchTotalTriangles(r3f.page);
1966
+ actual = await fetchTotalTriangles(r3f.page, r3f.canvasId);
1418
1967
  pass = actual === expected;
1419
1968
  return pass;
1420
1969
  }, { timeout, intervals: [interval] }).toBe(!isNot);
@@ -1442,7 +1991,7 @@ var expect = test$1.expect.extend({
1442
1991
  let pass = false;
1443
1992
  try {
1444
1993
  await test$1.expect.poll(async () => {
1445
- actual = await fetchTotalTriangles(r3f.page);
1994
+ actual = await fetchTotalTriangles(r3f.page, r3f.canvasId);
1446
1995
  pass = actual < max;
1447
1996
  return pass;
1448
1997
  }, { timeout, intervals: [interval] }).toBe(!isNot);
@@ -1455,8 +2004,276 @@ var expect = test$1.expect.extend({
1455
2004
  expected: `< ${max}`,
1456
2005
  actual
1457
2006
  };
2007
+ },
2008
+ // ===================== CAMERA STATE ASSERTIONS ===========================
2009
+ /**
2010
+ * Assert the camera position is close to the expected [x, y, z].
2011
+ *
2012
+ * @example expect(r3f).toHaveCameraPosition([0, 5, 10], 0.1);
2013
+ */
2014
+ async toHaveCameraPosition(r3f, expected, tolOpts) {
2015
+ const { timeout, interval, tolerance } = parseTol(tolOpts, 0.1);
2016
+ const isNot = this.isNot;
2017
+ let actual = [0, 0, 0];
2018
+ let pass = false;
2019
+ try {
2020
+ await test$1.expect.poll(async () => {
2021
+ const cam = await r3f.page.evaluate((cid) => {
2022
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
2023
+ return api.getCameraState();
2024
+ }, r3f.canvasId ?? null);
2025
+ actual = cam.position;
2026
+ pass = Math.abs(actual[0] - expected[0]) <= tolerance && Math.abs(actual[1] - expected[1]) <= tolerance && Math.abs(actual[2] - expected[2]) <= tolerance;
2027
+ return pass;
2028
+ }, { timeout, intervals: [interval] }).toBe(!isNot);
2029
+ } catch {
2030
+ }
2031
+ return {
2032
+ pass,
2033
+ message: () => pass ? `Expected camera NOT at [${expected}], but it is` : `Expected camera at [${expected}], got [${actual}] (tol=${tolerance}, waited ${timeout}ms)`,
2034
+ name: "toHaveCameraPosition",
2035
+ expected,
2036
+ actual
2037
+ };
2038
+ },
2039
+ /**
2040
+ * Assert the camera field of view (PerspectiveCamera only).
2041
+ *
2042
+ * @example expect(r3f).toHaveCameraFov(75);
2043
+ */
2044
+ async toHaveCameraFov(r3f, expected, tolOpts) {
2045
+ const { timeout, interval, tolerance } = parseTol(tolOpts, 0.1);
2046
+ const isNot = this.isNot;
2047
+ let actual;
2048
+ let pass = false;
2049
+ try {
2050
+ await test$1.expect.poll(async () => {
2051
+ const cam = await r3f.page.evaluate((cid) => {
2052
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
2053
+ return api.getCameraState();
2054
+ }, r3f.canvasId ?? null);
2055
+ actual = cam.fov;
2056
+ pass = actual !== void 0 && Math.abs(actual - expected) <= tolerance;
2057
+ return pass;
2058
+ }, { timeout, intervals: [interval] }).toBe(!isNot);
2059
+ } catch {
2060
+ }
2061
+ return {
2062
+ pass,
2063
+ message: () => pass ? `Expected camera NOT to have fov ${expected}, but it does` : `Expected camera fov ${expected}, got ${actual ?? "N/A"} (tol=${tolerance}, waited ${timeout}ms)`,
2064
+ name: "toHaveCameraFov",
2065
+ expected,
2066
+ actual
2067
+ };
2068
+ },
2069
+ /**
2070
+ * Assert the camera near clipping plane.
2071
+ *
2072
+ * @example expect(r3f).toHaveCameraNear(0.1);
2073
+ */
2074
+ async toHaveCameraNear(r3f, expected, tolOpts) {
2075
+ const { timeout, interval, tolerance } = parseTol(tolOpts, 0.01);
2076
+ const isNot = this.isNot;
2077
+ let actual = 0;
2078
+ let pass = false;
2079
+ try {
2080
+ await test$1.expect.poll(async () => {
2081
+ const cam = await r3f.page.evaluate((cid) => {
2082
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
2083
+ return api.getCameraState();
2084
+ }, r3f.canvasId ?? null);
2085
+ actual = cam.near;
2086
+ pass = Math.abs(actual - expected) <= tolerance;
2087
+ return pass;
2088
+ }, { timeout, intervals: [interval] }).toBe(!isNot);
2089
+ } catch {
2090
+ }
2091
+ return {
2092
+ pass,
2093
+ message: () => pass ? `Expected camera near NOT ${expected}, but it is` : `Expected camera near ${expected}, got ${actual} (tol=${tolerance}, waited ${timeout}ms)`,
2094
+ name: "toHaveCameraNear",
2095
+ expected,
2096
+ actual
2097
+ };
2098
+ },
2099
+ /**
2100
+ * Assert the camera far clipping plane.
2101
+ *
2102
+ * @example expect(r3f).toHaveCameraFar(1000);
2103
+ */
2104
+ async toHaveCameraFar(r3f, expected, tolOpts) {
2105
+ const { timeout, interval, tolerance } = parseTol(tolOpts, 1);
2106
+ const isNot = this.isNot;
2107
+ let actual = 0;
2108
+ let pass = false;
2109
+ try {
2110
+ await test$1.expect.poll(async () => {
2111
+ const cam = await r3f.page.evaluate((cid) => {
2112
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
2113
+ return api.getCameraState();
2114
+ }, r3f.canvasId ?? null);
2115
+ actual = cam.far;
2116
+ pass = Math.abs(actual - expected) <= tolerance;
2117
+ return pass;
2118
+ }, { timeout, intervals: [interval] }).toBe(!isNot);
2119
+ } catch {
2120
+ }
2121
+ return {
2122
+ pass,
2123
+ message: () => pass ? `Expected camera far NOT ${expected}, but it is` : `Expected camera far ${expected}, got ${actual} (tol=${tolerance}, waited ${timeout}ms)`,
2124
+ name: "toHaveCameraFar",
2125
+ expected,
2126
+ actual
2127
+ };
2128
+ },
2129
+ /**
2130
+ * Assert the camera zoom level.
2131
+ *
2132
+ * @example expect(r3f).toHaveCameraZoom(1);
2133
+ */
2134
+ async toHaveCameraZoom(r3f, expected, tolOpts) {
2135
+ const { timeout, interval, tolerance } = parseTol(tolOpts, 0.01);
2136
+ const isNot = this.isNot;
2137
+ let actual = 0;
2138
+ let pass = false;
2139
+ try {
2140
+ await test$1.expect.poll(async () => {
2141
+ const cam = await r3f.page.evaluate((cid) => {
2142
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
2143
+ return api.getCameraState();
2144
+ }, r3f.canvasId ?? null);
2145
+ actual = cam.zoom;
2146
+ pass = Math.abs(actual - expected) <= tolerance;
2147
+ return pass;
2148
+ }, { timeout, intervals: [interval] }).toBe(!isNot);
2149
+ } catch {
2150
+ }
2151
+ return {
2152
+ pass,
2153
+ message: () => pass ? `Expected camera zoom NOT ${expected}, but it is` : `Expected camera zoom ${expected}, got ${actual} (tol=${tolerance}, waited ${timeout}ms)`,
2154
+ name: "toHaveCameraZoom",
2155
+ expected,
2156
+ actual
2157
+ };
2158
+ },
2159
+ // ======================== BATCH ASSERTIONS ==============================
2160
+ /**
2161
+ * Assert that ALL given objects exist in the scene.
2162
+ * Accepts an array of testIds/uuids or a glob pattern (e.g. "wall-*").
2163
+ *
2164
+ * @example expect(r3f).toAllExist(['wall-1', 'wall-2', 'floor']);
2165
+ * @example expect(r3f).toAllExist('wall-*');
2166
+ */
2167
+ async toAllExist(r3f, idsOrPattern, opts) {
2168
+ const timeout = opts?.timeout ?? DEFAULT_TIMEOUT;
2169
+ const interval = opts?.interval ?? DEFAULT_INTERVAL;
2170
+ const isNot = this.isNot;
2171
+ let missing = [];
2172
+ let pass = false;
2173
+ try {
2174
+ await test$1.expect.poll(async () => {
2175
+ const ids = typeof idsOrPattern === "string" ? await resolvePattern(r3f.page, idsOrPattern, r3f.canvasId) : idsOrPattern;
2176
+ missing = [];
2177
+ for (const id of ids) {
2178
+ const m = await r3f.getObject(id);
2179
+ if (!m) missing.push(id);
2180
+ }
2181
+ pass = missing.length === 0 && ids.length > 0;
2182
+ return pass;
2183
+ }, { timeout, intervals: [interval] }).toBe(!isNot);
2184
+ } catch {
2185
+ }
2186
+ return {
2187
+ pass,
2188
+ message: () => pass ? `Expected some objects to NOT exist, but all do` : `Objects not found: [${missing.join(", ")}] (waited ${timeout}ms)`,
2189
+ name: "toAllExist",
2190
+ expected: idsOrPattern,
2191
+ actual: { missing }
2192
+ };
2193
+ },
2194
+ /**
2195
+ * Assert that ALL given objects are visible.
2196
+ *
2197
+ * @example expect(r3f).toAllBeVisible(['wall-1', 'wall-2', 'floor']);
2198
+ * @example expect(r3f).toAllBeVisible('wall-*');
2199
+ */
2200
+ async toAllBeVisible(r3f, idsOrPattern, opts) {
2201
+ const timeout = opts?.timeout ?? DEFAULT_TIMEOUT;
2202
+ const interval = opts?.interval ?? DEFAULT_INTERVAL;
2203
+ const isNot = this.isNot;
2204
+ let hidden = [];
2205
+ let pass = false;
2206
+ try {
2207
+ await test$1.expect.poll(async () => {
2208
+ const ids = typeof idsOrPattern === "string" ? await resolvePattern(r3f.page, idsOrPattern, r3f.canvasId) : idsOrPattern;
2209
+ hidden = [];
2210
+ for (const id of ids) {
2211
+ const m = await r3f.getObject(id);
2212
+ if (!m || !m.visible) hidden.push(id);
2213
+ }
2214
+ pass = hidden.length === 0 && ids.length > 0;
2215
+ return pass;
2216
+ }, { timeout, intervals: [interval] }).toBe(!isNot);
2217
+ } catch {
2218
+ }
2219
+ return {
2220
+ pass,
2221
+ message: () => pass ? `Expected some objects to NOT be visible, but all are` : `Objects not visible: [${hidden.join(", ")}] (waited ${timeout}ms)`,
2222
+ name: "toAllBeVisible",
2223
+ expected: idsOrPattern,
2224
+ actual: { hidden }
2225
+ };
2226
+ },
2227
+ /**
2228
+ * Assert that NONE of the given objects exist in the scene.
2229
+ *
2230
+ * @example expect(r3f).toNoneExist(['deleted-wall', 'old-floor']);
2231
+ * @example expect(r3f).toNoneExist('temp-*');
2232
+ */
2233
+ async toNoneExist(r3f, idsOrPattern, opts) {
2234
+ const timeout = opts?.timeout ?? DEFAULT_TIMEOUT;
2235
+ const interval = opts?.interval ?? DEFAULT_INTERVAL;
2236
+ const isNot = this.isNot;
2237
+ let found = [];
2238
+ let pass = false;
2239
+ try {
2240
+ await test$1.expect.poll(async () => {
2241
+ const ids = typeof idsOrPattern === "string" ? await resolvePattern(r3f.page, idsOrPattern, r3f.canvasId) : idsOrPattern;
2242
+ found = [];
2243
+ for (const id of ids) {
2244
+ const m = await r3f.getObject(id);
2245
+ if (m) found.push(id);
2246
+ }
2247
+ pass = found.length === 0;
2248
+ return pass;
2249
+ }, { timeout, intervals: [interval] }).toBe(!isNot);
2250
+ } catch {
2251
+ }
2252
+ return {
2253
+ pass,
2254
+ message: () => pass ? `Expected some objects to exist, but none do` : `Objects still exist: [${found.join(", ")}] (waited ${timeout}ms)`,
2255
+ name: "toNoneExist",
2256
+ expected: idsOrPattern,
2257
+ actual: { found }
2258
+ };
1458
2259
  }
1459
- });
2260
+ };
2261
+ async function resolvePattern(page, pattern, canvasId) {
2262
+ return page.evaluate(([p, cid]) => {
2263
+ const api = cid ? window.__R3F_DOM_INSTANCES__?.[cid] : window.__R3F_DOM__;
2264
+ if (!api) return [];
2265
+ const snap = api.snapshot();
2266
+ const ids = [];
2267
+ const regex = new RegExp("^" + p.replace(/\*/g, ".*").replace(/\?/g, ".") + "$");
2268
+ function walk(node) {
2269
+ const testId = node.testId ?? node.name;
2270
+ if (regex.test(testId) || regex.test(node.uuid)) ids.push(testId || node.uuid);
2271
+ for (const child of node.children ?? []) walk(child);
2272
+ }
2273
+ walk(snap.tree);
2274
+ return ids;
2275
+ }, [pattern, canvasId ?? null]);
2276
+ }
1460
2277
 
1461
2278
  // src/pathGenerators.ts
1462
2279
  function linePath(start, end, steps = 10, pressure = 0.5) {
@@ -1523,23 +2340,28 @@ function circlePath(center, radiusX, radiusY, steps = 36, pressure = 0.5) {
1523
2340
  }
1524
2341
 
1525
2342
  exports.R3FFixture = R3FFixture;
2343
+ exports.R3FReporter = R3FReporter;
1526
2344
  exports.circlePath = circlePath;
1527
2345
  exports.click = click;
1528
2346
  exports.contextMenu = contextMenu;
1529
2347
  exports.createR3FTest = createR3FTest;
1530
2348
  exports.curvePath = curvePath;
2349
+ exports.diffSnapshots = diffSnapshots;
1531
2350
  exports.doubleClick = doubleClick;
1532
2351
  exports.drag = drag;
1533
2352
  exports.drawPathOnCanvas = drawPathOnCanvas;
1534
- exports.expect = expect;
2353
+ exports.getCameraState = getCameraState;
1535
2354
  exports.hover = hover;
1536
2355
  exports.linePath = linePath;
1537
2356
  exports.pointerMiss = pointerMiss;
2357
+ exports.r3fMatchers = r3fMatchers;
1538
2358
  exports.rectPath = rectPath;
1539
2359
  exports.test = test;
2360
+ exports.unhover = unhover;
1540
2361
  exports.waitForIdle = waitForIdle;
1541
2362
  exports.waitForNewObject = waitForNewObject;
1542
2363
  exports.waitForObject = waitForObject;
2364
+ exports.waitForObjectRemoved = waitForObjectRemoved;
1543
2365
  exports.waitForSceneReady = waitForSceneReady;
1544
2366
  exports.wheel = wheel;
1545
2367
  //# sourceMappingURL=index.cjs.map