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