teamshare-bridge 0.26.23 → 0.26.24
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 +383 -383
- package/dist/bridge/install-verify.js +8 -8
- package/dist/lib/agent-scope.js +10 -10
- package/dist/lib/browser/browse-llm.js +44 -44
- package/dist/lib/browser/human-input-guard.js +106 -106
- package/dist/lib/browser/login.js +141 -141
- package/dist/lib/browser/otp.js +72 -72
- package/dist/lib/browser/page-context.js +592 -592
- package/dist/lib/browser/page-events.js +91 -91
- package/dist/lib/browser/refs.js +264 -264
- package/dist/lib/browser/screen-change.js +79 -79
- package/dist/lib/human-style.js +32 -32
- package/dist/lib/llm/usage-reader.js +4 -4
- package/dist/lib/orchestrator/worktree.js +118 -50
- package/dist/lib/orchestrator/worktree.js.map +1 -1
- package/dist/lib/workspace/mirror.d.ts +13 -0
- package/dist/lib/workspace/mirror.js +200 -43
- package/dist/lib/workspace/mirror.js.map +1 -1
- package/dist/lib/workspace/workspace-audit.d.ts +2 -2
- package/dist/lib/workspace/workspace-audit.js +27 -17
- package/dist/lib/workspace/workspace-audit.js.map +1 -1
- package/package.json +1 -1
- package/skills/THIRD_PARTY_NOTICES.md +26 -26
- package/skills/architect/LICENSE +21 -21
- package/skills/codebase-design/LICENSE +21 -21
- package/skills/diagnosing-bugs/LICENSE +21 -21
- package/skills/domain-modeling/LICENSE +21 -21
- package/skills/improve-codebase-architecture/LICENSE +21 -21
- package/skills/recover/LICENSE +21 -21
- package/skills/review/LICENSE +21 -21
- package/skills/tdd/LICENSE +21 -21
- package/skills/teamshare-auth-rbac/SKILL.md +78 -78
- package/skills/teamshare-cdd/SKILL.md +215 -215
- package/skills/teamshare-modularity/SKILL.md +146 -146
- package/skills/teamshare-orchestrator-ask/SKILL.md +77 -77
- package/skills/teamshare-scaffold/SKILL.md +116 -116
- package/templates/backend/.env.example.tpl +13 -13
- package/templates/backend/nest-cli.json.tpl +8 -8
- package/templates/backend/package.json.tpl +41 -41
- package/templates/backend/prisma/schema.prisma.tpl +39 -39
- package/templates/backend/src/app.module.ts.tpl +24 -24
- package/templates/backend/src/common/filters/all-exceptions.filter.ts.tpl +59 -59
- package/templates/backend/src/common/guards/jwt-auth.guard.ts.tpl +27 -27
- package/templates/backend/src/common/guards/rbac.guard.ts.tpl +48 -48
- package/templates/backend/src/common/interceptors/transform.interceptor.ts.tpl +44 -44
- package/templates/backend/src/common/pipes/zod-validation.pipe.ts.tpl +29 -29
- package/templates/backend/src/common/swagger/envelope.schemas.ts.tpl +53 -53
- package/templates/backend/src/common/swagger/error.schemas.ts.tpl +62 -62
- package/templates/backend/src/main.ts.tpl +27 -27
- package/templates/backend/src/modules/example/example.controller.ts.tpl +102 -102
- package/templates/backend/src/modules/example/example.dto.ts.tpl +14 -14
- package/templates/backend/src/modules/example/example.module.ts.tpl +10 -10
- package/templates/backend/src/modules/example/example.service.ts.tpl +45 -45
- package/templates/backend/tsconfig.json.tpl +26 -26
- package/templates/common/.gitignore.tpl +42 -42
- package/templates/common/AGENTS.md.tpl +58 -58
- package/templates/common/docs/adr/README.md +25 -25
- package/templates/common/docs/api-contract.md.tpl +72 -72
- package/templates/web/next.config.ts.tpl +7 -7
- package/templates/web/package.json.tpl +37 -37
- package/templates/web/src/app/globals.css.tpl +48 -48
- package/templates/web/src/app/layout.tsx.tpl +25 -25
- package/templates/web/src/components/shared/app-button.tsx.tpl +59 -59
- package/templates/web/src/components/shared/app-empty-state.tsx.tpl +39 -39
- package/templates/web/src/components/shared/app-list-toolbar.tsx.tpl +63 -63
- package/templates/web/src/components/shared/app-skeleton.tsx.tpl +14 -14
- package/templates/web/src/components/shared/app-table.tsx.tpl +128 -128
- package/templates/web/src/components/shared/index.ts.tpl +5 -5
- package/templates/web/src/hooks/use-list-state.ts.tpl +72 -72
- package/templates/web/src/hooks/use-url-tab.ts.tpl +25 -25
- package/templates/web/src/lib/api/client.ts.tpl +123 -123
- package/templates/web/src/lib/providers.tsx.tpl +35 -35
- package/templates/web/src/lib/query-keys.ts.tpl +21 -21
- package/templates/web/src/lib/utils.ts.tpl +7 -7
- package/templates/web/src/lib/validation/schemas.ts.tpl +15 -15
- package/templates/web/src/modules/example/views/example-list-view.tsx.tpl +97 -97
- package/templates/web/tsconfig.json.tpl +23 -23
- package/tools/workspace-arbitration-check.mjs +359 -359
- package/dist/lib/git/ensure-excludes.d.ts +0 -5
- package/dist/lib/git/ensure-excludes.js +0 -55
- package/dist/lib/git/ensure-excludes.js.map +0 -1
|
@@ -58,66 +58,66 @@ exports.POPOVER_SELECTOR = '[data-radix-popper-content-wrapper],[data-radix-port
|
|
|
58
58
|
* the screen instead of an invisible portal.
|
|
59
59
|
*/
|
|
60
60
|
function menuDetectSource() {
|
|
61
|
-
return `
|
|
62
|
-
function detectMenus(maxMenus, maxItems) {
|
|
63
|
-
const clean = (v, cap) => String(v == null ? '' : v)
|
|
64
|
-
.replace(/\\s+/g, ' ').trim().slice(0, cap || 120);
|
|
65
|
-
const vis = (el) => {
|
|
66
|
-
if (!el || el.nodeType !== 1) return false;
|
|
67
|
-
const r = el.getBoundingClientRect();
|
|
68
|
-
if (r.width === 0 || r.height === 0) return false;
|
|
69
|
-
const cs = getComputedStyle(el);
|
|
70
|
-
return cs.visibility !== 'hidden' && cs.display !== 'none' && cs.opacity !== '0';
|
|
71
|
-
};
|
|
72
|
-
const rect = (el) => {
|
|
73
|
-
const r = el.getBoundingClientRect();
|
|
74
|
-
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
|
75
|
-
};
|
|
76
|
-
const SEL = ${JSON.stringify(exports.POPOVER_SELECTOR)};
|
|
77
|
-
const DIALOG = 'dialog[open],[role="dialog"],[aria-modal="true"]';
|
|
78
|
-
const menus = [];
|
|
79
|
-
const roots = [...document.querySelectorAll(SEL)].filter(vis);
|
|
80
|
-
for (const el of roots) {
|
|
81
|
-
if (menus.length >= maxMenus) break;
|
|
82
|
-
let nested = false;
|
|
83
|
-
let anc = el.parentElement;
|
|
84
|
-
while (anc && anc !== document.body) {
|
|
85
|
-
if (anc.matches && anc.matches(SEL) && vis(anc)) { nested = true; break; }
|
|
86
|
-
anc = anc.parentElement;
|
|
87
|
-
}
|
|
88
|
-
if (nested) continue;
|
|
89
|
-
const inner = el.querySelector('[role="menu"],[role="listbox"]');
|
|
90
|
-
const role = clean(
|
|
91
|
-
el.getAttribute('role') ||
|
|
92
|
-
(inner && inner.getAttribute('role')) ||
|
|
93
|
-
(el.hasAttribute('data-radix-popper-content-wrapper') ? 'popover' : ''),
|
|
94
|
-
24,
|
|
95
|
-
) || 'popover';
|
|
96
|
-
const items = [...el.querySelectorAll('[role="menuitem"],[role="menuitemcheckbox"],[role="menuitemradio"],[role="option"],[role="tab"],button,a[href]')]
|
|
97
|
-
.filter(vis)
|
|
98
|
-
.slice(0, maxItems)
|
|
99
|
-
.map((it) => ({
|
|
100
|
-
text: clean(
|
|
101
|
-
it.innerText || it.textContent || it.getAttribute('aria-label') || it.value || '',
|
|
102
|
-
80,
|
|
103
|
-
),
|
|
104
|
-
role: clean(it.getAttribute('role') || 'button', 20),
|
|
105
|
-
disabled: it.disabled === true || it.getAttribute('aria-disabled') === 'true',
|
|
106
|
-
...rect(it),
|
|
107
|
-
}));
|
|
108
|
-
const r = rect(el);
|
|
109
|
-
menus.push({
|
|
110
|
-
role,
|
|
111
|
-
label: clean(el.getAttribute('aria-label') || (inner && inner.getAttribute('aria-label')) || '', 80),
|
|
112
|
-
text: clean(el.innerText || el.textContent || '', 400),
|
|
113
|
-
inDialog: !!(el.closest && el.closest(DIALOG)),
|
|
114
|
-
x: r.x,
|
|
115
|
-
y: r.y,
|
|
116
|
-
items,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
return menus;
|
|
120
|
-
}
|
|
61
|
+
return `
|
|
62
|
+
function detectMenus(maxMenus, maxItems) {
|
|
63
|
+
const clean = (v, cap) => String(v == null ? '' : v)
|
|
64
|
+
.replace(/\\s+/g, ' ').trim().slice(0, cap || 120);
|
|
65
|
+
const vis = (el) => {
|
|
66
|
+
if (!el || el.nodeType !== 1) return false;
|
|
67
|
+
const r = el.getBoundingClientRect();
|
|
68
|
+
if (r.width === 0 || r.height === 0) return false;
|
|
69
|
+
const cs = getComputedStyle(el);
|
|
70
|
+
return cs.visibility !== 'hidden' && cs.display !== 'none' && cs.opacity !== '0';
|
|
71
|
+
};
|
|
72
|
+
const rect = (el) => {
|
|
73
|
+
const r = el.getBoundingClientRect();
|
|
74
|
+
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
|
75
|
+
};
|
|
76
|
+
const SEL = ${JSON.stringify(exports.POPOVER_SELECTOR)};
|
|
77
|
+
const DIALOG = 'dialog[open],[role="dialog"],[aria-modal="true"]';
|
|
78
|
+
const menus = [];
|
|
79
|
+
const roots = [...document.querySelectorAll(SEL)].filter(vis);
|
|
80
|
+
for (const el of roots) {
|
|
81
|
+
if (menus.length >= maxMenus) break;
|
|
82
|
+
let nested = false;
|
|
83
|
+
let anc = el.parentElement;
|
|
84
|
+
while (anc && anc !== document.body) {
|
|
85
|
+
if (anc.matches && anc.matches(SEL) && vis(anc)) { nested = true; break; }
|
|
86
|
+
anc = anc.parentElement;
|
|
87
|
+
}
|
|
88
|
+
if (nested) continue;
|
|
89
|
+
const inner = el.querySelector('[role="menu"],[role="listbox"]');
|
|
90
|
+
const role = clean(
|
|
91
|
+
el.getAttribute('role') ||
|
|
92
|
+
(inner && inner.getAttribute('role')) ||
|
|
93
|
+
(el.hasAttribute('data-radix-popper-content-wrapper') ? 'popover' : ''),
|
|
94
|
+
24,
|
|
95
|
+
) || 'popover';
|
|
96
|
+
const items = [...el.querySelectorAll('[role="menuitem"],[role="menuitemcheckbox"],[role="menuitemradio"],[role="option"],[role="tab"],button,a[href]')]
|
|
97
|
+
.filter(vis)
|
|
98
|
+
.slice(0, maxItems)
|
|
99
|
+
.map((it) => ({
|
|
100
|
+
text: clean(
|
|
101
|
+
it.innerText || it.textContent || it.getAttribute('aria-label') || it.value || '',
|
|
102
|
+
80,
|
|
103
|
+
),
|
|
104
|
+
role: clean(it.getAttribute('role') || 'button', 20),
|
|
105
|
+
disabled: it.disabled === true || it.getAttribute('aria-disabled') === 'true',
|
|
106
|
+
...rect(it),
|
|
107
|
+
}));
|
|
108
|
+
const r = rect(el);
|
|
109
|
+
menus.push({
|
|
110
|
+
role,
|
|
111
|
+
label: clean(el.getAttribute('aria-label') || (inner && inner.getAttribute('aria-label')) || '', 80),
|
|
112
|
+
text: clean(el.innerText || el.textContent || '', 400),
|
|
113
|
+
inDialog: !!(el.closest && el.closest(DIALOG)),
|
|
114
|
+
x: r.x,
|
|
115
|
+
y: r.y,
|
|
116
|
+
items,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return menus;
|
|
120
|
+
}
|
|
121
121
|
`;
|
|
122
122
|
}
|
|
123
123
|
/**
|
|
@@ -126,151 +126,151 @@ function menuDetectSource() {
|
|
|
126
126
|
* row count and total, so the agent can traverse a table instead of guessing.
|
|
127
127
|
*/
|
|
128
128
|
function paginationDetectSource() {
|
|
129
|
-
return `
|
|
130
|
-
function detectPagination(scope) {
|
|
131
|
-
const clean = (v, cap) => String(v == null ? '' : v)
|
|
132
|
-
.replace(/\\s+/g, ' ')
|
|
133
|
-
.trim()
|
|
134
|
-
.slice(0, cap || 200);
|
|
135
|
-
const vis = (el) => {
|
|
136
|
-
if (!el || el.nodeType !== 1) return false;
|
|
137
|
-
const r = el.getBoundingClientRect();
|
|
138
|
-
if (r.width === 0 || r.height === 0) return false;
|
|
139
|
-
const cs = getComputedStyle(el);
|
|
140
|
-
return cs.visibility !== 'hidden' && cs.display !== 'none' && cs.opacity !== '0';
|
|
141
|
-
};
|
|
142
|
-
const rect = (el) => {
|
|
143
|
-
const r = el.getBoundingClientRect();
|
|
144
|
-
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
|
145
|
-
};
|
|
146
|
-
const controls = [];
|
|
147
|
-
const candidates = [...scope.querySelectorAll('button,a,[role="button"]')]
|
|
148
|
-
.filter(vis)
|
|
149
|
-
.slice(0, 200);
|
|
150
|
-
for (const el of candidates) {
|
|
151
|
-
const label = clean(
|
|
152
|
-
el.getAttribute('aria-label') ||
|
|
153
|
-
el.getAttribute('title') ||
|
|
154
|
-
el.innerText ||
|
|
155
|
-
el.textContent ||
|
|
156
|
-
'',
|
|
157
|
-
60,
|
|
158
|
-
);
|
|
159
|
-
const cls = String(el.className || '');
|
|
160
|
-
const aria = String(el.getAttribute('aria-current') || '');
|
|
161
|
-
const disabled = el.disabled === true ||
|
|
162
|
-
el.getAttribute('aria-disabled') === 'true' ||
|
|
163
|
-
/(?:^|[\\s_-])disabled(?:$|[\\s_-])/i.test(cls);
|
|
164
|
-
const pageMatch = label.match(/^(?:page\\s*)?(\\d{1,5})$/i);
|
|
165
|
-
let kind = null;
|
|
166
|
-
if (/^(next|next page|›|»|older|forward)$/i.test(label) || /next page/i.test(label)) kind = 'next';
|
|
167
|
-
else if (/^(prev|prev(ious)?|‹|«|newer|back)$/i.test(label) || /previous page/i.test(label)) kind = 'prev';
|
|
168
|
-
else if (/load more|show more|more results|see more/i.test(label)) kind = 'loadMore';
|
|
169
|
-
else if (pageMatch) kind = 'page';
|
|
170
|
-
if (!kind) continue;
|
|
171
|
-
controls.push({
|
|
172
|
-
label,
|
|
173
|
-
kind,
|
|
174
|
-
...(pageMatch ? { page: Number(pageMatch[1]) } : {}),
|
|
175
|
-
disabled,
|
|
176
|
-
current: aria === 'page' || /(?:^|[\\s_-])current(?:$|[\\s_-])/i.test(cls),
|
|
177
|
-
...rect(el),
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
let pageSize;
|
|
182
|
-
let pageSizeOptions = [];
|
|
183
|
-
for (const sel of [...scope.querySelectorAll('select')].filter(vis)) {
|
|
184
|
-
const opts = [...sel.options]
|
|
185
|
-
.map((o) => Number(String(o.textContent || o.label || '').trim()))
|
|
186
|
-
.filter((n) => Number.isFinite(n) && n > 0);
|
|
187
|
-
if (!opts.length) continue;
|
|
188
|
-
pageSize = Number(sel.value) || opts[0];
|
|
189
|
-
pageSizeOptions = [...new Set(opts)].sort((a, b) => a - b);
|
|
190
|
-
break;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const rows = [...scope.querySelectorAll('tbody tr,[role="row"]')]
|
|
194
|
-
.filter(vis)
|
|
195
|
-
.filter((r) => r.querySelectorAll('td').length > 0 || !r.querySelector('th'));
|
|
196
|
-
const rowCount = rows.length;
|
|
197
|
-
|
|
198
|
-
const pager = scope.querySelector(
|
|
199
|
-
'nav[aria-label*="pagination" i],[class*="pagination"],[class*="pager"],[role="navigation"]',
|
|
200
|
-
) || scope;
|
|
201
|
-
const text = clean(pager.innerText || pager.textContent || '', 3000);
|
|
202
|
-
let page;
|
|
203
|
-
let total;
|
|
204
|
-
let totalPages;
|
|
205
|
-
let m = text.match(/showing\\s+(\\d+)\\s*(?:to|-|–)\\s*(\\d+)\\s+of\\s+(\\d+)/i);
|
|
206
|
-
if (m) {
|
|
207
|
-
const start = Number(m[1]);
|
|
208
|
-
const end = Number(m[2]);
|
|
209
|
-
total = Number(m[3]);
|
|
210
|
-
pageSize = pageSize || Math.max(1, end - start + 1);
|
|
211
|
-
page = Math.floor((start - 1) / pageSize) + 1;
|
|
212
|
-
}
|
|
213
|
-
if (!total) {
|
|
214
|
-
m = text.match(/(\\d+)\\s*(?:-|–|to)\\s*(\\d+)\\s+of\\s+(\\d+)/i);
|
|
215
|
-
if (m) {
|
|
216
|
-
const start = Number(m[1]);
|
|
217
|
-
const end = Number(m[2]);
|
|
218
|
-
total = Number(m[3]);
|
|
219
|
-
pageSize = pageSize || Math.max(1, end - start + 1);
|
|
220
|
-
page = page || Math.floor((start - 1) / pageSize) + 1;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
if (!total) {
|
|
224
|
-
m = text.match(/(\\d+)\\s+results?\\b/i);
|
|
225
|
-
if (m) total = Number(m[1]);
|
|
226
|
-
}
|
|
227
|
-
if (!page) {
|
|
228
|
-
m = text.match(/page\\s+(\\d+)\\s+of\\s+(\\d+)/i);
|
|
229
|
-
if (m) {
|
|
230
|
-
page = Number(m[1]);
|
|
231
|
-
totalPages = Number(m[2]);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
if (!page) {
|
|
235
|
-
const currentPage = controls.find((c) => c.kind === 'page' && c.current);
|
|
236
|
-
if (currentPage) page = currentPage.page;
|
|
237
|
-
}
|
|
238
|
-
if (!totalPages && total && pageSize) totalPages = Math.ceil(total / pageSize);
|
|
239
|
-
|
|
240
|
-
const next = controls.find((c) => c.kind === 'next');
|
|
241
|
-
const prev = controls.find((c) => c.kind === 'prev');
|
|
242
|
-
const loadMore = controls.find((c) => c.kind === 'loadMore');
|
|
243
|
-
const hasNext = !!(
|
|
244
|
-
(next && !next.disabled) ||
|
|
245
|
-
loadMore ||
|
|
246
|
-
(totalPages && page && page < totalPages)
|
|
247
|
-
);
|
|
248
|
-
const hasPrev = !!(
|
|
249
|
-
(prev && !prev.disabled) ||
|
|
250
|
-
(page && page > 1)
|
|
251
|
-
);
|
|
252
|
-
const kind = loadMore
|
|
253
|
-
? 'load-more'
|
|
254
|
-
: controls.some((c) => c.kind === 'page')
|
|
255
|
-
? 'numbered'
|
|
256
|
-
: next || prev
|
|
257
|
-
? 'prev-next'
|
|
258
|
-
: scope.querySelector('[class*="infinite"],[data-infinite]')
|
|
259
|
-
? 'infinite'
|
|
260
|
-
: 'none';
|
|
261
|
-
return {
|
|
262
|
-
kind,
|
|
263
|
-
...(page ? { page } : {}),
|
|
264
|
-
...(pageSize ? { pageSize } : {}),
|
|
265
|
-
...(total ? { total } : {}),
|
|
266
|
-
...(totalPages ? { totalPages } : {}),
|
|
267
|
-
hasNext,
|
|
268
|
-
hasPrev,
|
|
269
|
-
rowCount,
|
|
270
|
-
pageSizeOptions,
|
|
271
|
-
controls: controls.slice(0, 20),
|
|
272
|
-
};
|
|
273
|
-
}
|
|
129
|
+
return `
|
|
130
|
+
function detectPagination(scope) {
|
|
131
|
+
const clean = (v, cap) => String(v == null ? '' : v)
|
|
132
|
+
.replace(/\\s+/g, ' ')
|
|
133
|
+
.trim()
|
|
134
|
+
.slice(0, cap || 200);
|
|
135
|
+
const vis = (el) => {
|
|
136
|
+
if (!el || el.nodeType !== 1) return false;
|
|
137
|
+
const r = el.getBoundingClientRect();
|
|
138
|
+
if (r.width === 0 || r.height === 0) return false;
|
|
139
|
+
const cs = getComputedStyle(el);
|
|
140
|
+
return cs.visibility !== 'hidden' && cs.display !== 'none' && cs.opacity !== '0';
|
|
141
|
+
};
|
|
142
|
+
const rect = (el) => {
|
|
143
|
+
const r = el.getBoundingClientRect();
|
|
144
|
+
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
|
145
|
+
};
|
|
146
|
+
const controls = [];
|
|
147
|
+
const candidates = [...scope.querySelectorAll('button,a,[role="button"]')]
|
|
148
|
+
.filter(vis)
|
|
149
|
+
.slice(0, 200);
|
|
150
|
+
for (const el of candidates) {
|
|
151
|
+
const label = clean(
|
|
152
|
+
el.getAttribute('aria-label') ||
|
|
153
|
+
el.getAttribute('title') ||
|
|
154
|
+
el.innerText ||
|
|
155
|
+
el.textContent ||
|
|
156
|
+
'',
|
|
157
|
+
60,
|
|
158
|
+
);
|
|
159
|
+
const cls = String(el.className || '');
|
|
160
|
+
const aria = String(el.getAttribute('aria-current') || '');
|
|
161
|
+
const disabled = el.disabled === true ||
|
|
162
|
+
el.getAttribute('aria-disabled') === 'true' ||
|
|
163
|
+
/(?:^|[\\s_-])disabled(?:$|[\\s_-])/i.test(cls);
|
|
164
|
+
const pageMatch = label.match(/^(?:page\\s*)?(\\d{1,5})$/i);
|
|
165
|
+
let kind = null;
|
|
166
|
+
if (/^(next|next page|›|»|older|forward)$/i.test(label) || /next page/i.test(label)) kind = 'next';
|
|
167
|
+
else if (/^(prev|prev(ious)?|‹|«|newer|back)$/i.test(label) || /previous page/i.test(label)) kind = 'prev';
|
|
168
|
+
else if (/load more|show more|more results|see more/i.test(label)) kind = 'loadMore';
|
|
169
|
+
else if (pageMatch) kind = 'page';
|
|
170
|
+
if (!kind) continue;
|
|
171
|
+
controls.push({
|
|
172
|
+
label,
|
|
173
|
+
kind,
|
|
174
|
+
...(pageMatch ? { page: Number(pageMatch[1]) } : {}),
|
|
175
|
+
disabled,
|
|
176
|
+
current: aria === 'page' || /(?:^|[\\s_-])current(?:$|[\\s_-])/i.test(cls),
|
|
177
|
+
...rect(el),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let pageSize;
|
|
182
|
+
let pageSizeOptions = [];
|
|
183
|
+
for (const sel of [...scope.querySelectorAll('select')].filter(vis)) {
|
|
184
|
+
const opts = [...sel.options]
|
|
185
|
+
.map((o) => Number(String(o.textContent || o.label || '').trim()))
|
|
186
|
+
.filter((n) => Number.isFinite(n) && n > 0);
|
|
187
|
+
if (!opts.length) continue;
|
|
188
|
+
pageSize = Number(sel.value) || opts[0];
|
|
189
|
+
pageSizeOptions = [...new Set(opts)].sort((a, b) => a - b);
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const rows = [...scope.querySelectorAll('tbody tr,[role="row"]')]
|
|
194
|
+
.filter(vis)
|
|
195
|
+
.filter((r) => r.querySelectorAll('td').length > 0 || !r.querySelector('th'));
|
|
196
|
+
const rowCount = rows.length;
|
|
197
|
+
|
|
198
|
+
const pager = scope.querySelector(
|
|
199
|
+
'nav[aria-label*="pagination" i],[class*="pagination"],[class*="pager"],[role="navigation"]',
|
|
200
|
+
) || scope;
|
|
201
|
+
const text = clean(pager.innerText || pager.textContent || '', 3000);
|
|
202
|
+
let page;
|
|
203
|
+
let total;
|
|
204
|
+
let totalPages;
|
|
205
|
+
let m = text.match(/showing\\s+(\\d+)\\s*(?:to|-|–)\\s*(\\d+)\\s+of\\s+(\\d+)/i);
|
|
206
|
+
if (m) {
|
|
207
|
+
const start = Number(m[1]);
|
|
208
|
+
const end = Number(m[2]);
|
|
209
|
+
total = Number(m[3]);
|
|
210
|
+
pageSize = pageSize || Math.max(1, end - start + 1);
|
|
211
|
+
page = Math.floor((start - 1) / pageSize) + 1;
|
|
212
|
+
}
|
|
213
|
+
if (!total) {
|
|
214
|
+
m = text.match(/(\\d+)\\s*(?:-|–|to)\\s*(\\d+)\\s+of\\s+(\\d+)/i);
|
|
215
|
+
if (m) {
|
|
216
|
+
const start = Number(m[1]);
|
|
217
|
+
const end = Number(m[2]);
|
|
218
|
+
total = Number(m[3]);
|
|
219
|
+
pageSize = pageSize || Math.max(1, end - start + 1);
|
|
220
|
+
page = page || Math.floor((start - 1) / pageSize) + 1;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (!total) {
|
|
224
|
+
m = text.match(/(\\d+)\\s+results?\\b/i);
|
|
225
|
+
if (m) total = Number(m[1]);
|
|
226
|
+
}
|
|
227
|
+
if (!page) {
|
|
228
|
+
m = text.match(/page\\s+(\\d+)\\s+of\\s+(\\d+)/i);
|
|
229
|
+
if (m) {
|
|
230
|
+
page = Number(m[1]);
|
|
231
|
+
totalPages = Number(m[2]);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (!page) {
|
|
235
|
+
const currentPage = controls.find((c) => c.kind === 'page' && c.current);
|
|
236
|
+
if (currentPage) page = currentPage.page;
|
|
237
|
+
}
|
|
238
|
+
if (!totalPages && total && pageSize) totalPages = Math.ceil(total / pageSize);
|
|
239
|
+
|
|
240
|
+
const next = controls.find((c) => c.kind === 'next');
|
|
241
|
+
const prev = controls.find((c) => c.kind === 'prev');
|
|
242
|
+
const loadMore = controls.find((c) => c.kind === 'loadMore');
|
|
243
|
+
const hasNext = !!(
|
|
244
|
+
(next && !next.disabled) ||
|
|
245
|
+
loadMore ||
|
|
246
|
+
(totalPages && page && page < totalPages)
|
|
247
|
+
);
|
|
248
|
+
const hasPrev = !!(
|
|
249
|
+
(prev && !prev.disabled) ||
|
|
250
|
+
(page && page > 1)
|
|
251
|
+
);
|
|
252
|
+
const kind = loadMore
|
|
253
|
+
? 'load-more'
|
|
254
|
+
: controls.some((c) => c.kind === 'page')
|
|
255
|
+
? 'numbered'
|
|
256
|
+
: next || prev
|
|
257
|
+
? 'prev-next'
|
|
258
|
+
: scope.querySelector('[class*="infinite"],[data-infinite]')
|
|
259
|
+
? 'infinite'
|
|
260
|
+
: 'none';
|
|
261
|
+
return {
|
|
262
|
+
kind,
|
|
263
|
+
...(page ? { page } : {}),
|
|
264
|
+
...(pageSize ? { pageSize } : {}),
|
|
265
|
+
...(total ? { total } : {}),
|
|
266
|
+
...(totalPages ? { totalPages } : {}),
|
|
267
|
+
hasNext,
|
|
268
|
+
hasPrev,
|
|
269
|
+
rowCount,
|
|
270
|
+
pageSizeOptions,
|
|
271
|
+
controls: controls.slice(0, 20),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
274
|
`;
|
|
275
275
|
}
|
|
276
276
|
/**
|
|
@@ -305,248 +305,248 @@ function buildPageStateScript(opts = {}) {
|
|
|
305
305
|
const maxText = Math.min(Math.max(Number(opts.maxTextChars) || DEFAULT_TEXT_CAP, 500), 40_000);
|
|
306
306
|
const maxFields = Math.min(Math.max(Number(opts.maxFields) || MAX_FIELDS, 1), 100);
|
|
307
307
|
const errorSelectors = JSON.stringify(exports.PAGE_ERROR_SELECTORS);
|
|
308
|
-
return `
|
|
309
|
-
(() => {
|
|
310
|
-
const MAX_TEXT = ${maxText};
|
|
311
|
-
const MAX_FIELDS = ${maxFields};
|
|
312
|
-
const MAX_DIALOGS = ${MAX_DIALOGS};
|
|
313
|
-
const MAX_ERRORS = ${MAX_ERRORS};
|
|
314
|
-
const MAX_TOASTS = ${MAX_TOASTS};
|
|
315
|
-
const INCLUDE_TEXT = ${includeText ? 'true' : 'false'};
|
|
316
|
-
const ERROR_SEL = ${errorSelectors};
|
|
317
|
-
const detectPagination = ${paginationDetectSource()};
|
|
318
|
-
const detectMenus = ${menuDetectSource()};
|
|
319
|
-
|
|
320
|
-
const clean = (v, cap) => String(v == null ? '' : v)
|
|
321
|
-
.replace(/\\s+/g, ' ')
|
|
322
|
-
.trim()
|
|
323
|
-
.slice(0, cap || 300);
|
|
324
|
-
const vis = (el) => {
|
|
325
|
-
if (!el || el.nodeType !== 1) return false;
|
|
326
|
-
const r = el.getBoundingClientRect();
|
|
327
|
-
if (r.width === 0 || r.height === 0) return false;
|
|
328
|
-
const cs = getComputedStyle(el);
|
|
329
|
-
return cs.visibility !== 'hidden' && cs.display !== 'none' && cs.opacity !== '0';
|
|
330
|
-
};
|
|
331
|
-
const rect = (el) => {
|
|
332
|
-
const r = el.getBoundingClientRect();
|
|
333
|
-
return {
|
|
334
|
-
x: Math.round(r.x + r.width / 2),
|
|
335
|
-
y: Math.round(r.y + r.height / 2),
|
|
336
|
-
width: Math.round(r.width),
|
|
337
|
-
height: Math.round(r.height),
|
|
338
|
-
};
|
|
339
|
-
};
|
|
340
|
-
const labelOf = (el) => {
|
|
341
|
-
const aria = el.getAttribute('aria-label');
|
|
342
|
-
if (aria) return clean(aria, 120);
|
|
343
|
-
const by = el.getAttribute('aria-labelledby');
|
|
344
|
-
if (by) {
|
|
345
|
-
const t = by.split(/\\s+/).map((id) => {
|
|
346
|
-
const n = document.getElementById(id);
|
|
347
|
-
return n ? (n.innerText || n.textContent || '') : '';
|
|
348
|
-
}).join(' ');
|
|
349
|
-
if (t.trim()) return clean(t, 120);
|
|
350
|
-
}
|
|
351
|
-
const id = el.id;
|
|
352
|
-
if (id) {
|
|
353
|
-
try {
|
|
354
|
-
const l = document.querySelector('label[for="' + CSS.escape(id) + '"]');
|
|
355
|
-
if (l) return clean(l.innerText || l.textContent, 120);
|
|
356
|
-
} catch (e) {}
|
|
357
|
-
}
|
|
358
|
-
const wrap = el.closest && el.closest('label');
|
|
359
|
-
if (wrap) return clean(wrap.innerText || wrap.textContent, 120);
|
|
360
|
-
return clean(el.getAttribute('placeholder') || el.getAttribute('name') || el.id || '', 120);
|
|
361
|
-
};
|
|
362
|
-
const fieldOf = (el, inDialog) => {
|
|
363
|
-
const r = rect(el);
|
|
364
|
-
const type = String(el.type || el.tagName || '').toLowerCase();
|
|
365
|
-
const valueLength = typeof el.value === 'string' ? el.value.length : 0;
|
|
366
|
-
const invalid = el.getAttribute('aria-invalid') === 'true' ||
|
|
367
|
-
(typeof el.checkValidity === 'function' && !el.checkValidity() && valueLength > 0);
|
|
368
|
-
return {
|
|
369
|
-
tag: String(el.tagName || '').toLowerCase(),
|
|
370
|
-
type: type,
|
|
371
|
-
label: labelOf(el),
|
|
372
|
-
name: clean(el.getAttribute('name') || el.id || '', 80),
|
|
373
|
-
autocomplete: clean(el.getAttribute('autocomplete') || '', 60),
|
|
374
|
-
valueLength: valueLength,
|
|
375
|
-
required: el.required === true,
|
|
376
|
-
invalid: invalid,
|
|
377
|
-
disabled: el.disabled === true,
|
|
378
|
-
readonly: el.readOnly === true,
|
|
379
|
-
focused: document.activeElement === el,
|
|
380
|
-
inDialog: !!inDialog,
|
|
381
|
-
x: r.x,
|
|
382
|
-
y: r.y,
|
|
383
|
-
};
|
|
384
|
-
};
|
|
385
|
-
const buttonOf = (el) => {
|
|
386
|
-
const r = rect(el);
|
|
387
|
-
return {
|
|
388
|
-
text: clean(el.innerText || el.textContent || el.value || el.getAttribute('aria-label') || '', 100),
|
|
389
|
-
type: clean(el.getAttribute('type') || el.tagName || '', 30).toLowerCase(),
|
|
390
|
-
disabled: el.disabled === true || el.getAttribute('aria-disabled') === 'true',
|
|
391
|
-
x: r.x,
|
|
392
|
-
y: r.y,
|
|
393
|
-
};
|
|
394
|
-
};
|
|
395
|
-
const zOf = (el) => {
|
|
396
|
-
let z = 0;
|
|
397
|
-
let n = el;
|
|
398
|
-
let guard = 0;
|
|
399
|
-
while (n && n !== document.body && guard++ < 8) {
|
|
400
|
-
const cs = getComputedStyle(n);
|
|
401
|
-
const v = parseInt(cs.zIndex, 10);
|
|
402
|
-
if (!isNaN(v)) z = Math.max(z, v);
|
|
403
|
-
n = n.parentElement;
|
|
404
|
-
}
|
|
405
|
-
return z;
|
|
406
|
-
};
|
|
407
|
-
const dialogSel = 'dialog[open],[role="dialog"],[aria-modal="true"],' +
|
|
408
|
-
'[class*="modal"],[class*="Modal"],[class*="dialog"],[class*="Dialog"]';
|
|
409
|
-
const dialogEls = [...document.querySelectorAll(dialogSel)]
|
|
410
|
-
.filter(vis)
|
|
411
|
-
.slice(0, MAX_DIALOGS);
|
|
412
|
-
const dialogs = dialogEls.map((el, index) => {
|
|
413
|
-
const title =
|
|
414
|
-
clean(el.getAttribute('aria-label') || '', 120) ||
|
|
415
|
-
(() => {
|
|
416
|
-
const by = el.getAttribute('aria-labelledby');
|
|
417
|
-
if (by) {
|
|
418
|
-
const t = by.split(/\\s+/).map((id) => {
|
|
419
|
-
const n = document.getElementById(id);
|
|
420
|
-
return n ? (n.innerText || n.textContent || '') : '';
|
|
421
|
-
}).join(' ');
|
|
422
|
-
if (t.trim()) return clean(t, 120);
|
|
423
|
-
}
|
|
424
|
-
const h = el.querySelector('h1,h2,h3,[class*="title"],[class*="Title"]');
|
|
425
|
-
return h ? clean(h.innerText || h.textContent, 120) : '';
|
|
426
|
-
})();
|
|
427
|
-
const r = rect(el);
|
|
428
|
-
return {
|
|
429
|
-
index: index,
|
|
430
|
-
role: clean(el.getAttribute('role') || (el.tagName || '').toLowerCase(), 30),
|
|
431
|
-
title: title,
|
|
432
|
-
text: clean(el.innerText || el.textContent || '', 1600),
|
|
433
|
-
z: zOf(el),
|
|
434
|
-
x: r.x,
|
|
435
|
-
y: r.y,
|
|
436
|
-
width: r.width,
|
|
437
|
-
height: r.height,
|
|
438
|
-
fields: [...el.querySelectorAll('input,textarea,select')]
|
|
439
|
-
.filter(vis)
|
|
440
|
-
.filter((f) => String(f.type || '').toLowerCase() !== 'hidden')
|
|
441
|
-
.slice(0, MAX_FIELDS)
|
|
442
|
-
.map((f) => fieldOf(f, true)),
|
|
443
|
-
buttons: [...el.querySelectorAll('button,input[type="submit"],input[type="button"],[role="button"],a')]
|
|
444
|
-
.filter(vis)
|
|
445
|
-
.slice(0, 20)
|
|
446
|
-
.map(buttonOf),
|
|
447
|
-
};
|
|
448
|
-
}).sort((a, b) => (a.z - b.z) || (a.index - b.index));
|
|
449
|
-
// Prefer the dialog that actually holds a login field: a cookie banner
|
|
450
|
-
// or a newsletter modal can have a higher z-index, but it is not what
|
|
451
|
-
// the agent is interacting with.
|
|
452
|
-
const loginTop = [...dialogs].reverse().find((d) =>
|
|
453
|
-
d.fields.some(
|
|
454
|
-
(f) =>
|
|
455
|
-
f.type === 'password' ||
|
|
456
|
-
f.type === 'email' ||
|
|
457
|
-
/username|one-time-code|current-password|new-password/.test(
|
|
458
|
-
f.autocomplete || '',
|
|
459
|
-
),
|
|
460
|
-
),
|
|
461
|
-
);
|
|
462
|
-
const top = loginTop ?? (dialogs.length ? dialogs[dialogs.length - 1] : null);
|
|
463
|
-
const topEl = dialogEls[top ? top.index : -1] || null;
|
|
464
|
-
const scope = topEl || document;
|
|
465
|
-
const pagination = detectPagination(scope);
|
|
466
|
-
const menus = detectMenus(3, 12);
|
|
467
|
-
|
|
468
|
-
const fields = (top ? top.fields : [...document.querySelectorAll('input,textarea,select')]
|
|
469
|
-
.slice(0, 200)
|
|
470
|
-
.filter(vis)
|
|
471
|
-
.filter((f) => String(f.type || '').toLowerCase() !== 'hidden')
|
|
472
|
-
.slice(0, MAX_FIELDS)
|
|
473
|
-
.map((f) => fieldOf(f, false)));
|
|
474
|
-
|
|
475
|
-
const errors = [];
|
|
476
|
-
const pushError = (text, role) => {
|
|
477
|
-
const t = clean(text, 240);
|
|
478
|
-
if (t.length < 3) return;
|
|
479
|
-
if (/^(error|errors|warning|warnings)$/i.test(t)) return;
|
|
480
|
-
if (errors.some((e) => e.text === t)) return;
|
|
481
|
-
if (errors.length < MAX_ERRORS) errors.push({ text: t, role: clean(role || '', 30) });
|
|
482
|
-
};
|
|
483
|
-
// Alerts and invalid fields first, then the broad class-based sweep.
|
|
484
|
-
for (const el of [...scope.querySelectorAll('[role="alert"],[aria-invalid="true"]')].filter(vis).slice(0, 4)) {
|
|
485
|
-
pushError(el.innerText || el.textContent || el.getAttribute('aria-label') || '', el.getAttribute('role') || 'invalid');
|
|
486
|
-
}
|
|
487
|
-
for (const el of [...scope.querySelectorAll(ERROR_SEL)].filter(vis).slice(0, 20)) {
|
|
488
|
-
pushError(el.innerText || el.textContent || '', el.getAttribute('role') || 'error');
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const toasts = (window.__tsPageEvents || [])
|
|
492
|
-
.filter((e) => e && e.type !== 'dialog' && e.type !== 'dialog-closed')
|
|
493
|
-
.slice(-MAX_TOASTS);
|
|
494
|
-
|
|
495
|
-
const headings = [...document.querySelectorAll('h1,h2,h3')]
|
|
496
|
-
.filter(vis)
|
|
497
|
-
.map((h) => clean(h.innerText || h.textContent || '', 160))
|
|
498
|
-
.filter(Boolean)
|
|
499
|
-
.slice(0, 8);
|
|
500
|
-
|
|
501
|
-
const active = document.activeElement;
|
|
502
|
-
const focused = active && ['input', 'textarea', 'select'].includes(String(active.tagName || '').toLowerCase())
|
|
503
|
-
? {
|
|
504
|
-
tag: String(active.tagName || '').toLowerCase(),
|
|
505
|
-
type: String(active.type || '').toLowerCase(),
|
|
506
|
-
label: labelOf(active),
|
|
507
|
-
valueLength: typeof active.value === 'string' ? active.value.length : 0,
|
|
508
|
-
inDialog: !!(topEl && topEl.contains(active)),
|
|
509
|
-
}
|
|
510
|
-
: null;
|
|
511
|
-
|
|
512
|
-
const busy = [...document.querySelectorAll('[aria-busy="true"],[data-loading="true"],[class*="loading"],[class*="spinner"]')]
|
|
513
|
-
.some(vis);
|
|
514
|
-
|
|
515
|
-
let text = '';
|
|
516
|
-
let textTruncated = false;
|
|
517
|
-
if (INCLUDE_TEXT) {
|
|
518
|
-
const root = document.querySelector('main,article,[role="main"]') || document.body;
|
|
519
|
-
const raw = root ? (root.innerText || root.textContent || '') : '';
|
|
520
|
-
const normalized = raw
|
|
521
|
-
.replace(/\\r/g, '')
|
|
522
|
-
.replace(/[ \\t]+\\n/g, '\\n')
|
|
523
|
-
.replace(/\\n{3,}/g, '\\n\\n')
|
|
524
|
-
.trim();
|
|
525
|
-
textTruncated = normalized.length > MAX_TEXT;
|
|
526
|
-
text = textTruncated ? normalized.slice(0, MAX_TEXT) : normalized;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
return {
|
|
530
|
-
url: location.href,
|
|
531
|
-
title: document.title || '',
|
|
532
|
-
readyState: document.readyState || 'unknown',
|
|
533
|
-
headings: headings,
|
|
534
|
-
dialogs: dialogs,
|
|
535
|
-
menus: menus,
|
|
536
|
-
modal: !!top,
|
|
537
|
-
fields: fields,
|
|
538
|
-
focused: focused,
|
|
539
|
-
errors: errors,
|
|
540
|
-
toasts: toasts,
|
|
541
|
-
busy: busy,
|
|
542
|
-
scroll: { x: Math.round(window.scrollX || 0), y: Math.round(window.scrollY || 0) },
|
|
543
|
-
viewport: { width: Math.round(window.innerWidth || 0), height: Math.round(window.innerHeight || 0), dpr: window.devicePixelRatio || 1 },
|
|
544
|
-
pagination: pagination,
|
|
545
|
-
text: text,
|
|
546
|
-
textTruncated: textTruncated,
|
|
547
|
-
maxTextChars: MAX_TEXT,
|
|
548
|
-
};
|
|
549
|
-
})()
|
|
308
|
+
return `
|
|
309
|
+
(() => {
|
|
310
|
+
const MAX_TEXT = ${maxText};
|
|
311
|
+
const MAX_FIELDS = ${maxFields};
|
|
312
|
+
const MAX_DIALOGS = ${MAX_DIALOGS};
|
|
313
|
+
const MAX_ERRORS = ${MAX_ERRORS};
|
|
314
|
+
const MAX_TOASTS = ${MAX_TOASTS};
|
|
315
|
+
const INCLUDE_TEXT = ${includeText ? 'true' : 'false'};
|
|
316
|
+
const ERROR_SEL = ${errorSelectors};
|
|
317
|
+
const detectPagination = ${paginationDetectSource()};
|
|
318
|
+
const detectMenus = ${menuDetectSource()};
|
|
319
|
+
|
|
320
|
+
const clean = (v, cap) => String(v == null ? '' : v)
|
|
321
|
+
.replace(/\\s+/g, ' ')
|
|
322
|
+
.trim()
|
|
323
|
+
.slice(0, cap || 300);
|
|
324
|
+
const vis = (el) => {
|
|
325
|
+
if (!el || el.nodeType !== 1) return false;
|
|
326
|
+
const r = el.getBoundingClientRect();
|
|
327
|
+
if (r.width === 0 || r.height === 0) return false;
|
|
328
|
+
const cs = getComputedStyle(el);
|
|
329
|
+
return cs.visibility !== 'hidden' && cs.display !== 'none' && cs.opacity !== '0';
|
|
330
|
+
};
|
|
331
|
+
const rect = (el) => {
|
|
332
|
+
const r = el.getBoundingClientRect();
|
|
333
|
+
return {
|
|
334
|
+
x: Math.round(r.x + r.width / 2),
|
|
335
|
+
y: Math.round(r.y + r.height / 2),
|
|
336
|
+
width: Math.round(r.width),
|
|
337
|
+
height: Math.round(r.height),
|
|
338
|
+
};
|
|
339
|
+
};
|
|
340
|
+
const labelOf = (el) => {
|
|
341
|
+
const aria = el.getAttribute('aria-label');
|
|
342
|
+
if (aria) return clean(aria, 120);
|
|
343
|
+
const by = el.getAttribute('aria-labelledby');
|
|
344
|
+
if (by) {
|
|
345
|
+
const t = by.split(/\\s+/).map((id) => {
|
|
346
|
+
const n = document.getElementById(id);
|
|
347
|
+
return n ? (n.innerText || n.textContent || '') : '';
|
|
348
|
+
}).join(' ');
|
|
349
|
+
if (t.trim()) return clean(t, 120);
|
|
350
|
+
}
|
|
351
|
+
const id = el.id;
|
|
352
|
+
if (id) {
|
|
353
|
+
try {
|
|
354
|
+
const l = document.querySelector('label[for="' + CSS.escape(id) + '"]');
|
|
355
|
+
if (l) return clean(l.innerText || l.textContent, 120);
|
|
356
|
+
} catch (e) {}
|
|
357
|
+
}
|
|
358
|
+
const wrap = el.closest && el.closest('label');
|
|
359
|
+
if (wrap) return clean(wrap.innerText || wrap.textContent, 120);
|
|
360
|
+
return clean(el.getAttribute('placeholder') || el.getAttribute('name') || el.id || '', 120);
|
|
361
|
+
};
|
|
362
|
+
const fieldOf = (el, inDialog) => {
|
|
363
|
+
const r = rect(el);
|
|
364
|
+
const type = String(el.type || el.tagName || '').toLowerCase();
|
|
365
|
+
const valueLength = typeof el.value === 'string' ? el.value.length : 0;
|
|
366
|
+
const invalid = el.getAttribute('aria-invalid') === 'true' ||
|
|
367
|
+
(typeof el.checkValidity === 'function' && !el.checkValidity() && valueLength > 0);
|
|
368
|
+
return {
|
|
369
|
+
tag: String(el.tagName || '').toLowerCase(),
|
|
370
|
+
type: type,
|
|
371
|
+
label: labelOf(el),
|
|
372
|
+
name: clean(el.getAttribute('name') || el.id || '', 80),
|
|
373
|
+
autocomplete: clean(el.getAttribute('autocomplete') || '', 60),
|
|
374
|
+
valueLength: valueLength,
|
|
375
|
+
required: el.required === true,
|
|
376
|
+
invalid: invalid,
|
|
377
|
+
disabled: el.disabled === true,
|
|
378
|
+
readonly: el.readOnly === true,
|
|
379
|
+
focused: document.activeElement === el,
|
|
380
|
+
inDialog: !!inDialog,
|
|
381
|
+
x: r.x,
|
|
382
|
+
y: r.y,
|
|
383
|
+
};
|
|
384
|
+
};
|
|
385
|
+
const buttonOf = (el) => {
|
|
386
|
+
const r = rect(el);
|
|
387
|
+
return {
|
|
388
|
+
text: clean(el.innerText || el.textContent || el.value || el.getAttribute('aria-label') || '', 100),
|
|
389
|
+
type: clean(el.getAttribute('type') || el.tagName || '', 30).toLowerCase(),
|
|
390
|
+
disabled: el.disabled === true || el.getAttribute('aria-disabled') === 'true',
|
|
391
|
+
x: r.x,
|
|
392
|
+
y: r.y,
|
|
393
|
+
};
|
|
394
|
+
};
|
|
395
|
+
const zOf = (el) => {
|
|
396
|
+
let z = 0;
|
|
397
|
+
let n = el;
|
|
398
|
+
let guard = 0;
|
|
399
|
+
while (n && n !== document.body && guard++ < 8) {
|
|
400
|
+
const cs = getComputedStyle(n);
|
|
401
|
+
const v = parseInt(cs.zIndex, 10);
|
|
402
|
+
if (!isNaN(v)) z = Math.max(z, v);
|
|
403
|
+
n = n.parentElement;
|
|
404
|
+
}
|
|
405
|
+
return z;
|
|
406
|
+
};
|
|
407
|
+
const dialogSel = 'dialog[open],[role="dialog"],[aria-modal="true"],' +
|
|
408
|
+
'[class*="modal"],[class*="Modal"],[class*="dialog"],[class*="Dialog"]';
|
|
409
|
+
const dialogEls = [...document.querySelectorAll(dialogSel)]
|
|
410
|
+
.filter(vis)
|
|
411
|
+
.slice(0, MAX_DIALOGS);
|
|
412
|
+
const dialogs = dialogEls.map((el, index) => {
|
|
413
|
+
const title =
|
|
414
|
+
clean(el.getAttribute('aria-label') || '', 120) ||
|
|
415
|
+
(() => {
|
|
416
|
+
const by = el.getAttribute('aria-labelledby');
|
|
417
|
+
if (by) {
|
|
418
|
+
const t = by.split(/\\s+/).map((id) => {
|
|
419
|
+
const n = document.getElementById(id);
|
|
420
|
+
return n ? (n.innerText || n.textContent || '') : '';
|
|
421
|
+
}).join(' ');
|
|
422
|
+
if (t.trim()) return clean(t, 120);
|
|
423
|
+
}
|
|
424
|
+
const h = el.querySelector('h1,h2,h3,[class*="title"],[class*="Title"]');
|
|
425
|
+
return h ? clean(h.innerText || h.textContent, 120) : '';
|
|
426
|
+
})();
|
|
427
|
+
const r = rect(el);
|
|
428
|
+
return {
|
|
429
|
+
index: index,
|
|
430
|
+
role: clean(el.getAttribute('role') || (el.tagName || '').toLowerCase(), 30),
|
|
431
|
+
title: title,
|
|
432
|
+
text: clean(el.innerText || el.textContent || '', 1600),
|
|
433
|
+
z: zOf(el),
|
|
434
|
+
x: r.x,
|
|
435
|
+
y: r.y,
|
|
436
|
+
width: r.width,
|
|
437
|
+
height: r.height,
|
|
438
|
+
fields: [...el.querySelectorAll('input,textarea,select')]
|
|
439
|
+
.filter(vis)
|
|
440
|
+
.filter((f) => String(f.type || '').toLowerCase() !== 'hidden')
|
|
441
|
+
.slice(0, MAX_FIELDS)
|
|
442
|
+
.map((f) => fieldOf(f, true)),
|
|
443
|
+
buttons: [...el.querySelectorAll('button,input[type="submit"],input[type="button"],[role="button"],a')]
|
|
444
|
+
.filter(vis)
|
|
445
|
+
.slice(0, 20)
|
|
446
|
+
.map(buttonOf),
|
|
447
|
+
};
|
|
448
|
+
}).sort((a, b) => (a.z - b.z) || (a.index - b.index));
|
|
449
|
+
// Prefer the dialog that actually holds a login field: a cookie banner
|
|
450
|
+
// or a newsletter modal can have a higher z-index, but it is not what
|
|
451
|
+
// the agent is interacting with.
|
|
452
|
+
const loginTop = [...dialogs].reverse().find((d) =>
|
|
453
|
+
d.fields.some(
|
|
454
|
+
(f) =>
|
|
455
|
+
f.type === 'password' ||
|
|
456
|
+
f.type === 'email' ||
|
|
457
|
+
/username|one-time-code|current-password|new-password/.test(
|
|
458
|
+
f.autocomplete || '',
|
|
459
|
+
),
|
|
460
|
+
),
|
|
461
|
+
);
|
|
462
|
+
const top = loginTop ?? (dialogs.length ? dialogs[dialogs.length - 1] : null);
|
|
463
|
+
const topEl = dialogEls[top ? top.index : -1] || null;
|
|
464
|
+
const scope = topEl || document;
|
|
465
|
+
const pagination = detectPagination(scope);
|
|
466
|
+
const menus = detectMenus(3, 12);
|
|
467
|
+
|
|
468
|
+
const fields = (top ? top.fields : [...document.querySelectorAll('input,textarea,select')]
|
|
469
|
+
.slice(0, 200)
|
|
470
|
+
.filter(vis)
|
|
471
|
+
.filter((f) => String(f.type || '').toLowerCase() !== 'hidden')
|
|
472
|
+
.slice(0, MAX_FIELDS)
|
|
473
|
+
.map((f) => fieldOf(f, false)));
|
|
474
|
+
|
|
475
|
+
const errors = [];
|
|
476
|
+
const pushError = (text, role) => {
|
|
477
|
+
const t = clean(text, 240);
|
|
478
|
+
if (t.length < 3) return;
|
|
479
|
+
if (/^(error|errors|warning|warnings)$/i.test(t)) return;
|
|
480
|
+
if (errors.some((e) => e.text === t)) return;
|
|
481
|
+
if (errors.length < MAX_ERRORS) errors.push({ text: t, role: clean(role || '', 30) });
|
|
482
|
+
};
|
|
483
|
+
// Alerts and invalid fields first, then the broad class-based sweep.
|
|
484
|
+
for (const el of [...scope.querySelectorAll('[role="alert"],[aria-invalid="true"]')].filter(vis).slice(0, 4)) {
|
|
485
|
+
pushError(el.innerText || el.textContent || el.getAttribute('aria-label') || '', el.getAttribute('role') || 'invalid');
|
|
486
|
+
}
|
|
487
|
+
for (const el of [...scope.querySelectorAll(ERROR_SEL)].filter(vis).slice(0, 20)) {
|
|
488
|
+
pushError(el.innerText || el.textContent || '', el.getAttribute('role') || 'error');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const toasts = (window.__tsPageEvents || [])
|
|
492
|
+
.filter((e) => e && e.type !== 'dialog' && e.type !== 'dialog-closed')
|
|
493
|
+
.slice(-MAX_TOASTS);
|
|
494
|
+
|
|
495
|
+
const headings = [...document.querySelectorAll('h1,h2,h3')]
|
|
496
|
+
.filter(vis)
|
|
497
|
+
.map((h) => clean(h.innerText || h.textContent || '', 160))
|
|
498
|
+
.filter(Boolean)
|
|
499
|
+
.slice(0, 8);
|
|
500
|
+
|
|
501
|
+
const active = document.activeElement;
|
|
502
|
+
const focused = active && ['input', 'textarea', 'select'].includes(String(active.tagName || '').toLowerCase())
|
|
503
|
+
? {
|
|
504
|
+
tag: String(active.tagName || '').toLowerCase(),
|
|
505
|
+
type: String(active.type || '').toLowerCase(),
|
|
506
|
+
label: labelOf(active),
|
|
507
|
+
valueLength: typeof active.value === 'string' ? active.value.length : 0,
|
|
508
|
+
inDialog: !!(topEl && topEl.contains(active)),
|
|
509
|
+
}
|
|
510
|
+
: null;
|
|
511
|
+
|
|
512
|
+
const busy = [...document.querySelectorAll('[aria-busy="true"],[data-loading="true"],[class*="loading"],[class*="spinner"]')]
|
|
513
|
+
.some(vis);
|
|
514
|
+
|
|
515
|
+
let text = '';
|
|
516
|
+
let textTruncated = false;
|
|
517
|
+
if (INCLUDE_TEXT) {
|
|
518
|
+
const root = document.querySelector('main,article,[role="main"]') || document.body;
|
|
519
|
+
const raw = root ? (root.innerText || root.textContent || '') : '';
|
|
520
|
+
const normalized = raw
|
|
521
|
+
.replace(/\\r/g, '')
|
|
522
|
+
.replace(/[ \\t]+\\n/g, '\\n')
|
|
523
|
+
.replace(/\\n{3,}/g, '\\n\\n')
|
|
524
|
+
.trim();
|
|
525
|
+
textTruncated = normalized.length > MAX_TEXT;
|
|
526
|
+
text = textTruncated ? normalized.slice(0, MAX_TEXT) : normalized;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return {
|
|
530
|
+
url: location.href,
|
|
531
|
+
title: document.title || '',
|
|
532
|
+
readyState: document.readyState || 'unknown',
|
|
533
|
+
headings: headings,
|
|
534
|
+
dialogs: dialogs,
|
|
535
|
+
menus: menus,
|
|
536
|
+
modal: !!top,
|
|
537
|
+
fields: fields,
|
|
538
|
+
focused: focused,
|
|
539
|
+
errors: errors,
|
|
540
|
+
toasts: toasts,
|
|
541
|
+
busy: busy,
|
|
542
|
+
scroll: { x: Math.round(window.scrollX || 0), y: Math.round(window.scrollY || 0) },
|
|
543
|
+
viewport: { width: Math.round(window.innerWidth || 0), height: Math.round(window.innerHeight || 0), dpr: window.devicePixelRatio || 1 },
|
|
544
|
+
pagination: pagination,
|
|
545
|
+
text: text,
|
|
546
|
+
textTruncated: textTruncated,
|
|
547
|
+
maxTextChars: MAX_TEXT,
|
|
548
|
+
};
|
|
549
|
+
})()
|
|
550
550
|
`;
|
|
551
551
|
}
|
|
552
552
|
function emptyState() {
|
|
@@ -643,128 +643,128 @@ function compactPageState(state) {
|
|
|
643
643
|
*/
|
|
644
644
|
function buildCompactStateScript() {
|
|
645
645
|
const errorSelectors = JSON.stringify('[role="alert"],[aria-invalid="true"],[class*="field-error"],[class*="form-error"],[class*="error-message"],[class*="invalid-feedback"],[data-sonner-toast]');
|
|
646
|
-
return `
|
|
647
|
-
(() => {
|
|
648
|
-
const detectPagination = ${paginationDetectSource()};
|
|
649
|
-
const detectMenus = ${menuDetectSource()};
|
|
650
|
-
const clean = (v, cap) => String(v == null ? '' : v).replace(/\\s+/g, ' ').trim().slice(0, cap || 200);
|
|
651
|
-
const vis = (el) => {
|
|
652
|
-
if (!el || el.nodeType !== 1) return false;
|
|
653
|
-
const r = el.getBoundingClientRect();
|
|
654
|
-
if (r.width === 0 || r.height === 0) return false;
|
|
655
|
-
const cs = getComputedStyle(el);
|
|
656
|
-
return cs.visibility !== 'hidden' && cs.display !== 'none' && cs.opacity !== '0';
|
|
657
|
-
};
|
|
658
|
-
const rect = (el) => {
|
|
659
|
-
const r = el.getBoundingClientRect();
|
|
660
|
-
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
|
661
|
-
};
|
|
662
|
-
const labelOf = (el) => clean(
|
|
663
|
-
el.getAttribute('aria-label') ||
|
|
664
|
-
el.getAttribute('placeholder') ||
|
|
665
|
-
el.getAttribute('name') ||
|
|
666
|
-
el.id ||
|
|
667
|
-
'',
|
|
668
|
-
100,
|
|
669
|
-
);
|
|
670
|
-
const fieldOf = (el, inDialog) => {
|
|
671
|
-
const r = rect(el);
|
|
672
|
-
return {
|
|
673
|
-
tag: String(el.tagName || '').toLowerCase(),
|
|
674
|
-
type: String(el.type || el.tagName || '').toLowerCase(),
|
|
675
|
-
label: labelOf(el),
|
|
676
|
-
name: clean(el.getAttribute('name') || el.id || '', 80),
|
|
677
|
-
autocomplete: clean(el.getAttribute('autocomplete') || '', 60),
|
|
678
|
-
valueLength: typeof el.value === 'string' ? el.value.length : 0,
|
|
679
|
-
required: el.required === true,
|
|
680
|
-
invalid: el.getAttribute('aria-invalid') === 'true',
|
|
681
|
-
disabled: el.disabled === true,
|
|
682
|
-
readonly: el.readOnly === true,
|
|
683
|
-
focused: document.activeElement === el,
|
|
684
|
-
inDialog: !!inDialog,
|
|
685
|
-
x: r.x,
|
|
686
|
-
y: r.y,
|
|
687
|
-
};
|
|
688
|
-
};
|
|
689
|
-
const buttonOf = (el) => {
|
|
690
|
-
const r = rect(el);
|
|
691
|
-
return {
|
|
692
|
-
text: clean(el.innerText || el.textContent || el.value || el.getAttribute('aria-label') || '', 80),
|
|
693
|
-
type: clean(el.getAttribute('type') || el.tagName || '', 30).toLowerCase(),
|
|
694
|
-
disabled: el.disabled === true || el.getAttribute('aria-disabled') === 'true',
|
|
695
|
-
x: r.x,
|
|
696
|
-
y: r.y,
|
|
697
|
-
};
|
|
698
|
-
};
|
|
699
|
-
const dialogEls = [...document.querySelectorAll('dialog[open],[role="dialog"],[aria-modal="true"],[class*="modal"],[class*="Modal"],[class*="dialog"],[class*="Dialog"]')]
|
|
700
|
-
.filter(vis)
|
|
701
|
-
.slice(0, 3);
|
|
702
|
-
const loginTop = [...dialogEls].reverse().find((d) =>
|
|
703
|
-
d.querySelector('input[type="password"],input[type="email"],input[autocomplete="username"],input[autocomplete="one-time-code"]'),
|
|
704
|
-
);
|
|
705
|
-
const topEl = loginTop ||
|
|
706
|
-
[...dialogEls].reverse().find((d) => d.querySelector('input,textarea')) ||
|
|
707
|
-
dialogEls[dialogEls.length - 1] ||
|
|
708
|
-
null;
|
|
709
|
-
const top = topEl ? {
|
|
710
|
-
role: clean(topEl.getAttribute('role') || (topEl.tagName || '').toLowerCase(), 30),
|
|
711
|
-
title: clean(
|
|
712
|
-
topEl.getAttribute('aria-label') ||
|
|
713
|
-
(topEl.querySelector('h1,h2,h3') ? topEl.querySelector('h1,h2,h3').innerText : '') ||
|
|
714
|
-
'',
|
|
715
|
-
120,
|
|
716
|
-
),
|
|
717
|
-
text: clean(topEl.innerText || topEl.textContent || '', 300),
|
|
718
|
-
fields: [...topEl.querySelectorAll('input,textarea,select')]
|
|
719
|
-
.filter(vis)
|
|
720
|
-
.filter((f) => String(f.type || '').toLowerCase() !== 'hidden')
|
|
721
|
-
.slice(0, 10)
|
|
722
|
-
.map((f) => fieldOf(f, true)),
|
|
723
|
-
buttons: [...topEl.querySelectorAll('button,input[type="submit"],input[type="button"],[role="button"],a')]
|
|
724
|
-
.filter(vis)
|
|
725
|
-
.slice(0, 8)
|
|
726
|
-
.map(buttonOf),
|
|
727
|
-
} : null;
|
|
728
|
-
const scope = topEl || document;
|
|
729
|
-
const errors = [];
|
|
730
|
-
const pushError = (t, role) => {
|
|
731
|
-
const text = clean(t, 200);
|
|
732
|
-
if (text.length < 3 || errors.length >= 3) return;
|
|
733
|
-
if (/^(error|errors|warning|warnings)$/i.test(text)) return;
|
|
734
|
-
if (!errors.some((e) => e.text === text)) errors.push({ text: text, role: clean(role || '', 24) });
|
|
735
|
-
};
|
|
736
|
-
for (const el of [...scope.querySelectorAll('[role="alert"],[aria-invalid="true"]')].filter(vis).slice(0, 3)) {
|
|
737
|
-
pushError(el.innerText || el.textContent || el.getAttribute('aria-label') || '', el.getAttribute('role') || 'invalid');
|
|
738
|
-
}
|
|
739
|
-
for (const el of [...scope.querySelectorAll(${errorSelectors})].filter(vis).slice(0, 8)) {
|
|
740
|
-
pushError(el.innerText || el.textContent || '', el.getAttribute('role') || 'error');
|
|
741
|
-
}
|
|
742
|
-
const active = document.activeElement;
|
|
743
|
-
const focused = active && ['input', 'textarea', 'select'].includes(String(active.tagName || '').toLowerCase())
|
|
744
|
-
? { tag: String(active.tagName || '').toLowerCase(), type: String(active.type || '').toLowerCase(), label: labelOf(active), valueLength: typeof active.value === 'string' ? active.value.length : 0, inDialog: !!(topEl && topEl.contains(active)) }
|
|
745
|
-
: null;
|
|
746
|
-
return {
|
|
747
|
-
url: location.href,
|
|
748
|
-
title: document.title || '',
|
|
749
|
-
readyState: document.readyState || 'unknown',
|
|
750
|
-
modal: !!topEl,
|
|
751
|
-
dialogs: top ? [top] : [],
|
|
752
|
-
menus: detectMenus(2, 8),
|
|
753
|
-
fields: top ? top.fields : [...document.querySelectorAll('input,textarea,select')]
|
|
754
|
-
.slice(0, 120)
|
|
755
|
-
.filter(vis)
|
|
756
|
-
.filter((f) => String(f.type || '').toLowerCase() !== 'hidden')
|
|
757
|
-
.slice(0, 12)
|
|
758
|
-
.map((f) => fieldOf(f, false)),
|
|
759
|
-
focused: focused,
|
|
760
|
-
errors: errors,
|
|
761
|
-
toasts: (window.__tsPageEvents || []).slice(-4),
|
|
762
|
-
busy: [...document.querySelectorAll('[aria-busy="true"],[data-loading="true"],[class*="loading"],[class*="spinner"]')].some(vis),
|
|
763
|
-
scroll: { x: Math.round(window.scrollX || 0), y: Math.round(window.scrollY || 0) },
|
|
764
|
-
viewport: { width: Math.round(window.innerWidth || 0), height: Math.round(window.innerHeight || 0), dpr: window.devicePixelRatio || 1 },
|
|
765
|
-
pagination: detectPagination(topEl || document),
|
|
766
|
-
};
|
|
767
|
-
})()
|
|
646
|
+
return `
|
|
647
|
+
(() => {
|
|
648
|
+
const detectPagination = ${paginationDetectSource()};
|
|
649
|
+
const detectMenus = ${menuDetectSource()};
|
|
650
|
+
const clean = (v, cap) => String(v == null ? '' : v).replace(/\\s+/g, ' ').trim().slice(0, cap || 200);
|
|
651
|
+
const vis = (el) => {
|
|
652
|
+
if (!el || el.nodeType !== 1) return false;
|
|
653
|
+
const r = el.getBoundingClientRect();
|
|
654
|
+
if (r.width === 0 || r.height === 0) return false;
|
|
655
|
+
const cs = getComputedStyle(el);
|
|
656
|
+
return cs.visibility !== 'hidden' && cs.display !== 'none' && cs.opacity !== '0';
|
|
657
|
+
};
|
|
658
|
+
const rect = (el) => {
|
|
659
|
+
const r = el.getBoundingClientRect();
|
|
660
|
+
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
|
661
|
+
};
|
|
662
|
+
const labelOf = (el) => clean(
|
|
663
|
+
el.getAttribute('aria-label') ||
|
|
664
|
+
el.getAttribute('placeholder') ||
|
|
665
|
+
el.getAttribute('name') ||
|
|
666
|
+
el.id ||
|
|
667
|
+
'',
|
|
668
|
+
100,
|
|
669
|
+
);
|
|
670
|
+
const fieldOf = (el, inDialog) => {
|
|
671
|
+
const r = rect(el);
|
|
672
|
+
return {
|
|
673
|
+
tag: String(el.tagName || '').toLowerCase(),
|
|
674
|
+
type: String(el.type || el.tagName || '').toLowerCase(),
|
|
675
|
+
label: labelOf(el),
|
|
676
|
+
name: clean(el.getAttribute('name') || el.id || '', 80),
|
|
677
|
+
autocomplete: clean(el.getAttribute('autocomplete') || '', 60),
|
|
678
|
+
valueLength: typeof el.value === 'string' ? el.value.length : 0,
|
|
679
|
+
required: el.required === true,
|
|
680
|
+
invalid: el.getAttribute('aria-invalid') === 'true',
|
|
681
|
+
disabled: el.disabled === true,
|
|
682
|
+
readonly: el.readOnly === true,
|
|
683
|
+
focused: document.activeElement === el,
|
|
684
|
+
inDialog: !!inDialog,
|
|
685
|
+
x: r.x,
|
|
686
|
+
y: r.y,
|
|
687
|
+
};
|
|
688
|
+
};
|
|
689
|
+
const buttonOf = (el) => {
|
|
690
|
+
const r = rect(el);
|
|
691
|
+
return {
|
|
692
|
+
text: clean(el.innerText || el.textContent || el.value || el.getAttribute('aria-label') || '', 80),
|
|
693
|
+
type: clean(el.getAttribute('type') || el.tagName || '', 30).toLowerCase(),
|
|
694
|
+
disabled: el.disabled === true || el.getAttribute('aria-disabled') === 'true',
|
|
695
|
+
x: r.x,
|
|
696
|
+
y: r.y,
|
|
697
|
+
};
|
|
698
|
+
};
|
|
699
|
+
const dialogEls = [...document.querySelectorAll('dialog[open],[role="dialog"],[aria-modal="true"],[class*="modal"],[class*="Modal"],[class*="dialog"],[class*="Dialog"]')]
|
|
700
|
+
.filter(vis)
|
|
701
|
+
.slice(0, 3);
|
|
702
|
+
const loginTop = [...dialogEls].reverse().find((d) =>
|
|
703
|
+
d.querySelector('input[type="password"],input[type="email"],input[autocomplete="username"],input[autocomplete="one-time-code"]'),
|
|
704
|
+
);
|
|
705
|
+
const topEl = loginTop ||
|
|
706
|
+
[...dialogEls].reverse().find((d) => d.querySelector('input,textarea')) ||
|
|
707
|
+
dialogEls[dialogEls.length - 1] ||
|
|
708
|
+
null;
|
|
709
|
+
const top = topEl ? {
|
|
710
|
+
role: clean(topEl.getAttribute('role') || (topEl.tagName || '').toLowerCase(), 30),
|
|
711
|
+
title: clean(
|
|
712
|
+
topEl.getAttribute('aria-label') ||
|
|
713
|
+
(topEl.querySelector('h1,h2,h3') ? topEl.querySelector('h1,h2,h3').innerText : '') ||
|
|
714
|
+
'',
|
|
715
|
+
120,
|
|
716
|
+
),
|
|
717
|
+
text: clean(topEl.innerText || topEl.textContent || '', 300),
|
|
718
|
+
fields: [...topEl.querySelectorAll('input,textarea,select')]
|
|
719
|
+
.filter(vis)
|
|
720
|
+
.filter((f) => String(f.type || '').toLowerCase() !== 'hidden')
|
|
721
|
+
.slice(0, 10)
|
|
722
|
+
.map((f) => fieldOf(f, true)),
|
|
723
|
+
buttons: [...topEl.querySelectorAll('button,input[type="submit"],input[type="button"],[role="button"],a')]
|
|
724
|
+
.filter(vis)
|
|
725
|
+
.slice(0, 8)
|
|
726
|
+
.map(buttonOf),
|
|
727
|
+
} : null;
|
|
728
|
+
const scope = topEl || document;
|
|
729
|
+
const errors = [];
|
|
730
|
+
const pushError = (t, role) => {
|
|
731
|
+
const text = clean(t, 200);
|
|
732
|
+
if (text.length < 3 || errors.length >= 3) return;
|
|
733
|
+
if (/^(error|errors|warning|warnings)$/i.test(text)) return;
|
|
734
|
+
if (!errors.some((e) => e.text === text)) errors.push({ text: text, role: clean(role || '', 24) });
|
|
735
|
+
};
|
|
736
|
+
for (const el of [...scope.querySelectorAll('[role="alert"],[aria-invalid="true"]')].filter(vis).slice(0, 3)) {
|
|
737
|
+
pushError(el.innerText || el.textContent || el.getAttribute('aria-label') || '', el.getAttribute('role') || 'invalid');
|
|
738
|
+
}
|
|
739
|
+
for (const el of [...scope.querySelectorAll(${errorSelectors})].filter(vis).slice(0, 8)) {
|
|
740
|
+
pushError(el.innerText || el.textContent || '', el.getAttribute('role') || 'error');
|
|
741
|
+
}
|
|
742
|
+
const active = document.activeElement;
|
|
743
|
+
const focused = active && ['input', 'textarea', 'select'].includes(String(active.tagName || '').toLowerCase())
|
|
744
|
+
? { tag: String(active.tagName || '').toLowerCase(), type: String(active.type || '').toLowerCase(), label: labelOf(active), valueLength: typeof active.value === 'string' ? active.value.length : 0, inDialog: !!(topEl && topEl.contains(active)) }
|
|
745
|
+
: null;
|
|
746
|
+
return {
|
|
747
|
+
url: location.href,
|
|
748
|
+
title: document.title || '',
|
|
749
|
+
readyState: document.readyState || 'unknown',
|
|
750
|
+
modal: !!topEl,
|
|
751
|
+
dialogs: top ? [top] : [],
|
|
752
|
+
menus: detectMenus(2, 8),
|
|
753
|
+
fields: top ? top.fields : [...document.querySelectorAll('input,textarea,select')]
|
|
754
|
+
.slice(0, 120)
|
|
755
|
+
.filter(vis)
|
|
756
|
+
.filter((f) => String(f.type || '').toLowerCase() !== 'hidden')
|
|
757
|
+
.slice(0, 12)
|
|
758
|
+
.map((f) => fieldOf(f, false)),
|
|
759
|
+
focused: focused,
|
|
760
|
+
errors: errors,
|
|
761
|
+
toasts: (window.__tsPageEvents || []).slice(-4),
|
|
762
|
+
busy: [...document.querySelectorAll('[aria-busy="true"],[data-loading="true"],[class*="loading"],[class*="spinner"]')].some(vis),
|
|
763
|
+
scroll: { x: Math.round(window.scrollX || 0), y: Math.round(window.scrollY || 0) },
|
|
764
|
+
viewport: { width: Math.round(window.innerWidth || 0), height: Math.round(window.innerHeight || 0), dpr: window.devicePixelRatio || 1 },
|
|
765
|
+
pagination: detectPagination(topEl || document),
|
|
766
|
+
};
|
|
767
|
+
})()
|
|
768
768
|
`;
|
|
769
769
|
}
|
|
770
770
|
/** Read the fast compact state; null on a malformed result. */
|
|
@@ -854,27 +854,27 @@ function buildWaitProbeScript(opts) {
|
|
|
854
854
|
switch (opts.for) {
|
|
855
855
|
case 'selector': {
|
|
856
856
|
const sel = JSON.stringify(opts.selector ?? '');
|
|
857
|
-
return `(() => {
|
|
858
|
-
const vis = (el) => { if (!el) return false; const r = el.getBoundingClientRect(); if (r.width === 0 || r.height === 0) return false; const cs = getComputedStyle(el); return cs.visibility !== 'hidden' && cs.display !== 'none'; };
|
|
859
|
-
const el = document.querySelector(${sel});
|
|
860
|
-
return !!(el && vis(el));
|
|
857
|
+
return `(() => {
|
|
858
|
+
const vis = (el) => { if (!el) return false; const r = el.getBoundingClientRect(); if (r.width === 0 || r.height === 0) return false; const cs = getComputedStyle(el); return cs.visibility !== 'hidden' && cs.display !== 'none'; };
|
|
859
|
+
const el = document.querySelector(${sel});
|
|
860
|
+
return !!(el && vis(el));
|
|
861
861
|
})()`;
|
|
862
862
|
}
|
|
863
863
|
case 'text': {
|
|
864
864
|
const needle = JSON.stringify((opts.text ?? '').toLowerCase());
|
|
865
|
-
return `(() => {
|
|
866
|
-
const body = document.body;
|
|
867
|
-
if (!body) return false;
|
|
868
|
-
const t = (body.innerText || body.textContent || '').toLowerCase();
|
|
869
|
-
return t.includes(${needle});
|
|
865
|
+
return `(() => {
|
|
866
|
+
const body = document.body;
|
|
867
|
+
if (!body) return false;
|
|
868
|
+
const t = (body.innerText || body.textContent || '').toLowerCase();
|
|
869
|
+
return t.includes(${needle});
|
|
870
870
|
})()`;
|
|
871
871
|
}
|
|
872
872
|
case 'dialog': {
|
|
873
873
|
const wantClosed = opts.state === 'closed';
|
|
874
|
-
return `(() => {
|
|
875
|
-
const vis = (el) => { if (!el) return false; const r = el.getBoundingClientRect(); if (r.width === 0 || r.height === 0) return false; const cs = getComputedStyle(el); return cs.visibility !== 'hidden' && cs.display !== 'none'; };
|
|
876
|
-
const n = [...document.querySelectorAll('dialog[open],[role="dialog"],[aria-modal="true"]')].filter(vis).length;
|
|
877
|
-
return ${wantClosed ? 'n === 0' : 'n > 0'};
|
|
874
|
+
return `(() => {
|
|
875
|
+
const vis = (el) => { if (!el) return false; const r = el.getBoundingClientRect(); if (r.width === 0 || r.height === 0) return false; const cs = getComputedStyle(el); return cs.visibility !== 'hidden' && cs.display !== 'none'; };
|
|
876
|
+
const n = [...document.querySelectorAll('dialog[open],[role="dialog"],[aria-modal="true"]')].filter(vis).length;
|
|
877
|
+
return ${wantClosed ? 'n === 0' : 'n > 0'};
|
|
878
878
|
})()`;
|
|
879
879
|
}
|
|
880
880
|
case 'popover': {
|
|
@@ -882,20 +882,20 @@ function buildWaitProbeScript(opts) {
|
|
|
882
882
|
// "closed"` waits for the layer to disappear - the check after choosing
|
|
883
883
|
// a menu item, where a still-open menu usually means the click missed.
|
|
884
884
|
const wantClosed = opts.state === 'closed';
|
|
885
|
-
return `(() => {
|
|
886
|
-
const vis = (el) => { if (!el) return false; const r = el.getBoundingClientRect(); if (r.width === 0 || r.height === 0) return false; const cs = getComputedStyle(el); return cs.visibility !== 'hidden' && cs.display !== 'none'; };
|
|
887
|
-
const n = [...document.querySelectorAll(${JSON.stringify(exports.POPOVER_SELECTOR)})].filter(vis).length;
|
|
888
|
-
return ${wantClosed ? 'n === 0' : 'n > 0'};
|
|
885
|
+
return `(() => {
|
|
886
|
+
const vis = (el) => { if (!el) return false; const r = el.getBoundingClientRect(); if (r.width === 0 || r.height === 0) return false; const cs = getComputedStyle(el); return cs.visibility !== 'hidden' && cs.display !== 'none'; };
|
|
887
|
+
const n = [...document.querySelectorAll(${JSON.stringify(exports.POPOVER_SELECTOR)})].filter(vis).length;
|
|
888
|
+
return ${wantClosed ? 'n === 0' : 'n > 0'};
|
|
889
889
|
})()`;
|
|
890
890
|
}
|
|
891
891
|
case 'toast': {
|
|
892
892
|
const needle = JSON.stringify((opts.text ?? '').toLowerCase());
|
|
893
|
-
return `(() => {
|
|
894
|
-
const ev = (window.__tsPageEvents || []).filter((e) => e && e.at >= ${since} &&
|
|
895
|
-
(e.type === 'toast' || e.type === 'alert' || e.type === 'status' || e.type === 'error'));
|
|
896
|
-
const needle = ${needle};
|
|
897
|
-
if (needle) return ev.some((e) => String(e.text || '').toLowerCase().includes(needle));
|
|
898
|
-
return ev.length > 0;
|
|
893
|
+
return `(() => {
|
|
894
|
+
const ev = (window.__tsPageEvents || []).filter((e) => e && e.at >= ${since} &&
|
|
895
|
+
(e.type === 'toast' || e.type === 'alert' || e.type === 'status' || e.type === 'error'));
|
|
896
|
+
const needle = ${needle};
|
|
897
|
+
if (needle) return ev.some((e) => String(e.text || '').toLowerCase().includes(needle));
|
|
898
|
+
return ev.length > 0;
|
|
899
899
|
})()`;
|
|
900
900
|
}
|
|
901
901
|
case 'url': {
|