@react-three-dom/cypress 0.1.1 → 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 +1278 -96
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +227 -17
- package/dist/index.d.ts +227 -17
- package/dist/index.js +1276 -96
- package/dist/index.js.map +1 -1
- package/package.json +12 -9
- package/src/index.d.ts +175 -18
package/dist/index.cjs
CHANGED
|
@@ -1,65 +1,504 @@
|
|
|
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
|
+
}
|
|
316
|
+
function formatCypressSceneTree(node, prefix = "", isLast = true) {
|
|
317
|
+
const connector = prefix === "" ? "" : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
|
|
318
|
+
const childPrefix = prefix === "" ? "" : prefix + (isLast ? " " : "\u2502 ");
|
|
319
|
+
let label = node.type;
|
|
320
|
+
if (node.name) label += ` "${node.name}"`;
|
|
321
|
+
if (node.testId) label += ` [testId: ${node.testId}]`;
|
|
322
|
+
label += node.visible ? " visible" : " HIDDEN";
|
|
323
|
+
let result = prefix + connector + label + "\n";
|
|
324
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
325
|
+
const child = node.children[i];
|
|
326
|
+
const last = i === node.children.length - 1;
|
|
327
|
+
result += formatCypressSceneTree(child, childPrefix, last);
|
|
328
|
+
}
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
13
331
|
function registerCommands() {
|
|
14
|
-
Cypress.Commands.add("
|
|
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
|
+
});
|
|
352
|
+
Cypress.Commands.add("r3fEnableDebug", () => {
|
|
15
353
|
cy.window({ log: false }).then((win) => {
|
|
16
|
-
|
|
354
|
+
win.__R3F_DOM_DEBUG__ = true;
|
|
355
|
+
});
|
|
356
|
+
Cypress.on("window:before:load", (win) => {
|
|
357
|
+
const origLog = win.console.log;
|
|
358
|
+
win.console.log = (...args) => {
|
|
359
|
+
origLog.apply(win.console, args);
|
|
360
|
+
const text = String(args[0]);
|
|
361
|
+
if (text.startsWith("[r3f-dom:")) {
|
|
362
|
+
Cypress.log({
|
|
363
|
+
name: "r3f-dom",
|
|
364
|
+
message: args.slice(1).map(String).join(" "),
|
|
365
|
+
consoleProps: () => ({ raw: args })
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
Cypress.Commands.add("r3fClick", (idOrUuid) => {
|
|
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);
|
|
17
378
|
});
|
|
18
379
|
});
|
|
19
380
|
Cypress.Commands.add("r3fDoubleClick", (idOrUuid) => {
|
|
20
|
-
|
|
21
|
-
|
|
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);
|
|
22
387
|
});
|
|
23
388
|
});
|
|
24
389
|
Cypress.Commands.add("r3fContextMenu", (idOrUuid) => {
|
|
25
|
-
|
|
26
|
-
|
|
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);
|
|
27
396
|
});
|
|
28
397
|
});
|
|
29
398
|
Cypress.Commands.add("r3fHover", (idOrUuid) => {
|
|
30
|
-
|
|
31
|
-
|
|
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);
|
|
32
414
|
});
|
|
33
415
|
});
|
|
34
416
|
Cypress.Commands.add(
|
|
35
417
|
"r3fDrag",
|
|
36
418
|
(idOrUuid, delta) => {
|
|
37
|
-
|
|
38
|
-
|
|
419
|
+
return autoWaitForObject(idOrUuid).then((api) => {
|
|
420
|
+
return Cypress.Promise.resolve(api.drag(idOrUuid, delta));
|
|
39
421
|
});
|
|
40
422
|
}
|
|
41
423
|
);
|
|
42
424
|
Cypress.Commands.add(
|
|
43
425
|
"r3fWheel",
|
|
44
426
|
(idOrUuid, options) => {
|
|
45
|
-
|
|
46
|
-
|
|
427
|
+
return autoWaitForObject(idOrUuid).then((api) => {
|
|
428
|
+
api.wheel(idOrUuid, options);
|
|
47
429
|
});
|
|
48
430
|
}
|
|
49
431
|
);
|
|
50
432
|
Cypress.Commands.add("r3fPointerMiss", () => {
|
|
51
|
-
|
|
52
|
-
|
|
433
|
+
return autoWaitForBridge().then((api) => {
|
|
434
|
+
api.pointerMiss();
|
|
53
435
|
});
|
|
54
436
|
});
|
|
55
437
|
Cypress.Commands.add("r3fSelect", (idOrUuid) => {
|
|
56
|
-
|
|
57
|
-
|
|
438
|
+
return autoWaitForObject(idOrUuid).then((api) => {
|
|
439
|
+
api.select(idOrUuid);
|
|
58
440
|
});
|
|
59
441
|
});
|
|
60
442
|
Cypress.Commands.add("r3fClearSelection", () => {
|
|
61
|
-
|
|
62
|
-
|
|
443
|
+
return autoWaitForBridge().then((api) => {
|
|
444
|
+
api.clearSelection();
|
|
445
|
+
});
|
|
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
|
+
);
|
|
455
|
+
Cypress.Commands.add("r3fLogScene", () => {
|
|
456
|
+
return cy.window({ log: false }).then((win) => {
|
|
457
|
+
const api = getR3F(win);
|
|
458
|
+
const snap = api.snapshot();
|
|
459
|
+
const lines = formatCypressSceneTree(snap.tree);
|
|
460
|
+
const output = `Scene tree (${snap.objectCount} objects):
|
|
461
|
+
${lines}`;
|
|
462
|
+
Cypress.log({
|
|
463
|
+
name: "r3fLogScene",
|
|
464
|
+
message: `${snap.objectCount} objects`,
|
|
465
|
+
consoleProps: () => ({ snapshot: snap, tree: output })
|
|
466
|
+
});
|
|
467
|
+
console.log(`[r3f-dom] ${output}`);
|
|
468
|
+
});
|
|
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}`);
|
|
63
502
|
});
|
|
64
503
|
});
|
|
65
504
|
Cypress.Commands.add("r3fGetObject", (idOrUuid) => {
|
|
@@ -68,6 +507,58 @@ function registerCommands() {
|
|
|
68
507
|
return api.getByTestId(idOrUuid) ?? api.getByUuid(idOrUuid) ?? null;
|
|
69
508
|
});
|
|
70
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
|
+
});
|
|
71
562
|
Cypress.Commands.add("r3fInspect", (idOrUuid) => {
|
|
72
563
|
return cy.window({ log: false }).then((win) => {
|
|
73
564
|
return getR3F(win).inspect(idOrUuid);
|
|
@@ -83,20 +574,101 @@ function registerCommands() {
|
|
|
83
574
|
return getR3F(win).getCount();
|
|
84
575
|
});
|
|
85
576
|
});
|
|
577
|
+
Cypress.Commands.add("r3fGetByType", (type) => {
|
|
578
|
+
return cy.window({ log: false }).then((win) => {
|
|
579
|
+
return getR3F(win).getByType(type);
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
Cypress.Commands.add("r3fGetByUserData", (key, value) => {
|
|
583
|
+
return cy.window({ log: false }).then((win) => {
|
|
584
|
+
return getR3F(win).getByUserData(key, value);
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
Cypress.Commands.add("r3fGetCountByType", (type) => {
|
|
588
|
+
return cy.window({ log: false }).then((win) => {
|
|
589
|
+
return getR3F(win).getCountByType(type);
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
Cypress.Commands.add("r3fGetObjects", (ids) => {
|
|
593
|
+
return cy.window({ log: false }).then((win) => {
|
|
594
|
+
return getR3F(win).getObjects(ids);
|
|
595
|
+
});
|
|
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
|
+
});
|
|
86
620
|
}
|
|
87
621
|
|
|
88
622
|
// src/assertions.ts
|
|
89
623
|
function getR3FFromWindow() {
|
|
90
624
|
const win = cy.state("window");
|
|
91
|
-
const
|
|
625
|
+
const cid = _getActiveCanvasId();
|
|
626
|
+
const api = cid ? win?.__R3F_DOM_INSTANCES__?.[cid] : win?.__R3F_DOM__;
|
|
92
627
|
if (!api) {
|
|
93
|
-
throw new Error(
|
|
628
|
+
throw new Error(
|
|
629
|
+
`react-three-dom bridge not found${cid ? ` (canvas: "${cid}")` : ""}. Is <ThreeDom${cid ? ` canvasId="${cid}"` : ""}> mounted?`
|
|
630
|
+
);
|
|
94
631
|
}
|
|
95
632
|
return api;
|
|
96
633
|
}
|
|
97
634
|
function resolveObject(api, idOrUuid) {
|
|
98
635
|
return api.getByTestId(idOrUuid) ?? api.getByUuid(idOrUuid) ?? null;
|
|
99
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
|
+
}
|
|
100
672
|
function registerAssertions() {
|
|
101
673
|
chai.use((_chai) => {
|
|
102
674
|
const { Assertion } = _chai;
|
|
@@ -113,97 +685,480 @@ function registerAssertions() {
|
|
|
113
685
|
});
|
|
114
686
|
Assertion.addMethod("r3fVisible", function(idOrUuid) {
|
|
115
687
|
const api = getR3FFromWindow();
|
|
116
|
-
const meta =
|
|
117
|
-
if (!meta) {
|
|
118
|
-
throw new Error(`3D object "${idOrUuid}" not found in the scene`);
|
|
119
|
-
}
|
|
688
|
+
const meta = requireObject(api, idOrUuid, "r3fVisible");
|
|
120
689
|
this.assert(
|
|
121
690
|
meta.visible,
|
|
122
|
-
`expected
|
|
123
|
-
`expected
|
|
691
|
+
`expected "${idOrUuid}" to be visible`,
|
|
692
|
+
`expected "${idOrUuid}" to NOT be visible`,
|
|
124
693
|
true,
|
|
125
694
|
meta.visible
|
|
126
695
|
);
|
|
127
696
|
});
|
|
128
|
-
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) {
|
|
129
736
|
const api = getR3FFromWindow();
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
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) {
|
|
792
|
+
const api = getR3FFromWindow();
|
|
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)`);
|
|
133
808
|
}
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
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;
|
|
137
824
|
this.assert(
|
|
138
|
-
|
|
139
|
-
`expected
|
|
140
|
-
`expected
|
|
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);
|
|
837
|
+
this.assert(
|
|
838
|
+
pass,
|
|
839
|
+
`expected "${idOrUuid}" to be in the camera frustum`,
|
|
840
|
+
`expected "${idOrUuid}" to NOT be in the camera frustum`,
|
|
141
841
|
"in frustum",
|
|
142
|
-
|
|
842
|
+
pass ? "in frustum" : "invalid bounds"
|
|
143
843
|
);
|
|
144
844
|
});
|
|
145
|
-
Assertion.addMethod(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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);
|
|
156
935
|
this.assert(
|
|
157
936
|
pass,
|
|
158
|
-
`expected
|
|
159
|
-
`expected
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
162
941
|
);
|
|
163
942
|
}
|
|
164
|
-
);
|
|
165
|
-
Assertion.addMethod(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (!meta) {
|
|
171
|
-
throw new Error(`3D object "${idOrUuid}" not found in the scene`);
|
|
172
|
-
}
|
|
173
|
-
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) {
|
|
174
949
|
this.assert(
|
|
175
|
-
actual
|
|
176
|
-
`expected
|
|
177
|
-
`expected
|
|
178
|
-
|
|
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",
|
|
179
954
|
actual
|
|
180
955
|
);
|
|
181
|
-
}
|
|
182
|
-
);
|
|
183
|
-
Assertion.addMethod(
|
|
184
|
-
"r3fBounds",
|
|
185
|
-
function(idOrUuid, expected, tolerance = 0.1) {
|
|
186
|
-
const api = getR3FFromWindow();
|
|
187
|
-
const inspection = api.inspect(idOrUuid);
|
|
188
|
-
if (!inspection) {
|
|
189
|
-
throw new Error(`3D object "${idOrUuid}" not found in the scene`);
|
|
190
|
-
}
|
|
191
|
-
const { bounds } = inspection;
|
|
192
|
-
const withinTol = (a, b) => a.every((v, i) => Math.abs(v - b[i]) <= tolerance);
|
|
193
|
-
const pass = withinTol(bounds.min, expected.min) && withinTol(bounds.max, expected.max);
|
|
956
|
+
} else {
|
|
194
957
|
this.assert(
|
|
195
|
-
|
|
196
|
-
`expected
|
|
197
|
-
`expected
|
|
198
|
-
|
|
199
|
-
|
|
958
|
+
actual === expectedName,
|
|
959
|
+
`expected "${idOrUuid}" map "${expectedName}", got "${actual ?? "none"}"`,
|
|
960
|
+
`expected "${idOrUuid}" to NOT have map "${expectedName}"`,
|
|
961
|
+
expectedName,
|
|
962
|
+
actual
|
|
200
963
|
);
|
|
201
964
|
}
|
|
202
|
-
);
|
|
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
|
+
});
|
|
203
1121
|
});
|
|
204
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
|
+
}
|
|
205
1135
|
|
|
206
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
|
+
}
|
|
1142
|
+
function getBridgeState(win) {
|
|
1143
|
+
const api = resolveApiFromWindow(win);
|
|
1144
|
+
if (!api) return { exists: false, ready: false, error: null, count: 0 };
|
|
1145
|
+
return {
|
|
1146
|
+
exists: true,
|
|
1147
|
+
ready: api._ready,
|
|
1148
|
+
error: api._error ?? null,
|
|
1149
|
+
count: api.getCount()
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
function assertBridgeNotErrored(state) {
|
|
1153
|
+
if (state.exists && !state.ready && state.error) {
|
|
1154
|
+
const reporter = _getReporter();
|
|
1155
|
+
reporter?.logBridgeError(state.error);
|
|
1156
|
+
throw new Error(
|
|
1157
|
+
`[react-three-dom] Bridge initialization failed: ${state.error}
|
|
1158
|
+
The <ThreeDom> component mounted but threw during setup. Check the browser console for the full stack trace.`
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
207
1162
|
function registerWaiters() {
|
|
208
1163
|
Cypress.Commands.add(
|
|
209
1164
|
"r3fWaitForSceneReady",
|
|
@@ -213,24 +1168,41 @@ function registerWaiters() {
|
|
|
213
1168
|
pollIntervalMs = 100,
|
|
214
1169
|
timeout = 1e4
|
|
215
1170
|
} = options;
|
|
1171
|
+
const reporter = _getReporter();
|
|
1172
|
+
reporter?.logBridgeWaiting();
|
|
216
1173
|
const startTime = Date.now();
|
|
217
1174
|
let lastCount = -1;
|
|
218
1175
|
let stableRuns = 0;
|
|
1176
|
+
let bridgeLogged = false;
|
|
219
1177
|
function poll() {
|
|
220
1178
|
if (Date.now() - startTime > timeout) {
|
|
1179
|
+
reporter?.logBridgeError(
|
|
1180
|
+
`Timed out after ${timeout}ms. Last count: ${lastCount}, stable: ${stableRuns}/${stableChecks}`
|
|
1181
|
+
);
|
|
221
1182
|
throw new Error(
|
|
222
1183
|
`r3fWaitForSceneReady timed out after ${timeout}ms. Last count: ${lastCount}, stable runs: ${stableRuns}/${stableChecks}`
|
|
223
1184
|
);
|
|
224
1185
|
}
|
|
225
1186
|
return cy.window({ log: false }).then((win) => {
|
|
226
|
-
const
|
|
227
|
-
|
|
1187
|
+
const state = getBridgeState(win);
|
|
1188
|
+
assertBridgeNotErrored(state);
|
|
1189
|
+
if (!state.exists || !state.ready) {
|
|
228
1190
|
return cy.wait(pollIntervalMs, { log: false }).then(() => poll());
|
|
229
1191
|
}
|
|
230
|
-
|
|
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
|
+
}
|
|
1201
|
+
const count = state.count;
|
|
231
1202
|
if (count === lastCount && count > 0) {
|
|
232
1203
|
stableRuns++;
|
|
233
1204
|
if (stableRuns >= stableChecks) {
|
|
1205
|
+
reporter?.logSceneReady(count);
|
|
234
1206
|
return;
|
|
235
1207
|
}
|
|
236
1208
|
} else {
|
|
@@ -252,20 +1224,27 @@ function registerWaiters() {
|
|
|
252
1224
|
timeout = 1e4
|
|
253
1225
|
} = options;
|
|
254
1226
|
const startTime = Date.now();
|
|
255
|
-
let
|
|
1227
|
+
let lastHash = "";
|
|
256
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
|
+
}
|
|
257
1234
|
function poll() {
|
|
258
1235
|
if (Date.now() - startTime > timeout) {
|
|
259
1236
|
throw new Error(`r3fWaitForIdle timed out after ${timeout}ms`);
|
|
260
1237
|
}
|
|
261
1238
|
return cy.window({ log: false }).then((win) => {
|
|
262
|
-
const
|
|
263
|
-
|
|
1239
|
+
const state = getBridgeState(win);
|
|
1240
|
+
assertBridgeNotErrored(state);
|
|
1241
|
+
if (!state.exists || !state.ready) {
|
|
264
1242
|
return cy.wait(pollIntervalMs, { log: false }).then(() => poll());
|
|
265
1243
|
}
|
|
1244
|
+
const api = resolveApiFromWindow(win);
|
|
266
1245
|
const snap = api.snapshot();
|
|
267
|
-
const
|
|
268
|
-
if (
|
|
1246
|
+
const hash = hashTree(snap.tree);
|
|
1247
|
+
if (hash === lastHash && hash !== "") {
|
|
269
1248
|
stableCount++;
|
|
270
1249
|
if (stableCount >= idleChecks) {
|
|
271
1250
|
return;
|
|
@@ -273,18 +1252,221 @@ function registerWaiters() {
|
|
|
273
1252
|
} else {
|
|
274
1253
|
stableCount = 0;
|
|
275
1254
|
}
|
|
276
|
-
|
|
1255
|
+
lastHash = hash;
|
|
277
1256
|
return cy.wait(pollIntervalMs, { log: false }).then(() => poll());
|
|
278
1257
|
});
|
|
279
1258
|
}
|
|
280
1259
|
return poll();
|
|
281
1260
|
}
|
|
282
1261
|
);
|
|
1262
|
+
Cypress.Commands.add(
|
|
1263
|
+
"r3fWaitForObject",
|
|
1264
|
+
(idOrUuid, options = {}) => {
|
|
1265
|
+
const {
|
|
1266
|
+
bridgeTimeout = 3e4,
|
|
1267
|
+
objectTimeout = 4e4,
|
|
1268
|
+
pollIntervalMs = 200
|
|
1269
|
+
} = options;
|
|
1270
|
+
const reporter = _getReporter();
|
|
1271
|
+
const bridgeStart = Date.now();
|
|
1272
|
+
function waitForBridge() {
|
|
1273
|
+
if (Date.now() - bridgeStart > bridgeTimeout) {
|
|
1274
|
+
reporter?.logBridgeError(
|
|
1275
|
+
`Timed out after ${bridgeTimeout}ms waiting for bridge`
|
|
1276
|
+
);
|
|
1277
|
+
throw new Error(
|
|
1278
|
+
`r3fWaitForObject("${idOrUuid}") timed out after ${bridgeTimeout}ms waiting for the bridge. Ensure <ThreeDom> is mounted inside your <Canvas>.`
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
return cy.window({ log: false }).then((win) => {
|
|
1282
|
+
const state = getBridgeState(win);
|
|
1283
|
+
assertBridgeNotErrored(state);
|
|
1284
|
+
if (state.exists && state.ready) {
|
|
1285
|
+
return pollForObject();
|
|
1286
|
+
}
|
|
1287
|
+
return cy.wait(pollIntervalMs, { log: false }).then(() => waitForBridge());
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
const objectStart = Date.now();
|
|
1291
|
+
function pollForObject() {
|
|
1292
|
+
if (Date.now() - objectStart > objectTimeout) {
|
|
1293
|
+
return cy.window({ log: false }).then((win) => {
|
|
1294
|
+
const api = resolveApiFromWindow(win);
|
|
1295
|
+
const state = getBridgeState(win);
|
|
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);
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
return cy.window({ log: false }).then((win) => {
|
|
1314
|
+
const api = resolveApiFromWindow(win);
|
|
1315
|
+
if (!api || !api._ready) {
|
|
1316
|
+
return cy.wait(pollIntervalMs, { log: false }).then(() => pollForObject());
|
|
1317
|
+
}
|
|
1318
|
+
const meta = api.getByTestId(idOrUuid) ?? api.getByUuid(idOrUuid);
|
|
1319
|
+
if (meta) {
|
|
1320
|
+
reporter?.logObjectFound(idOrUuid, meta.type, meta.name || void 0);
|
|
1321
|
+
Cypress.log({
|
|
1322
|
+
name: "r3fWaitForObject",
|
|
1323
|
+
message: `"${idOrUuid}" found`
|
|
1324
|
+
});
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
return cy.wait(pollIntervalMs, { log: false }).then(() => pollForObject());
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
return waitForBridge();
|
|
1331
|
+
}
|
|
1332
|
+
);
|
|
1333
|
+
Cypress.Commands.add(
|
|
1334
|
+
"r3fWaitForNewObject",
|
|
1335
|
+
(options = {}) => {
|
|
1336
|
+
const {
|
|
1337
|
+
type: filterType,
|
|
1338
|
+
nameContains,
|
|
1339
|
+
pollIntervalMs = 100,
|
|
1340
|
+
timeout = 1e4
|
|
1341
|
+
} = options;
|
|
1342
|
+
const baselineUuids = [];
|
|
1343
|
+
function collectUuids(node) {
|
|
1344
|
+
baselineUuids.push(node.uuid);
|
|
1345
|
+
for (const child of node.children) {
|
|
1346
|
+
collectUuids(child);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
return cy.window({ log: false }).then((win) => {
|
|
1350
|
+
const state = getBridgeState(win);
|
|
1351
|
+
assertBridgeNotErrored(state);
|
|
1352
|
+
if (state.exists && state.ready) {
|
|
1353
|
+
const api = resolveApiFromWindow(win);
|
|
1354
|
+
const snap = api.snapshot();
|
|
1355
|
+
collectUuids(snap.tree);
|
|
1356
|
+
}
|
|
1357
|
+
const baselineSet = new Set(baselineUuids);
|
|
1358
|
+
const startTime = Date.now();
|
|
1359
|
+
function poll() {
|
|
1360
|
+
if (Date.now() - startTime > timeout) {
|
|
1361
|
+
const filterDesc = [
|
|
1362
|
+
filterType ? `type="${filterType}"` : null,
|
|
1363
|
+
nameContains ? `nameContains="${nameContains}"` : null
|
|
1364
|
+
].filter(Boolean).join(", ");
|
|
1365
|
+
throw new Error(
|
|
1366
|
+
`r3fWaitForNewObject timed out after ${timeout}ms. No new objects appeared${filterDesc ? ` matching ${filterDesc}` : ""}. Baseline had ${baselineUuids.length} objects.`
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
return cy.wait(pollIntervalMs, { log: false }).then(() => {
|
|
1370
|
+
return cy.window({ log: false }).then((w) => {
|
|
1371
|
+
const bridgeState = getBridgeState(w);
|
|
1372
|
+
assertBridgeNotErrored(bridgeState);
|
|
1373
|
+
if (!bridgeState.exists || !bridgeState.ready) return poll();
|
|
1374
|
+
const bridge = resolveApiFromWindow(w);
|
|
1375
|
+
const snap = bridge.snapshot();
|
|
1376
|
+
const newObjects = [];
|
|
1377
|
+
const newUuids = [];
|
|
1378
|
+
function scanForNew(node) {
|
|
1379
|
+
if (!baselineSet.has(node.uuid)) {
|
|
1380
|
+
const typeMatch = !filterType || node.type === filterType;
|
|
1381
|
+
const nameMatch = !nameContains || node.name.includes(nameContains);
|
|
1382
|
+
if (typeMatch && nameMatch) {
|
|
1383
|
+
const meta = bridge.getByUuid(node.uuid);
|
|
1384
|
+
if (meta) {
|
|
1385
|
+
newObjects.push(meta);
|
|
1386
|
+
newUuids.push(node.uuid);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
for (const child of node.children) {
|
|
1391
|
+
scanForNew(child);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
scanForNew(snap.tree);
|
|
1395
|
+
if (newObjects.length > 0) {
|
|
1396
|
+
Cypress.log({
|
|
1397
|
+
name: "r3fWaitForNewObject",
|
|
1398
|
+
message: `${newObjects.length} new object(s) found`,
|
|
1399
|
+
consoleProps: () => ({ newObjects, newUuids })
|
|
1400
|
+
});
|
|
1401
|
+
return { newObjects, newUuids, count: newObjects.length };
|
|
1402
|
+
}
|
|
1403
|
+
return poll();
|
|
1404
|
+
});
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
return poll();
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
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
|
+
);
|
|
283
1461
|
}
|
|
284
1462
|
|
|
285
1463
|
// src/index.ts
|
|
286
1464
|
registerCommands();
|
|
287
1465
|
registerAssertions();
|
|
288
1466
|
registerWaiters();
|
|
1467
|
+
|
|
1468
|
+
exports.R3FReporter = R3FReporter;
|
|
1469
|
+
exports.diffSnapshots = diffSnapshots;
|
|
1470
|
+
exports.registerR3FTasks = registerR3FTasks;
|
|
289
1471
|
//# sourceMappingURL=index.cjs.map
|
|
290
1472
|
//# sourceMappingURL=index.cjs.map
|