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