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