@react-three-dom/cypress 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 +1062 -110
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +144 -12
- package/dist/index.d.ts +144 -12
- package/dist/index.js +1060 -110
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.d.ts +92 -13
package/dist/index.cjs
CHANGED
|
@@ -1,15 +1,318 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// src/diffSnapshots.ts
|
|
4
|
+
var FIELDS_TO_COMPARE = [
|
|
5
|
+
"name",
|
|
6
|
+
"type",
|
|
7
|
+
"testId",
|
|
8
|
+
"visible",
|
|
9
|
+
"position",
|
|
10
|
+
"rotation",
|
|
11
|
+
"scale"
|
|
12
|
+
];
|
|
13
|
+
function flattenTree(node) {
|
|
14
|
+
const map = /* @__PURE__ */ new Map();
|
|
15
|
+
function walk(n) {
|
|
16
|
+
map.set(n.uuid, n);
|
|
17
|
+
n.children.forEach(walk);
|
|
18
|
+
}
|
|
19
|
+
walk(node);
|
|
20
|
+
return map;
|
|
21
|
+
}
|
|
22
|
+
function valueEqual(a, b) {
|
|
23
|
+
if (a === b) return true;
|
|
24
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
25
|
+
if (a.length !== b.length) return false;
|
|
26
|
+
return a.every((v, i) => valueEqual(v, b[i]));
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
function diffSnapshots(before, after) {
|
|
31
|
+
const beforeMap = flattenTree(before.tree);
|
|
32
|
+
const afterMap = flattenTree(after.tree);
|
|
33
|
+
const added = [];
|
|
34
|
+
const removed = [];
|
|
35
|
+
const changed = [];
|
|
36
|
+
for (const [, node] of afterMap) {
|
|
37
|
+
if (!beforeMap.has(node.uuid)) added.push(node);
|
|
38
|
+
}
|
|
39
|
+
for (const [, node] of beforeMap) {
|
|
40
|
+
if (!afterMap.has(node.uuid)) removed.push(node);
|
|
41
|
+
}
|
|
42
|
+
for (const [uuid, afterNode] of afterMap) {
|
|
43
|
+
const beforeNode = beforeMap.get(uuid);
|
|
44
|
+
if (!beforeNode) continue;
|
|
45
|
+
for (const field of FIELDS_TO_COMPARE) {
|
|
46
|
+
const from = beforeNode[field];
|
|
47
|
+
const to = afterNode[field];
|
|
48
|
+
if (!valueEqual(from, to)) {
|
|
49
|
+
changed.push({ uuid, field, from, to });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { added, removed, changed };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/reporter.ts
|
|
57
|
+
var RESET = "\x1B[0m";
|
|
58
|
+
var BOLD = "\x1B[1m";
|
|
59
|
+
var DIM = "\x1B[2m";
|
|
60
|
+
var GREEN = "\x1B[32m";
|
|
61
|
+
var RED = "\x1B[31m";
|
|
62
|
+
var YELLOW = "\x1B[33m";
|
|
63
|
+
var CYAN = "\x1B[36m";
|
|
64
|
+
var MAGENTA = "\x1B[35m";
|
|
65
|
+
var TAG = `${CYAN}[r3f-dom]${RESET}`;
|
|
66
|
+
function ok(msg) {
|
|
67
|
+
return `${TAG} ${GREEN}\u2713${RESET} ${msg}`;
|
|
68
|
+
}
|
|
69
|
+
function fail(msg) {
|
|
70
|
+
return `${TAG} ${RED}\u2717${RESET} ${msg}`;
|
|
71
|
+
}
|
|
72
|
+
function warn(msg) {
|
|
73
|
+
return `${TAG} ${YELLOW}\u26A0${RESET} ${msg}`;
|
|
74
|
+
}
|
|
75
|
+
function info(msg) {
|
|
76
|
+
return `${TAG} ${DIM}${msg}${RESET}`;
|
|
77
|
+
}
|
|
78
|
+
function heading(msg) {
|
|
79
|
+
return `
|
|
80
|
+
${TAG} ${BOLD}${MAGENTA}${msg}${RESET}`;
|
|
81
|
+
}
|
|
82
|
+
function registerR3FTasks(on) {
|
|
83
|
+
on("task", {
|
|
84
|
+
r3fLog(message) {
|
|
85
|
+
process.stdout.write(message + "\n");
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
var _enabled = true;
|
|
91
|
+
function termLog(message) {
|
|
92
|
+
if (!_enabled) return;
|
|
93
|
+
cy.task("r3fLog", message, { log: false });
|
|
94
|
+
}
|
|
95
|
+
function cmdLog(name, message, props) {
|
|
96
|
+
Cypress.log({
|
|
97
|
+
name,
|
|
98
|
+
message,
|
|
99
|
+
consoleProps: () => props ?? {}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function getAPI(win) {
|
|
103
|
+
return win.__R3F_DOM__ ?? null;
|
|
104
|
+
}
|
|
105
|
+
var R3FReporter = class {
|
|
106
|
+
constructor(enabled = true) {
|
|
107
|
+
_enabled = enabled;
|
|
108
|
+
}
|
|
109
|
+
enable() {
|
|
110
|
+
_enabled = true;
|
|
111
|
+
}
|
|
112
|
+
disable() {
|
|
113
|
+
_enabled = false;
|
|
114
|
+
}
|
|
115
|
+
// ---- Lifecycle ----
|
|
116
|
+
logBridgeWaiting() {
|
|
117
|
+
termLog(info("Waiting for bridge (window.__R3F_DOM__)..."));
|
|
118
|
+
cmdLog("r3f", "Waiting for bridge...");
|
|
119
|
+
}
|
|
120
|
+
logBridgeConnected(diag) {
|
|
121
|
+
if (diag) {
|
|
122
|
+
termLog(ok(`Bridge connected \u2014 v${diag.version}, ${diag.objectCount} objects, ${diag.meshCount} meshes`));
|
|
123
|
+
termLog(info(` Canvas: ${diag.canvasWidth}\xD7${diag.canvasHeight} GPU: ${diag.webglRenderer}`));
|
|
124
|
+
termLog(info(` DOM nodes: ${diag.materializedDomNodes}/${diag.maxDomNodes} Dirty queue: ${diag.dirtyQueueSize}`));
|
|
125
|
+
cmdLog("r3f", `Bridge connected v${diag.version} \u2014 ${diag.objectCount} objects`, { diagnostics: diag });
|
|
126
|
+
} else {
|
|
127
|
+
termLog(ok("Bridge connected"));
|
|
128
|
+
cmdLog("r3f", "Bridge connected");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
logBridgeError(error) {
|
|
132
|
+
termLog(fail(`Bridge error: ${error}`));
|
|
133
|
+
cmdLog("r3f", `Bridge error: ${error}`);
|
|
134
|
+
}
|
|
135
|
+
logSceneReady(objectCount) {
|
|
136
|
+
termLog(ok(`Scene ready \u2014 ${objectCount} objects stabilized`));
|
|
137
|
+
cmdLog("r3f", `Scene ready \u2014 ${objectCount} objects`);
|
|
138
|
+
}
|
|
139
|
+
logObjectFound(idOrUuid, type, name) {
|
|
140
|
+
const label = name ? `"${name}" (${type})` : type;
|
|
141
|
+
termLog(ok(`Object found: "${idOrUuid}" \u2192 ${label}`));
|
|
142
|
+
cmdLog("r3f", `Object found: "${idOrUuid}" \u2192 ${label}`);
|
|
143
|
+
}
|
|
144
|
+
logObjectNotFound(idOrUuid, suggestions) {
|
|
145
|
+
termLog(fail(`Object not found: "${idOrUuid}"`));
|
|
146
|
+
if (suggestions && suggestions.length > 0) {
|
|
147
|
+
termLog(warn("Did you mean:"));
|
|
148
|
+
for (const s of suggestions.slice(0, 5)) {
|
|
149
|
+
const id = s.testId ? `testId="${s.testId}"` : `uuid="${s.uuid}"`;
|
|
150
|
+
termLog(info(` \u2192 ${s.name || "(unnamed)"} [${id}]`));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
cmdLog("r3f", `Object not found: "${idOrUuid}"`, { suggestions });
|
|
154
|
+
}
|
|
155
|
+
// ---- Interactions ----
|
|
156
|
+
logInteraction(action, idOrUuid, extra) {
|
|
157
|
+
const suffix = extra ? ` ${extra}` : "";
|
|
158
|
+
termLog(info(`${action}("${idOrUuid}")${suffix}`));
|
|
159
|
+
}
|
|
160
|
+
logInteractionDone(action, idOrUuid, durationMs) {
|
|
161
|
+
termLog(ok(`${action}("${idOrUuid}") \u2014 ${durationMs}ms`));
|
|
162
|
+
}
|
|
163
|
+
// ---- Assertions ----
|
|
164
|
+
logAssertionFailure(matcherName, id, detail, diag) {
|
|
165
|
+
termLog(heading(`Assertion failed: ${matcherName}("${id}")`));
|
|
166
|
+
termLog(fail(detail));
|
|
167
|
+
if (diag) {
|
|
168
|
+
this._printDiagnosticsSummary(diag);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// ---- Diagnostics ----
|
|
172
|
+
logDiagnostics() {
|
|
173
|
+
return cy.window({ log: false }).then((win) => {
|
|
174
|
+
const api = getAPI(win);
|
|
175
|
+
if (!api || typeof api.getDiagnostics !== "function") {
|
|
176
|
+
termLog(fail("Cannot fetch diagnostics \u2014 bridge not available"));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const d = api.getDiagnostics();
|
|
180
|
+
termLog(heading("Bridge Diagnostics"));
|
|
181
|
+
this._printDiagnosticsFull(d);
|
|
182
|
+
cmdLog("r3fDiagnostics", `v${d.version} ${d.objectCount} objects`, { diagnostics: d });
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
fetchDiagnostics() {
|
|
186
|
+
return cy.window({ log: false }).then((win) => {
|
|
187
|
+
const api = getAPI(win);
|
|
188
|
+
if (!api || typeof api.getDiagnostics !== "function") return null;
|
|
189
|
+
return api.getDiagnostics();
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
fetchFuzzyMatches(query, limit = 5) {
|
|
193
|
+
return cy.window({ log: false }).then((win) => {
|
|
194
|
+
const api = getAPI(win);
|
|
195
|
+
if (!api || typeof api.fuzzyFind !== "function") return [];
|
|
196
|
+
return api.fuzzyFind(query, limit).map((m) => ({
|
|
197
|
+
testId: m.testId,
|
|
198
|
+
name: m.name,
|
|
199
|
+
uuid: m.uuid
|
|
200
|
+
}));
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
// ---- Private formatting ----
|
|
204
|
+
_printDiagnosticsSummary(d) {
|
|
205
|
+
termLog(info(` Bridge: v${d.version} ready=${d.ready}${d.error ? ` error="${d.error}"` : ""}`));
|
|
206
|
+
termLog(info(` Scene: ${d.objectCount} objects (${d.meshCount} meshes, ${d.groupCount} groups, ${d.lightCount} lights)`));
|
|
207
|
+
termLog(info(` Canvas: ${d.canvasWidth}\xD7${d.canvasHeight} GPU: ${d.webglRenderer}`));
|
|
208
|
+
}
|
|
209
|
+
_printDiagnosticsFull(d) {
|
|
210
|
+
const status = d.ready ? `${GREEN}READY${RESET}` : `${RED}NOT READY${RESET}`;
|
|
211
|
+
termLog(` ${BOLD}Status:${RESET} ${status} v${d.version}`);
|
|
212
|
+
if (d.error) termLog(` ${BOLD}Error:${RESET} ${RED}${d.error}${RESET}`);
|
|
213
|
+
termLog(` ${BOLD}Objects:${RESET} ${d.objectCount} total`);
|
|
214
|
+
termLog(` ${d.meshCount} meshes, ${d.groupCount} groups, ${d.lightCount} lights, ${d.cameraCount} cameras`);
|
|
215
|
+
termLog(` ${BOLD}DOM:${RESET} ${d.materializedDomNodes}/${d.maxDomNodes} materialized`);
|
|
216
|
+
termLog(` ${BOLD}Canvas:${RESET} ${d.canvasWidth}\xD7${d.canvasHeight}`);
|
|
217
|
+
termLog(` ${BOLD}GPU:${RESET} ${d.webglRenderer}`);
|
|
218
|
+
termLog(` ${BOLD}Dirty:${RESET} ${d.dirtyQueueSize} queued updates`);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// src/reporterState.ts
|
|
223
|
+
var _reporter = null;
|
|
224
|
+
function _setReporter(r) {
|
|
225
|
+
_reporter = r;
|
|
226
|
+
}
|
|
227
|
+
function _getReporter() {
|
|
228
|
+
return _reporter;
|
|
229
|
+
}
|
|
230
|
+
|
|
3
231
|
// src/commands.ts
|
|
232
|
+
var _activeCanvasId = null;
|
|
233
|
+
function _getActiveCanvasId() {
|
|
234
|
+
return _activeCanvasId;
|
|
235
|
+
}
|
|
4
236
|
function getR3F(win) {
|
|
5
|
-
const
|
|
237
|
+
const w = win;
|
|
238
|
+
const api = _activeCanvasId ? w.__R3F_DOM_INSTANCES__?.[_activeCanvasId] : w.__R3F_DOM__;
|
|
6
239
|
if (!api) {
|
|
7
240
|
throw new Error(
|
|
8
|
-
|
|
241
|
+
`react-three-dom bridge not found${_activeCanvasId ? ` (canvas: "${_activeCanvasId}")` : ""}. Is <ThreeDom${_activeCanvasId ? ` canvasId="${_activeCanvasId}"` : ""}> mounted in your app?`
|
|
9
242
|
);
|
|
10
243
|
}
|
|
11
244
|
return api;
|
|
12
245
|
}
|
|
246
|
+
var AUTO_WAIT_TIMEOUT = 5e3;
|
|
247
|
+
var AUTO_WAIT_POLL_MS = 100;
|
|
248
|
+
function resolveApi(win) {
|
|
249
|
+
const w = win;
|
|
250
|
+
return _activeCanvasId ? w.__R3F_DOM_INSTANCES__?.[_activeCanvasId] : w.__R3F_DOM__;
|
|
251
|
+
}
|
|
252
|
+
function autoWaitForBridge(timeout = AUTO_WAIT_TIMEOUT) {
|
|
253
|
+
const deadline = Date.now() + timeout;
|
|
254
|
+
function poll() {
|
|
255
|
+
return cy.window({ log: false }).then((win) => {
|
|
256
|
+
const api = resolveApi(win);
|
|
257
|
+
if (api && api._ready) return api;
|
|
258
|
+
if (api && !api._ready && api._error) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`[react-three-dom] Bridge initialization failed: ${api._error}`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
if (Date.now() > deadline) {
|
|
264
|
+
throw new Error(
|
|
265
|
+
`[react-three-dom] Auto-wait timed out after ${timeout}ms: bridge not ready${_activeCanvasId ? ` (canvas: "${_activeCanvasId}")` : ""}.
|
|
266
|
+
Ensure <ThreeDom${_activeCanvasId ? ` canvasId="${_activeCanvasId}"` : ""}> is mounted inside your <Canvas> component.`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
return cy.wait(AUTO_WAIT_POLL_MS, { log: false }).then(() => poll());
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return poll();
|
|
273
|
+
}
|
|
274
|
+
function autoWaitForObject(idOrUuid, timeout = AUTO_WAIT_TIMEOUT) {
|
|
275
|
+
const deadline = Date.now() + timeout;
|
|
276
|
+
function poll() {
|
|
277
|
+
return cy.window({ log: false }).then((win) => {
|
|
278
|
+
const api = resolveApi(win);
|
|
279
|
+
if (!api) {
|
|
280
|
+
if (Date.now() > deadline) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
`[react-three-dom] Auto-wait timed out after ${timeout}ms: bridge not found${_activeCanvasId ? ` (canvas: "${_activeCanvasId}")` : ""}.`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return cy.wait(AUTO_WAIT_POLL_MS, { log: false }).then(() => poll());
|
|
286
|
+
}
|
|
287
|
+
if (!api._ready && api._error) {
|
|
288
|
+
throw new Error(
|
|
289
|
+
`[react-three-dom] Bridge initialization failed: ${api._error}`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
if (api._ready) {
|
|
293
|
+
const found = (api.getByTestId(idOrUuid) ?? api.getByUuid(idOrUuid)) !== null;
|
|
294
|
+
if (found) return api;
|
|
295
|
+
}
|
|
296
|
+
if (Date.now() > deadline) {
|
|
297
|
+
let msg = `[react-three-dom] Auto-wait timed out after ${timeout}ms: object "${idOrUuid}" not found${_activeCanvasId ? ` (canvas: "${_activeCanvasId}")` : ""}.
|
|
298
|
+
Bridge: ready=${api._ready}, objectCount=${api.getCount()}.
|
|
299
|
+
Ensure the object has userData.testId="${idOrUuid}" or uuid="${idOrUuid}".`;
|
|
300
|
+
if (typeof api.fuzzyFind === "function") {
|
|
301
|
+
const suggestions = api.fuzzyFind(idOrUuid, 5);
|
|
302
|
+
if (suggestions.length > 0) {
|
|
303
|
+
msg += "\nDid you mean:\n" + suggestions.map((s) => {
|
|
304
|
+
const id = s.testId ? `testId="${s.testId}"` : `uuid="${s.uuid}"`;
|
|
305
|
+
return ` \u2192 ${s.name || "(unnamed)"} [${id}]`;
|
|
306
|
+
}).join("\n");
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
throw new Error(msg);
|
|
310
|
+
}
|
|
311
|
+
return cy.wait(AUTO_WAIT_POLL_MS, { log: false }).then(() => poll());
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
return poll();
|
|
315
|
+
}
|
|
13
316
|
function formatCypressSceneTree(node, prefix = "", isLast = true) {
|
|
14
317
|
const connector = prefix === "" ? "" : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
|
|
15
318
|
const childPrefix = prefix === "" ? "" : prefix + (isLast ? " " : "\u2502 ");
|
|
@@ -26,6 +329,26 @@ function formatCypressSceneTree(node, prefix = "", isLast = true) {
|
|
|
26
329
|
return result;
|
|
27
330
|
}
|
|
28
331
|
function registerCommands() {
|
|
332
|
+
Cypress.Commands.add("r3fUseCanvas", (canvasId) => {
|
|
333
|
+
_activeCanvasId = canvasId;
|
|
334
|
+
Cypress.log({
|
|
335
|
+
name: "r3fUseCanvas",
|
|
336
|
+
message: canvasId ? `Switched to canvas "${canvasId}"` : "Switched to default canvas"
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
Cypress.Commands.add("r3fGetCanvasIds", () => {
|
|
340
|
+
return cy.window({ log: false }).then((win) => {
|
|
341
|
+
const instances = win.__R3F_DOM_INSTANCES__;
|
|
342
|
+
return instances ? Object.keys(instances) : [];
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
Cypress.Commands.add("r3fEnableReporter", (enabled = true) => {
|
|
346
|
+
if (enabled) {
|
|
347
|
+
_setReporter(new R3FReporter(true));
|
|
348
|
+
} else {
|
|
349
|
+
_setReporter(null);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
29
352
|
Cypress.Commands.add("r3fEnableDebug", () => {
|
|
30
353
|
cy.window({ log: false }).then((win) => {
|
|
31
354
|
win.__R3F_DOM_DEBUG__ = true;
|
|
@@ -46,56 +369,89 @@ function registerCommands() {
|
|
|
46
369
|
});
|
|
47
370
|
});
|
|
48
371
|
Cypress.Commands.add("r3fClick", (idOrUuid) => {
|
|
49
|
-
|
|
50
|
-
|
|
372
|
+
const reporter = _getReporter();
|
|
373
|
+
const t0 = Date.now();
|
|
374
|
+
reporter?.logInteraction("click", idOrUuid);
|
|
375
|
+
return autoWaitForObject(idOrUuid).then((api) => {
|
|
376
|
+
api.click(idOrUuid);
|
|
377
|
+
reporter?.logInteractionDone("click", idOrUuid, Date.now() - t0);
|
|
51
378
|
});
|
|
52
379
|
});
|
|
53
380
|
Cypress.Commands.add("r3fDoubleClick", (idOrUuid) => {
|
|
54
|
-
|
|
55
|
-
|
|
381
|
+
const reporter = _getReporter();
|
|
382
|
+
const t0 = Date.now();
|
|
383
|
+
reporter?.logInteraction("doubleClick", idOrUuid);
|
|
384
|
+
return autoWaitForObject(idOrUuid).then((api) => {
|
|
385
|
+
api.doubleClick(idOrUuid);
|
|
386
|
+
reporter?.logInteractionDone("doubleClick", idOrUuid, Date.now() - t0);
|
|
56
387
|
});
|
|
57
388
|
});
|
|
58
389
|
Cypress.Commands.add("r3fContextMenu", (idOrUuid) => {
|
|
59
|
-
|
|
60
|
-
|
|
390
|
+
const reporter = _getReporter();
|
|
391
|
+
const t0 = Date.now();
|
|
392
|
+
reporter?.logInteraction("contextMenu", idOrUuid);
|
|
393
|
+
return autoWaitForObject(idOrUuid).then((api) => {
|
|
394
|
+
api.contextMenu(idOrUuid);
|
|
395
|
+
reporter?.logInteractionDone("contextMenu", idOrUuid, Date.now() - t0);
|
|
61
396
|
});
|
|
62
397
|
});
|
|
63
398
|
Cypress.Commands.add("r3fHover", (idOrUuid) => {
|
|
64
|
-
|
|
65
|
-
|
|
399
|
+
const reporter = _getReporter();
|
|
400
|
+
const t0 = Date.now();
|
|
401
|
+
reporter?.logInteraction("hover", idOrUuid);
|
|
402
|
+
return autoWaitForObject(idOrUuid).then((api) => {
|
|
403
|
+
api.hover(idOrUuid);
|
|
404
|
+
reporter?.logInteractionDone("hover", idOrUuid, Date.now() - t0);
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
Cypress.Commands.add("r3fUnhover", () => {
|
|
408
|
+
const reporter = _getReporter();
|
|
409
|
+
const t0 = Date.now();
|
|
410
|
+
reporter?.logInteraction("unhover", "(canvas)");
|
|
411
|
+
return autoWaitForBridge().then((api) => {
|
|
412
|
+
api.unhover();
|
|
413
|
+
reporter?.logInteractionDone("unhover", "(canvas)", Date.now() - t0);
|
|
66
414
|
});
|
|
67
415
|
});
|
|
68
416
|
Cypress.Commands.add(
|
|
69
417
|
"r3fDrag",
|
|
70
418
|
(idOrUuid, delta) => {
|
|
71
|
-
|
|
72
|
-
return
|
|
419
|
+
return autoWaitForObject(idOrUuid).then((api) => {
|
|
420
|
+
return Cypress.Promise.resolve(api.drag(idOrUuid, delta));
|
|
73
421
|
});
|
|
74
422
|
}
|
|
75
423
|
);
|
|
76
424
|
Cypress.Commands.add(
|
|
77
425
|
"r3fWheel",
|
|
78
426
|
(idOrUuid, options) => {
|
|
79
|
-
|
|
80
|
-
|
|
427
|
+
return autoWaitForObject(idOrUuid).then((api) => {
|
|
428
|
+
api.wheel(idOrUuid, options);
|
|
81
429
|
});
|
|
82
430
|
}
|
|
83
431
|
);
|
|
84
432
|
Cypress.Commands.add("r3fPointerMiss", () => {
|
|
85
|
-
|
|
86
|
-
|
|
433
|
+
return autoWaitForBridge().then((api) => {
|
|
434
|
+
api.pointerMiss();
|
|
87
435
|
});
|
|
88
436
|
});
|
|
89
437
|
Cypress.Commands.add("r3fSelect", (idOrUuid) => {
|
|
90
|
-
|
|
91
|
-
|
|
438
|
+
return autoWaitForObject(idOrUuid).then((api) => {
|
|
439
|
+
api.select(idOrUuid);
|
|
92
440
|
});
|
|
93
441
|
});
|
|
94
442
|
Cypress.Commands.add("r3fClearSelection", () => {
|
|
95
|
-
|
|
96
|
-
|
|
443
|
+
return autoWaitForBridge().then((api) => {
|
|
444
|
+
api.clearSelection();
|
|
97
445
|
});
|
|
98
446
|
});
|
|
447
|
+
Cypress.Commands.add(
|
|
448
|
+
"r3fDrawPath",
|
|
449
|
+
(points, options) => {
|
|
450
|
+
return autoWaitForBridge().then((api) => {
|
|
451
|
+
return Cypress.Promise.resolve(api.drawPath(points, options));
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
);
|
|
99
455
|
Cypress.Commands.add("r3fLogScene", () => {
|
|
100
456
|
return cy.window({ log: false }).then((win) => {
|
|
101
457
|
const api = getR3F(win);
|
|
@@ -111,12 +467,98 @@ ${lines}`;
|
|
|
111
467
|
console.log(`[r3f-dom] ${output}`);
|
|
112
468
|
});
|
|
113
469
|
});
|
|
470
|
+
Cypress.Commands.add("r3fGetDiagnostics", () => {
|
|
471
|
+
return cy.window({ log: false }).then((win) => {
|
|
472
|
+
const api = getR3F(win);
|
|
473
|
+
if (typeof api.getDiagnostics !== "function") return null;
|
|
474
|
+
return api.getDiagnostics();
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
Cypress.Commands.add("r3fLogDiagnostics", () => {
|
|
478
|
+
return cy.window({ log: false }).then((win) => {
|
|
479
|
+
const api = getR3F(win);
|
|
480
|
+
if (typeof api.getDiagnostics !== "function") {
|
|
481
|
+
Cypress.log({ name: "r3fLogDiagnostics", message: "getDiagnostics not available" });
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const d = api.getDiagnostics();
|
|
485
|
+
const lines = [
|
|
486
|
+
`Bridge Diagnostics`,
|
|
487
|
+
` Status: ${d.ready ? "READY" : "NOT READY"} v${d.version}`,
|
|
488
|
+
d.error ? ` Error: ${d.error}` : null,
|
|
489
|
+
` Objects: ${d.objectCount} total`,
|
|
490
|
+
` ${d.meshCount} meshes, ${d.groupCount} groups, ${d.lightCount} lights, ${d.cameraCount} cameras`,
|
|
491
|
+
` DOM: ${d.materializedDomNodes}/${d.maxDomNodes} materialized`,
|
|
492
|
+
` Canvas: ${d.canvasWidth}\xD7${d.canvasHeight}`,
|
|
493
|
+
` GPU: ${d.webglRenderer}`,
|
|
494
|
+
` Dirty: ${d.dirtyQueueSize} queued updates`
|
|
495
|
+
].filter(Boolean).join("\n");
|
|
496
|
+
Cypress.log({
|
|
497
|
+
name: "r3fLogDiagnostics",
|
|
498
|
+
message: `v${d.version} ${d.objectCount} objects`,
|
|
499
|
+
consoleProps: () => ({ diagnostics: d, formatted: lines })
|
|
500
|
+
});
|
|
501
|
+
console.log(`[r3f-dom] ${lines}`);
|
|
502
|
+
});
|
|
503
|
+
});
|
|
114
504
|
Cypress.Commands.add("r3fGetObject", (idOrUuid) => {
|
|
115
505
|
return cy.window({ log: false }).then((win) => {
|
|
116
506
|
const api = getR3F(win);
|
|
117
507
|
return api.getByTestId(idOrUuid) ?? api.getByUuid(idOrUuid) ?? null;
|
|
118
508
|
});
|
|
119
509
|
});
|
|
510
|
+
Cypress.Commands.add("r3fGetByTestId", (testId) => {
|
|
511
|
+
return cy.window({ log: false }).then((win) => {
|
|
512
|
+
return getR3F(win).getByTestId(testId);
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
Cypress.Commands.add("r3fGetByName", (name) => {
|
|
516
|
+
return cy.window({ log: false }).then((win) => {
|
|
517
|
+
return getR3F(win).getByName(name);
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
Cypress.Commands.add("r3fGetByUuid", (uuid) => {
|
|
521
|
+
return cy.window({ log: false }).then((win) => {
|
|
522
|
+
return getR3F(win).getByUuid(uuid);
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
Cypress.Commands.add("r3fGetChildren", (idOrUuid) => {
|
|
526
|
+
return cy.window({ log: false }).then((win) => {
|
|
527
|
+
return getR3F(win).getChildren(idOrUuid);
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
Cypress.Commands.add("r3fGetParent", (idOrUuid) => {
|
|
531
|
+
return cy.window({ log: false }).then((win) => {
|
|
532
|
+
return getR3F(win).getParent(idOrUuid);
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
Cypress.Commands.add("r3fGetCanvas", () => {
|
|
536
|
+
return cy.get("[data-r3f-canvas]");
|
|
537
|
+
});
|
|
538
|
+
Cypress.Commands.add("r3fGetWorldPosition", (idOrUuid) => {
|
|
539
|
+
return cy.window({ log: false }).then((win) => {
|
|
540
|
+
const api = getR3F(win);
|
|
541
|
+
const insp = api.inspect(idOrUuid);
|
|
542
|
+
if (!insp?.worldMatrix || insp.worldMatrix.length < 15) return null;
|
|
543
|
+
const m = insp.worldMatrix;
|
|
544
|
+
return [m[12], m[13], m[14]];
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
Cypress.Commands.add("r3fDiffSnapshots", (before, after) => {
|
|
548
|
+
return cy.wrap(diffSnapshots(before, after), { log: false });
|
|
549
|
+
});
|
|
550
|
+
Cypress.Commands.add("r3fTrackObjectCount", (action) => {
|
|
551
|
+
return cy.window({ log: false }).then((win) => {
|
|
552
|
+
const before = getR3F(win).snapshot();
|
|
553
|
+
return action().then(
|
|
554
|
+
() => cy.window({ log: false }).then((w) => {
|
|
555
|
+
const after = getR3F(w).snapshot();
|
|
556
|
+
const d = diffSnapshots(before, after);
|
|
557
|
+
return { added: d.added.length, removed: d.removed.length };
|
|
558
|
+
})
|
|
559
|
+
);
|
|
560
|
+
});
|
|
561
|
+
});
|
|
120
562
|
Cypress.Commands.add("r3fInspect", (idOrUuid) => {
|
|
121
563
|
return cy.window({ log: false }).then((win) => {
|
|
122
564
|
return getR3F(win).inspect(idOrUuid);
|
|
@@ -132,15 +574,6 @@ ${lines}`;
|
|
|
132
574
|
return getR3F(win).getCount();
|
|
133
575
|
});
|
|
134
576
|
});
|
|
135
|
-
Cypress.Commands.add(
|
|
136
|
-
"r3fDrawPath",
|
|
137
|
-
(points, options) => {
|
|
138
|
-
return cy.window({ log: false }).then(async (win) => {
|
|
139
|
-
const api = getR3F(win);
|
|
140
|
-
return api.drawPath(points, options);
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
);
|
|
144
577
|
Cypress.Commands.add("r3fGetByType", (type) => {
|
|
145
578
|
return cy.window({ log: false }).then((win) => {
|
|
146
579
|
return getR3F(win).getByType(type);
|
|
@@ -161,20 +594,81 @@ ${lines}`;
|
|
|
161
594
|
return getR3F(win).getObjects(ids);
|
|
162
595
|
});
|
|
163
596
|
});
|
|
597
|
+
Cypress.Commands.add("r3fGetByGeometryType", (type) => {
|
|
598
|
+
return cy.window({ log: false }).then((win) => {
|
|
599
|
+
return getR3F(win).getByGeometryType(type);
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
Cypress.Commands.add("r3fGetByMaterialType", (type) => {
|
|
603
|
+
return cy.window({ log: false }).then((win) => {
|
|
604
|
+
return getR3F(win).getByMaterialType(type);
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
Cypress.Commands.add("r3fFuzzyFind", (query, limit) => {
|
|
608
|
+
return cy.window({ log: false }).then((win) => {
|
|
609
|
+
const api = getR3F(win);
|
|
610
|
+
if (typeof api.fuzzyFind !== "function") return [];
|
|
611
|
+
return api.fuzzyFind(query, limit);
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
Cypress.Commands.add("r3fGetCameraState", () => {
|
|
615
|
+
return cy.window({ log: false }).then((win) => {
|
|
616
|
+
const api = getR3F(win);
|
|
617
|
+
return api.getCameraState();
|
|
618
|
+
});
|
|
619
|
+
});
|
|
164
620
|
}
|
|
165
621
|
|
|
166
622
|
// src/assertions.ts
|
|
167
623
|
function getR3FFromWindow() {
|
|
168
624
|
const win = cy.state("window");
|
|
169
|
-
const
|
|
625
|
+
const cid = _getActiveCanvasId();
|
|
626
|
+
const api = cid ? win?.__R3F_DOM_INSTANCES__?.[cid] : win?.__R3F_DOM__;
|
|
170
627
|
if (!api) {
|
|
171
|
-
throw new Error(
|
|
628
|
+
throw new Error(
|
|
629
|
+
`react-three-dom bridge not found${cid ? ` (canvas: "${cid}")` : ""}. Is <ThreeDom${cid ? ` canvasId="${cid}"` : ""}> mounted?`
|
|
630
|
+
);
|
|
172
631
|
}
|
|
173
632
|
return api;
|
|
174
633
|
}
|
|
175
634
|
function resolveObject(api, idOrUuid) {
|
|
176
635
|
return api.getByTestId(idOrUuid) ?? api.getByUuid(idOrUuid) ?? null;
|
|
177
636
|
}
|
|
637
|
+
function requireObject(api, idOrUuid, matcherName) {
|
|
638
|
+
const meta = resolveObject(api, idOrUuid);
|
|
639
|
+
if (!meta) {
|
|
640
|
+
let msg = `[${matcherName}] 3D object "${idOrUuid}" not found in the scene`;
|
|
641
|
+
if (typeof api.fuzzyFind === "function") {
|
|
642
|
+
const suggestions = api.fuzzyFind(idOrUuid, 5);
|
|
643
|
+
if (suggestions.length > 0) {
|
|
644
|
+
msg += "\nDid you mean:\n" + suggestions.map((s) => {
|
|
645
|
+
const id = s.testId ? `testId="${s.testId}"` : `uuid="${s.uuid}"`;
|
|
646
|
+
return ` \u2192 ${s.name || "(unnamed)"} [${id}]`;
|
|
647
|
+
}).join("\n");
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
throw new Error(msg);
|
|
651
|
+
}
|
|
652
|
+
return meta;
|
|
653
|
+
}
|
|
654
|
+
function requireInspection(api, idOrUuid, matcherName) {
|
|
655
|
+
const insp = api.inspect(idOrUuid);
|
|
656
|
+
if (!insp) {
|
|
657
|
+
throw new Error(`[${matcherName}] 3D object "${idOrUuid}" not found for inspection`);
|
|
658
|
+
}
|
|
659
|
+
return insp;
|
|
660
|
+
}
|
|
661
|
+
function collectTriangles(api) {
|
|
662
|
+
const snap = api.snapshot();
|
|
663
|
+
let total = 0;
|
|
664
|
+
function walk(node) {
|
|
665
|
+
const meta = api.getByUuid(node.uuid);
|
|
666
|
+
if (meta?.triangleCount) total += meta.triangleCount;
|
|
667
|
+
for (const child of node.children) walk(child);
|
|
668
|
+
}
|
|
669
|
+
walk(snap.tree);
|
|
670
|
+
return total;
|
|
671
|
+
}
|
|
178
672
|
function registerAssertions() {
|
|
179
673
|
chai.use((_chai) => {
|
|
180
674
|
const { Assertion } = _chai;
|
|
@@ -191,99 +685,462 @@ function registerAssertions() {
|
|
|
191
685
|
});
|
|
192
686
|
Assertion.addMethod("r3fVisible", function(idOrUuid) {
|
|
193
687
|
const api = getR3FFromWindow();
|
|
194
|
-
const meta =
|
|
195
|
-
if (!meta) {
|
|
196
|
-
throw new Error(`3D object "${idOrUuid}" not found in the scene`);
|
|
197
|
-
}
|
|
688
|
+
const meta = requireObject(api, idOrUuid, "r3fVisible");
|
|
198
689
|
this.assert(
|
|
199
690
|
meta.visible,
|
|
200
|
-
`expected
|
|
201
|
-
`expected
|
|
691
|
+
`expected "${idOrUuid}" to be visible`,
|
|
692
|
+
`expected "${idOrUuid}" to NOT be visible`,
|
|
202
693
|
true,
|
|
203
694
|
meta.visible
|
|
204
695
|
);
|
|
205
696
|
});
|
|
206
|
-
Assertion.addMethod("
|
|
697
|
+
Assertion.addMethod("r3fPosition", function(idOrUuid, expected, tolerance = 0.01) {
|
|
698
|
+
const api = getR3FFromWindow();
|
|
699
|
+
const meta = requireObject(api, idOrUuid, "r3fPosition");
|
|
700
|
+
const pass = expected.every((v, i) => Math.abs(meta.position[i] - v) <= tolerance);
|
|
701
|
+
this.assert(
|
|
702
|
+
pass,
|
|
703
|
+
`expected "${idOrUuid}" at [${expected}] (\xB1${tolerance}), got [${meta.position}]`,
|
|
704
|
+
`expected "${idOrUuid}" to NOT be at [${expected}] (\xB1${tolerance})`,
|
|
705
|
+
expected,
|
|
706
|
+
meta.position
|
|
707
|
+
);
|
|
708
|
+
});
|
|
709
|
+
Assertion.addMethod("r3fWorldPosition", function(idOrUuid, expected, tolerance = 0.01) {
|
|
710
|
+
const api = getR3FFromWindow();
|
|
711
|
+
const insp = requireInspection(api, idOrUuid, "r3fWorldPosition");
|
|
712
|
+
const m = insp.worldMatrix;
|
|
713
|
+
const wp = [m[12], m[13], m[14]];
|
|
714
|
+
const pass = expected.every((v, i) => Math.abs(wp[i] - v) <= tolerance);
|
|
715
|
+
this.assert(
|
|
716
|
+
pass,
|
|
717
|
+
`expected "${idOrUuid}" world position [${expected}] (\xB1${tolerance}), got [${wp.map((v) => v.toFixed(4))}]`,
|
|
718
|
+
`expected "${idOrUuid}" to NOT have world position [${expected}] (\xB1${tolerance})`,
|
|
719
|
+
expected,
|
|
720
|
+
wp
|
|
721
|
+
);
|
|
722
|
+
});
|
|
723
|
+
Assertion.addMethod("r3fRotation", function(idOrUuid, expected, tolerance = 0.01) {
|
|
724
|
+
const api = getR3FFromWindow();
|
|
725
|
+
const meta = requireObject(api, idOrUuid, "r3fRotation");
|
|
726
|
+
const pass = expected.every((v, i) => Math.abs(meta.rotation[i] - v) <= tolerance);
|
|
727
|
+
this.assert(
|
|
728
|
+
pass,
|
|
729
|
+
`expected "${idOrUuid}" rotation [${expected}] (\xB1${tolerance}), got [${meta.rotation}]`,
|
|
730
|
+
`expected "${idOrUuid}" to NOT have rotation [${expected}] (\xB1${tolerance})`,
|
|
731
|
+
expected,
|
|
732
|
+
meta.rotation
|
|
733
|
+
);
|
|
734
|
+
});
|
|
735
|
+
Assertion.addMethod("r3fScale", function(idOrUuid, expected, tolerance = 0.01) {
|
|
736
|
+
const api = getR3FFromWindow();
|
|
737
|
+
const meta = requireObject(api, idOrUuid, "r3fScale");
|
|
738
|
+
const pass = expected.every((v, i) => Math.abs(meta.scale[i] - v) <= tolerance);
|
|
739
|
+
this.assert(
|
|
740
|
+
pass,
|
|
741
|
+
`expected "${idOrUuid}" scale [${expected}] (\xB1${tolerance}), got [${meta.scale}]`,
|
|
742
|
+
`expected "${idOrUuid}" to NOT have scale [${expected}] (\xB1${tolerance})`,
|
|
743
|
+
expected,
|
|
744
|
+
meta.scale
|
|
745
|
+
);
|
|
746
|
+
});
|
|
747
|
+
Assertion.addMethod("r3fType", function(idOrUuid, expectedType) {
|
|
748
|
+
const api = getR3FFromWindow();
|
|
749
|
+
const meta = requireObject(api, idOrUuid, "r3fType");
|
|
750
|
+
this.assert(
|
|
751
|
+
meta.type === expectedType,
|
|
752
|
+
`expected "${idOrUuid}" type "${expectedType}", got "${meta.type}"`,
|
|
753
|
+
`expected "${idOrUuid}" to NOT have type "${expectedType}"`,
|
|
754
|
+
expectedType,
|
|
755
|
+
meta.type
|
|
756
|
+
);
|
|
757
|
+
});
|
|
758
|
+
Assertion.addMethod("r3fName", function(idOrUuid, expectedName) {
|
|
759
|
+
const api = getR3FFromWindow();
|
|
760
|
+
const meta = requireObject(api, idOrUuid, "r3fName");
|
|
761
|
+
this.assert(
|
|
762
|
+
meta.name === expectedName,
|
|
763
|
+
`expected "${idOrUuid}" name "${expectedName}", got "${meta.name}"`,
|
|
764
|
+
`expected "${idOrUuid}" to NOT have name "${expectedName}"`,
|
|
765
|
+
expectedName,
|
|
766
|
+
meta.name
|
|
767
|
+
);
|
|
768
|
+
});
|
|
769
|
+
Assertion.addMethod("r3fGeometryType", function(idOrUuid, expectedGeo) {
|
|
770
|
+
const api = getR3FFromWindow();
|
|
771
|
+
const meta = requireObject(api, idOrUuid, "r3fGeometryType");
|
|
772
|
+
this.assert(
|
|
773
|
+
meta.geometryType === expectedGeo,
|
|
774
|
+
`expected "${idOrUuid}" geometry "${expectedGeo}", got "${meta.geometryType ?? "none"}"`,
|
|
775
|
+
`expected "${idOrUuid}" to NOT have geometry "${expectedGeo}"`,
|
|
776
|
+
expectedGeo,
|
|
777
|
+
meta.geometryType
|
|
778
|
+
);
|
|
779
|
+
});
|
|
780
|
+
Assertion.addMethod("r3fMaterialType", function(idOrUuid, expectedMat) {
|
|
781
|
+
const api = getR3FFromWindow();
|
|
782
|
+
const meta = requireObject(api, idOrUuid, "r3fMaterialType");
|
|
783
|
+
this.assert(
|
|
784
|
+
meta.materialType === expectedMat,
|
|
785
|
+
`expected "${idOrUuid}" material "${expectedMat}", got "${meta.materialType ?? "none"}"`,
|
|
786
|
+
`expected "${idOrUuid}" to NOT have material "${expectedMat}"`,
|
|
787
|
+
expectedMat,
|
|
788
|
+
meta.materialType
|
|
789
|
+
);
|
|
790
|
+
});
|
|
791
|
+
Assertion.addMethod("r3fChildCount", function(idOrUuid, expectedCount) {
|
|
207
792
|
const api = getR3FFromWindow();
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
793
|
+
const meta = requireObject(api, idOrUuid, "r3fChildCount");
|
|
794
|
+
const actual = meta.childrenUuids.length;
|
|
795
|
+
this.assert(
|
|
796
|
+
actual === expectedCount,
|
|
797
|
+
`expected "${idOrUuid}" ${expectedCount} children, got ${actual}`,
|
|
798
|
+
`expected "${idOrUuid}" to NOT have ${expectedCount} children`,
|
|
799
|
+
expectedCount,
|
|
800
|
+
actual
|
|
801
|
+
);
|
|
802
|
+
});
|
|
803
|
+
Assertion.addMethod("r3fParent", function(idOrUuid, expectedParent) {
|
|
804
|
+
const api = getR3FFromWindow();
|
|
805
|
+
const meta = requireObject(api, idOrUuid, "r3fParent");
|
|
806
|
+
if (!meta.parentUuid) {
|
|
807
|
+
throw new Error(`[r3fParent] "${idOrUuid}" has no parent (is scene root)`);
|
|
211
808
|
}
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
const
|
|
809
|
+
const parentMeta = api.getByUuid(meta.parentUuid);
|
|
810
|
+
const pass = parentMeta !== null && (parentMeta.uuid === expectedParent || parentMeta.testId === expectedParent || parentMeta.name === expectedParent);
|
|
811
|
+
const parentLabel = parentMeta?.testId ?? parentMeta?.name ?? meta.parentUuid;
|
|
812
|
+
this.assert(
|
|
813
|
+
pass,
|
|
814
|
+
`expected "${idOrUuid}" parent "${expectedParent}", got "${parentLabel}"`,
|
|
815
|
+
`expected "${idOrUuid}" to NOT have parent "${expectedParent}"`,
|
|
816
|
+
expectedParent,
|
|
817
|
+
parentLabel
|
|
818
|
+
);
|
|
819
|
+
});
|
|
820
|
+
Assertion.addMethod("r3fInstanceCount", function(idOrUuid, expectedCount) {
|
|
821
|
+
const api = getR3FFromWindow();
|
|
822
|
+
const meta = requireObject(api, idOrUuid, "r3fInstanceCount");
|
|
823
|
+
const actual = meta.instanceCount ?? 0;
|
|
824
|
+
this.assert(
|
|
825
|
+
actual === expectedCount,
|
|
826
|
+
`expected "${idOrUuid}" instance count ${expectedCount}, got ${actual}`,
|
|
827
|
+
`expected "${idOrUuid}" to NOT have instance count ${expectedCount}`,
|
|
828
|
+
expectedCount,
|
|
829
|
+
actual
|
|
830
|
+
);
|
|
831
|
+
});
|
|
832
|
+
Assertion.addMethod("r3fInFrustum", function(idOrUuid) {
|
|
833
|
+
const api = getR3FFromWindow();
|
|
834
|
+
const insp = requireInspection(api, idOrUuid, "r3fInFrustum");
|
|
835
|
+
const fin = (v) => v.every(Number.isFinite);
|
|
836
|
+
const pass = fin(insp.bounds.min) && fin(insp.bounds.max);
|
|
215
837
|
this.assert(
|
|
216
|
-
|
|
217
|
-
`expected
|
|
218
|
-
`expected
|
|
838
|
+
pass,
|
|
839
|
+
`expected "${idOrUuid}" to be in the camera frustum`,
|
|
840
|
+
`expected "${idOrUuid}" to NOT be in the camera frustum`,
|
|
219
841
|
"in frustum",
|
|
220
|
-
|
|
842
|
+
pass ? "in frustum" : "invalid bounds"
|
|
221
843
|
);
|
|
222
844
|
});
|
|
223
|
-
Assertion.addMethod(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
845
|
+
Assertion.addMethod("r3fBounds", function(idOrUuid, expected, tolerance = 0.1) {
|
|
846
|
+
const api = getR3FFromWindow();
|
|
847
|
+
const insp = requireInspection(api, idOrUuid, "r3fBounds");
|
|
848
|
+
const w = (a, b) => a.every((v, i) => Math.abs(v - b[i]) <= tolerance);
|
|
849
|
+
const pass = w(insp.bounds.min, expected.min) && w(insp.bounds.max, expected.max);
|
|
850
|
+
this.assert(
|
|
851
|
+
pass,
|
|
852
|
+
`expected "${idOrUuid}" bounds min:${JSON.stringify(expected.min)} max:${JSON.stringify(expected.max)} (\xB1${tolerance}), got min:${JSON.stringify(insp.bounds.min)} max:${JSON.stringify(insp.bounds.max)}`,
|
|
853
|
+
`expected "${idOrUuid}" bounds to NOT match`,
|
|
854
|
+
expected,
|
|
855
|
+
insp.bounds
|
|
856
|
+
);
|
|
857
|
+
});
|
|
858
|
+
Assertion.addMethod("r3fColor", function(idOrUuid, expectedColor) {
|
|
859
|
+
const api = getR3FFromWindow();
|
|
860
|
+
const insp = requireInspection(api, idOrUuid, "r3fColor");
|
|
861
|
+
const norm = expectedColor.startsWith("#") ? expectedColor.toLowerCase() : `#${expectedColor.toLowerCase()}`;
|
|
862
|
+
const actual = insp.material?.color?.toLowerCase();
|
|
863
|
+
this.assert(
|
|
864
|
+
actual === norm,
|
|
865
|
+
`expected "${idOrUuid}" color "${norm}", got "${actual ?? "no color"}"`,
|
|
866
|
+
`expected "${idOrUuid}" to NOT have color "${norm}"`,
|
|
867
|
+
norm,
|
|
868
|
+
actual
|
|
869
|
+
);
|
|
870
|
+
});
|
|
871
|
+
Assertion.addMethod("r3fOpacity", function(idOrUuid, expectedOpacity, tolerance = 0.01) {
|
|
872
|
+
const api = getR3FFromWindow();
|
|
873
|
+
const insp = requireInspection(api, idOrUuid, "r3fOpacity");
|
|
874
|
+
const actual = insp.material?.opacity;
|
|
875
|
+
const pass = actual !== void 0 && Math.abs(actual - expectedOpacity) <= tolerance;
|
|
876
|
+
this.assert(
|
|
877
|
+
pass,
|
|
878
|
+
`expected "${idOrUuid}" opacity ${expectedOpacity} (\xB1${tolerance}), got ${actual ?? "no material"}`,
|
|
879
|
+
`expected "${idOrUuid}" to NOT have opacity ${expectedOpacity} (\xB1${tolerance})`,
|
|
880
|
+
expectedOpacity,
|
|
881
|
+
actual
|
|
882
|
+
);
|
|
883
|
+
});
|
|
884
|
+
Assertion.addMethod("r3fTransparent", function(idOrUuid) {
|
|
885
|
+
const api = getR3FFromWindow();
|
|
886
|
+
const insp = requireInspection(api, idOrUuid, "r3fTransparent");
|
|
887
|
+
const pass = insp.material?.transparent === true;
|
|
888
|
+
this.assert(
|
|
889
|
+
pass,
|
|
890
|
+
`expected "${idOrUuid}" transparent=true, got ${insp.material?.transparent ?? "no material"}`,
|
|
891
|
+
`expected "${idOrUuid}" to NOT be transparent`,
|
|
892
|
+
true,
|
|
893
|
+
insp.material?.transparent
|
|
894
|
+
);
|
|
895
|
+
});
|
|
896
|
+
Assertion.addMethod("r3fVertexCount", function(idOrUuid, expectedCount) {
|
|
897
|
+
const api = getR3FFromWindow();
|
|
898
|
+
const meta = requireObject(api, idOrUuid, "r3fVertexCount");
|
|
899
|
+
const actual = meta.vertexCount ?? 0;
|
|
900
|
+
this.assert(
|
|
901
|
+
actual === expectedCount,
|
|
902
|
+
`expected "${idOrUuid}" ${expectedCount} vertices, got ${actual}`,
|
|
903
|
+
`expected "${idOrUuid}" to NOT have ${expectedCount} vertices`,
|
|
904
|
+
expectedCount,
|
|
905
|
+
actual
|
|
906
|
+
);
|
|
907
|
+
});
|
|
908
|
+
Assertion.addMethod("r3fTriangleCount", function(idOrUuid, expectedCount) {
|
|
909
|
+
const api = getR3FFromWindow();
|
|
910
|
+
const meta = requireObject(api, idOrUuid, "r3fTriangleCount");
|
|
911
|
+
const actual = meta.triangleCount ?? 0;
|
|
912
|
+
this.assert(
|
|
913
|
+
actual === expectedCount,
|
|
914
|
+
`expected "${idOrUuid}" ${expectedCount} triangles, got ${actual}`,
|
|
915
|
+
`expected "${idOrUuid}" to NOT have ${expectedCount} triangles`,
|
|
916
|
+
expectedCount,
|
|
917
|
+
actual
|
|
918
|
+
);
|
|
919
|
+
});
|
|
920
|
+
Assertion.addMethod("r3fUserData", function(idOrUuid, key, expectedValue) {
|
|
921
|
+
const api = getR3FFromWindow();
|
|
922
|
+
const insp = requireInspection(api, idOrUuid, "r3fUserData");
|
|
923
|
+
const hasKey = key in insp.userData;
|
|
924
|
+
if (expectedValue === void 0) {
|
|
925
|
+
this.assert(
|
|
926
|
+
hasKey,
|
|
927
|
+
`expected "${idOrUuid}" to have userData key "${key}"`,
|
|
928
|
+
`expected "${idOrUuid}" to NOT have userData key "${key}"`,
|
|
929
|
+
`key "${key}"`,
|
|
930
|
+
hasKey ? "present" : "missing"
|
|
931
|
+
);
|
|
932
|
+
} else {
|
|
933
|
+
const actual = insp.userData[key];
|
|
934
|
+
const pass = hasKey && JSON.stringify(actual) === JSON.stringify(expectedValue);
|
|
234
935
|
this.assert(
|
|
235
936
|
pass,
|
|
236
|
-
`expected
|
|
237
|
-
`expected
|
|
238
|
-
|
|
239
|
-
|
|
937
|
+
`expected "${idOrUuid}" userData.${key} = ${JSON.stringify(expectedValue)}, got ${JSON.stringify(actual)}`,
|
|
938
|
+
`expected "${idOrUuid}" to NOT have userData.${key} = ${JSON.stringify(expectedValue)}`,
|
|
939
|
+
expectedValue,
|
|
940
|
+
actual
|
|
240
941
|
);
|
|
241
942
|
}
|
|
242
|
-
);
|
|
243
|
-
Assertion.addMethod(
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (!meta) {
|
|
249
|
-
throw new Error(`3D object "${idOrUuid}" not found in the scene`);
|
|
250
|
-
}
|
|
251
|
-
const actual = meta.instanceCount ?? 0;
|
|
943
|
+
});
|
|
944
|
+
Assertion.addMethod("r3fMapTexture", function(idOrUuid, expectedName) {
|
|
945
|
+
const api = getR3FFromWindow();
|
|
946
|
+
const insp = requireInspection(api, idOrUuid, "r3fMapTexture");
|
|
947
|
+
const actual = insp.material?.map;
|
|
948
|
+
if (!expectedName) {
|
|
252
949
|
this.assert(
|
|
253
|
-
actual
|
|
254
|
-
`expected
|
|
255
|
-
`expected
|
|
256
|
-
|
|
950
|
+
actual !== void 0 && actual !== null,
|
|
951
|
+
`expected "${idOrUuid}" to have a map texture`,
|
|
952
|
+
`expected "${idOrUuid}" to NOT have a map texture`,
|
|
953
|
+
"any map",
|
|
257
954
|
actual
|
|
258
955
|
);
|
|
259
|
-
}
|
|
260
|
-
);
|
|
261
|
-
Assertion.addMethod(
|
|
262
|
-
"r3fBounds",
|
|
263
|
-
function(idOrUuid, expected, tolerance = 0.1) {
|
|
264
|
-
const api = getR3FFromWindow();
|
|
265
|
-
const inspection = api.inspect(idOrUuid);
|
|
266
|
-
if (!inspection) {
|
|
267
|
-
throw new Error(`3D object "${idOrUuid}" not found in the scene`);
|
|
268
|
-
}
|
|
269
|
-
const { bounds } = inspection;
|
|
270
|
-
const withinTol = (a, b) => a.every((v, i) => Math.abs(v - b[i]) <= tolerance);
|
|
271
|
-
const pass = withinTol(bounds.min, expected.min) && withinTol(bounds.max, expected.max);
|
|
956
|
+
} else {
|
|
272
957
|
this.assert(
|
|
273
|
-
|
|
274
|
-
`expected
|
|
275
|
-
`expected
|
|
276
|
-
|
|
277
|
-
|
|
958
|
+
actual === expectedName,
|
|
959
|
+
`expected "${idOrUuid}" map "${expectedName}", got "${actual ?? "none"}"`,
|
|
960
|
+
`expected "${idOrUuid}" to NOT have map "${expectedName}"`,
|
|
961
|
+
expectedName,
|
|
962
|
+
actual
|
|
278
963
|
);
|
|
279
964
|
}
|
|
280
|
-
);
|
|
965
|
+
});
|
|
966
|
+
Assertion.addMethod("r3fObjectCount", function(expected) {
|
|
967
|
+
const api = getR3FFromWindow();
|
|
968
|
+
const actual = api.getCount();
|
|
969
|
+
this.assert(
|
|
970
|
+
actual === expected,
|
|
971
|
+
`expected scene to have ${expected} objects, got ${actual}`,
|
|
972
|
+
`expected scene to NOT have ${expected} objects`,
|
|
973
|
+
expected,
|
|
974
|
+
actual
|
|
975
|
+
);
|
|
976
|
+
});
|
|
977
|
+
Assertion.addMethod("r3fObjectCountGreaterThan", function(min) {
|
|
978
|
+
const api = getR3FFromWindow();
|
|
979
|
+
const actual = api.getCount();
|
|
980
|
+
this.assert(
|
|
981
|
+
actual > min,
|
|
982
|
+
`expected scene to have more than ${min} objects, got ${actual}`,
|
|
983
|
+
`expected scene to have at most ${min} objects, but has ${actual}`,
|
|
984
|
+
`> ${min}`,
|
|
985
|
+
actual
|
|
986
|
+
);
|
|
987
|
+
});
|
|
988
|
+
Assertion.addMethod("r3fCountByType", function(type, expected) {
|
|
989
|
+
const api = getR3FFromWindow();
|
|
990
|
+
const actual = api.getCountByType(type);
|
|
991
|
+
this.assert(
|
|
992
|
+
actual === expected,
|
|
993
|
+
`expected ${expected} "${type}" objects, got ${actual}`,
|
|
994
|
+
`expected scene to NOT have ${expected} "${type}" objects`,
|
|
995
|
+
expected,
|
|
996
|
+
actual
|
|
997
|
+
);
|
|
998
|
+
});
|
|
999
|
+
Assertion.addMethod("r3fTotalTriangleCount", function(expected) {
|
|
1000
|
+
const api = getR3FFromWindow();
|
|
1001
|
+
const actual = collectTriangles(api);
|
|
1002
|
+
this.assert(
|
|
1003
|
+
actual === expected,
|
|
1004
|
+
`expected ${expected} total triangles, got ${actual}`,
|
|
1005
|
+
`expected scene to NOT have ${expected} total triangles`,
|
|
1006
|
+
expected,
|
|
1007
|
+
actual
|
|
1008
|
+
);
|
|
1009
|
+
});
|
|
1010
|
+
Assertion.addMethod("r3fTotalTriangleCountLessThan", function(max) {
|
|
1011
|
+
const api = getR3FFromWindow();
|
|
1012
|
+
const actual = collectTriangles(api);
|
|
1013
|
+
this.assert(
|
|
1014
|
+
actual < max,
|
|
1015
|
+
`expected fewer than ${max} total triangles, got ${actual}`,
|
|
1016
|
+
`expected at least ${max} total triangles, but has ${actual}`,
|
|
1017
|
+
`< ${max}`,
|
|
1018
|
+
actual
|
|
1019
|
+
);
|
|
1020
|
+
});
|
|
1021
|
+
Assertion.addMethod("r3fCameraPosition", function(expected, tolerance = 0.1) {
|
|
1022
|
+
const api = getR3FFromWindow();
|
|
1023
|
+
const cam = api.getCameraState();
|
|
1024
|
+
const pass = expected.every((v, i) => Math.abs(cam.position[i] - v) <= tolerance);
|
|
1025
|
+
this.assert(
|
|
1026
|
+
pass,
|
|
1027
|
+
`expected camera at [${expected}], got [${cam.position}] (tol=${tolerance})`,
|
|
1028
|
+
`expected camera NOT at [${expected}]`,
|
|
1029
|
+
expected,
|
|
1030
|
+
cam.position
|
|
1031
|
+
);
|
|
1032
|
+
});
|
|
1033
|
+
Assertion.addMethod("r3fCameraFov", function(expected, tolerance = 0.1) {
|
|
1034
|
+
const api = getR3FFromWindow();
|
|
1035
|
+
const cam = api.getCameraState();
|
|
1036
|
+
const actual = cam.fov;
|
|
1037
|
+
const pass = actual !== void 0 && Math.abs(actual - expected) <= tolerance;
|
|
1038
|
+
this.assert(
|
|
1039
|
+
pass,
|
|
1040
|
+
`expected camera fov ${expected}, got ${actual ?? "N/A"} (tol=${tolerance})`,
|
|
1041
|
+
`expected camera fov NOT ${expected}`,
|
|
1042
|
+
expected,
|
|
1043
|
+
actual
|
|
1044
|
+
);
|
|
1045
|
+
});
|
|
1046
|
+
Assertion.addMethod("r3fCameraNear", function(expected, tolerance = 0.01) {
|
|
1047
|
+
const api = getR3FFromWindow();
|
|
1048
|
+
const cam = api.getCameraState();
|
|
1049
|
+
const pass = Math.abs(cam.near - expected) <= tolerance;
|
|
1050
|
+
this.assert(
|
|
1051
|
+
pass,
|
|
1052
|
+
`expected camera near ${expected}, got ${cam.near} (tol=${tolerance})`,
|
|
1053
|
+
`expected camera near NOT ${expected}`,
|
|
1054
|
+
expected,
|
|
1055
|
+
cam.near
|
|
1056
|
+
);
|
|
1057
|
+
});
|
|
1058
|
+
Assertion.addMethod("r3fCameraFar", function(expected, tolerance = 1) {
|
|
1059
|
+
const api = getR3FFromWindow();
|
|
1060
|
+
const cam = api.getCameraState();
|
|
1061
|
+
const pass = Math.abs(cam.far - expected) <= tolerance;
|
|
1062
|
+
this.assert(
|
|
1063
|
+
pass,
|
|
1064
|
+
`expected camera far ${expected}, got ${cam.far} (tol=${tolerance})`,
|
|
1065
|
+
`expected camera far NOT ${expected}`,
|
|
1066
|
+
expected,
|
|
1067
|
+
cam.far
|
|
1068
|
+
);
|
|
1069
|
+
});
|
|
1070
|
+
Assertion.addMethod("r3fCameraZoom", function(expected, tolerance = 0.01) {
|
|
1071
|
+
const api = getR3FFromWindow();
|
|
1072
|
+
const cam = api.getCameraState();
|
|
1073
|
+
const pass = Math.abs(cam.zoom - expected) <= tolerance;
|
|
1074
|
+
this.assert(
|
|
1075
|
+
pass,
|
|
1076
|
+
`expected camera zoom ${expected}, got ${cam.zoom} (tol=${tolerance})`,
|
|
1077
|
+
`expected camera zoom NOT ${expected}`,
|
|
1078
|
+
expected,
|
|
1079
|
+
cam.zoom
|
|
1080
|
+
);
|
|
1081
|
+
});
|
|
1082
|
+
Assertion.addMethod("r3fAllExist", function(idsOrPattern) {
|
|
1083
|
+
const api = getR3FFromWindow();
|
|
1084
|
+
const ids = typeof idsOrPattern === "string" ? resolvePatternSync(api, idsOrPattern) : idsOrPattern;
|
|
1085
|
+
const missing = ids.filter((id) => resolveObject(api, id) === null);
|
|
1086
|
+
this.assert(
|
|
1087
|
+
missing.length === 0 && ids.length > 0,
|
|
1088
|
+
`expected all objects to exist, missing: [${missing.join(", ")}]`,
|
|
1089
|
+
`expected some objects to NOT exist, but all do`,
|
|
1090
|
+
ids,
|
|
1091
|
+
{ missing }
|
|
1092
|
+
);
|
|
1093
|
+
});
|
|
1094
|
+
Assertion.addMethod("r3fAllVisible", function(idsOrPattern) {
|
|
1095
|
+
const api = getR3FFromWindow();
|
|
1096
|
+
const ids = typeof idsOrPattern === "string" ? resolvePatternSync(api, idsOrPattern) : idsOrPattern;
|
|
1097
|
+
const hidden = ids.filter((id) => {
|
|
1098
|
+
const m = resolveObject(api, id);
|
|
1099
|
+
return !m || !m.visible;
|
|
1100
|
+
});
|
|
1101
|
+
this.assert(
|
|
1102
|
+
hidden.length === 0 && ids.length > 0,
|
|
1103
|
+
`expected all objects to be visible, hidden/missing: [${hidden.join(", ")}]`,
|
|
1104
|
+
`expected some objects to NOT be visible, but all are`,
|
|
1105
|
+
ids,
|
|
1106
|
+
{ hidden }
|
|
1107
|
+
);
|
|
1108
|
+
});
|
|
1109
|
+
Assertion.addMethod("r3fNoneExist", function(idsOrPattern) {
|
|
1110
|
+
const api = getR3FFromWindow();
|
|
1111
|
+
const ids = typeof idsOrPattern === "string" ? resolvePatternSync(api, idsOrPattern) : idsOrPattern;
|
|
1112
|
+
const found = ids.filter((id) => resolveObject(api, id) !== null);
|
|
1113
|
+
this.assert(
|
|
1114
|
+
found.length === 0,
|
|
1115
|
+
`expected no objects to exist, but found: [${found.join(", ")}]`,
|
|
1116
|
+
`expected some objects to exist, but none do`,
|
|
1117
|
+
ids,
|
|
1118
|
+
{ found }
|
|
1119
|
+
);
|
|
1120
|
+
});
|
|
281
1121
|
});
|
|
282
1122
|
}
|
|
1123
|
+
function resolvePatternSync(api, pattern) {
|
|
1124
|
+
const snap = api.snapshot();
|
|
1125
|
+
const ids = [];
|
|
1126
|
+
const regex = new RegExp("^" + pattern.replace(/\*/g, ".*").replace(/\?/g, ".") + "$");
|
|
1127
|
+
function walk(node) {
|
|
1128
|
+
const testId = node.testId ?? node.name;
|
|
1129
|
+
if (regex.test(testId) || regex.test(node.uuid)) ids.push(testId || node.uuid);
|
|
1130
|
+
for (const child of node.children) walk(child);
|
|
1131
|
+
}
|
|
1132
|
+
walk(snap.tree);
|
|
1133
|
+
return ids;
|
|
1134
|
+
}
|
|
283
1135
|
|
|
284
1136
|
// src/waiters.ts
|
|
1137
|
+
function resolveApiFromWindow(win) {
|
|
1138
|
+
const w = win;
|
|
1139
|
+
const cid = _getActiveCanvasId();
|
|
1140
|
+
return cid ? w.__R3F_DOM_INSTANCES__?.[cid] : w.__R3F_DOM__;
|
|
1141
|
+
}
|
|
285
1142
|
function getBridgeState(win) {
|
|
286
|
-
const api = win
|
|
1143
|
+
const api = resolveApiFromWindow(win);
|
|
287
1144
|
if (!api) return { exists: false, ready: false, error: null, count: 0 };
|
|
288
1145
|
return {
|
|
289
1146
|
exists: true,
|
|
@@ -294,6 +1151,8 @@ function getBridgeState(win) {
|
|
|
294
1151
|
}
|
|
295
1152
|
function assertBridgeNotErrored(state) {
|
|
296
1153
|
if (state.exists && !state.ready && state.error) {
|
|
1154
|
+
const reporter = _getReporter();
|
|
1155
|
+
reporter?.logBridgeError(state.error);
|
|
297
1156
|
throw new Error(
|
|
298
1157
|
`[react-three-dom] Bridge initialization failed: ${state.error}
|
|
299
1158
|
The <ThreeDom> component mounted but threw during setup. Check the browser console for the full stack trace.`
|
|
@@ -309,11 +1168,17 @@ function registerWaiters() {
|
|
|
309
1168
|
pollIntervalMs = 100,
|
|
310
1169
|
timeout = 1e4
|
|
311
1170
|
} = options;
|
|
1171
|
+
const reporter = _getReporter();
|
|
1172
|
+
reporter?.logBridgeWaiting();
|
|
312
1173
|
const startTime = Date.now();
|
|
313
1174
|
let lastCount = -1;
|
|
314
1175
|
let stableRuns = 0;
|
|
1176
|
+
let bridgeLogged = false;
|
|
315
1177
|
function poll() {
|
|
316
1178
|
if (Date.now() - startTime > timeout) {
|
|
1179
|
+
reporter?.logBridgeError(
|
|
1180
|
+
`Timed out after ${timeout}ms. Last count: ${lastCount}, stable: ${stableRuns}/${stableChecks}`
|
|
1181
|
+
);
|
|
317
1182
|
throw new Error(
|
|
318
1183
|
`r3fWaitForSceneReady timed out after ${timeout}ms. Last count: ${lastCount}, stable runs: ${stableRuns}/${stableChecks}`
|
|
319
1184
|
);
|
|
@@ -324,10 +1189,20 @@ function registerWaiters() {
|
|
|
324
1189
|
if (!state.exists || !state.ready) {
|
|
325
1190
|
return cy.wait(pollIntervalMs, { log: false }).then(() => poll());
|
|
326
1191
|
}
|
|
1192
|
+
if (!bridgeLogged) {
|
|
1193
|
+
bridgeLogged = true;
|
|
1194
|
+
const api = resolveApiFromWindow(win);
|
|
1195
|
+
let diag;
|
|
1196
|
+
if (typeof api.getDiagnostics === "function") {
|
|
1197
|
+
diag = api.getDiagnostics();
|
|
1198
|
+
}
|
|
1199
|
+
reporter?.logBridgeConnected(diag);
|
|
1200
|
+
}
|
|
327
1201
|
const count = state.count;
|
|
328
1202
|
if (count === lastCount && count > 0) {
|
|
329
1203
|
stableRuns++;
|
|
330
1204
|
if (stableRuns >= stableChecks) {
|
|
1205
|
+
reporter?.logSceneReady(count);
|
|
331
1206
|
return;
|
|
332
1207
|
}
|
|
333
1208
|
} else {
|
|
@@ -349,8 +1224,13 @@ function registerWaiters() {
|
|
|
349
1224
|
timeout = 1e4
|
|
350
1225
|
} = options;
|
|
351
1226
|
const startTime = Date.now();
|
|
352
|
-
let
|
|
1227
|
+
let lastHash = "";
|
|
353
1228
|
let stableCount = 0;
|
|
1229
|
+
function hashTree(node) {
|
|
1230
|
+
let h = `${node.uuid}:${node.visible ? 1 : 0}:${node.position[0].toFixed(3)},${node.position[1].toFixed(3)},${node.position[2].toFixed(3)}:${node.children.length}`;
|
|
1231
|
+
for (const c of node.children) h += "|" + hashTree(c);
|
|
1232
|
+
return h;
|
|
1233
|
+
}
|
|
354
1234
|
function poll() {
|
|
355
1235
|
if (Date.now() - startTime > timeout) {
|
|
356
1236
|
throw new Error(`r3fWaitForIdle timed out after ${timeout}ms`);
|
|
@@ -361,10 +1241,10 @@ function registerWaiters() {
|
|
|
361
1241
|
if (!state.exists || !state.ready) {
|
|
362
1242
|
return cy.wait(pollIntervalMs, { log: false }).then(() => poll());
|
|
363
1243
|
}
|
|
364
|
-
const api = win
|
|
1244
|
+
const api = resolveApiFromWindow(win);
|
|
365
1245
|
const snap = api.snapshot();
|
|
366
|
-
const
|
|
367
|
-
if (
|
|
1246
|
+
const hash = hashTree(snap.tree);
|
|
1247
|
+
if (hash === lastHash && hash !== "") {
|
|
368
1248
|
stableCount++;
|
|
369
1249
|
if (stableCount >= idleChecks) {
|
|
370
1250
|
return;
|
|
@@ -372,7 +1252,7 @@ function registerWaiters() {
|
|
|
372
1252
|
} else {
|
|
373
1253
|
stableCount = 0;
|
|
374
1254
|
}
|
|
375
|
-
|
|
1255
|
+
lastHash = hash;
|
|
376
1256
|
return cy.wait(pollIntervalMs, { log: false }).then(() => poll());
|
|
377
1257
|
});
|
|
378
1258
|
}
|
|
@@ -387,9 +1267,13 @@ function registerWaiters() {
|
|
|
387
1267
|
objectTimeout = 4e4,
|
|
388
1268
|
pollIntervalMs = 200
|
|
389
1269
|
} = options;
|
|
1270
|
+
const reporter = _getReporter();
|
|
390
1271
|
const bridgeStart = Date.now();
|
|
391
1272
|
function waitForBridge() {
|
|
392
1273
|
if (Date.now() - bridgeStart > bridgeTimeout) {
|
|
1274
|
+
reporter?.logBridgeError(
|
|
1275
|
+
`Timed out after ${bridgeTimeout}ms waiting for bridge`
|
|
1276
|
+
);
|
|
393
1277
|
throw new Error(
|
|
394
1278
|
`r3fWaitForObject("${idOrUuid}") timed out after ${bridgeTimeout}ms waiting for the bridge. Ensure <ThreeDom> is mounted inside your <Canvas>.`
|
|
395
1279
|
);
|
|
@@ -407,19 +1291,33 @@ function registerWaiters() {
|
|
|
407
1291
|
function pollForObject() {
|
|
408
1292
|
if (Date.now() - objectStart > objectTimeout) {
|
|
409
1293
|
return cy.window({ log: false }).then((win) => {
|
|
1294
|
+
const api = resolveApiFromWindow(win);
|
|
410
1295
|
const state = getBridgeState(win);
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
1296
|
+
let msg = `r3fWaitForObject("${idOrUuid}") timed out after ${objectTimeout}ms.
|
|
1297
|
+
Bridge: ${state.exists ? "exists" : "missing"}, ready: ${state.ready}, objectCount: ${state.count}.
|
|
1298
|
+
Ensure the object has userData.testId="${idOrUuid}" or uuid="${idOrUuid}".`;
|
|
1299
|
+
let suggestions = [];
|
|
1300
|
+
if (api && typeof api.fuzzyFind === "function") {
|
|
1301
|
+
suggestions = api.fuzzyFind(idOrUuid, 5);
|
|
1302
|
+
if (suggestions.length > 0) {
|
|
1303
|
+
msg += "\nDid you mean:\n" + suggestions.map((s) => {
|
|
1304
|
+
const id = s.testId ? `testId="${s.testId}"` : `uuid="${s.uuid}"`;
|
|
1305
|
+
return ` \u2192 ${s.name || "(unnamed)"} [${id}]`;
|
|
1306
|
+
}).join("\n");
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
reporter?.logObjectNotFound(idOrUuid, suggestions);
|
|
1310
|
+
throw new Error(msg);
|
|
414
1311
|
});
|
|
415
1312
|
}
|
|
416
1313
|
return cy.window({ log: false }).then((win) => {
|
|
417
|
-
const api = win
|
|
1314
|
+
const api = resolveApiFromWindow(win);
|
|
418
1315
|
if (!api || !api._ready) {
|
|
419
1316
|
return cy.wait(pollIntervalMs, { log: false }).then(() => pollForObject());
|
|
420
1317
|
}
|
|
421
|
-
const
|
|
422
|
-
if (
|
|
1318
|
+
const meta = api.getByTestId(idOrUuid) ?? api.getByUuid(idOrUuid);
|
|
1319
|
+
if (meta) {
|
|
1320
|
+
reporter?.logObjectFound(idOrUuid, meta.type, meta.name || void 0);
|
|
423
1321
|
Cypress.log({
|
|
424
1322
|
name: "r3fWaitForObject",
|
|
425
1323
|
message: `"${idOrUuid}" found`
|
|
@@ -452,7 +1350,7 @@ function registerWaiters() {
|
|
|
452
1350
|
const state = getBridgeState(win);
|
|
453
1351
|
assertBridgeNotErrored(state);
|
|
454
1352
|
if (state.exists && state.ready) {
|
|
455
|
-
const api = win
|
|
1353
|
+
const api = resolveApiFromWindow(win);
|
|
456
1354
|
const snap = api.snapshot();
|
|
457
1355
|
collectUuids(snap.tree);
|
|
458
1356
|
}
|
|
@@ -473,7 +1371,7 @@ function registerWaiters() {
|
|
|
473
1371
|
const bridgeState = getBridgeState(w);
|
|
474
1372
|
assertBridgeNotErrored(bridgeState);
|
|
475
1373
|
if (!bridgeState.exists || !bridgeState.ready) return poll();
|
|
476
|
-
const bridge = w
|
|
1374
|
+
const bridge = resolveApiFromWindow(w);
|
|
477
1375
|
const snap = bridge.snapshot();
|
|
478
1376
|
const newObjects = [];
|
|
479
1377
|
const newUuids = [];
|
|
@@ -510,11 +1408,65 @@ function registerWaiters() {
|
|
|
510
1408
|
});
|
|
511
1409
|
}
|
|
512
1410
|
);
|
|
1411
|
+
Cypress.Commands.add(
|
|
1412
|
+
"r3fWaitForObjectRemoved",
|
|
1413
|
+
(idOrUuid, options = {}) => {
|
|
1414
|
+
const {
|
|
1415
|
+
bridgeTimeout = 3e4,
|
|
1416
|
+
pollIntervalMs = 100,
|
|
1417
|
+
timeout = 1e4
|
|
1418
|
+
} = options;
|
|
1419
|
+
const bridgeStart = Date.now();
|
|
1420
|
+
function waitForBridge() {
|
|
1421
|
+
if (Date.now() - bridgeStart > bridgeTimeout) {
|
|
1422
|
+
throw new Error(
|
|
1423
|
+
`r3fWaitForObjectRemoved("${idOrUuid}") timed out after ${bridgeTimeout}ms waiting for the bridge.`
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
return cy.window({ log: false }).then((win) => {
|
|
1427
|
+
const state = getBridgeState(win);
|
|
1428
|
+
assertBridgeNotErrored(state);
|
|
1429
|
+
if (state.exists && state.ready) {
|
|
1430
|
+
return pollForRemoved();
|
|
1431
|
+
}
|
|
1432
|
+
return cy.wait(pollIntervalMs, { log: false }).then(() => waitForBridge());
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
const removeDeadline = Date.now() + timeout;
|
|
1436
|
+
function pollForRemoved() {
|
|
1437
|
+
if (Date.now() > removeDeadline) {
|
|
1438
|
+
throw new Error(
|
|
1439
|
+
`r3fWaitForObjectRemoved("${idOrUuid}") timed out after ${timeout}ms. Object is still in the scene.`
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
return cy.window({ log: false }).then((win) => {
|
|
1443
|
+
const api = resolveApiFromWindow(win);
|
|
1444
|
+
if (!api || !api._ready) {
|
|
1445
|
+
return cy.wait(pollIntervalMs, { log: false }).then(() => pollForRemoved());
|
|
1446
|
+
}
|
|
1447
|
+
const stillPresent = (api.getByTestId(idOrUuid) ?? api.getByUuid(idOrUuid)) !== null;
|
|
1448
|
+
if (!stillPresent) {
|
|
1449
|
+
Cypress.log({
|
|
1450
|
+
name: "r3fWaitForObjectRemoved",
|
|
1451
|
+
message: `"${idOrUuid}" removed`
|
|
1452
|
+
});
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
return cy.wait(pollIntervalMs, { log: false }).then(() => pollForRemoved());
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
return waitForBridge();
|
|
1459
|
+
}
|
|
1460
|
+
);
|
|
513
1461
|
}
|
|
514
1462
|
|
|
515
1463
|
// src/index.ts
|
|
516
1464
|
registerCommands();
|
|
517
1465
|
registerAssertions();
|
|
518
1466
|
registerWaiters();
|
|
1467
|
+
|
|
1468
|
+
exports.R3FReporter = R3FReporter;
|
|
1469
|
+
exports.diffSnapshots = diffSnapshots;
|
|
1470
|
+
exports.registerR3FTasks = registerR3FTasks;
|
|
519
1471
|
//# sourceMappingURL=index.cjs.map
|
|
520
1472
|
//# sourceMappingURL=index.cjs.map
|