@reconcrap/boss-recommend-mcp 2.1.23 → 2.1.25
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/README.md +5 -0
- package/bin/boss-recommend-mcp.js +4 -4
- package/package.json +6 -1
- package/scripts/install-macos.sh +280 -280
- package/scripts/postinstall.cjs +44 -44
- package/skills/boss-chat/README.md +43 -42
- package/skills/boss-chat/SKILL.md +107 -106
- package/skills/boss-recommend-pipeline/README.md +13 -13
- package/skills/boss-recommend-pipeline/SKILL.md +47 -47
- package/skills/boss-recruit-pipeline/README.md +19 -19
- package/skills/boss-recruit-pipeline/SKILL.md +89 -89
- package/src/chat-mcp.js +301 -127
- package/src/chat-runtime-config.js +7 -5
- package/src/core/boss-cards/index.js +199 -199
- package/src/core/browser/index.js +302 -145
- package/src/core/capture/index.js +2930 -1201
- package/src/core/cv-acquisition/index.js +512 -238
- package/src/core/cv-capture-target/index.js +513 -299
- package/src/core/greet-quota/index.js +71 -71
- package/src/core/infinite-list/index.js +31 -31
- package/src/core/reporting/legacy-csv.js +12 -12
- package/src/core/run/detached-launcher.js +305 -0
- package/src/core/run/index.js +109 -55
- package/src/core/run/timing.js +33 -33
- package/src/core/run/windows-detached-worker.ps1 +106 -0
- package/src/core/screening/index.js +2400 -2135
- package/src/core/self-heal/index.js +989 -973
- package/src/core/self-heal/viewport.js +1505 -564
- package/src/detached-worker.js +99 -99
- package/src/domains/chat/action-journal.js +536 -0
- package/src/domains/chat/cards.js +137 -137
- package/src/domains/chat/constants.js +9 -9
- package/src/domains/chat/detail.js +1684 -401
- package/src/domains/chat/index.js +8 -7
- package/src/domains/chat/jobs.js +620 -620
- package/src/domains/chat/page-guard.js +157 -122
- package/src/domains/chat/roots.js +56 -56
- package/src/domains/chat/run-service.js +1801 -760
- package/src/domains/common/account-rights-panel.js +314 -314
- package/src/domains/common/recovery-settle.js +159 -159
- package/src/domains/recommend/actions.js +1219 -472
- package/src/domains/recommend/cards.js +243 -243
- package/src/domains/recommend/colleague-contact.js +1079 -333
- package/src/domains/recommend/constants.js +92 -92
- package/src/domains/recommend/detail.js +4037 -136
- package/src/domains/recommend/filters.js +608 -590
- package/src/domains/recommend/index.js +9 -9
- package/src/domains/recommend/jobs.js +571 -542
- package/src/domains/recommend/location.js +754 -707
- package/src/domains/recommend/refresh.js +677 -392
- package/src/domains/recommend/roots.js +80 -80
- package/src/domains/recommend/run-service.js +4048 -1447
- package/src/domains/recommend/scopes.js +246 -246
- package/src/domains/recruit/actions.js +277 -277
- package/src/domains/recruit/cards.js +74 -74
- package/src/domains/recruit/constants.js +236 -236
- package/src/domains/recruit/detail.js +588 -588
- package/src/domains/recruit/index.js +9 -9
- package/src/domains/recruit/instruction-parser.js +866 -866
- package/src/domains/recruit/refresh.js +45 -45
- package/src/domains/recruit/roots.js +68 -68
- package/src/domains/recruit/run-service.js +1817 -1620
- package/src/domains/recruit/search.js +3229 -3229
- package/src/index.js +16 -1
- package/src/parser.js +1296 -1296
- package/src/recommend-mcp.js +1061 -450
- package/src/recommend-scheduler.js +75 -75
|
@@ -1,564 +1,1505 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getNodeBox,
|
|
3
|
-
querySelector,
|
|
4
|
-
sleep
|
|
5
|
-
} from "../browser/index.js";
|
|
6
|
-
|
|
7
|
-
export const VIEWPORT_COLLAPSE_RATIO_THRESHOLD = 0.6;
|
|
8
|
-
export const VIEWPORT_COLLAPSE_MIN_EXPECTED_WIDTH = 1000;
|
|
9
|
-
export const VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO = 0.85;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
const
|
|
328
|
-
const
|
|
329
|
-
?
|
|
330
|
-
:
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
export function
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
1
|
+
import {
|
|
2
|
+
getNodeBox,
|
|
3
|
+
querySelector,
|
|
4
|
+
sleep
|
|
5
|
+
} from "../browser/index.js";
|
|
6
|
+
|
|
7
|
+
export const VIEWPORT_COLLAPSE_RATIO_THRESHOLD = 0.6;
|
|
8
|
+
export const VIEWPORT_COLLAPSE_MIN_EXPECTED_WIDTH = 1000;
|
|
9
|
+
export const VIEWPORT_COLLAPSE_NEAR_FULLSCREEN_RATIO = 0.85;
|
|
10
|
+
|
|
11
|
+
// Desktop visual viewports are stable to roughly a pixel. A small absolute
|
|
12
|
+
// floor filters rounding noise while the ratio catches cumulative losses long
|
|
13
|
+
// before they grow into the old catastrophic-collapse thresholds.
|
|
14
|
+
const VIEWPORT_BASELINE_MIN_RATIO = 0.995;
|
|
15
|
+
const VIEWPORT_BASELINE_MIN_LOSS_PX = 4;
|
|
16
|
+
const WINDOW_BOUNDS_CHANGE_TOLERANCE_PX = 2;
|
|
17
|
+
|
|
18
|
+
const ABSOLUTE_COLLAPSE_LIMITS = Object.freeze({
|
|
19
|
+
clientHeight: 260,
|
|
20
|
+
clientWidth: 280,
|
|
21
|
+
frameHeight: 320,
|
|
22
|
+
frameWidth: 460,
|
|
23
|
+
viewportHeight: 260,
|
|
24
|
+
viewportWidth: 360
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function normalizeText(value) {
|
|
28
|
+
return String(value ?? "").replace(/\s+/g, " ").trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getPositiveNumber(...values) {
|
|
32
|
+
for (const value of values) {
|
|
33
|
+
const number = Number(value);
|
|
34
|
+
if (Number.isFinite(number) && number > 0) return number;
|
|
35
|
+
}
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function rootNodeId(roots = {}, name) {
|
|
40
|
+
for (const root of [roots?.[name], roots?.rootNodes?.[name], roots?.roots?.[name]]) {
|
|
41
|
+
if (typeof root === "number" && root > 0) return root;
|
|
42
|
+
if (root?.nodeId) return root.nodeId;
|
|
43
|
+
if (root?.documentNodeId) return root.documentNodeId;
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getFiniteNumber(value) {
|
|
49
|
+
if (value === null || value === undefined || value === "") return null;
|
|
50
|
+
const number = Number(value);
|
|
51
|
+
return Number.isFinite(number) ? number : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function compactRect(rect = {}) {
|
|
55
|
+
return {
|
|
56
|
+
width: getPositiveNumber(rect.width),
|
|
57
|
+
height: getPositiveNumber(rect.height)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function pickViewportSize(layoutMetrics = {}, axis = "width") {
|
|
62
|
+
const clientKey = axis === "width" ? "clientWidth" : "clientHeight";
|
|
63
|
+
return getPositiveNumber(
|
|
64
|
+
layoutMetrics?.cssVisualViewport?.[clientKey],
|
|
65
|
+
layoutMetrics?.cssLayoutViewport?.[clientKey],
|
|
66
|
+
layoutMetrics?.visualViewport?.[clientKey],
|
|
67
|
+
layoutMetrics?.layoutViewport?.[clientKey]
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const BASELINE_DIMENSIONS = Object.freeze([
|
|
72
|
+
["clientWidth", (state) => state?.clientWidth],
|
|
73
|
+
["clientHeight", (state) => state?.clientHeight],
|
|
74
|
+
["frameWidth", (state) => state?.frameRect?.width],
|
|
75
|
+
["frameHeight", (state) => state?.frameRect?.height],
|
|
76
|
+
["viewportWidth", (state) => state?.viewport?.width],
|
|
77
|
+
["viewportHeight", (state) => state?.viewport?.height]
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
function readDimensionSnapshot(state = {}) {
|
|
81
|
+
return Object.fromEntries(BASELINE_DIMENSIONS.map(([name, read]) => [
|
|
82
|
+
name,
|
|
83
|
+
getPositiveNumber(read(state))
|
|
84
|
+
]));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readWindowSnapshot(state = {}) {
|
|
88
|
+
const info = state?.windowInfo || {};
|
|
89
|
+
const bounds = info?.bounds || {};
|
|
90
|
+
return {
|
|
91
|
+
ok: Boolean(info.ok && info.windowId != null && getPositiveNumber(bounds.width) && getPositiveNumber(bounds.height)),
|
|
92
|
+
windowId: info.windowId ?? null,
|
|
93
|
+
left: getFiniteNumber(bounds.left),
|
|
94
|
+
top: getFiniteNumber(bounds.top),
|
|
95
|
+
width: getPositiveNumber(bounds.width),
|
|
96
|
+
height: getPositiveNumber(bounds.height),
|
|
97
|
+
windowState: normalizeText(bounds.windowState || "").toLowerCase() || null
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function createViewportBaseline(state, {
|
|
102
|
+
reason = "initial_healthy_viewport",
|
|
103
|
+
stableSamples = 1,
|
|
104
|
+
establishedAt = new Date().toISOString()
|
|
105
|
+
} = {}) {
|
|
106
|
+
return {
|
|
107
|
+
version: 1,
|
|
108
|
+
establishedAt,
|
|
109
|
+
reason,
|
|
110
|
+
stableSamples: Math.max(1, Number(stableSamples) || 1),
|
|
111
|
+
dimensions: readDimensionSnapshot(state),
|
|
112
|
+
window: readWindowSnapshot(state)
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function mergeStableBaselineSamples(firstState, secondState, options = {}) {
|
|
117
|
+
const baseline = createViewportBaseline(firstState, {
|
|
118
|
+
...options,
|
|
119
|
+
stableSamples: 2
|
|
120
|
+
});
|
|
121
|
+
const second = readDimensionSnapshot(secondState);
|
|
122
|
+
for (const [name] of BASELINE_DIMENSIONS) {
|
|
123
|
+
baseline.dimensions[name] = Math.max(
|
|
124
|
+
getPositiveNumber(baseline.dimensions[name]),
|
|
125
|
+
getPositiveNumber(second[name])
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
baseline.window = readWindowSnapshot(secondState);
|
|
129
|
+
return baseline;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function compareViewportToBaseline(state, baseline) {
|
|
133
|
+
if (!baseline?.dimensions) {
|
|
134
|
+
return {
|
|
135
|
+
available: false,
|
|
136
|
+
drifted: false,
|
|
137
|
+
dimensions: {}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const current = readDimensionSnapshot(state);
|
|
142
|
+
const dimensions = {};
|
|
143
|
+
const driftedDimensions = [];
|
|
144
|
+
const missingDimensions = [];
|
|
145
|
+
for (const [name] of BASELINE_DIMENSIONS) {
|
|
146
|
+
const expected = getPositiveNumber(baseline.dimensions[name]);
|
|
147
|
+
const actual = getPositiveNumber(current[name]);
|
|
148
|
+
const missing = expected > 0 && actual <= 0;
|
|
149
|
+
const lossPx = expected > 0 && actual > 0
|
|
150
|
+
? Math.max(0, expected - actual)
|
|
151
|
+
: expected > 0
|
|
152
|
+
? expected
|
|
153
|
+
: 0;
|
|
154
|
+
const ratio = expected > 0 && actual > 0 ? actual / expected : null;
|
|
155
|
+
const drifted = Boolean(
|
|
156
|
+
missing
|
|
157
|
+
|| (
|
|
158
|
+
lossPx > VIEWPORT_BASELINE_MIN_LOSS_PX
|
|
159
|
+
&& ratio !== null
|
|
160
|
+
&& ratio < VIEWPORT_BASELINE_MIN_RATIO
|
|
161
|
+
)
|
|
162
|
+
);
|
|
163
|
+
dimensions[name] = {
|
|
164
|
+
expected,
|
|
165
|
+
actual,
|
|
166
|
+
lossPx,
|
|
167
|
+
ratio,
|
|
168
|
+
missing,
|
|
169
|
+
drifted
|
|
170
|
+
};
|
|
171
|
+
if (missing) missingDimensions.push(name);
|
|
172
|
+
if (drifted) driftedDimensions.push(name);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
available: true,
|
|
177
|
+
thresholdRatio: VIEWPORT_BASELINE_MIN_RATIO,
|
|
178
|
+
minLossPx: VIEWPORT_BASELINE_MIN_LOSS_PX,
|
|
179
|
+
drifted: driftedDimensions.length > 0,
|
|
180
|
+
driftedDimensions,
|
|
181
|
+
missingDimensions,
|
|
182
|
+
dimensions
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function compareWindowToBaseline(state, baseline) {
|
|
187
|
+
const expected = baseline?.window || null;
|
|
188
|
+
const actual = readWindowSnapshot(state);
|
|
189
|
+
if (!expected?.ok || !actual.ok) {
|
|
190
|
+
return {
|
|
191
|
+
verified: false,
|
|
192
|
+
changed: false,
|
|
193
|
+
reason: "window_bounds_unreadable",
|
|
194
|
+
expected,
|
|
195
|
+
actual
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (expected.windowId !== actual.windowId) {
|
|
199
|
+
return {
|
|
200
|
+
verified: false,
|
|
201
|
+
changed: false,
|
|
202
|
+
reason: "window_id_changed",
|
|
203
|
+
expected,
|
|
204
|
+
actual
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const widthDelta = actual.width - expected.width;
|
|
208
|
+
const heightDelta = actual.height - expected.height;
|
|
209
|
+
const changed = Boolean(
|
|
210
|
+
Math.abs(widthDelta) > WINDOW_BOUNDS_CHANGE_TOLERANCE_PX
|
|
211
|
+
|| Math.abs(heightDelta) > WINDOW_BOUNDS_CHANGE_TOLERANCE_PX
|
|
212
|
+
);
|
|
213
|
+
return {
|
|
214
|
+
verified: true,
|
|
215
|
+
changed,
|
|
216
|
+
reason: changed ? "verified_browser_bounds_change" : "browser_bounds_unchanged",
|
|
217
|
+
widthDelta,
|
|
218
|
+
heightDelta,
|
|
219
|
+
stateChanged: expected.windowState !== actual.windowState,
|
|
220
|
+
expected,
|
|
221
|
+
actual
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function getLayoutMetrics(client) {
|
|
226
|
+
if (typeof client?.Page?.getLayoutMetrics !== "function") return null;
|
|
227
|
+
try {
|
|
228
|
+
return await client.Page.getLayoutMetrics();
|
|
229
|
+
} catch {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function getCurrentWindowInfo(client) {
|
|
235
|
+
if (typeof client?.Browser?.getWindowForTarget !== "function") {
|
|
236
|
+
return {
|
|
237
|
+
ok: false,
|
|
238
|
+
unsupported: true,
|
|
239
|
+
error: "Browser.getWindowForTarget is not available"
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
const targetWindow = await client.Browser.getWindowForTarget({});
|
|
245
|
+
let bounds = targetWindow?.bounds || null;
|
|
246
|
+
if (
|
|
247
|
+
targetWindow?.windowId != null
|
|
248
|
+
&& typeof client?.Browser?.getWindowBounds === "function"
|
|
249
|
+
) {
|
|
250
|
+
const currentBounds = await client.Browser.getWindowBounds({
|
|
251
|
+
windowId: targetWindow.windowId
|
|
252
|
+
});
|
|
253
|
+
bounds = currentBounds?.bounds || bounds;
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
ok: true,
|
|
257
|
+
windowId: targetWindow?.windowId ?? null,
|
|
258
|
+
bounds
|
|
259
|
+
};
|
|
260
|
+
} catch (error) {
|
|
261
|
+
return {
|
|
262
|
+
ok: false,
|
|
263
|
+
error: error?.message || String(error)
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function readBox(client, nodeId) {
|
|
269
|
+
if (!nodeId) return null;
|
|
270
|
+
try {
|
|
271
|
+
return await getNodeBox(client, nodeId);
|
|
272
|
+
} catch {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function readBestContentBox(client, rootNodeIdValue) {
|
|
278
|
+
const directBox = await readBox(client, rootNodeIdValue);
|
|
279
|
+
if (directBox?.rect?.width && directBox?.rect?.height) return directBox;
|
|
280
|
+
|
|
281
|
+
for (const selector of ["body", "html"]) {
|
|
282
|
+
const nodeId = await querySelector(client, rootNodeIdValue, selector).catch(() => 0);
|
|
283
|
+
const box = await readBox(client, nodeId);
|
|
284
|
+
if (box?.rect?.width && box?.rect?.height) return box;
|
|
285
|
+
}
|
|
286
|
+
return directBox;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function buildViewportHealthDiagnostics(state, windowInfo = null, layoutMetrics = null) {
|
|
290
|
+
const topViewport = state?.topViewport || {};
|
|
291
|
+
const bounds = windowInfo?.bounds || null;
|
|
292
|
+
const windowState = normalizeText(bounds?.windowState || "").toLowerCase() || null;
|
|
293
|
+
const browserWindowWidth = getPositiveNumber(
|
|
294
|
+
bounds?.width,
|
|
295
|
+
topViewport.browserWindowWidth,
|
|
296
|
+
topViewport.outerWidth
|
|
297
|
+
);
|
|
298
|
+
const browserWindowHeight = getPositiveNumber(
|
|
299
|
+
bounds?.height,
|
|
300
|
+
topViewport.browserWindowHeight,
|
|
301
|
+
topViewport.outerHeight
|
|
302
|
+
);
|
|
303
|
+
const topOuterWidth = getPositiveNumber(topViewport.outerWidth);
|
|
304
|
+
const actualWidth = getPositiveNumber(
|
|
305
|
+
layoutMetrics?.cssVisualViewport?.clientWidth,
|
|
306
|
+
layoutMetrics?.cssLayoutViewport?.clientWidth,
|
|
307
|
+
topViewport.visualWidth,
|
|
308
|
+
topViewport.innerWidth,
|
|
309
|
+
state?.viewport?.width,
|
|
310
|
+
state?.clientWidth,
|
|
311
|
+
state?.frameRect?.width
|
|
312
|
+
);
|
|
313
|
+
const actualHeight = getPositiveNumber(
|
|
314
|
+
layoutMetrics?.cssVisualViewport?.clientHeight,
|
|
315
|
+
layoutMetrics?.cssLayoutViewport?.clientHeight,
|
|
316
|
+
topViewport.visualHeight,
|
|
317
|
+
topViewport.innerHeight,
|
|
318
|
+
state?.viewport?.height,
|
|
319
|
+
state?.clientHeight,
|
|
320
|
+
state?.frameRect?.height
|
|
321
|
+
);
|
|
322
|
+
const nearFullscreen = Boolean(
|
|
323
|
+
windowState === "maximized"
|
|
324
|
+
|| windowState === "fullscreen"
|
|
325
|
+
);
|
|
326
|
+
const expectedWidth = getPositiveNumber(browserWindowWidth, topOuterWidth);
|
|
327
|
+
const expectedHeight = getPositiveNumber(browserWindowHeight, topViewport.outerHeight);
|
|
328
|
+
const widthRatio = actualWidth > 0 && expectedWidth > 0
|
|
329
|
+
? actualWidth / expectedWidth
|
|
330
|
+
: null;
|
|
331
|
+
const heightRatio = actualHeight > 0 && expectedHeight > 0
|
|
332
|
+
? actualHeight / expectedHeight
|
|
333
|
+
: null;
|
|
334
|
+
const relativeWidthCollapsed = Boolean(
|
|
335
|
+
expectedWidth >= VIEWPORT_COLLAPSE_MIN_EXPECTED_WIDTH
|
|
336
|
+
&& actualWidth > 0
|
|
337
|
+
&& widthRatio !== null
|
|
338
|
+
&& widthRatio <= VIEWPORT_COLLAPSE_RATIO_THRESHOLD
|
|
339
|
+
);
|
|
340
|
+
const relativeCollapsed = relativeWidthCollapsed;
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
threshold: VIEWPORT_COLLAPSE_RATIO_THRESHOLD,
|
|
344
|
+
minExpectedWidth: VIEWPORT_COLLAPSE_MIN_EXPECTED_WIDTH,
|
|
345
|
+
nearFullscreen,
|
|
346
|
+
windowState,
|
|
347
|
+
browserWindowWidth,
|
|
348
|
+
browserWindowHeight,
|
|
349
|
+
topOuterWidth,
|
|
350
|
+
actualWidth,
|
|
351
|
+
actualHeight,
|
|
352
|
+
expectedWidth,
|
|
353
|
+
expectedHeight,
|
|
354
|
+
widthRatio,
|
|
355
|
+
heightRatio,
|
|
356
|
+
relativeWidthCollapsed,
|
|
357
|
+
relativeCollapsed
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function isListViewportCollapsed(state) {
|
|
362
|
+
if (!state?.ok) return false;
|
|
363
|
+
if (state.viewportDiagnostics?.relativeCollapsed === true) return true;
|
|
364
|
+
const clientHeight = Number(state.clientHeight || 0);
|
|
365
|
+
const clientWidth = Number(state.clientWidth || 0);
|
|
366
|
+
const frameWidth = Number(state.frameRect?.width || 0);
|
|
367
|
+
const frameHeight = Number(state.frameRect?.height || 0);
|
|
368
|
+
const viewportWidth = Number(state.viewport?.width || 0);
|
|
369
|
+
const viewportHeight = Number(state.viewport?.height || 0);
|
|
370
|
+
|
|
371
|
+
return (
|
|
372
|
+
(clientHeight > 0 && clientHeight < ABSOLUTE_COLLAPSE_LIMITS.clientHeight)
|
|
373
|
+
|| (clientWidth > 0 && clientWidth < ABSOLUTE_COLLAPSE_LIMITS.clientWidth)
|
|
374
|
+
|| (frameHeight > 0 && frameHeight < ABSOLUTE_COLLAPSE_LIMITS.frameHeight)
|
|
375
|
+
|| (frameWidth > 0 && frameWidth < ABSOLUTE_COLLAPSE_LIMITS.frameWidth)
|
|
376
|
+
|| (viewportHeight > 0 && viewportHeight < ABSOLUTE_COLLAPSE_LIMITS.viewportHeight)
|
|
377
|
+
|| (viewportWidth > 0 && viewportWidth < ABSOLUTE_COLLAPSE_LIMITS.viewportWidth)
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export async function readViewportState(client, {
|
|
382
|
+
roots = {},
|
|
383
|
+
root = "frame",
|
|
384
|
+
frameOwnerRoot = "frameOwner"
|
|
385
|
+
} = {}) {
|
|
386
|
+
const targetRootNodeId = rootNodeId(roots, root);
|
|
387
|
+
if (!targetRootNodeId) {
|
|
388
|
+
return {
|
|
389
|
+
ok: false,
|
|
390
|
+
root,
|
|
391
|
+
error: `Root not found: ${root}`
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const layoutMetrics = await getLayoutMetrics(client);
|
|
396
|
+
const windowInfo = await getCurrentWindowInfo(client);
|
|
397
|
+
const contentBox = await readBestContentBox(client, targetRootNodeId);
|
|
398
|
+
const ownerNodeId = rootNodeId(roots, frameOwnerRoot);
|
|
399
|
+
const ownerBox = ownerNodeId ? await readBox(client, ownerNodeId) : null;
|
|
400
|
+
const contentRectReadable = Boolean(
|
|
401
|
+
getPositiveNumber(contentBox?.rect?.width)
|
|
402
|
+
&& getPositiveNumber(contentBox?.rect?.height)
|
|
403
|
+
);
|
|
404
|
+
const ownerRectReadable = Boolean(
|
|
405
|
+
!ownerNodeId
|
|
406
|
+
|| (
|
|
407
|
+
getPositiveNumber(ownerBox?.rect?.width)
|
|
408
|
+
&& getPositiveNumber(ownerBox?.rect?.height)
|
|
409
|
+
)
|
|
410
|
+
);
|
|
411
|
+
const measurementEvidence = {
|
|
412
|
+
targetRootNodeId,
|
|
413
|
+
contentRectReadable,
|
|
414
|
+
frameOwnerNodeId: ownerNodeId || null,
|
|
415
|
+
ownerRectReadable,
|
|
416
|
+
layoutMetricsReadable: Boolean(layoutMetrics),
|
|
417
|
+
browserWindowReadable: Boolean(windowInfo?.ok)
|
|
418
|
+
};
|
|
419
|
+
if (!contentRectReadable || !ownerRectReadable) {
|
|
420
|
+
return {
|
|
421
|
+
ok: false,
|
|
422
|
+
root,
|
|
423
|
+
rootNodeId: targetRootNodeId,
|
|
424
|
+
frameOwnerRoot,
|
|
425
|
+
frameOwnerNodeId: ownerNodeId || null,
|
|
426
|
+
windowInfo,
|
|
427
|
+
measurementEvidence,
|
|
428
|
+
error: !contentRectReadable
|
|
429
|
+
? `Viewport root geometry is unreadable: ${root}`
|
|
430
|
+
: `Viewport frame-owner geometry is unreadable: ${frameOwnerRoot}`
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const frameRect = compactRect(ownerBox?.rect || contentBox?.rect || {});
|
|
434
|
+
const clientWidth = getPositiveNumber(
|
|
435
|
+
contentBox?.rect?.width,
|
|
436
|
+
frameRect.width,
|
|
437
|
+
pickViewportSize(layoutMetrics, "width")
|
|
438
|
+
);
|
|
439
|
+
const clientHeight = getPositiveNumber(
|
|
440
|
+
contentBox?.rect?.height,
|
|
441
|
+
frameRect.height,
|
|
442
|
+
pickViewportSize(layoutMetrics, "height")
|
|
443
|
+
);
|
|
444
|
+
const viewportWidth = pickViewportSize(layoutMetrics, "width") || clientWidth;
|
|
445
|
+
const viewportHeight = pickViewportSize(layoutMetrics, "height") || clientHeight;
|
|
446
|
+
const bounds = windowInfo?.bounds || {};
|
|
447
|
+
const topViewport = {
|
|
448
|
+
innerWidth: viewportWidth,
|
|
449
|
+
innerHeight: viewportHeight,
|
|
450
|
+
outerWidth: getPositiveNumber(bounds.width, viewportWidth),
|
|
451
|
+
outerHeight: getPositiveNumber(bounds.height, viewportHeight),
|
|
452
|
+
visualWidth: getPositiveNumber(layoutMetrics?.cssVisualViewport?.clientWidth, viewportWidth),
|
|
453
|
+
visualHeight: getPositiveNumber(layoutMetrics?.cssVisualViewport?.clientHeight, viewportHeight),
|
|
454
|
+
browserWindowWidth: getPositiveNumber(bounds.width),
|
|
455
|
+
browserWindowHeight: getPositiveNumber(bounds.height),
|
|
456
|
+
visualViewportScale: getPositiveNumber(layoutMetrics?.cssVisualViewport?.scale, 1)
|
|
457
|
+
};
|
|
458
|
+
const state = {
|
|
459
|
+
ok: true,
|
|
460
|
+
root,
|
|
461
|
+
rootNodeId: targetRootNodeId,
|
|
462
|
+
frameOwnerRoot,
|
|
463
|
+
frameOwnerNodeId: ownerNodeId || null,
|
|
464
|
+
clientWidth,
|
|
465
|
+
clientHeight,
|
|
466
|
+
frameRect,
|
|
467
|
+
viewport: {
|
|
468
|
+
width: viewportWidth,
|
|
469
|
+
height: viewportHeight
|
|
470
|
+
},
|
|
471
|
+
topViewport,
|
|
472
|
+
windowInfo,
|
|
473
|
+
measurementEvidence
|
|
474
|
+
};
|
|
475
|
+
state.viewportDiagnostics = buildViewportHealthDiagnostics(state, windowInfo, layoutMetrics);
|
|
476
|
+
state.collapsed = isListViewportCollapsed(state);
|
|
477
|
+
return state;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export async function setWindowStateIfPossible(client, windowState, reason = "viewport_recovery") {
|
|
481
|
+
const windowInfo = await getCurrentWindowInfo(client);
|
|
482
|
+
if (!windowInfo.ok || windowInfo.windowId == null || typeof client?.Browser?.setWindowBounds !== "function") {
|
|
483
|
+
return {
|
|
484
|
+
ok: false,
|
|
485
|
+
reason,
|
|
486
|
+
windowState,
|
|
487
|
+
error: windowInfo.error || "Browser.setWindowBounds is not available"
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
await client.Browser.setWindowBounds({
|
|
493
|
+
windowId: windowInfo.windowId,
|
|
494
|
+
bounds: {
|
|
495
|
+
windowState
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
return {
|
|
499
|
+
ok: true,
|
|
500
|
+
reason,
|
|
501
|
+
windowState,
|
|
502
|
+
windowId: windowInfo.windowId,
|
|
503
|
+
before: windowInfo.bounds || null
|
|
504
|
+
};
|
|
505
|
+
} catch (error) {
|
|
506
|
+
return {
|
|
507
|
+
ok: false,
|
|
508
|
+
reason,
|
|
509
|
+
windowState,
|
|
510
|
+
windowId: windowInfo.windowId,
|
|
511
|
+
error: error?.message || String(error)
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function captureRestorableWindowSnapshot(windowInfo = {}) {
|
|
517
|
+
const bounds = windowInfo?.bounds || {};
|
|
518
|
+
const windowState = normalizeText(bounds.windowState || "").toLowerCase();
|
|
519
|
+
const width = getPositiveNumber(bounds.width);
|
|
520
|
+
const height = getPositiveNumber(bounds.height);
|
|
521
|
+
const restorableState = ["normal", "maximized", "fullscreen"].includes(windowState);
|
|
522
|
+
return {
|
|
523
|
+
ok: Boolean(
|
|
524
|
+
windowInfo?.ok
|
|
525
|
+
&& windowInfo.windowId != null
|
|
526
|
+
&& restorableState
|
|
527
|
+
&& width
|
|
528
|
+
&& height
|
|
529
|
+
),
|
|
530
|
+
windowId: windowInfo?.windowId ?? null,
|
|
531
|
+
windowState: restorableState ? windowState : null,
|
|
532
|
+
left: getFiniteNumber(bounds.left),
|
|
533
|
+
top: getFiniteNumber(bounds.top),
|
|
534
|
+
width,
|
|
535
|
+
height
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function buildOriginalWindowRestoreBounds(snapshot = {}) {
|
|
540
|
+
const bounds = {
|
|
541
|
+
windowState: snapshot.windowState
|
|
542
|
+
};
|
|
543
|
+
if (snapshot.windowState === "normal") {
|
|
544
|
+
if (snapshot.left !== null) bounds.left = snapshot.left;
|
|
545
|
+
if (snapshot.top !== null) bounds.top = snapshot.top;
|
|
546
|
+
bounds.width = snapshot.width;
|
|
547
|
+
bounds.height = snapshot.height;
|
|
548
|
+
}
|
|
549
|
+
return bounds;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function verifyOriginalWindowRestoration(original, currentInfo) {
|
|
553
|
+
const actual = captureRestorableWindowSnapshot(currentInfo);
|
|
554
|
+
if (!original?.ok || !actual.ok) {
|
|
555
|
+
return {
|
|
556
|
+
verified: false,
|
|
557
|
+
reason: "window_restoration_readback_unavailable",
|
|
558
|
+
expected: original || null,
|
|
559
|
+
actual
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
if (actual.windowId !== original.windowId) {
|
|
563
|
+
return {
|
|
564
|
+
verified: false,
|
|
565
|
+
reason: "window_restoration_target_changed",
|
|
566
|
+
expected: original,
|
|
567
|
+
actual
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const deltas = {
|
|
572
|
+
left: original.left !== null && actual.left !== null ? actual.left - original.left : null,
|
|
573
|
+
top: original.top !== null && actual.top !== null ? actual.top - original.top : null,
|
|
574
|
+
width: actual.width - original.width,
|
|
575
|
+
height: actual.height - original.height
|
|
576
|
+
};
|
|
577
|
+
const stateMatches = actual.windowState === original.windowState;
|
|
578
|
+
const positionMatches = Boolean(
|
|
579
|
+
(
|
|
580
|
+
original.left === null
|
|
581
|
+
|| (actual.left !== null && Math.abs(deltas.left) <= WINDOW_BOUNDS_CHANGE_TOLERANCE_PX)
|
|
582
|
+
)
|
|
583
|
+
&& (
|
|
584
|
+
original.top === null
|
|
585
|
+
|| (actual.top !== null && Math.abs(deltas.top) <= WINDOW_BOUNDS_CHANGE_TOLERANCE_PX)
|
|
586
|
+
)
|
|
587
|
+
);
|
|
588
|
+
const sizeMatches = Boolean(
|
|
589
|
+
Math.abs(deltas.width) <= WINDOW_BOUNDS_CHANGE_TOLERANCE_PX
|
|
590
|
+
&& Math.abs(deltas.height) <= WINDOW_BOUNDS_CHANGE_TOLERANCE_PX
|
|
591
|
+
);
|
|
592
|
+
const verified = stateMatches && positionMatches && sizeMatches;
|
|
593
|
+
return {
|
|
594
|
+
verified,
|
|
595
|
+
reason: verified
|
|
596
|
+
? "original_window_state_and_bounds_verified"
|
|
597
|
+
: !stateMatches
|
|
598
|
+
? "original_window_state_not_restored"
|
|
599
|
+
: !positionMatches
|
|
600
|
+
? "original_window_position_not_restored"
|
|
601
|
+
: "original_window_size_not_restored",
|
|
602
|
+
stateMatches,
|
|
603
|
+
positionMatches,
|
|
604
|
+
sizeMatches,
|
|
605
|
+
tolerancePx: WINDOW_BOUNDS_CHANGE_TOLERANCE_PX,
|
|
606
|
+
deltas,
|
|
607
|
+
expected: original,
|
|
608
|
+
actual
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async function setExactWindowBounds(client, {
|
|
613
|
+
windowId,
|
|
614
|
+
bounds,
|
|
615
|
+
reason,
|
|
616
|
+
step
|
|
617
|
+
}) {
|
|
618
|
+
if (windowId == null || typeof client?.Browser?.setWindowBounds !== "function") {
|
|
619
|
+
return {
|
|
620
|
+
ok: false,
|
|
621
|
+
reason,
|
|
622
|
+
step,
|
|
623
|
+
windowId: windowId ?? null,
|
|
624
|
+
bounds,
|
|
625
|
+
windowState: bounds?.windowState || null,
|
|
626
|
+
error: "Browser.setWindowBounds is not available"
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
try {
|
|
630
|
+
await client.Browser.setWindowBounds({ windowId, bounds });
|
|
631
|
+
return {
|
|
632
|
+
ok: true,
|
|
633
|
+
reason,
|
|
634
|
+
step,
|
|
635
|
+
windowId,
|
|
636
|
+
bounds,
|
|
637
|
+
windowState: bounds?.windowState || null
|
|
638
|
+
};
|
|
639
|
+
} catch (error) {
|
|
640
|
+
return {
|
|
641
|
+
ok: false,
|
|
642
|
+
reason,
|
|
643
|
+
step,
|
|
644
|
+
windowId,
|
|
645
|
+
bounds,
|
|
646
|
+
windowState: bounds?.windowState || null,
|
|
647
|
+
error: error?.message || String(error)
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
export async function toggleWindowStateForViewportRecovery(client, {
|
|
653
|
+
reason = "viewport_recovery",
|
|
654
|
+
settleMs = 520,
|
|
655
|
+
bringToFront = true
|
|
656
|
+
} = {}) {
|
|
657
|
+
const currentInfo = await getCurrentWindowInfo(client);
|
|
658
|
+
const original = captureRestorableWindowSnapshot(currentInfo);
|
|
659
|
+
const currentState = original.windowState;
|
|
660
|
+
const sequence = currentState === "normal"
|
|
661
|
+
? ["maximized", "normal"]
|
|
662
|
+
: ["normal", currentState].filter(Boolean);
|
|
663
|
+
const attempts = [];
|
|
664
|
+
|
|
665
|
+
if (original.ok) {
|
|
666
|
+
const perturb = await setExactWindowBounds(client, {
|
|
667
|
+
windowId: original.windowId,
|
|
668
|
+
bounds: { windowState: sequence[0] },
|
|
669
|
+
reason,
|
|
670
|
+
step: "perturb_window_state"
|
|
671
|
+
});
|
|
672
|
+
attempts.push(perturb);
|
|
673
|
+
if (perturb.ok && settleMs > 0) await sleep(settleMs);
|
|
674
|
+
|
|
675
|
+
const restore = await setExactWindowBounds(client, {
|
|
676
|
+
windowId: original.windowId,
|
|
677
|
+
bounds: buildOriginalWindowRestoreBounds(original),
|
|
678
|
+
reason,
|
|
679
|
+
step: "restore_original_window"
|
|
680
|
+
});
|
|
681
|
+
attempts.push(restore);
|
|
682
|
+
if (restore.ok && settleMs > 0) await sleep(settleMs);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (bringToFront && typeof client?.Page?.bringToFront === "function") {
|
|
686
|
+
await client.Page.bringToFront();
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const restoredInfo = original.ok ? await getCurrentWindowInfo(client) : null;
|
|
690
|
+
const restoration = verifyOriginalWindowRestoration(original, restoredInfo);
|
|
691
|
+
const applied = attempts.some((attempt) => attempt.ok);
|
|
692
|
+
|
|
693
|
+
return {
|
|
694
|
+
ok: Boolean(applied && restoration.verified),
|
|
695
|
+
applied,
|
|
696
|
+
reason,
|
|
697
|
+
current_state: currentState || null,
|
|
698
|
+
restored_state: sequence[sequence.length - 1] || null,
|
|
699
|
+
original_window: original,
|
|
700
|
+
restored_window: captureRestorableWindowSnapshot(restoredInfo || {}),
|
|
701
|
+
original_state_restored: Boolean(restoration.verified),
|
|
702
|
+
restoration,
|
|
703
|
+
sequence,
|
|
704
|
+
attempts,
|
|
705
|
+
error: original.ok
|
|
706
|
+
? restoration.verified
|
|
707
|
+
? null
|
|
708
|
+
: restoration.reason
|
|
709
|
+
: "original window state and bounds are unreadable"
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
export function compactViewportState(state = null) {
|
|
714
|
+
if (!state) return null;
|
|
715
|
+
return {
|
|
716
|
+
ok: Boolean(state.ok),
|
|
717
|
+
root: state.root || null,
|
|
718
|
+
error: state.error || null,
|
|
719
|
+
clientWidth: state.clientWidth || 0,
|
|
720
|
+
clientHeight: state.clientHeight || 0,
|
|
721
|
+
frameRect: state.frameRect || null,
|
|
722
|
+
viewport: state.viewport || null,
|
|
723
|
+
topViewport: state.topViewport
|
|
724
|
+
? {
|
|
725
|
+
innerWidth: state.topViewport.innerWidth || 0,
|
|
726
|
+
innerHeight: state.topViewport.innerHeight || 0,
|
|
727
|
+
outerWidth: state.topViewport.outerWidth || 0,
|
|
728
|
+
outerHeight: state.topViewport.outerHeight || 0,
|
|
729
|
+
visualWidth: state.topViewport.visualWidth || 0,
|
|
730
|
+
visualHeight: state.topViewport.visualHeight || 0,
|
|
731
|
+
browserWindowWidth: state.topViewport.browserWindowWidth || 0,
|
|
732
|
+
browserWindowHeight: state.topViewport.browserWindowHeight || 0,
|
|
733
|
+
visualViewportScale: state.topViewport.visualViewportScale || 0
|
|
734
|
+
}
|
|
735
|
+
: null,
|
|
736
|
+
measurementEvidence: state.measurementEvidence || null,
|
|
737
|
+
viewportDiagnostics: state.viewportDiagnostics || null,
|
|
738
|
+
baselineComparison: state.baselineComparison || null,
|
|
739
|
+
collapseEvidence: state.collapseEvidence || null,
|
|
740
|
+
collapsed: Boolean(state.collapsed)
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function compactViewportBaseline(baseline = null) {
|
|
745
|
+
if (!baseline) return null;
|
|
746
|
+
return {
|
|
747
|
+
version: baseline.version || 1,
|
|
748
|
+
establishedAt: baseline.establishedAt || null,
|
|
749
|
+
reason: baseline.reason || null,
|
|
750
|
+
stableSamples: Number(baseline.stableSamples || 0),
|
|
751
|
+
dimensions: baseline.dimensions || null,
|
|
752
|
+
window: baseline.window || null
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export function compactViewportHealthResult(result = null) {
|
|
757
|
+
if (!result) return null;
|
|
758
|
+
return {
|
|
759
|
+
ok: Boolean(result.ok),
|
|
760
|
+
collapsed: Boolean(result.collapsed),
|
|
761
|
+
recovered: Boolean(result.recovered),
|
|
762
|
+
recoveryMode: result.recoveryMode || null,
|
|
763
|
+
reason: result.reason || null,
|
|
764
|
+
baselineEstablished: Boolean(result.baselineEstablished),
|
|
765
|
+
rebaselined: Boolean(result.rebaselined),
|
|
766
|
+
baseline: compactViewportBaseline(result.baseline),
|
|
767
|
+
baselineComparison: result.baselineComparison || null,
|
|
768
|
+
windowBoundsChange: result.windowBoundsChange || null,
|
|
769
|
+
stability: result.stability || null,
|
|
770
|
+
state: compactViewportState(result.state),
|
|
771
|
+
before: compactViewportState(result.before),
|
|
772
|
+
repair: result.repair
|
|
773
|
+
? {
|
|
774
|
+
ok: Boolean(result.repair.ok),
|
|
775
|
+
applied: Boolean(result.repair.applied),
|
|
776
|
+
current_state: result.repair.current_state || null,
|
|
777
|
+
restored_state: result.repair.restored_state || null,
|
|
778
|
+
original_state_restored: Boolean(result.repair.original_state_restored),
|
|
779
|
+
original_window: result.repair.original_window || null,
|
|
780
|
+
restored_window: result.repair.restored_window || null,
|
|
781
|
+
restoration: result.repair.restoration || null,
|
|
782
|
+
sequence: result.repair.sequence || [],
|
|
783
|
+
attempts: (result.repair.attempts || []).map((attempt) => ({
|
|
784
|
+
ok: Boolean(attempt.ok),
|
|
785
|
+
step: attempt.step || null,
|
|
786
|
+
windowId: attempt.windowId ?? null,
|
|
787
|
+
windowState: attempt.windowState,
|
|
788
|
+
bounds: attempt.bounds || null,
|
|
789
|
+
error: attempt.error || null
|
|
790
|
+
})),
|
|
791
|
+
error: result.repair.error || null
|
|
792
|
+
}
|
|
793
|
+
: null,
|
|
794
|
+
rootReacquisition: result.rootReacquisition || null,
|
|
795
|
+
error: result.error || null
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function annotateViewportState(state, baseline = null) {
|
|
800
|
+
if (!state?.ok) {
|
|
801
|
+
if (state) {
|
|
802
|
+
state.collapseEvidence = {
|
|
803
|
+
unreadable: true,
|
|
804
|
+
catastrophic: false,
|
|
805
|
+
baselineDrift: false
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
return state;
|
|
809
|
+
}
|
|
810
|
+
const catastrophic = isListViewportCollapsed(state);
|
|
811
|
+
const baselineComparison = compareViewportToBaseline(state, baseline);
|
|
812
|
+
const baselineDrift = Boolean(baselineComparison.drifted);
|
|
813
|
+
state.baselineComparison = baselineComparison;
|
|
814
|
+
state.collapseEvidence = {
|
|
815
|
+
unreadable: false,
|
|
816
|
+
catastrophic,
|
|
817
|
+
baselineDrift,
|
|
818
|
+
driftedDimensions: baselineComparison.driftedDimensions || []
|
|
819
|
+
};
|
|
820
|
+
state.collapsed = catastrophic || baselineDrift;
|
|
821
|
+
return state;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function buildStabilityEvidence(firstState, secondState) {
|
|
825
|
+
const reference = createViewportBaseline(firstState, {
|
|
826
|
+
reason: "stability_reference"
|
|
827
|
+
});
|
|
828
|
+
const comparison = compareViewportToBaseline(secondState, reference);
|
|
829
|
+
const windowBounds = compareWindowToBaseline(secondState, reference);
|
|
830
|
+
return {
|
|
831
|
+
required: true,
|
|
832
|
+
verified: Boolean(
|
|
833
|
+
secondState?.ok
|
|
834
|
+
&& !comparison.drifted
|
|
835
|
+
&& windowBounds.verified
|
|
836
|
+
&& !windowBounds.changed
|
|
837
|
+
),
|
|
838
|
+
sampleCount: 2,
|
|
839
|
+
comparison,
|
|
840
|
+
windowBounds
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
async function reacquireViewportRoots(reacquireRoots, {
|
|
845
|
+
phase,
|
|
846
|
+
root,
|
|
847
|
+
frameOwnerRoot
|
|
848
|
+
}) {
|
|
849
|
+
if (typeof reacquireRoots !== "function") {
|
|
850
|
+
return {
|
|
851
|
+
ok: false,
|
|
852
|
+
phase,
|
|
853
|
+
error: "viewport root reacquisition is unavailable"
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
try {
|
|
857
|
+
const roots = await reacquireRoots({ phase, root, frameOwnerRoot });
|
|
858
|
+
const targetRootNodeId = rootNodeId(roots, root);
|
|
859
|
+
if (!targetRootNodeId) {
|
|
860
|
+
return {
|
|
861
|
+
ok: false,
|
|
862
|
+
phase,
|
|
863
|
+
error: `Reacquired viewport root was not found: ${root}`
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
return {
|
|
867
|
+
ok: true,
|
|
868
|
+
phase,
|
|
869
|
+
roots,
|
|
870
|
+
targetRootNodeId,
|
|
871
|
+
frameOwnerNodeId: rootNodeId(roots, frameOwnerRoot) || null
|
|
872
|
+
};
|
|
873
|
+
} catch (error) {
|
|
874
|
+
return {
|
|
875
|
+
ok: false,
|
|
876
|
+
phase,
|
|
877
|
+
error: error?.message || String(error)
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function compactRootReacquisition(samples = []) {
|
|
883
|
+
return {
|
|
884
|
+
required: true,
|
|
885
|
+
verified: samples.length === 2 && samples.every((sample) => sample?.ok),
|
|
886
|
+
samples: samples.map((sample) => ({
|
|
887
|
+
ok: Boolean(sample?.ok),
|
|
888
|
+
phase: sample?.phase || null,
|
|
889
|
+
targetRootNodeId: sample?.targetRootNodeId || null,
|
|
890
|
+
frameOwnerNodeId: sample?.frameOwnerNodeId || null,
|
|
891
|
+
error: sample?.error || null
|
|
892
|
+
}))
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
export async function ensureHealthyViewport(client, {
|
|
897
|
+
roots = {},
|
|
898
|
+
root = "frame",
|
|
899
|
+
frameOwnerRoot = "frameOwner",
|
|
900
|
+
reason = "viewport_recovery",
|
|
901
|
+
repair = true,
|
|
902
|
+
recoveryDelayMs = 900,
|
|
903
|
+
recoverySettleMs = 520,
|
|
904
|
+
baseline = null,
|
|
905
|
+
allowVerifiedWindowRebaseline = true,
|
|
906
|
+
reacquireRoots = null
|
|
907
|
+
} = {}) {
|
|
908
|
+
let before = await readViewportState(client, {
|
|
909
|
+
roots,
|
|
910
|
+
root,
|
|
911
|
+
frameOwnerRoot
|
|
912
|
+
});
|
|
913
|
+
if (!before.ok) {
|
|
914
|
+
annotateViewportState(before, baseline);
|
|
915
|
+
const unreadableBefore = before;
|
|
916
|
+
const rootSamples = [];
|
|
917
|
+
const firstRoots = await reacquireViewportRoots(reacquireRoots, {
|
|
918
|
+
phase: "unreadable_root_sample_1",
|
|
919
|
+
root,
|
|
920
|
+
frameOwnerRoot
|
|
921
|
+
});
|
|
922
|
+
rootSamples.push(firstRoots);
|
|
923
|
+
if (!firstRoots.ok) {
|
|
924
|
+
return {
|
|
925
|
+
ok: false,
|
|
926
|
+
collapsed: false,
|
|
927
|
+
recovered: false,
|
|
928
|
+
reason,
|
|
929
|
+
before: unreadableBefore,
|
|
930
|
+
state: unreadableBefore,
|
|
931
|
+
rootReacquisition: compactRootReacquisition(rootSamples),
|
|
932
|
+
baseline,
|
|
933
|
+
baselineEstablished: false,
|
|
934
|
+
rebaselined: false,
|
|
935
|
+
error: unreadableBefore.error || firstRoots.error || "viewport state could not be read"
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const firstFreshState = await readViewportState(client, {
|
|
940
|
+
roots: firstRoots.roots,
|
|
941
|
+
root,
|
|
942
|
+
frameOwnerRoot
|
|
943
|
+
});
|
|
944
|
+
annotateViewportState(firstFreshState, baseline);
|
|
945
|
+
if (!firstFreshState.ok) {
|
|
946
|
+
return {
|
|
947
|
+
ok: false,
|
|
948
|
+
collapsed: false,
|
|
949
|
+
recovered: false,
|
|
950
|
+
reason,
|
|
951
|
+
before: unreadableBefore,
|
|
952
|
+
state: firstFreshState,
|
|
953
|
+
rootReacquisition: compactRootReacquisition(rootSamples),
|
|
954
|
+
baseline,
|
|
955
|
+
baselineEstablished: false,
|
|
956
|
+
rebaselined: false,
|
|
957
|
+
baselineComparison: firstFreshState.baselineComparison || null,
|
|
958
|
+
error: firstFreshState.error || "reacquired viewport state could not be read"
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
if (firstFreshState.collapsed) {
|
|
962
|
+
// The stale handle was replaced successfully and the fresh geometry now
|
|
963
|
+
// proves a real collapse. Continue through the normal window-repair path
|
|
964
|
+
// with that fresh root instead of misclassifying it as unreadable.
|
|
965
|
+
before = firstFreshState;
|
|
966
|
+
roots = firstRoots.roots;
|
|
967
|
+
} else {
|
|
968
|
+
const secondRoots = await reacquireViewportRoots(reacquireRoots, {
|
|
969
|
+
phase: "unreadable_root_sample_2",
|
|
970
|
+
root,
|
|
971
|
+
frameOwnerRoot
|
|
972
|
+
});
|
|
973
|
+
rootSamples.push(secondRoots);
|
|
974
|
+
if (!secondRoots.ok) {
|
|
975
|
+
return {
|
|
976
|
+
ok: false,
|
|
977
|
+
collapsed: false,
|
|
978
|
+
recovered: false,
|
|
979
|
+
reason,
|
|
980
|
+
before: unreadableBefore,
|
|
981
|
+
state: firstFreshState,
|
|
982
|
+
rootReacquisition: compactRootReacquisition(rootSamples),
|
|
983
|
+
baseline,
|
|
984
|
+
baselineEstablished: false,
|
|
985
|
+
rebaselined: false,
|
|
986
|
+
baselineComparison: firstFreshState.baselineComparison || null,
|
|
987
|
+
error: secondRoots.error || "viewport roots could not be reacquired for stability verification"
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const secondFreshState = await readViewportState(client, {
|
|
992
|
+
roots: secondRoots.roots,
|
|
993
|
+
root,
|
|
994
|
+
frameOwnerRoot
|
|
995
|
+
});
|
|
996
|
+
annotateViewportState(secondFreshState, baseline);
|
|
997
|
+
const stability = secondFreshState.ok
|
|
998
|
+
? buildStabilityEvidence(firstFreshState, secondFreshState)
|
|
999
|
+
: {
|
|
1000
|
+
required: true,
|
|
1001
|
+
verified: false,
|
|
1002
|
+
sampleCount: 2,
|
|
1003
|
+
error: secondFreshState.error || "reacquired viewport stability sample could not be read"
|
|
1004
|
+
};
|
|
1005
|
+
const rootsRecovered = Boolean(
|
|
1006
|
+
secondFreshState.ok
|
|
1007
|
+
&& !secondFreshState.collapsed
|
|
1008
|
+
&& stability.verified
|
|
1009
|
+
&& rootSamples.every((sample) => sample.ok)
|
|
1010
|
+
);
|
|
1011
|
+
const nextBaseline = rootsRecovered
|
|
1012
|
+
? baseline || mergeStableBaselineSamples(firstFreshState, secondFreshState, {
|
|
1013
|
+
reason: "reacquired_healthy_viewport"
|
|
1014
|
+
})
|
|
1015
|
+
: baseline;
|
|
1016
|
+
return {
|
|
1017
|
+
ok: rootsRecovered,
|
|
1018
|
+
collapsed: Boolean(secondFreshState.collapsed),
|
|
1019
|
+
recovered: rootsRecovered,
|
|
1020
|
+
recoveryMode: rootsRecovered ? "root_reacquisition" : null,
|
|
1021
|
+
reason,
|
|
1022
|
+
before: unreadableBefore,
|
|
1023
|
+
state: secondFreshState,
|
|
1024
|
+
rootReacquisition: compactRootReacquisition(rootSamples),
|
|
1025
|
+
baseline: nextBaseline,
|
|
1026
|
+
baselineEstablished: Boolean(rootsRecovered && !baseline),
|
|
1027
|
+
rebaselined: false,
|
|
1028
|
+
windowBoundsChange: compareWindowToBaseline(secondFreshState, baseline),
|
|
1029
|
+
baselineComparison: secondFreshState.baselineComparison || null,
|
|
1030
|
+
stability,
|
|
1031
|
+
error: rootsRecovered
|
|
1032
|
+
? null
|
|
1033
|
+
: secondFreshState.ok && !secondFreshState.collapsed
|
|
1034
|
+
? "reacquired viewport roots did not stabilize across two healthy readings"
|
|
1035
|
+
: secondFreshState.error || "reacquired viewport state is unsafe"
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
let effectiveBaseline = baseline;
|
|
1041
|
+
const windowBoundsChange = compareWindowToBaseline(before, baseline);
|
|
1042
|
+
const windowResizeCandidate = Boolean(
|
|
1043
|
+
baseline
|
|
1044
|
+
&& allowVerifiedWindowRebaseline
|
|
1045
|
+
&& windowBoundsChange.verified
|
|
1046
|
+
&& windowBoundsChange.changed
|
|
1047
|
+
);
|
|
1048
|
+
if (windowResizeCandidate) {
|
|
1049
|
+
const candidateBaseline = createViewportBaseline(before, {
|
|
1050
|
+
reason: "verified_browser_bounds_change"
|
|
1051
|
+
});
|
|
1052
|
+
annotateViewportState(before, candidateBaseline);
|
|
1053
|
+
const confirmation = await readViewportState(client, {
|
|
1054
|
+
roots,
|
|
1055
|
+
root,
|
|
1056
|
+
frameOwnerRoot
|
|
1057
|
+
});
|
|
1058
|
+
if (!confirmation.ok) {
|
|
1059
|
+
annotateViewportState(confirmation, candidateBaseline);
|
|
1060
|
+
return {
|
|
1061
|
+
ok: false,
|
|
1062
|
+
collapsed: false,
|
|
1063
|
+
recovered: false,
|
|
1064
|
+
reason,
|
|
1065
|
+
before,
|
|
1066
|
+
state: confirmation,
|
|
1067
|
+
baseline,
|
|
1068
|
+
baselineEstablished: false,
|
|
1069
|
+
rebaselined: false,
|
|
1070
|
+
windowBoundsChange,
|
|
1071
|
+
stability: {
|
|
1072
|
+
required: true,
|
|
1073
|
+
verified: false,
|
|
1074
|
+
sampleCount: 2,
|
|
1075
|
+
error: confirmation.error || "viewport stability sample could not be read"
|
|
1076
|
+
},
|
|
1077
|
+
error: confirmation.error || "external browser resize verification could not be read"
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
const stability = buildStabilityEvidence(before, confirmation);
|
|
1081
|
+
annotateViewportState(confirmation, candidateBaseline);
|
|
1082
|
+
if (!before.collapsed && !confirmation.collapsed && stability.verified) {
|
|
1083
|
+
const nextBaseline = mergeStableBaselineSamples(before, confirmation, {
|
|
1084
|
+
reason: "verified_browser_bounds_change"
|
|
1085
|
+
});
|
|
1086
|
+
return {
|
|
1087
|
+
ok: true,
|
|
1088
|
+
collapsed: false,
|
|
1089
|
+
recovered: false,
|
|
1090
|
+
reason,
|
|
1091
|
+
before,
|
|
1092
|
+
state: confirmation,
|
|
1093
|
+
baseline: nextBaseline,
|
|
1094
|
+
baselineEstablished: false,
|
|
1095
|
+
rebaselined: true,
|
|
1096
|
+
windowBoundsChange,
|
|
1097
|
+
baselineComparison: confirmation.baselineComparison,
|
|
1098
|
+
stability
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
return {
|
|
1102
|
+
ok: false,
|
|
1103
|
+
collapsed: Boolean(before.collapsed || confirmation.collapsed),
|
|
1104
|
+
recovered: false,
|
|
1105
|
+
reason,
|
|
1106
|
+
before,
|
|
1107
|
+
state: confirmation,
|
|
1108
|
+
baseline,
|
|
1109
|
+
baselineEstablished: false,
|
|
1110
|
+
rebaselined: false,
|
|
1111
|
+
windowBoundsChange,
|
|
1112
|
+
baselineComparison: confirmation.baselineComparison,
|
|
1113
|
+
stability,
|
|
1114
|
+
error: "external browser resize was not stable across two healthy readings"
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
const rebaselined = false;
|
|
1119
|
+
annotateViewportState(before, effectiveBaseline);
|
|
1120
|
+
|
|
1121
|
+
if (!before.collapsed && !baseline) {
|
|
1122
|
+
const confirmation = await readViewportState(client, {
|
|
1123
|
+
roots,
|
|
1124
|
+
root,
|
|
1125
|
+
frameOwnerRoot
|
|
1126
|
+
});
|
|
1127
|
+
if (!confirmation.ok) {
|
|
1128
|
+
annotateViewportState(confirmation, effectiveBaseline);
|
|
1129
|
+
return {
|
|
1130
|
+
ok: false,
|
|
1131
|
+
collapsed: false,
|
|
1132
|
+
recovered: false,
|
|
1133
|
+
reason,
|
|
1134
|
+
before,
|
|
1135
|
+
state: confirmation,
|
|
1136
|
+
baseline,
|
|
1137
|
+
baselineEstablished: false,
|
|
1138
|
+
rebaselined: false,
|
|
1139
|
+
windowBoundsChange,
|
|
1140
|
+
stability: {
|
|
1141
|
+
required: true,
|
|
1142
|
+
verified: false,
|
|
1143
|
+
sampleCount: 2,
|
|
1144
|
+
error: confirmation.error || "viewport stability sample could not be read"
|
|
1145
|
+
},
|
|
1146
|
+
error: confirmation.error || "viewport stability sample could not be read"
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
const stability = buildStabilityEvidence(before, confirmation);
|
|
1150
|
+
const candidateBaseline = createViewportBaseline(before, {
|
|
1151
|
+
reason: "initial_healthy_viewport"
|
|
1152
|
+
});
|
|
1153
|
+
annotateViewportState(confirmation, candidateBaseline);
|
|
1154
|
+
if (!confirmation.collapsed && stability.verified) {
|
|
1155
|
+
const nextBaseline = mergeStableBaselineSamples(before, confirmation, {
|
|
1156
|
+
reason: "initial_healthy_viewport"
|
|
1157
|
+
});
|
|
1158
|
+
return {
|
|
1159
|
+
ok: true,
|
|
1160
|
+
collapsed: false,
|
|
1161
|
+
recovered: false,
|
|
1162
|
+
reason,
|
|
1163
|
+
before,
|
|
1164
|
+
state: confirmation,
|
|
1165
|
+
baseline: nextBaseline,
|
|
1166
|
+
baselineEstablished: true,
|
|
1167
|
+
rebaselined: false,
|
|
1168
|
+
windowBoundsChange,
|
|
1169
|
+
baselineComparison: confirmation.baselineComparison,
|
|
1170
|
+
stability
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
if (!stability.verified && !confirmation.collapsed) {
|
|
1174
|
+
return {
|
|
1175
|
+
ok: false,
|
|
1176
|
+
collapsed: false,
|
|
1177
|
+
recovered: false,
|
|
1178
|
+
reason,
|
|
1179
|
+
before,
|
|
1180
|
+
state: confirmation,
|
|
1181
|
+
baseline,
|
|
1182
|
+
baselineEstablished: false,
|
|
1183
|
+
rebaselined: false,
|
|
1184
|
+
windowBoundsChange,
|
|
1185
|
+
baselineComparison: confirmation.baselineComparison,
|
|
1186
|
+
stability,
|
|
1187
|
+
error: "viewport baseline did not stabilize across consecutive readings"
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
before = confirmation;
|
|
1191
|
+
effectiveBaseline = candidateBaseline;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
if (!before.collapsed) {
|
|
1195
|
+
return {
|
|
1196
|
+
ok: true,
|
|
1197
|
+
collapsed: false,
|
|
1198
|
+
recovered: false,
|
|
1199
|
+
reason,
|
|
1200
|
+
state: before,
|
|
1201
|
+
baseline: effectiveBaseline,
|
|
1202
|
+
baselineEstablished: false,
|
|
1203
|
+
rebaselined: false,
|
|
1204
|
+
windowBoundsChange,
|
|
1205
|
+
baselineComparison: before.baselineComparison,
|
|
1206
|
+
stability: {
|
|
1207
|
+
required: false,
|
|
1208
|
+
verified: true,
|
|
1209
|
+
sampleCount: 1
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
if (!repair) {
|
|
1215
|
+
return {
|
|
1216
|
+
ok: false,
|
|
1217
|
+
collapsed: true,
|
|
1218
|
+
recovered: false,
|
|
1219
|
+
reason,
|
|
1220
|
+
before,
|
|
1221
|
+
state: before,
|
|
1222
|
+
baseline: effectiveBaseline,
|
|
1223
|
+
baselineEstablished: false,
|
|
1224
|
+
rebaselined,
|
|
1225
|
+
windowBoundsChange,
|
|
1226
|
+
baselineComparison: before.baselineComparison,
|
|
1227
|
+
error: "viewport collapsed and repair disabled"
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
const repairResult = await toggleWindowStateForViewportRecovery(client, {
|
|
1232
|
+
reason,
|
|
1233
|
+
settleMs: recoverySettleMs
|
|
1234
|
+
});
|
|
1235
|
+
if (!repairResult.ok || !repairResult.original_state_restored) {
|
|
1236
|
+
return {
|
|
1237
|
+
ok: false,
|
|
1238
|
+
collapsed: true,
|
|
1239
|
+
recovered: false,
|
|
1240
|
+
reason,
|
|
1241
|
+
before,
|
|
1242
|
+
state: before,
|
|
1243
|
+
repair: repairResult,
|
|
1244
|
+
baseline: effectiveBaseline,
|
|
1245
|
+
baselineEstablished: false,
|
|
1246
|
+
rebaselined,
|
|
1247
|
+
windowBoundsChange,
|
|
1248
|
+
baselineComparison: before.baselineComparison || null,
|
|
1249
|
+
error: "original browser window state and bounds were not verified after recovery"
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
if (recoveryDelayMs > 0) await sleep(recoveryDelayMs);
|
|
1253
|
+
const rootSamples = [];
|
|
1254
|
+
const firstRoots = await reacquireViewportRoots(reacquireRoots, {
|
|
1255
|
+
phase: "post_repair_sample_1",
|
|
1256
|
+
root,
|
|
1257
|
+
frameOwnerRoot
|
|
1258
|
+
});
|
|
1259
|
+
rootSamples.push(firstRoots);
|
|
1260
|
+
if (!firstRoots.ok) {
|
|
1261
|
+
return {
|
|
1262
|
+
ok: false,
|
|
1263
|
+
collapsed: true,
|
|
1264
|
+
recovered: false,
|
|
1265
|
+
reason,
|
|
1266
|
+
before,
|
|
1267
|
+
state: before,
|
|
1268
|
+
repair: repairResult,
|
|
1269
|
+
rootReacquisition: compactRootReacquisition(rootSamples),
|
|
1270
|
+
baseline: effectiveBaseline,
|
|
1271
|
+
baselineEstablished: false,
|
|
1272
|
+
rebaselined,
|
|
1273
|
+
windowBoundsChange,
|
|
1274
|
+
baselineComparison: before.baselineComparison || null,
|
|
1275
|
+
error: firstRoots.error || "viewport roots could not be reacquired after recovery"
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
const after = await readViewportState(client, {
|
|
1279
|
+
roots: firstRoots.roots,
|
|
1280
|
+
root,
|
|
1281
|
+
frameOwnerRoot
|
|
1282
|
+
});
|
|
1283
|
+
annotateViewportState(after, effectiveBaseline);
|
|
1284
|
+
if (!after.ok || after.collapsed) {
|
|
1285
|
+
return {
|
|
1286
|
+
ok: false,
|
|
1287
|
+
collapsed: Boolean(after.collapsed),
|
|
1288
|
+
recovered: false,
|
|
1289
|
+
reason,
|
|
1290
|
+
before,
|
|
1291
|
+
state: after,
|
|
1292
|
+
repair: repairResult,
|
|
1293
|
+
rootReacquisition: compactRootReacquisition(rootSamples),
|
|
1294
|
+
baseline: effectiveBaseline,
|
|
1295
|
+
baselineEstablished: false,
|
|
1296
|
+
rebaselined,
|
|
1297
|
+
windowBoundsChange,
|
|
1298
|
+
baselineComparison: after.baselineComparison || null,
|
|
1299
|
+
error: after.ok
|
|
1300
|
+
? "viewport collapsed after recovery attempt"
|
|
1301
|
+
: after.error || "viewport state could not be read after recovery"
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
const secondRoots = await reacquireViewportRoots(reacquireRoots, {
|
|
1306
|
+
phase: "post_repair_sample_2",
|
|
1307
|
+
root,
|
|
1308
|
+
frameOwnerRoot
|
|
1309
|
+
});
|
|
1310
|
+
rootSamples.push(secondRoots);
|
|
1311
|
+
if (!secondRoots.ok) {
|
|
1312
|
+
return {
|
|
1313
|
+
ok: false,
|
|
1314
|
+
collapsed: false,
|
|
1315
|
+
recovered: false,
|
|
1316
|
+
reason,
|
|
1317
|
+
before,
|
|
1318
|
+
state: after,
|
|
1319
|
+
repair: repairResult,
|
|
1320
|
+
rootReacquisition: compactRootReacquisition(rootSamples),
|
|
1321
|
+
baseline: effectiveBaseline,
|
|
1322
|
+
baselineEstablished: false,
|
|
1323
|
+
rebaselined,
|
|
1324
|
+
windowBoundsChange,
|
|
1325
|
+
baselineComparison: after.baselineComparison || null,
|
|
1326
|
+
error: secondRoots.error || "viewport roots could not be reacquired for recovery verification"
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
const verification = await readViewportState(client, {
|
|
1330
|
+
roots: secondRoots.roots,
|
|
1331
|
+
root,
|
|
1332
|
+
frameOwnerRoot
|
|
1333
|
+
});
|
|
1334
|
+
annotateViewportState(verification, effectiveBaseline);
|
|
1335
|
+
const stability = verification.ok
|
|
1336
|
+
? buildStabilityEvidence(after, verification)
|
|
1337
|
+
: {
|
|
1338
|
+
required: true,
|
|
1339
|
+
verified: false,
|
|
1340
|
+
sampleCount: 2,
|
|
1341
|
+
error: verification.error || "viewport recovery verification could not be read"
|
|
1342
|
+
};
|
|
1343
|
+
const recovered = Boolean(
|
|
1344
|
+
verification.ok
|
|
1345
|
+
&& !verification.collapsed
|
|
1346
|
+
&& stability.verified
|
|
1347
|
+
&& repairResult.original_state_restored
|
|
1348
|
+
&& rootSamples.every((sample) => sample.ok)
|
|
1349
|
+
);
|
|
1350
|
+
const nextBaseline = recovered
|
|
1351
|
+
? effectiveBaseline || mergeStableBaselineSamples(after, verification, {
|
|
1352
|
+
reason: "recovered_healthy_viewport"
|
|
1353
|
+
})
|
|
1354
|
+
: effectiveBaseline;
|
|
1355
|
+
return {
|
|
1356
|
+
ok: recovered,
|
|
1357
|
+
collapsed: Boolean(verification.collapsed),
|
|
1358
|
+
recovered,
|
|
1359
|
+
reason,
|
|
1360
|
+
before,
|
|
1361
|
+
state: verification,
|
|
1362
|
+
repair: repairResult,
|
|
1363
|
+
rootReacquisition: compactRootReacquisition(rootSamples),
|
|
1364
|
+
baseline: nextBaseline,
|
|
1365
|
+
baselineEstablished: Boolean(recovered && !baseline),
|
|
1366
|
+
rebaselined,
|
|
1367
|
+
windowBoundsChange,
|
|
1368
|
+
baselineComparison: verification.baselineComparison || null,
|
|
1369
|
+
stability,
|
|
1370
|
+
error: recovered
|
|
1371
|
+
? null
|
|
1372
|
+
: verification.ok && !verification.collapsed
|
|
1373
|
+
? "viewport did not stabilize after recovery attempt with verified window restoration"
|
|
1374
|
+
: verification.error || "viewport collapsed after recovery attempt"
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
export function createViewportRunGuard({
|
|
1379
|
+
client,
|
|
1380
|
+
domain = "boss",
|
|
1381
|
+
root = "frame",
|
|
1382
|
+
frameOwnerRoot = "frameOwner",
|
|
1383
|
+
runControl = null,
|
|
1384
|
+
getRoots = null,
|
|
1385
|
+
rootNodesFromState = (rootState) => rootState?.rootNodes || rootState?.roots || rootState || {},
|
|
1386
|
+
repair = true,
|
|
1387
|
+
recoveryDelayMs = 900,
|
|
1388
|
+
recoverySettleMs = 520,
|
|
1389
|
+
maxEvents = 10
|
|
1390
|
+
} = {}) {
|
|
1391
|
+
if (!client) throw new Error("createViewportRunGuard requires a guarded CDP client");
|
|
1392
|
+
const events = [];
|
|
1393
|
+
let baseline = null;
|
|
1394
|
+
const stats = {
|
|
1395
|
+
checks: 0,
|
|
1396
|
+
recoveries: 0,
|
|
1397
|
+
failures: 0,
|
|
1398
|
+
baseline_establishments: 0,
|
|
1399
|
+
rebaselines: 0,
|
|
1400
|
+
baseline_drift_detections: 0,
|
|
1401
|
+
unreadable_measurements: 0
|
|
1402
|
+
};
|
|
1403
|
+
|
|
1404
|
+
function recordEvent(phase, health) {
|
|
1405
|
+
const compact = compactViewportHealthResult(health);
|
|
1406
|
+
const shouldRecord = Boolean(
|
|
1407
|
+
health?.recovered
|
|
1408
|
+
|| health?.rebaselined
|
|
1409
|
+
|| !health?.ok
|
|
1410
|
+
|| health?.collapsed
|
|
1411
|
+
);
|
|
1412
|
+
if (!shouldRecord) return compact;
|
|
1413
|
+
const event = {
|
|
1414
|
+
phase,
|
|
1415
|
+
at: new Date().toISOString(),
|
|
1416
|
+
...compact
|
|
1417
|
+
};
|
|
1418
|
+
events.push(event);
|
|
1419
|
+
if (events.length > maxEvents) events.shift();
|
|
1420
|
+
if (runControl) {
|
|
1421
|
+
runControl.checkpoint({
|
|
1422
|
+
viewport_health: event,
|
|
1423
|
+
viewport_health_events: events.slice(),
|
|
1424
|
+
viewport_health_stats: { ...stats }
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
return compact;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
async function ensure(rootState, {
|
|
1431
|
+
phase = "run",
|
|
1432
|
+
reason = `${domain}:${phase}`
|
|
1433
|
+
} = {}) {
|
|
1434
|
+
let currentRootState = rootState;
|
|
1435
|
+
if (!currentRootState && typeof getRoots === "function") {
|
|
1436
|
+
currentRootState = await getRoots(client);
|
|
1437
|
+
}
|
|
1438
|
+
const roots = rootNodesFromState(currentRootState);
|
|
1439
|
+
let latestReacquiredRootState = null;
|
|
1440
|
+
const reacquireRoots = typeof getRoots === "function"
|
|
1441
|
+
? async () => {
|
|
1442
|
+
const freshRootState = await getRoots(client);
|
|
1443
|
+
latestReacquiredRootState = freshRootState;
|
|
1444
|
+
return rootNodesFromState(freshRootState);
|
|
1445
|
+
}
|
|
1446
|
+
: null;
|
|
1447
|
+
stats.checks += 1;
|
|
1448
|
+
const health = await ensureHealthyViewport(client, {
|
|
1449
|
+
roots,
|
|
1450
|
+
root,
|
|
1451
|
+
frameOwnerRoot,
|
|
1452
|
+
reason,
|
|
1453
|
+
repair,
|
|
1454
|
+
recoveryDelayMs,
|
|
1455
|
+
recoverySettleMs,
|
|
1456
|
+
baseline,
|
|
1457
|
+
reacquireRoots
|
|
1458
|
+
});
|
|
1459
|
+
if (health.baselineEstablished) stats.baseline_establishments += 1;
|
|
1460
|
+
if (health.rebaselined) stats.rebaselines += 1;
|
|
1461
|
+
if (health.before?.collapseEvidence?.baselineDrift) stats.baseline_drift_detections += 1;
|
|
1462
|
+
if (
|
|
1463
|
+
!health.state?.ok
|
|
1464
|
+
|| health.state?.collapseEvidence?.unreadable
|
|
1465
|
+
|| health.before?.collapseEvidence?.unreadable
|
|
1466
|
+
) {
|
|
1467
|
+
stats.unreadable_measurements += 1;
|
|
1468
|
+
}
|
|
1469
|
+
if (health.ok && health.baseline) baseline = health.baseline;
|
|
1470
|
+
if (health.recovered) stats.recoveries += 1;
|
|
1471
|
+
if (!health.ok) stats.failures += 1;
|
|
1472
|
+
const compact = recordEvent(phase, health);
|
|
1473
|
+
if (!health.ok) {
|
|
1474
|
+
const error = new Error(`${String(domain).toUpperCase()}_LIST_VIEWPORT_COLLAPSED`);
|
|
1475
|
+
error.code = "LIST_VIEWPORT_COLLAPSED";
|
|
1476
|
+
error.domain = domain;
|
|
1477
|
+
error.phase = phase;
|
|
1478
|
+
error.viewport_health = compact;
|
|
1479
|
+
throw error;
|
|
1480
|
+
}
|
|
1481
|
+
if (health.recovered && latestReacquiredRootState) {
|
|
1482
|
+
currentRootState = latestReacquiredRootState;
|
|
1483
|
+
}
|
|
1484
|
+
return {
|
|
1485
|
+
rootState: currentRootState,
|
|
1486
|
+
health,
|
|
1487
|
+
compact,
|
|
1488
|
+
stats: { ...stats },
|
|
1489
|
+
events: events.slice()
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
return {
|
|
1494
|
+
ensure,
|
|
1495
|
+
getStats() {
|
|
1496
|
+
return { ...stats };
|
|
1497
|
+
},
|
|
1498
|
+
getEvents() {
|
|
1499
|
+
return events.slice();
|
|
1500
|
+
},
|
|
1501
|
+
getBaseline() {
|
|
1502
|
+
return compactViewportBaseline(baseline);
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
}
|