@vibecheckai/cli 3.2.2 → 3.2.4
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/bin/.generated +25 -25
- package/bin/dev/run-v2-torture.js +30 -30
- package/bin/runners/ENHANCEMENT_GUIDE.md +121 -121
- package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -295
- package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +474 -0
- package/bin/runners/lib/agent-firewall/claims/extractor.js +117 -28
- package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +23 -14
- package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +72 -1
- package/bin/runners/lib/agent-firewall/interceptor/base.js +2 -2
- package/bin/runners/lib/agent-firewall/policy/default-policy.json +6 -0
- package/bin/runners/lib/agent-firewall/policy/engine.js +34 -3
- package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +29 -4
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +12 -0
- package/bin/runners/lib/agent-firewall/truthpack/loader.js +21 -0
- package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +118 -0
- package/bin/runners/lib/analyzers.js +606 -325
- package/bin/runners/lib/auth-truth.js +193 -193
- package/bin/runners/lib/backup.js +62 -62
- package/bin/runners/lib/billing.js +107 -107
- package/bin/runners/lib/claims.js +118 -118
- package/bin/runners/lib/cli-ui.js +540 -540
- package/bin/runners/lib/contracts/auth-contract.js +202 -202
- package/bin/runners/lib/contracts/env-contract.js +181 -181
- package/bin/runners/lib/contracts/external-contract.js +206 -206
- package/bin/runners/lib/contracts/guard.js +168 -168
- package/bin/runners/lib/contracts/index.js +89 -89
- package/bin/runners/lib/contracts/plan-validator.js +311 -311
- package/bin/runners/lib/contracts/route-contract.js +199 -199
- package/bin/runners/lib/contracts.js +804 -804
- package/bin/runners/lib/detect.js +89 -89
- package/bin/runners/lib/doctor/autofix.js +254 -254
- package/bin/runners/lib/doctor/index.js +37 -37
- package/bin/runners/lib/doctor/modules/dependencies.js +325 -325
- package/bin/runners/lib/doctor/modules/index.js +46 -46
- package/bin/runners/lib/doctor/modules/network.js +250 -250
- package/bin/runners/lib/doctor/modules/project.js +312 -312
- package/bin/runners/lib/doctor/modules/runtime.js +224 -224
- package/bin/runners/lib/doctor/modules/security.js +348 -348
- package/bin/runners/lib/doctor/modules/system.js +213 -213
- package/bin/runners/lib/doctor/modules/vibecheck.js +394 -394
- package/bin/runners/lib/doctor/reporter.js +262 -262
- package/bin/runners/lib/doctor/service.js +262 -262
- package/bin/runners/lib/doctor/types.js +113 -113
- package/bin/runners/lib/doctor/ui.js +263 -263
- package/bin/runners/lib/doctor-v2.js +608 -608
- package/bin/runners/lib/drift.js +425 -425
- package/bin/runners/lib/enforcement.js +72 -72
- package/bin/runners/lib/engines/accessibility-engine.js +190 -0
- package/bin/runners/lib/engines/api-consistency-engine.js +162 -0
- package/bin/runners/lib/engines/ast-cache.js +99 -0
- package/bin/runners/lib/engines/code-quality-engine.js +255 -0
- package/bin/runners/lib/engines/console-logs-engine.js +115 -0
- package/bin/runners/lib/engines/cross-file-analysis-engine.js +268 -0
- package/bin/runners/lib/engines/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/deprecated-api-engine.js +226 -0
- package/bin/runners/lib/engines/empty-catch-engine.js +150 -0
- package/bin/runners/lib/engines/file-filter.js +131 -0
- package/bin/runners/lib/engines/hardcoded-secrets-engine.js +251 -0
- package/bin/runners/lib/engines/mock-data-engine.js +272 -0
- package/bin/runners/lib/engines/parallel-processor.js +71 -0
- package/bin/runners/lib/engines/performance-issues-engine.js +265 -0
- package/bin/runners/lib/engines/security-vulnerabilities-engine.js +243 -0
- package/bin/runners/lib/engines/todo-fixme-engine.js +115 -0
- package/bin/runners/lib/engines/type-aware-engine.js +152 -0
- package/bin/runners/lib/engines/unsafe-regex-engine.js +225 -0
- package/bin/runners/lib/engines/vibecheck-engines/README.md +53 -0
- package/bin/runners/lib/engines/vibecheck-engines/index.js +15 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +139 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
- package/bin/runners/lib/engines/vibecheck-engines/package.json +13 -0
- package/bin/runners/lib/enterprise-detect.js +603 -603
- package/bin/runners/lib/enterprise-init.js +942 -942
- package/bin/runners/lib/env-resolver.js +417 -417
- package/bin/runners/lib/env-template.js +66 -66
- package/bin/runners/lib/env.js +189 -189
- package/bin/runners/lib/extractors/client-calls.js +990 -990
- package/bin/runners/lib/extractors/fastify-route-dump.js +573 -573
- package/bin/runners/lib/extractors/fastify-routes.js +426 -426
- package/bin/runners/lib/extractors/index.js +363 -363
- package/bin/runners/lib/extractors/next-routes.js +524 -524
- package/bin/runners/lib/extractors/proof-graph.js +431 -431
- package/bin/runners/lib/extractors/route-matcher.js +451 -451
- package/bin/runners/lib/extractors/truthpack-v2.js +377 -377
- package/bin/runners/lib/extractors/ui-bindings.js +547 -547
- package/bin/runners/lib/findings-schema.js +281 -281
- package/bin/runners/lib/firewall-prompt.js +50 -50
- package/bin/runners/lib/global-flags.js +213 -213
- package/bin/runners/lib/graph/graph-builder.js +265 -265
- package/bin/runners/lib/graph/html-renderer.js +413 -413
- package/bin/runners/lib/graph/index.js +32 -32
- package/bin/runners/lib/graph/runtime-collector.js +215 -215
- package/bin/runners/lib/graph/static-extractor.js +518 -518
- package/bin/runners/lib/html-report.js +650 -650
- package/bin/runners/lib/interactive-menu.js +1496 -1496
- package/bin/runners/lib/llm.js +75 -75
- package/bin/runners/lib/meter.js +61 -61
- package/bin/runners/lib/missions/evidence.js +126 -126
- package/bin/runners/lib/patch.js +40 -40
- package/bin/runners/lib/permissions/auth-model.js +213 -213
- package/bin/runners/lib/permissions/idor-prover.js +205 -205
- package/bin/runners/lib/permissions/index.js +45 -45
- package/bin/runners/lib/permissions/matrix-builder.js +198 -198
- package/bin/runners/lib/pkgjson.js +28 -28
- package/bin/runners/lib/policy.js +295 -295
- package/bin/runners/lib/preflight.js +142 -142
- package/bin/runners/lib/reality/correlation-detectors.js +359 -359
- package/bin/runners/lib/reality/index.js +318 -318
- package/bin/runners/lib/reality/request-hashing.js +416 -416
- package/bin/runners/lib/reality/request-mapper.js +453 -453
- package/bin/runners/lib/reality/safety-rails.js +463 -463
- package/bin/runners/lib/reality/semantic-snapshot.js +408 -408
- package/bin/runners/lib/reality/toast-detector.js +393 -393
- package/bin/runners/lib/reality-findings.js +84 -84
- package/bin/runners/lib/receipts.js +179 -179
- package/bin/runners/lib/redact.js +29 -29
- package/bin/runners/lib/replay/capsule-manager.js +154 -154
- package/bin/runners/lib/replay/index.js +263 -263
- package/bin/runners/lib/replay/player.js +348 -348
- package/bin/runners/lib/replay/recorder.js +331 -331
- package/bin/runners/lib/report-output.js +187 -187
- package/bin/runners/lib/report.js +135 -135
- package/bin/runners/lib/route-detection.js +1140 -1140
- package/bin/runners/lib/sandbox/index.js +59 -59
- package/bin/runners/lib/sandbox/proof-chain.js +399 -399
- package/bin/runners/lib/sandbox/sandbox-runner.js +205 -205
- package/bin/runners/lib/sandbox/worktree.js +174 -174
- package/bin/runners/lib/scan-output.js +525 -190
- package/bin/runners/lib/schema-validator.js +350 -350
- package/bin/runners/lib/schemas/contracts.schema.json +160 -160
- package/bin/runners/lib/schemas/finding.schema.json +100 -100
- package/bin/runners/lib/schemas/mission-pack.schema.json +206 -206
- package/bin/runners/lib/schemas/proof-graph.schema.json +176 -176
- package/bin/runners/lib/schemas/reality-report.schema.json +162 -162
- package/bin/runners/lib/schemas/share-pack.schema.json +180 -180
- package/bin/runners/lib/schemas/ship-report.schema.json +117 -117
- package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -303
- package/bin/runners/lib/schemas/validator.js +438 -438
- package/bin/runners/lib/score-history.js +282 -282
- package/bin/runners/lib/share-pack.js +239 -239
- package/bin/runners/lib/snippets.js +67 -67
- package/bin/runners/lib/status-output.js +253 -253
- package/bin/runners/lib/terminal-ui.js +351 -271
- package/bin/runners/lib/upsell.js +510 -510
- package/bin/runners/lib/usage.js +153 -153
- package/bin/runners/lib/validate-patch.js +156 -156
- package/bin/runners/lib/verdict-engine.js +628 -628
- package/bin/runners/reality/engine.js +917 -917
- package/bin/runners/reality/flows.js +122 -122
- package/bin/runners/reality/report.js +378 -378
- package/bin/runners/reality/session.js +193 -193
- package/bin/runners/runGuard.js +168 -168
- package/bin/runners/runProof.zip +0 -0
- package/bin/runners/runProve.js +8 -0
- package/bin/runners/runReality.js +14 -0
- package/bin/runners/runScan.js +17 -1
- package/bin/runners/runTruth.js +15 -3
- package/mcp-server/tier-auth.js +4 -4
- package/mcp-server/tools/index.js +72 -72
- package/package.json +1 -1
|
@@ -1,393 +1,393 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Toast/Notification Detector v2
|
|
3
|
-
*
|
|
4
|
-
* Detects toasts via DOM observation with library-specific selectors.
|
|
5
|
-
* Emits signals: toast_success, toast_error, toast_info, toast_unknown
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
"use strict";
|
|
9
|
-
|
|
10
|
-
// =============================================================================
|
|
11
|
-
// LIBRARY-SPECIFIC SELECTORS
|
|
12
|
-
// =============================================================================
|
|
13
|
-
|
|
14
|
-
const TOAST_LIBRARIES = {
|
|
15
|
-
sonner: {
|
|
16
|
-
name: "sonner",
|
|
17
|
-
container: "[data-sonner-toaster]",
|
|
18
|
-
toast: "[data-sonner-toast]",
|
|
19
|
-
typeAttr: "data-type",
|
|
20
|
-
},
|
|
21
|
-
reactHotToast: {
|
|
22
|
-
name: "react-hot-toast",
|
|
23
|
-
container: "#_rht_toaster, .react-hot-toast",
|
|
24
|
-
toast: "[aria-live]",
|
|
25
|
-
},
|
|
26
|
-
reactToastify: {
|
|
27
|
-
name: "react-toastify",
|
|
28
|
-
container: ".Toastify__toast-container",
|
|
29
|
-
toast: ".Toastify__toast",
|
|
30
|
-
successClass: "Toastify__toast--success",
|
|
31
|
-
errorClass: "Toastify__toast--error",
|
|
32
|
-
infoClass: "Toastify__toast--info",
|
|
33
|
-
warningClass: "Toastify__toast--warning",
|
|
34
|
-
},
|
|
35
|
-
radix: {
|
|
36
|
-
name: "radix",
|
|
37
|
-
container: "[data-radix-toast-viewport]",
|
|
38
|
-
toast: "[data-radix-toast-root]",
|
|
39
|
-
stateAttr: "data-state",
|
|
40
|
-
},
|
|
41
|
-
mantine: {
|
|
42
|
-
name: "mantine",
|
|
43
|
-
container: ".mantine-Notifications-root",
|
|
44
|
-
toast: ".mantine-Notification-root",
|
|
45
|
-
},
|
|
46
|
-
chakra: {
|
|
47
|
-
name: "chakra",
|
|
48
|
-
container: "[id^='chakra-toast-manager']",
|
|
49
|
-
toast: "[id^='chakra-toast']",
|
|
50
|
-
},
|
|
51
|
-
mui: {
|
|
52
|
-
name: "mui",
|
|
53
|
-
container: ".MuiSnackbar-root",
|
|
54
|
-
toast: ".MuiSnackbarContent-root, .MuiAlert-root",
|
|
55
|
-
successClass: "MuiAlert-standardSuccess",
|
|
56
|
-
errorClass: "MuiAlert-standardError",
|
|
57
|
-
infoClass: "MuiAlert-standardInfo",
|
|
58
|
-
warningClass: "MuiAlert-standardWarning",
|
|
59
|
-
},
|
|
60
|
-
notistack: {
|
|
61
|
-
name: "notistack",
|
|
62
|
-
container: ".SnackbarContainer-root",
|
|
63
|
-
toast: "#notistack-snackbar",
|
|
64
|
-
},
|
|
65
|
-
antd: {
|
|
66
|
-
name: "antd",
|
|
67
|
-
container: ".ant-message, .ant-notification",
|
|
68
|
-
toast: ".ant-message-notice, .ant-notification-notice",
|
|
69
|
-
successClass: "ant-message-success, ant-notification-notice-success",
|
|
70
|
-
errorClass: "ant-message-error, ant-notification-notice-error",
|
|
71
|
-
},
|
|
72
|
-
bootstrap: {
|
|
73
|
-
name: "bootstrap",
|
|
74
|
-
container: ".toast-container",
|
|
75
|
-
toast: ".toast[aria-live]",
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
// Universal heuristic selectors
|
|
80
|
-
const UNIVERSAL_SELECTORS = {
|
|
81
|
-
ariaLive: '[aria-live="polite"], [aria-live="assertive"]',
|
|
82
|
-
roleAlert: '[role="alert"]',
|
|
83
|
-
roleStatus: '[role="status"]',
|
|
84
|
-
ariaAtomic: '[aria-atomic="true"]',
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
// Class tokens that indicate toast
|
|
88
|
-
const TOAST_CLASS_TOKENS = [
|
|
89
|
-
"toast", "snackbar", "notification", "noti", "sonner",
|
|
90
|
-
"Toastify", "chakra-toast", "mantine-Notification",
|
|
91
|
-
"MuiSnackbar", "ant-message", "ant-notification", "notistack"
|
|
92
|
-
];
|
|
93
|
-
|
|
94
|
-
// Success/error classification tokens
|
|
95
|
-
const SUCCESS_TOKENS = ["success", "check", "done", "saved", "complete", "✓", "✅"];
|
|
96
|
-
const ERROR_TOKENS = ["error", "fail", "warning", "alert", "danger", "❌", "⚠"];
|
|
97
|
-
const INFO_TOKENS = ["info", "notice", "ℹ"];
|
|
98
|
-
|
|
99
|
-
// =============================================================================
|
|
100
|
-
// TOAST DETECTOR SCRIPT (runs in browser context)
|
|
101
|
-
// =============================================================================
|
|
102
|
-
|
|
103
|
-
const TOAST_DETECTOR_SCRIPT = `
|
|
104
|
-
(function setupToastDetector(options = {}) {
|
|
105
|
-
const {
|
|
106
|
-
maxLifetimeMs = 15000,
|
|
107
|
-
captureScreenshots = true,
|
|
108
|
-
} = options;
|
|
109
|
-
|
|
110
|
-
const TOAST_CLASS_TOKENS = ${JSON.stringify(TOAST_CLASS_TOKENS)};
|
|
111
|
-
const SUCCESS_TOKENS = ${JSON.stringify(SUCCESS_TOKENS)};
|
|
112
|
-
const ERROR_TOKENS = ${JSON.stringify(ERROR_TOKENS)};
|
|
113
|
-
const INFO_TOKENS = ${JSON.stringify(INFO_TOKENS)};
|
|
114
|
-
|
|
115
|
-
const signals = [];
|
|
116
|
-
const seenToasts = new Set();
|
|
117
|
-
|
|
118
|
-
function normalizeText(text) {
|
|
119
|
-
return (text || '').trim().replace(/\\s+/g, ' ').slice(0, 200);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function classifyToast(el) {
|
|
123
|
-
const classes = el.className?.toLowerCase() || '';
|
|
124
|
-
const text = normalizeText(el.textContent);
|
|
125
|
-
const dataType = el.getAttribute('data-type')?.toLowerCase();
|
|
126
|
-
|
|
127
|
-
// Check data-type first (most reliable)
|
|
128
|
-
if (dataType) {
|
|
129
|
-
if (SUCCESS_TOKENS.some(t => dataType.includes(t))) return 'toast_success';
|
|
130
|
-
if (ERROR_TOKENS.some(t => dataType.includes(t))) return 'toast_error';
|
|
131
|
-
if (INFO_TOKENS.some(t => dataType.includes(t))) return 'toast_info';
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Check classes
|
|
135
|
-
if (SUCCESS_TOKENS.some(t => classes.includes(t))) return 'toast_success';
|
|
136
|
-
if (ERROR_TOKENS.some(t => classes.includes(t))) return 'toast_error';
|
|
137
|
-
if (INFO_TOKENS.some(t => classes.includes(t))) return 'toast_info';
|
|
138
|
-
|
|
139
|
-
// Check text content
|
|
140
|
-
const textLower = text.toLowerCase();
|
|
141
|
-
if (SUCCESS_TOKENS.some(t => textLower.includes(t))) return 'toast_success';
|
|
142
|
-
if (ERROR_TOKENS.some(t => textLower.startsWith(t))) return 'toast_error';
|
|
143
|
-
|
|
144
|
-
// Check for icon aria-labels
|
|
145
|
-
const icons = el.querySelectorAll('[aria-label]');
|
|
146
|
-
for (const icon of icons) {
|
|
147
|
-
const label = icon.getAttribute('aria-label')?.toLowerCase();
|
|
148
|
-
if (label && SUCCESS_TOKENS.some(t => label.includes(t))) return 'toast_success';
|
|
149
|
-
if (label && ERROR_TOKENS.some(t => label.includes(t))) return 'toast_error';
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return 'toast_unknown';
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function detectLibrary(el) {
|
|
156
|
-
// Check specific library markers
|
|
157
|
-
if (el.closest('[data-sonner-toaster]')) return 'sonner';
|
|
158
|
-
if (el.closest('.Toastify__toast-container')) return 'react-toastify';
|
|
159
|
-
if (el.closest('[data-radix-toast-viewport]')) return 'radix';
|
|
160
|
-
if (el.closest('.mantine-Notifications-root')) return 'mantine';
|
|
161
|
-
if (el.closest('[id^="chakra-toast"]')) return 'chakra';
|
|
162
|
-
if (el.closest('.MuiSnackbar-root')) return 'mui';
|
|
163
|
-
if (el.closest('.SnackbarContainer-root')) return 'notistack';
|
|
164
|
-
if (el.closest('.ant-message, .ant-notification')) return 'antd';
|
|
165
|
-
return 'unknown';
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function isToastCandidate(el) {
|
|
169
|
-
// Skip hidden elements
|
|
170
|
-
const style = window.getComputedStyle(el);
|
|
171
|
-
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
|
172
|
-
if (style.opacity === '0') return false;
|
|
173
|
-
|
|
174
|
-
// Check ARIA roles
|
|
175
|
-
const role = el.getAttribute('role');
|
|
176
|
-
if (role === 'alert' || role === 'status') return true;
|
|
177
|
-
if (el.getAttribute('aria-live')) return true;
|
|
178
|
-
if (el.getAttribute('aria-atomic') === 'true') return true;
|
|
179
|
-
|
|
180
|
-
// Check class tokens
|
|
181
|
-
const classes = el.className?.toLowerCase() || '';
|
|
182
|
-
if (TOAST_CLASS_TOKENS.some(t => classes.includes(t.toLowerCase()))) return true;
|
|
183
|
-
|
|
184
|
-
// Check positioning (fixed/sticky near edges)
|
|
185
|
-
if (style.position === 'fixed' || style.position === 'sticky') {
|
|
186
|
-
const rect = el.getBoundingClientRect();
|
|
187
|
-
const isNearEdge = rect.top < 100 || rect.bottom > window.innerHeight - 100;
|
|
188
|
-
if (isNearEdge && el.textContent?.trim().length > 0) return true;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return false;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function generateToastId(el) {
|
|
195
|
-
const text = normalizeText(el.textContent).slice(0, 50);
|
|
196
|
-
const rect = el.getBoundingClientRect();
|
|
197
|
-
return text + ':' + Math.round(rect.top) + ':' + Math.round(rect.left);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function processToast(el) {
|
|
201
|
-
const toastId = generateToastId(el);
|
|
202
|
-
if (seenToasts.has(toastId)) return;
|
|
203
|
-
seenToasts.add(toastId);
|
|
204
|
-
|
|
205
|
-
const signal = {
|
|
206
|
-
kind: classifyToast(el),
|
|
207
|
-
atMs: performance.now(),
|
|
208
|
-
libraryHint: detectLibrary(el),
|
|
209
|
-
message: normalizeText(el.textContent),
|
|
210
|
-
selectorHint: el.id ? '#' + el.id : (el.className ? '.' + el.className.split(' ')[0] : el.tagName.toLowerCase()),
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
signals.push(signal);
|
|
214
|
-
|
|
215
|
-
// Dispatch custom event for external listeners
|
|
216
|
-
window.dispatchEvent(new CustomEvent('vibecheck:toast', { detail: signal }));
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Mutation observer to catch toasts
|
|
220
|
-
const observer = new MutationObserver((mutations) => {
|
|
221
|
-
for (const mutation of mutations) {
|
|
222
|
-
// Check added nodes
|
|
223
|
-
for (const node of mutation.addedNodes) {
|
|
224
|
-
if (node.nodeType !== 1) continue;
|
|
225
|
-
|
|
226
|
-
// Check if this node is a toast
|
|
227
|
-
if (isToastCandidate(node)) {
|
|
228
|
-
processToast(node);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Check descendants
|
|
232
|
-
if (node.querySelectorAll) {
|
|
233
|
-
const candidates = node.querySelectorAll('[role="alert"], [role="status"], [aria-live]');
|
|
234
|
-
candidates.forEach(el => {
|
|
235
|
-
if (isToastCandidate(el)) processToast(el);
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Check attribute changes (e.g., data-state="open")
|
|
241
|
-
if (mutation.type === 'attributes' && mutation.attributeName === 'data-state') {
|
|
242
|
-
const el = mutation.target;
|
|
243
|
-
if (el.getAttribute('data-state') === 'open' && isToastCandidate(el)) {
|
|
244
|
-
processToast(el);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
observer.observe(document.body, {
|
|
251
|
-
childList: true,
|
|
252
|
-
subtree: true,
|
|
253
|
-
attributes: true,
|
|
254
|
-
attributeFilter: ['data-state', 'class', 'aria-live'],
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
// Also scan existing toasts
|
|
258
|
-
const existingToasts = document.querySelectorAll('[role="alert"], [role="status"], [aria-live]');
|
|
259
|
-
existingToasts.forEach(el => {
|
|
260
|
-
if (isToastCandidate(el)) processToast(el);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
// Return API
|
|
264
|
-
return {
|
|
265
|
-
getSignals: () => [...signals],
|
|
266
|
-
clearSignals: () => { signals.length = 0; seenToasts.clear(); },
|
|
267
|
-
stop: () => observer.disconnect(),
|
|
268
|
-
};
|
|
269
|
-
})
|
|
270
|
-
`;
|
|
271
|
-
|
|
272
|
-
// =============================================================================
|
|
273
|
-
// SERVER-SIDE HELPERS
|
|
274
|
-
// =============================================================================
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Classify toast kind from signal data
|
|
278
|
-
*/
|
|
279
|
-
function classifyToastSignal(signal) {
|
|
280
|
-
if (!signal) return "toast_unknown";
|
|
281
|
-
|
|
282
|
-
const text = (signal.message || "").toLowerCase();
|
|
283
|
-
const classes = (signal.classes || "").toLowerCase();
|
|
284
|
-
|
|
285
|
-
// Success patterns
|
|
286
|
-
if (SUCCESS_TOKENS.some(t => text.includes(t) || classes.includes(t))) {
|
|
287
|
-
return "toast_success";
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Error patterns
|
|
291
|
-
if (ERROR_TOKENS.some(t => text.includes(t) || classes.includes(t))) {
|
|
292
|
-
return "toast_error";
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Info patterns
|
|
296
|
-
if (INFO_TOKENS.some(t => text.includes(t) || classes.includes(t))) {
|
|
297
|
-
return "toast_info";
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
return "toast_unknown";
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Create toast signal payload
|
|
305
|
-
*/
|
|
306
|
-
function createToastSignal(options = {}) {
|
|
307
|
-
const {
|
|
308
|
-
kind = "toast_unknown",
|
|
309
|
-
atMs = Date.now(),
|
|
310
|
-
libraryHint = "unknown",
|
|
311
|
-
message = "",
|
|
312
|
-
selectorHint = "",
|
|
313
|
-
screenshot = null,
|
|
314
|
-
} = options;
|
|
315
|
-
|
|
316
|
-
return {
|
|
317
|
-
kind,
|
|
318
|
-
atMs,
|
|
319
|
-
libraryHint,
|
|
320
|
-
message: message.slice(0, 200),
|
|
321
|
-
selectorHint,
|
|
322
|
-
screenshot,
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Check if a toast is a false positive (persistent banner, etc.)
|
|
328
|
-
*/
|
|
329
|
-
function isToastFalsePositive(signal, options = {}) {
|
|
330
|
-
const { maxLifetimeMs = 15000 } = options;
|
|
331
|
-
|
|
332
|
-
// If it's been visible too long, likely a persistent banner
|
|
333
|
-
if (signal.visibleDuration && signal.visibleDuration > maxLifetimeMs) {
|
|
334
|
-
return true;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Common false positive patterns
|
|
338
|
-
const falsePositivePatterns = [
|
|
339
|
-
/cookie.*consent/i,
|
|
340
|
-
/accept.*cookies/i,
|
|
341
|
-
/privacy.*policy/i,
|
|
342
|
-
/subscribe.*newsletter/i,
|
|
343
|
-
/sign.*up/i,
|
|
344
|
-
];
|
|
345
|
-
|
|
346
|
-
const text = signal.message || "";
|
|
347
|
-
if (falsePositivePatterns.some(p => p.test(text))) {
|
|
348
|
-
return true;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
return false;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Get library-specific selectors for a known library
|
|
356
|
-
*/
|
|
357
|
-
function getLibrarySelectors(libraryName) {
|
|
358
|
-
return TOAST_LIBRARIES[libraryName] || null;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* Build combined selector for all toast libraries
|
|
363
|
-
*/
|
|
364
|
-
function buildToastSelector() {
|
|
365
|
-
const selectors = [];
|
|
366
|
-
|
|
367
|
-
// Add library-specific selectors
|
|
368
|
-
for (const lib of Object.values(TOAST_LIBRARIES)) {
|
|
369
|
-
if (lib.toast) selectors.push(lib.toast);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// Add universal selectors
|
|
373
|
-
selectors.push(UNIVERSAL_SELECTORS.roleAlert);
|
|
374
|
-
selectors.push(UNIVERSAL_SELECTORS.roleStatus);
|
|
375
|
-
selectors.push(UNIVERSAL_SELECTORS.ariaLive);
|
|
376
|
-
|
|
377
|
-
return selectors.join(", ");
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
module.exports = {
|
|
381
|
-
TOAST_DETECTOR_SCRIPT,
|
|
382
|
-
TOAST_LIBRARIES,
|
|
383
|
-
UNIVERSAL_SELECTORS,
|
|
384
|
-
TOAST_CLASS_TOKENS,
|
|
385
|
-
SUCCESS_TOKENS,
|
|
386
|
-
ERROR_TOKENS,
|
|
387
|
-
INFO_TOKENS,
|
|
388
|
-
classifyToastSignal,
|
|
389
|
-
createToastSignal,
|
|
390
|
-
isToastFalsePositive,
|
|
391
|
-
getLibrarySelectors,
|
|
392
|
-
buildToastSelector,
|
|
393
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Toast/Notification Detector v2
|
|
3
|
+
*
|
|
4
|
+
* Detects toasts via DOM observation with library-specific selectors.
|
|
5
|
+
* Emits signals: toast_success, toast_error, toast_info, toast_unknown
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// LIBRARY-SPECIFIC SELECTORS
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
const TOAST_LIBRARIES = {
|
|
15
|
+
sonner: {
|
|
16
|
+
name: "sonner",
|
|
17
|
+
container: "[data-sonner-toaster]",
|
|
18
|
+
toast: "[data-sonner-toast]",
|
|
19
|
+
typeAttr: "data-type",
|
|
20
|
+
},
|
|
21
|
+
reactHotToast: {
|
|
22
|
+
name: "react-hot-toast",
|
|
23
|
+
container: "#_rht_toaster, .react-hot-toast",
|
|
24
|
+
toast: "[aria-live]",
|
|
25
|
+
},
|
|
26
|
+
reactToastify: {
|
|
27
|
+
name: "react-toastify",
|
|
28
|
+
container: ".Toastify__toast-container",
|
|
29
|
+
toast: ".Toastify__toast",
|
|
30
|
+
successClass: "Toastify__toast--success",
|
|
31
|
+
errorClass: "Toastify__toast--error",
|
|
32
|
+
infoClass: "Toastify__toast--info",
|
|
33
|
+
warningClass: "Toastify__toast--warning",
|
|
34
|
+
},
|
|
35
|
+
radix: {
|
|
36
|
+
name: "radix",
|
|
37
|
+
container: "[data-radix-toast-viewport]",
|
|
38
|
+
toast: "[data-radix-toast-root]",
|
|
39
|
+
stateAttr: "data-state",
|
|
40
|
+
},
|
|
41
|
+
mantine: {
|
|
42
|
+
name: "mantine",
|
|
43
|
+
container: ".mantine-Notifications-root",
|
|
44
|
+
toast: ".mantine-Notification-root",
|
|
45
|
+
},
|
|
46
|
+
chakra: {
|
|
47
|
+
name: "chakra",
|
|
48
|
+
container: "[id^='chakra-toast-manager']",
|
|
49
|
+
toast: "[id^='chakra-toast']",
|
|
50
|
+
},
|
|
51
|
+
mui: {
|
|
52
|
+
name: "mui",
|
|
53
|
+
container: ".MuiSnackbar-root",
|
|
54
|
+
toast: ".MuiSnackbarContent-root, .MuiAlert-root",
|
|
55
|
+
successClass: "MuiAlert-standardSuccess",
|
|
56
|
+
errorClass: "MuiAlert-standardError",
|
|
57
|
+
infoClass: "MuiAlert-standardInfo",
|
|
58
|
+
warningClass: "MuiAlert-standardWarning",
|
|
59
|
+
},
|
|
60
|
+
notistack: {
|
|
61
|
+
name: "notistack",
|
|
62
|
+
container: ".SnackbarContainer-root",
|
|
63
|
+
toast: "#notistack-snackbar",
|
|
64
|
+
},
|
|
65
|
+
antd: {
|
|
66
|
+
name: "antd",
|
|
67
|
+
container: ".ant-message, .ant-notification",
|
|
68
|
+
toast: ".ant-message-notice, .ant-notification-notice",
|
|
69
|
+
successClass: "ant-message-success, ant-notification-notice-success",
|
|
70
|
+
errorClass: "ant-message-error, ant-notification-notice-error",
|
|
71
|
+
},
|
|
72
|
+
bootstrap: {
|
|
73
|
+
name: "bootstrap",
|
|
74
|
+
container: ".toast-container",
|
|
75
|
+
toast: ".toast[aria-live]",
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Universal heuristic selectors
|
|
80
|
+
const UNIVERSAL_SELECTORS = {
|
|
81
|
+
ariaLive: '[aria-live="polite"], [aria-live="assertive"]',
|
|
82
|
+
roleAlert: '[role="alert"]',
|
|
83
|
+
roleStatus: '[role="status"]',
|
|
84
|
+
ariaAtomic: '[aria-atomic="true"]',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Class tokens that indicate toast
|
|
88
|
+
const TOAST_CLASS_TOKENS = [
|
|
89
|
+
"toast", "snackbar", "notification", "noti", "sonner",
|
|
90
|
+
"Toastify", "chakra-toast", "mantine-Notification",
|
|
91
|
+
"MuiSnackbar", "ant-message", "ant-notification", "notistack"
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
// Success/error classification tokens
|
|
95
|
+
const SUCCESS_TOKENS = ["success", "check", "done", "saved", "complete", "✓", "✅"];
|
|
96
|
+
const ERROR_TOKENS = ["error", "fail", "warning", "alert", "danger", "❌", "⚠"];
|
|
97
|
+
const INFO_TOKENS = ["info", "notice", "ℹ"];
|
|
98
|
+
|
|
99
|
+
// =============================================================================
|
|
100
|
+
// TOAST DETECTOR SCRIPT (runs in browser context)
|
|
101
|
+
// =============================================================================
|
|
102
|
+
|
|
103
|
+
const TOAST_DETECTOR_SCRIPT = `
|
|
104
|
+
(function setupToastDetector(options = {}) {
|
|
105
|
+
const {
|
|
106
|
+
maxLifetimeMs = 15000,
|
|
107
|
+
captureScreenshots = true,
|
|
108
|
+
} = options;
|
|
109
|
+
|
|
110
|
+
const TOAST_CLASS_TOKENS = ${JSON.stringify(TOAST_CLASS_TOKENS)};
|
|
111
|
+
const SUCCESS_TOKENS = ${JSON.stringify(SUCCESS_TOKENS)};
|
|
112
|
+
const ERROR_TOKENS = ${JSON.stringify(ERROR_TOKENS)};
|
|
113
|
+
const INFO_TOKENS = ${JSON.stringify(INFO_TOKENS)};
|
|
114
|
+
|
|
115
|
+
const signals = [];
|
|
116
|
+
const seenToasts = new Set();
|
|
117
|
+
|
|
118
|
+
function normalizeText(text) {
|
|
119
|
+
return (text || '').trim().replace(/\\s+/g, ' ').slice(0, 200);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function classifyToast(el) {
|
|
123
|
+
const classes = el.className?.toLowerCase() || '';
|
|
124
|
+
const text = normalizeText(el.textContent);
|
|
125
|
+
const dataType = el.getAttribute('data-type')?.toLowerCase();
|
|
126
|
+
|
|
127
|
+
// Check data-type first (most reliable)
|
|
128
|
+
if (dataType) {
|
|
129
|
+
if (SUCCESS_TOKENS.some(t => dataType.includes(t))) return 'toast_success';
|
|
130
|
+
if (ERROR_TOKENS.some(t => dataType.includes(t))) return 'toast_error';
|
|
131
|
+
if (INFO_TOKENS.some(t => dataType.includes(t))) return 'toast_info';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check classes
|
|
135
|
+
if (SUCCESS_TOKENS.some(t => classes.includes(t))) return 'toast_success';
|
|
136
|
+
if (ERROR_TOKENS.some(t => classes.includes(t))) return 'toast_error';
|
|
137
|
+
if (INFO_TOKENS.some(t => classes.includes(t))) return 'toast_info';
|
|
138
|
+
|
|
139
|
+
// Check text content
|
|
140
|
+
const textLower = text.toLowerCase();
|
|
141
|
+
if (SUCCESS_TOKENS.some(t => textLower.includes(t))) return 'toast_success';
|
|
142
|
+
if (ERROR_TOKENS.some(t => textLower.startsWith(t))) return 'toast_error';
|
|
143
|
+
|
|
144
|
+
// Check for icon aria-labels
|
|
145
|
+
const icons = el.querySelectorAll('[aria-label]');
|
|
146
|
+
for (const icon of icons) {
|
|
147
|
+
const label = icon.getAttribute('aria-label')?.toLowerCase();
|
|
148
|
+
if (label && SUCCESS_TOKENS.some(t => label.includes(t))) return 'toast_success';
|
|
149
|
+
if (label && ERROR_TOKENS.some(t => label.includes(t))) return 'toast_error';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return 'toast_unknown';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function detectLibrary(el) {
|
|
156
|
+
// Check specific library markers
|
|
157
|
+
if (el.closest('[data-sonner-toaster]')) return 'sonner';
|
|
158
|
+
if (el.closest('.Toastify__toast-container')) return 'react-toastify';
|
|
159
|
+
if (el.closest('[data-radix-toast-viewport]')) return 'radix';
|
|
160
|
+
if (el.closest('.mantine-Notifications-root')) return 'mantine';
|
|
161
|
+
if (el.closest('[id^="chakra-toast"]')) return 'chakra';
|
|
162
|
+
if (el.closest('.MuiSnackbar-root')) return 'mui';
|
|
163
|
+
if (el.closest('.SnackbarContainer-root')) return 'notistack';
|
|
164
|
+
if (el.closest('.ant-message, .ant-notification')) return 'antd';
|
|
165
|
+
return 'unknown';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isToastCandidate(el) {
|
|
169
|
+
// Skip hidden elements
|
|
170
|
+
const style = window.getComputedStyle(el);
|
|
171
|
+
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
|
172
|
+
if (style.opacity === '0') return false;
|
|
173
|
+
|
|
174
|
+
// Check ARIA roles
|
|
175
|
+
const role = el.getAttribute('role');
|
|
176
|
+
if (role === 'alert' || role === 'status') return true;
|
|
177
|
+
if (el.getAttribute('aria-live')) return true;
|
|
178
|
+
if (el.getAttribute('aria-atomic') === 'true') return true;
|
|
179
|
+
|
|
180
|
+
// Check class tokens
|
|
181
|
+
const classes = el.className?.toLowerCase() || '';
|
|
182
|
+
if (TOAST_CLASS_TOKENS.some(t => classes.includes(t.toLowerCase()))) return true;
|
|
183
|
+
|
|
184
|
+
// Check positioning (fixed/sticky near edges)
|
|
185
|
+
if (style.position === 'fixed' || style.position === 'sticky') {
|
|
186
|
+
const rect = el.getBoundingClientRect();
|
|
187
|
+
const isNearEdge = rect.top < 100 || rect.bottom > window.innerHeight - 100;
|
|
188
|
+
if (isNearEdge && el.textContent?.trim().length > 0) return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function generateToastId(el) {
|
|
195
|
+
const text = normalizeText(el.textContent).slice(0, 50);
|
|
196
|
+
const rect = el.getBoundingClientRect();
|
|
197
|
+
return text + ':' + Math.round(rect.top) + ':' + Math.round(rect.left);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function processToast(el) {
|
|
201
|
+
const toastId = generateToastId(el);
|
|
202
|
+
if (seenToasts.has(toastId)) return;
|
|
203
|
+
seenToasts.add(toastId);
|
|
204
|
+
|
|
205
|
+
const signal = {
|
|
206
|
+
kind: classifyToast(el),
|
|
207
|
+
atMs: performance.now(),
|
|
208
|
+
libraryHint: detectLibrary(el),
|
|
209
|
+
message: normalizeText(el.textContent),
|
|
210
|
+
selectorHint: el.id ? '#' + el.id : (el.className ? '.' + el.className.split(' ')[0] : el.tagName.toLowerCase()),
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
signals.push(signal);
|
|
214
|
+
|
|
215
|
+
// Dispatch custom event for external listeners
|
|
216
|
+
window.dispatchEvent(new CustomEvent('vibecheck:toast', { detail: signal }));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Mutation observer to catch toasts
|
|
220
|
+
const observer = new MutationObserver((mutations) => {
|
|
221
|
+
for (const mutation of mutations) {
|
|
222
|
+
// Check added nodes
|
|
223
|
+
for (const node of mutation.addedNodes) {
|
|
224
|
+
if (node.nodeType !== 1) continue;
|
|
225
|
+
|
|
226
|
+
// Check if this node is a toast
|
|
227
|
+
if (isToastCandidate(node)) {
|
|
228
|
+
processToast(node);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check descendants
|
|
232
|
+
if (node.querySelectorAll) {
|
|
233
|
+
const candidates = node.querySelectorAll('[role="alert"], [role="status"], [aria-live]');
|
|
234
|
+
candidates.forEach(el => {
|
|
235
|
+
if (isToastCandidate(el)) processToast(el);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Check attribute changes (e.g., data-state="open")
|
|
241
|
+
if (mutation.type === 'attributes' && mutation.attributeName === 'data-state') {
|
|
242
|
+
const el = mutation.target;
|
|
243
|
+
if (el.getAttribute('data-state') === 'open' && isToastCandidate(el)) {
|
|
244
|
+
processToast(el);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
observer.observe(document.body, {
|
|
251
|
+
childList: true,
|
|
252
|
+
subtree: true,
|
|
253
|
+
attributes: true,
|
|
254
|
+
attributeFilter: ['data-state', 'class', 'aria-live'],
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Also scan existing toasts
|
|
258
|
+
const existingToasts = document.querySelectorAll('[role="alert"], [role="status"], [aria-live]');
|
|
259
|
+
existingToasts.forEach(el => {
|
|
260
|
+
if (isToastCandidate(el)) processToast(el);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Return API
|
|
264
|
+
return {
|
|
265
|
+
getSignals: () => [...signals],
|
|
266
|
+
clearSignals: () => { signals.length = 0; seenToasts.clear(); },
|
|
267
|
+
stop: () => observer.disconnect(),
|
|
268
|
+
};
|
|
269
|
+
})
|
|
270
|
+
`;
|
|
271
|
+
|
|
272
|
+
// =============================================================================
|
|
273
|
+
// SERVER-SIDE HELPERS
|
|
274
|
+
// =============================================================================
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Classify toast kind from signal data
|
|
278
|
+
*/
|
|
279
|
+
function classifyToastSignal(signal) {
|
|
280
|
+
if (!signal) return "toast_unknown";
|
|
281
|
+
|
|
282
|
+
const text = (signal.message || "").toLowerCase();
|
|
283
|
+
const classes = (signal.classes || "").toLowerCase();
|
|
284
|
+
|
|
285
|
+
// Success patterns
|
|
286
|
+
if (SUCCESS_TOKENS.some(t => text.includes(t) || classes.includes(t))) {
|
|
287
|
+
return "toast_success";
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Error patterns
|
|
291
|
+
if (ERROR_TOKENS.some(t => text.includes(t) || classes.includes(t))) {
|
|
292
|
+
return "toast_error";
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Info patterns
|
|
296
|
+
if (INFO_TOKENS.some(t => text.includes(t) || classes.includes(t))) {
|
|
297
|
+
return "toast_info";
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return "toast_unknown";
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Create toast signal payload
|
|
305
|
+
*/
|
|
306
|
+
function createToastSignal(options = {}) {
|
|
307
|
+
const {
|
|
308
|
+
kind = "toast_unknown",
|
|
309
|
+
atMs = Date.now(),
|
|
310
|
+
libraryHint = "unknown",
|
|
311
|
+
message = "",
|
|
312
|
+
selectorHint = "",
|
|
313
|
+
screenshot = null,
|
|
314
|
+
} = options;
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
kind,
|
|
318
|
+
atMs,
|
|
319
|
+
libraryHint,
|
|
320
|
+
message: message.slice(0, 200),
|
|
321
|
+
selectorHint,
|
|
322
|
+
screenshot,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Check if a toast is a false positive (persistent banner, etc.)
|
|
328
|
+
*/
|
|
329
|
+
function isToastFalsePositive(signal, options = {}) {
|
|
330
|
+
const { maxLifetimeMs = 15000 } = options;
|
|
331
|
+
|
|
332
|
+
// If it's been visible too long, likely a persistent banner
|
|
333
|
+
if (signal.visibleDuration && signal.visibleDuration > maxLifetimeMs) {
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Common false positive patterns
|
|
338
|
+
const falsePositivePatterns = [
|
|
339
|
+
/cookie.*consent/i,
|
|
340
|
+
/accept.*cookies/i,
|
|
341
|
+
/privacy.*policy/i,
|
|
342
|
+
/subscribe.*newsletter/i,
|
|
343
|
+
/sign.*up/i,
|
|
344
|
+
];
|
|
345
|
+
|
|
346
|
+
const text = signal.message || "";
|
|
347
|
+
if (falsePositivePatterns.some(p => p.test(text))) {
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Get library-specific selectors for a known library
|
|
356
|
+
*/
|
|
357
|
+
function getLibrarySelectors(libraryName) {
|
|
358
|
+
return TOAST_LIBRARIES[libraryName] || null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Build combined selector for all toast libraries
|
|
363
|
+
*/
|
|
364
|
+
function buildToastSelector() {
|
|
365
|
+
const selectors = [];
|
|
366
|
+
|
|
367
|
+
// Add library-specific selectors
|
|
368
|
+
for (const lib of Object.values(TOAST_LIBRARIES)) {
|
|
369
|
+
if (lib.toast) selectors.push(lib.toast);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Add universal selectors
|
|
373
|
+
selectors.push(UNIVERSAL_SELECTORS.roleAlert);
|
|
374
|
+
selectors.push(UNIVERSAL_SELECTORS.roleStatus);
|
|
375
|
+
selectors.push(UNIVERSAL_SELECTORS.ariaLive);
|
|
376
|
+
|
|
377
|
+
return selectors.join(", ");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
module.exports = {
|
|
381
|
+
TOAST_DETECTOR_SCRIPT,
|
|
382
|
+
TOAST_LIBRARIES,
|
|
383
|
+
UNIVERSAL_SELECTORS,
|
|
384
|
+
TOAST_CLASS_TOKENS,
|
|
385
|
+
SUCCESS_TOKENS,
|
|
386
|
+
ERROR_TOKENS,
|
|
387
|
+
INFO_TOKENS,
|
|
388
|
+
classifyToastSignal,
|
|
389
|
+
createToastSignal,
|
|
390
|
+
isToastFalsePositive,
|
|
391
|
+
getLibrarySelectors,
|
|
392
|
+
buildToastSelector,
|
|
393
|
+
};
|