unbrowse 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +455 -96
- package/dist/index.js +2 -6
- package/dist/mcp.js +695 -46
- package/dist/server.js +25811 -0
- package/package.json +1 -2
- package/vendor/kuri/darwin-arm64/kuri +0 -0
- package/vendor/kuri/darwin-x64/kuri +0 -0
- package/vendor/kuri/linux-arm64/kuri +0 -0
- package/vendor/kuri/linux-x64/kuri +0 -0
- package/vendor/kuri/manifest.json +7 -10
- package/runtime-src/agent-outcome.ts +0 -166
- package/runtime-src/analytics-session.ts +0 -55
- package/runtime-src/api/browse-index.ts +0 -317
- package/runtime-src/api/browse-session.ts +0 -572
- package/runtime-src/api/browse-submit-prereqs.ts +0 -48
- package/runtime-src/api/browse-submit.ts +0 -1184
- package/runtime-src/api/routes.ts +0 -1823
- package/runtime-src/auth/browser-cookies.ts +0 -423
- package/runtime-src/auth/index.ts +0 -535
- package/runtime-src/auth/runtime.ts +0 -116
- package/runtime-src/browser/index.ts +0 -659
- package/runtime-src/browser/types.ts +0 -41
- package/runtime-src/build-info.generated.ts +0 -6
- package/runtime-src/capture/index.ts +0 -1794
- package/runtime-src/capture/prefetch.ts +0 -95
- package/runtime-src/capture/rsc.ts +0 -45
- package/runtime-src/cli/shortcuts.ts +0 -273
- package/runtime-src/cli.ts +0 -1572
- package/runtime-src/client/graph-client.ts +0 -100
- package/runtime-src/client/index.ts +0 -1425
- package/runtime-src/debug-trace.ts +0 -18
- package/runtime-src/domain.ts +0 -38
- package/runtime-src/execution/index.ts +0 -3397
- package/runtime-src/execution/retry.ts +0 -46
- package/runtime-src/execution/robots.ts +0 -167
- package/runtime-src/execution/search-forms.ts +0 -188
- package/runtime-src/extraction/index.ts +0 -1507
- package/runtime-src/foundry/publish-bundle.ts +0 -392
- package/runtime-src/graph/agent-augment.ts +0 -315
- package/runtime-src/graph/index.ts +0 -1524
- package/runtime-src/graph/local-fixtures.ts +0 -393
- package/runtime-src/graph/local-harness.ts +0 -646
- package/runtime-src/graph/planner.ts +0 -411
- package/runtime-src/graph/session.ts +0 -294
- package/runtime-src/graph/trace-store.ts +0 -136
- package/runtime-src/index.ts +0 -24
- package/runtime-src/indexer/index.ts +0 -465
- package/runtime-src/intent-match.ts +0 -1515
- package/runtime-src/kuri/client.ts +0 -1839
- package/runtime-src/logger.ts +0 -30
- package/runtime-src/marketplace/index.ts +0 -103
- package/runtime-src/mcp.ts +0 -1747
- package/runtime-src/orchestrator/browser-agent.ts +0 -374
- package/runtime-src/orchestrator/dag-advisor.ts +0 -59
- package/runtime-src/orchestrator/dag-feedback.ts +0 -257
- package/runtime-src/orchestrator/first-pass-action.ts +0 -403
- package/runtime-src/orchestrator/index.ts +0 -4480
- package/runtime-src/orchestrator/passive-publish.ts +0 -187
- package/runtime-src/orchestrator/timing-economics.ts +0 -80
- package/runtime-src/payments/cascade.ts +0 -137
- package/runtime-src/payments/index.ts +0 -270
- package/runtime-src/payments/lobster-pay.ts +0 -182
- package/runtime-src/payments/wallet.ts +0 -98
- package/runtime-src/publish/review-context.ts +0 -93
- package/runtime-src/publish/sanitize.ts +0 -197
- package/runtime-src/publish/schema-review.ts +0 -192
- package/runtime-src/publish-admission.ts +0 -388
- package/runtime-src/ratelimit/index.ts +0 -23
- package/runtime-src/reverse-engineer/bundle-scanner.ts +0 -127
- package/runtime-src/reverse-engineer/description-prompt.ts +0 -213
- package/runtime-src/reverse-engineer/index.ts +0 -1551
- package/runtime-src/router.ts +0 -17
- package/runtime-src/routing-telemetry.ts +0 -395
- package/runtime-src/runtime/browser-access.ts +0 -11
- package/runtime-src/runtime/browser-auth.ts +0 -12
- package/runtime-src/runtime/browser-host.ts +0 -48
- package/runtime-src/runtime/lifecycle.ts +0 -17
- package/runtime-src/runtime/local-server.ts +0 -311
- package/runtime-src/runtime/paths.ts +0 -99
- package/runtime-src/runtime/setup.ts +0 -251
- package/runtime-src/runtime/supervisor.ts +0 -69
- package/runtime-src/runtime/update-hints.ts +0 -351
- package/runtime-src/server.ts +0 -100
- package/runtime-src/session-logs.ts +0 -142
- package/runtime-src/settings.ts +0 -221
- package/runtime-src/single-binary.ts +0 -143
- package/runtime-src/site-policy.ts +0 -54
- package/runtime-src/stale-cleanup-runner.ts +0 -144
- package/runtime-src/stale-cleanup.ts +0 -133
- package/runtime-src/telemetry-attribution.ts +0 -120
- package/runtime-src/telemetry.ts +0 -253
- package/runtime-src/template-params.ts +0 -141
- package/runtime-src/transform/drift.ts +0 -60
- package/runtime-src/transform/index.ts +0 -277
- package/runtime-src/types/index.ts +0 -1
- package/runtime-src/types/skill.ts +0 -912
- package/runtime-src/vault/index.ts +0 -196
- package/runtime-src/verification/auth-gate.ts +0 -8
- package/runtime-src/verification/candidates.ts +0 -27
- package/runtime-src/verification/index.ts +0 -120
- package/runtime-src/verification/matrix.ts +0 -30
- package/runtime-src/version.ts +0 -148
- package/runtime-src/workflow/artifact.ts +0 -161
- package/runtime-src/workflow/compile.ts +0 -808
- package/runtime-src/workflow/publish.ts +0 -225
- package/runtime-src/workflow/runtime.ts +0 -213
- package/vendor/kuri/win-x64/kuri.exe +0 -0
|
@@ -1,1184 +0,0 @@
|
|
|
1
|
-
import type { BrowseSession } from "./browse-session.js";
|
|
2
|
-
import { isRecoverableBrowseFailure } from "./browse-session.js";
|
|
3
|
-
import { MONTH_INDEX } from "./browse-submit-prereqs.js";
|
|
4
|
-
|
|
5
|
-
export interface BrowseSubmitOptions {
|
|
6
|
-
formSelector?: string;
|
|
7
|
-
submitSelector?: string;
|
|
8
|
-
waitFor?: string;
|
|
9
|
-
sameOriginFetchFallback?: boolean;
|
|
10
|
-
timeoutMs?: number;
|
|
11
|
-
assistSiteState?: boolean;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface BrowseSubmitClient {
|
|
15
|
-
evaluate(tabId: string, expression: string): Promise<unknown>;
|
|
16
|
-
getCurrentUrl(tabId: string): Promise<string>;
|
|
17
|
-
getPageHtml(tabId: string): Promise<string>;
|
|
18
|
-
waitForSelector(tabId: string, selector?: string, timeoutMs?: number): Promise<{ status?: string }>;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface BrowseSubmitDeps {
|
|
22
|
-
client: BrowseSubmitClient;
|
|
23
|
-
session: BrowseSession;
|
|
24
|
-
flushCapture?: (session: BrowseSession) => Promise<BrowseSubmitCaptureSyncResult | null>;
|
|
25
|
-
restartCapture: (session: BrowseSession) => Promise<void>;
|
|
26
|
-
rehydratePlugins: (tabId: string) => Promise<unknown>;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface BrowseSubmitCaptureSyncResult {
|
|
30
|
-
indexed: boolean;
|
|
31
|
-
mode: "http" | "dom" | "none";
|
|
32
|
-
skill_id?: string | null;
|
|
33
|
-
endpoint_count: number;
|
|
34
|
-
request_count?: number;
|
|
35
|
-
background_publish_queued?: boolean;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface BrowseSubmitResult {
|
|
39
|
-
ok: boolean;
|
|
40
|
-
url: string;
|
|
41
|
-
mode: "dom" | "same_origin_fetch" | "noop";
|
|
42
|
-
fallback_used: boolean;
|
|
43
|
-
same_origin_html_rehydrated: boolean;
|
|
44
|
-
recoverable?: boolean;
|
|
45
|
-
reason?: string;
|
|
46
|
-
status?: number;
|
|
47
|
-
wait_for?: string;
|
|
48
|
-
submit_meta?: Record<string, unknown> | null;
|
|
49
|
-
capture_sync?: BrowseSubmitCaptureSyncResult | null;
|
|
50
|
-
rehydrate?: unknown;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
interface BrowseSubmitPageProbe {
|
|
54
|
-
action?: string;
|
|
55
|
-
step?: string;
|
|
56
|
-
ticketingType?: string;
|
|
57
|
-
hasActiveParkCard?: boolean;
|
|
58
|
-
hasResidentGate?: boolean;
|
|
59
|
-
hasQuantityControls?: boolean;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const DEFAULT_SUBMIT_TIMEOUT_MS = 8_000;
|
|
63
|
-
const SUBMIT_POLL_INTERVAL_MS = 250;
|
|
64
|
-
const SUBMIT_SETTLE_WINDOW_MS = 1_000;
|
|
65
|
-
const TRACE_SUBMIT_DEBUG = process.env.UNBROWSE_TRACE_DEBUG === "1";
|
|
66
|
-
const ENABLE_TRAVERSAL_FETCH_FALLBACK = process.env.UNBROWSE_ENABLE_TRAVERSAL_FETCH_FALLBACK === "1";
|
|
67
|
-
|
|
68
|
-
function sleep(ms: number): Promise<void> {
|
|
69
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
73
|
-
return value && typeof value === "object" ? value as Record<string, unknown> : null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function traceSubmit(event: string, payload: Record<string, unknown>): void {
|
|
77
|
-
if (!TRACE_SUBMIT_DEBUG) return;
|
|
78
|
-
try {
|
|
79
|
-
console.log(`[browse-submit] ${event} ${JSON.stringify(payload)}`);
|
|
80
|
-
} catch {
|
|
81
|
-
console.log(`[browse-submit] ${event}`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function isUrlWaitHint(value?: string): boolean {
|
|
86
|
-
if (!value) return false;
|
|
87
|
-
return /^https?:\/\//i.test(value) || value.startsWith("/");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function resolveSubmitWaitHint(baseUrl: string, value?: string): string | null {
|
|
91
|
-
if (!isUrlWaitHint(value)) return null;
|
|
92
|
-
|
|
93
|
-
const trimmed = value.trim();
|
|
94
|
-
if (!trimmed) return null;
|
|
95
|
-
if (/^https?:\/\//i.test(trimmed)) {
|
|
96
|
-
try {
|
|
97
|
-
return new URL(trimmed).href;
|
|
98
|
-
} catch {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
const base = new URL(baseUrl);
|
|
105
|
-
const rootResolved = new URL(trimmed, base.origin);
|
|
106
|
-
const rootRelativePath = rootResolved.pathname.replace(/^\/+/, "");
|
|
107
|
-
const isFilenameHint = rootRelativePath.length > 0
|
|
108
|
-
&& !rootRelativePath.includes("/")
|
|
109
|
-
&& /\.[a-z0-9]+$/i.test(rootRelativePath);
|
|
110
|
-
|
|
111
|
-
if (isFilenameHint) {
|
|
112
|
-
const baseDirectory = base.pathname.endsWith("/")
|
|
113
|
-
? base.pathname
|
|
114
|
-
: base.pathname.slice(0, base.pathname.lastIndexOf("/") + 1);
|
|
115
|
-
return new URL(`${baseDirectory}${rootRelativePath}${rootResolved.search}${rootResolved.hash}`, base.origin).href;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return rootResolved.href;
|
|
119
|
-
} catch {
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function hasMeaningfulPageChange(beforeHtml: string, afterHtml: string): boolean {
|
|
125
|
-
const before = beforeHtml.trim();
|
|
126
|
-
const after = afterHtml.trim();
|
|
127
|
-
if (!after) return false;
|
|
128
|
-
if (!before) return after.length > 64;
|
|
129
|
-
if (before === after) return false;
|
|
130
|
-
if (Math.abs(before.length - after.length) > 48) return true;
|
|
131
|
-
|
|
132
|
-
const beforeBody = before.match(/<body[\s\S]*?>([\s\S]*?)<\/body>/i)?.[1] ?? before;
|
|
133
|
-
const afterBody = after.match(/<body[\s\S]*?>([\s\S]*?)<\/body>/i)?.[1] ?? after;
|
|
134
|
-
const normalizeVisibleText = (html: string) => html
|
|
135
|
-
.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
|
136
|
-
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
|
137
|
-
.replace(/<[^>]+>/g, " ")
|
|
138
|
-
.replace(/ /gi, " ")
|
|
139
|
-
.replace(/&/gi, "&")
|
|
140
|
-
.replace(/"/gi, "\"")
|
|
141
|
-
.replace(/'/gi, "'")
|
|
142
|
-
.replace(/\s+/g, " ")
|
|
143
|
-
.trim();
|
|
144
|
-
|
|
145
|
-
const beforeText = normalizeVisibleText(beforeBody);
|
|
146
|
-
const afterText = normalizeVisibleText(afterBody);
|
|
147
|
-
if (beforeText || afterText) return beforeText !== afterText;
|
|
148
|
-
return beforeBody.trim() !== afterBody.trim();
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function buildDomSubmitExpression(options: BrowseSubmitOptions): string {
|
|
152
|
-
const monthIndex = JSON.stringify(MONTH_INDEX);
|
|
153
|
-
return `(function() {
|
|
154
|
-
var MONTH_INDEX = ${monthIndex};
|
|
155
|
-
function findForm(selector) {
|
|
156
|
-
if (selector) return document.querySelector(selector);
|
|
157
|
-
var active = document.activeElement;
|
|
158
|
-
if (active && active.closest) {
|
|
159
|
-
var fromActive = active.closest("form");
|
|
160
|
-
if (fromActive) return fromActive;
|
|
161
|
-
}
|
|
162
|
-
return document.querySelector("form");
|
|
163
|
-
}
|
|
164
|
-
function findSubmitter(form, selector) {
|
|
165
|
-
if (!form) return null;
|
|
166
|
-
if (selector) return document.querySelector(selector);
|
|
167
|
-
var active = document.activeElement;
|
|
168
|
-
if (active && form.contains(active) && /^(submit|image)$/i.test(active.getAttribute("type") || "")) return active;
|
|
169
|
-
return form.querySelector('button[type="submit"], input[type="submit"], input[type="image"], button:not([type])');
|
|
170
|
-
}
|
|
171
|
-
function isDisabled(node) {
|
|
172
|
-
if (!node) return false;
|
|
173
|
-
if (node.disabled) return true;
|
|
174
|
-
if (typeof node.getAttribute === "function") {
|
|
175
|
-
var ariaDisabled = node.getAttribute("aria-disabled");
|
|
176
|
-
if (ariaDisabled && ariaDisabled.toLowerCase() === "true") return true;
|
|
177
|
-
}
|
|
178
|
-
return !!(node.classList && node.classList.contains("disabled"));
|
|
179
|
-
}
|
|
180
|
-
function textValue(node) {
|
|
181
|
-
if (!node) return "";
|
|
182
|
-
if (typeof node.value === "string" && node.value.trim()) return node.value.trim();
|
|
183
|
-
return (node.textContent || "").trim();
|
|
184
|
-
}
|
|
185
|
-
function pushUnique(list, value) {
|
|
186
|
-
if (!value) return;
|
|
187
|
-
if (!list.includes(value)) list.push(value);
|
|
188
|
-
}
|
|
189
|
-
function inferIsoDate() {
|
|
190
|
-
var existing = document.querySelector("[data-input-date], input[name='selectedDate']");
|
|
191
|
-
var existingValue = textValue(existing);
|
|
192
|
-
if (/^\\d{4}-\\d{2}-\\d{2}$/.test(existingValue)) return existingValue;
|
|
193
|
-
|
|
194
|
-
var dayNode = document.querySelector(
|
|
195
|
-
".day.active, .day.current, .calendar-day.active, .calendar-day.selected, .calendar-day.current, [data-day].active, [data-day].current, [data-day].selected, [data-date].active, [data-date].selected, [aria-selected='true'][data-day], [aria-selected='true'][data-date], .ui-state-active"
|
|
196
|
-
);
|
|
197
|
-
var dayValue = dayNode
|
|
198
|
-
? (dayNode.getAttribute("data-day") || dayNode.getAttribute("data-date") || dayNode.getAttribute("data-value") || textValue(dayNode))
|
|
199
|
-
: "";
|
|
200
|
-
var dayMatch = dayValue.match(/\\b(\\d{1,2})\\b/);
|
|
201
|
-
if (!dayMatch) return null;
|
|
202
|
-
|
|
203
|
-
var monthNode = document.querySelector(
|
|
204
|
-
"[data-calendar-title], .ui-datepicker-title, .calendar-month-year, .month-year, .datepicker-switch, .calendar-header .title"
|
|
205
|
-
);
|
|
206
|
-
var monthLabel = monthNode
|
|
207
|
-
? (monthNode.getAttribute("data-calendar-title") || monthNode.getAttribute("data-current-month") || textValue(monthNode))
|
|
208
|
-
: "";
|
|
209
|
-
var yearMatch = monthLabel.match(/\\b(\\d{4})\\b/);
|
|
210
|
-
var tokens = monthLabel
|
|
211
|
-
.toLowerCase()
|
|
212
|
-
.replace(/[^a-z0-9\\s]+/g, " ")
|
|
213
|
-
.split(/\\s+/)
|
|
214
|
-
.map(function(token) { return token.trim(); })
|
|
215
|
-
.filter(Boolean);
|
|
216
|
-
var monthValue = null;
|
|
217
|
-
for (var i = 0; i < tokens.length; i++) {
|
|
218
|
-
var token = tokens[i];
|
|
219
|
-
monthValue = MONTH_INDEX[token] || MONTH_INDEX[token.slice(0, 3)];
|
|
220
|
-
if (monthValue) break;
|
|
221
|
-
}
|
|
222
|
-
if (!monthValue || !yearMatch) return null;
|
|
223
|
-
|
|
224
|
-
return yearMatch[1] + "-" + monthValue + "-" + String(dayMatch[1]).padStart(2, "0");
|
|
225
|
-
}
|
|
226
|
-
function dispatchValue(node, value) {
|
|
227
|
-
if (!node) return;
|
|
228
|
-
if ("value" in node) {
|
|
229
|
-
node.value = value;
|
|
230
|
-
} else {
|
|
231
|
-
node.textContent = value;
|
|
232
|
-
}
|
|
233
|
-
try {
|
|
234
|
-
node.dispatchEvent(new Event("input", { bubbles: true }));
|
|
235
|
-
node.dispatchEvent(new Event("change", { bubbles: true }));
|
|
236
|
-
} catch {}
|
|
237
|
-
}
|
|
238
|
-
function dispatchCheck(node, checked) {
|
|
239
|
-
if (!node) return;
|
|
240
|
-
if ("checked" in node) node.checked = checked;
|
|
241
|
-
try {
|
|
242
|
-
node.dispatchEvent(new Event("input", { bubbles: true }));
|
|
243
|
-
node.dispatchEvent(new Event("change", { bubbles: true }));
|
|
244
|
-
if (typeof node.click === "function") node.click();
|
|
245
|
-
} catch {}
|
|
246
|
-
if ("checked" in node && node.checked !== checked) node.checked = checked;
|
|
247
|
-
}
|
|
248
|
-
function prepareMandaiResidentGate(form, prereqState) {
|
|
249
|
-
if (!form) return;
|
|
250
|
-
var action = form.getAttribute("action") || "";
|
|
251
|
-
var step = String(form.getAttribute("data-step") || "");
|
|
252
|
-
if (!/\\/bin\\/wrs\\/ticket-selection/i.test(action) || step !== "2") return;
|
|
253
|
-
|
|
254
|
-
var residentRadio = form.querySelector("input.booking-selection--btn[type='radio'][value='resident'], input[name='booking-selection'][value='resident']");
|
|
255
|
-
var residentCheckbox = form.querySelector("#checkSingapore, input[name='isSingaporean'][type='checkbox']");
|
|
256
|
-
var quantityControls = form.querySelector("[data-number-ticket], .number-ticket, .qty-ticket, input[name='quantityTicket']");
|
|
257
|
-
var needsResidentGate = !!(residentRadio || residentCheckbox) && !quantityControls;
|
|
258
|
-
if (!needsResidentGate) return;
|
|
259
|
-
|
|
260
|
-
if (residentRadio && !residentRadio.checked) {
|
|
261
|
-
dispatchCheck(residentRadio, true);
|
|
262
|
-
pushUnique(prereqState.patched, "input[name='booking-selection'][value='resident']");
|
|
263
|
-
}
|
|
264
|
-
if (residentCheckbox && !residentCheckbox.checked) {
|
|
265
|
-
dispatchCheck(residentCheckbox, true);
|
|
266
|
-
pushUnique(prereqState.patched, "#checkSingapore");
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
pushUnique(prereqState.missing, "[data-number-ticket], input[name='quantityTicket']");
|
|
270
|
-
}
|
|
271
|
-
function findMandaiActiveParkCard(form) {
|
|
272
|
-
if (!form) return null;
|
|
273
|
-
var action = form.getAttribute("action") || "";
|
|
274
|
-
var step = String(form.getAttribute("data-step") || "");
|
|
275
|
-
var ticketingType = textValue(form.querySelector("input[name='type-ticketing']"));
|
|
276
|
-
if (!/\\/bin\\/wrs\\/product-selection/i.test(action)) return null;
|
|
277
|
-
if (step && step !== "1") return null;
|
|
278
|
-
if (ticketingType && ticketingType !== "park") return null;
|
|
279
|
-
return form.querySelector(".thumbnail-ticket-item.active");
|
|
280
|
-
}
|
|
281
|
-
function prepareSubmitState(submitter, selector) {
|
|
282
|
-
var prereqState = { patched: [], missing: [] };
|
|
283
|
-
var isoDate = inferIsoDate();
|
|
284
|
-
if (isoDate) {
|
|
285
|
-
Array.from(document.querySelectorAll("[data-input-date], input[name='selectedDate'], [data-summary-date], input[name='summaryDate']")).forEach(function(node) {
|
|
286
|
-
var before = textValue(node);
|
|
287
|
-
if (before !== isoDate) {
|
|
288
|
-
dispatchValue(node, isoDate);
|
|
289
|
-
pushUnique(prereqState.patched, node.matches ? node.matches("[data-input-date]") ? "[data-input-date]" : node.matches("input[name='selectedDate']") ? "input[name='selectedDate']" : node.matches("[data-summary-date]") ? "[data-summary-date]" : "input[name='summaryDate']" : "date_target");
|
|
290
|
-
}
|
|
291
|
-
});
|
|
292
|
-
} else {
|
|
293
|
-
var emptyDateInput = document.querySelector("[data-input-date], input[name='selectedDate']");
|
|
294
|
-
if (emptyDateInput && !textValue(emptyDateInput)) {
|
|
295
|
-
pushUnique(prereqState.missing, "selectedDate");
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
prepareMandaiResidentGate(form, prereqState);
|
|
300
|
-
var parkCard = findMandaiActiveParkCard(form);
|
|
301
|
-
if (form && /\\/bin\\/wrs\\/product-selection/i.test(form.getAttribute("action") || "") && !parkCard) {
|
|
302
|
-
pushUnique(prereqState.missing, ".thumbnail-ticket-item.active");
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (!submitter) {
|
|
306
|
-
pushUnique(prereqState.missing, selector || "submitter");
|
|
307
|
-
} else if (isDisabled(submitter)) {
|
|
308
|
-
pushUnique(prereqState.missing, selector || "[data-submit-next]:not(.disabled)");
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return prereqState;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
var form = findForm(${JSON.stringify(options.formSelector ?? "")});
|
|
315
|
-
if (!form) {
|
|
316
|
-
return JSON.stringify({ ok: false, reason: "form_not_found" });
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
var submitter = findSubmitter(form, ${JSON.stringify(options.submitSelector ?? "")});
|
|
320
|
-
var prereqState = prepareSubmitState(submitter, ${JSON.stringify(options.submitSelector ?? null)});
|
|
321
|
-
submitter = findSubmitter(form, ${JSON.stringify(options.submitSelector ?? "")});
|
|
322
|
-
var meta = {
|
|
323
|
-
ok: true,
|
|
324
|
-
form_action: form.getAttribute("action") || "",
|
|
325
|
-
form_method: (form.getAttribute("method") || "GET").toUpperCase(),
|
|
326
|
-
submitter: submitter ? (submitter.getAttribute("name") || submitter.id || submitter.textContent || submitter.tagName || "").trim() : null,
|
|
327
|
-
submit_selector_used: ${JSON.stringify(options.submitSelector ?? null)},
|
|
328
|
-
form_selector_used: ${JSON.stringify(options.formSelector ?? null)},
|
|
329
|
-
prereq_state: prereqState,
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
if (prereqState.missing.length > 0) {
|
|
333
|
-
return JSON.stringify({ ...meta, ok: false, reason: "prereq_state_incomplete" });
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
var parkCard = findMandaiActiveParkCard(form);
|
|
337
|
-
if (parkCard && typeof parkCard.click === "function") {
|
|
338
|
-
parkCard.click();
|
|
339
|
-
return JSON.stringify({ ...meta, submit_kind: "park_card_click" });
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (submitter && typeof submitter.click === "function") {
|
|
343
|
-
submitter.click();
|
|
344
|
-
return JSON.stringify({ ...meta, submit_kind: "click" });
|
|
345
|
-
}
|
|
346
|
-
if (typeof form.requestSubmit === "function") {
|
|
347
|
-
form.requestSubmit();
|
|
348
|
-
return JSON.stringify({ ...meta, submit_kind: "requestSubmit" });
|
|
349
|
-
}
|
|
350
|
-
if (typeof form.submit === "function") {
|
|
351
|
-
form.submit();
|
|
352
|
-
return JSON.stringify({ ...meta, submit_kind: "submit" });
|
|
353
|
-
}
|
|
354
|
-
return JSON.stringify({ ok: false, reason: "submit_unavailable" });
|
|
355
|
-
})()`;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function buildSubmitPageProbeExpression(options: BrowseSubmitOptions): string {
|
|
359
|
-
return `(function() {
|
|
360
|
-
function findForm(selector) {
|
|
361
|
-
if (selector) return document.querySelector(selector);
|
|
362
|
-
var active = document.activeElement;
|
|
363
|
-
if (active && active.closest) {
|
|
364
|
-
var fromActive = active.closest("form");
|
|
365
|
-
if (fromActive) return fromActive;
|
|
366
|
-
}
|
|
367
|
-
return document.querySelector("form");
|
|
368
|
-
}
|
|
369
|
-
var form = findForm(${JSON.stringify(options.formSelector ?? "")});
|
|
370
|
-
if (!form) return JSON.stringify({ ok: false, reason: "form_not_found" });
|
|
371
|
-
var typeInput = form.querySelector("input[name='type-ticketing']");
|
|
372
|
-
return JSON.stringify({
|
|
373
|
-
ok: true,
|
|
374
|
-
action: form.getAttribute("action") || "",
|
|
375
|
-
step: String(form.getAttribute("data-step") || ""),
|
|
376
|
-
ticketingType: typeInput && typeof typeInput.value === "string" ? typeInput.value : "",
|
|
377
|
-
hasActiveParkCard: !!form.querySelector(".thumbnail-ticket-item.active"),
|
|
378
|
-
hasResidentGate: !!form.querySelector("input[name='booking-selection'][value='resident'], #checkSingapore"),
|
|
379
|
-
hasQuantityControls: !!form.querySelector("[data-number-ticket], input[name='quantityTicket']"),
|
|
380
|
-
});
|
|
381
|
-
})()`;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function buildMandaiParkSubmitExpression(options: BrowseSubmitOptions): string {
|
|
385
|
-
return `(function() {
|
|
386
|
-
function findForm(selector) {
|
|
387
|
-
if (selector) return document.querySelector(selector);
|
|
388
|
-
return document.querySelector("form");
|
|
389
|
-
}
|
|
390
|
-
function isDisabled(node) {
|
|
391
|
-
if (!node) return false;
|
|
392
|
-
return !!(node.disabled || (node.classList && node.classList.contains("disabled")) || node.getAttribute("aria-disabled") === "true");
|
|
393
|
-
}
|
|
394
|
-
var form = findForm(${JSON.stringify(options.formSelector ?? "")});
|
|
395
|
-
if (!form) return JSON.stringify({ ok: false, reason: "form_not_found" });
|
|
396
|
-
var card = form.querySelector(".thumbnail-ticket-item.active");
|
|
397
|
-
var submitter = ${JSON.stringify(options.submitSelector ?? "")} ? document.querySelector(${JSON.stringify(options.submitSelector ?? "")}) : form.querySelector("[data-submit-next], .btn-proceed, button[type='submit'], input[type='submit']");
|
|
398
|
-
var meta = {
|
|
399
|
-
ok: true,
|
|
400
|
-
form_action: form.getAttribute("action") || "",
|
|
401
|
-
form_method: (form.getAttribute("method") || "GET").toUpperCase(),
|
|
402
|
-
submitter: submitter ? (submitter.getAttribute("name") || submitter.id || submitter.textContent || submitter.tagName || "").trim() : ".thumbnail-ticket-item.active",
|
|
403
|
-
submit_selector_used: ${JSON.stringify(options.submitSelector ?? null)},
|
|
404
|
-
form_selector_used: ${JSON.stringify(options.formSelector ?? null)},
|
|
405
|
-
prereq_state: { patched: [], missing: [] },
|
|
406
|
-
};
|
|
407
|
-
if (!card) {
|
|
408
|
-
meta.ok = false;
|
|
409
|
-
meta.reason = "prereq_state_incomplete";
|
|
410
|
-
meta.prereq_state.missing.push(".thumbnail-ticket-item.active");
|
|
411
|
-
return JSON.stringify(meta);
|
|
412
|
-
}
|
|
413
|
-
var cardPane = card.closest(".tab-pane");
|
|
414
|
-
if (cardPane && cardPane.id) {
|
|
415
|
-
Array.from(document.querySelectorAll("li[data-block-content]")).forEach(function(node) {
|
|
416
|
-
node.classList.remove("active");
|
|
417
|
-
});
|
|
418
|
-
Array.from(document.querySelectorAll(".tab-pane")).forEach(function(node) {
|
|
419
|
-
node.classList.remove("active");
|
|
420
|
-
});
|
|
421
|
-
var matchingTab = document.querySelector("li[data-block-content='#" + cardPane.id + "']");
|
|
422
|
-
if (matchingTab) matchingTab.classList.add("active");
|
|
423
|
-
cardPane.classList.add("active");
|
|
424
|
-
meta.prereq_state.patched.push("#" + cardPane.id);
|
|
425
|
-
}
|
|
426
|
-
var input = card.querySelector("input[name='ticket']");
|
|
427
|
-
if (input) {
|
|
428
|
-
input.checked = true;
|
|
429
|
-
input.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
|
430
|
-
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
431
|
-
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
432
|
-
}
|
|
433
|
-
card.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
|
434
|
-
card.click();
|
|
435
|
-
submitter = ${JSON.stringify(options.submitSelector ?? "")} ? document.querySelector(${JSON.stringify(options.submitSelector ?? "")}) : form.querySelector("[data-submit-next], .btn-proceed, button[type='submit'], input[type='submit']");
|
|
436
|
-
if (submitter && !isDisabled(submitter) && typeof submitter.click === "function") {
|
|
437
|
-
submitter.click();
|
|
438
|
-
meta.submit_kind = "park_card_click_then_submit";
|
|
439
|
-
return JSON.stringify(meta);
|
|
440
|
-
}
|
|
441
|
-
if (submitter && typeof submitter.click === "function") {
|
|
442
|
-
setTimeout(function() {
|
|
443
|
-
try {
|
|
444
|
-
if (!isDisabled(submitter)) submitter.click();
|
|
445
|
-
} catch {}
|
|
446
|
-
}, 150);
|
|
447
|
-
meta.submit_kind = "park_card_click_then_submit";
|
|
448
|
-
return JSON.stringify(meta);
|
|
449
|
-
}
|
|
450
|
-
meta.submit_kind = "park_card_click";
|
|
451
|
-
return JSON.stringify(meta);
|
|
452
|
-
})()`;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function buildMandaiResidentGateSubmitExpression(options: BrowseSubmitOptions): string {
|
|
456
|
-
return `(function() {
|
|
457
|
-
function findForm(selector) {
|
|
458
|
-
if (selector) return document.querySelector(selector);
|
|
459
|
-
return document.querySelector("form");
|
|
460
|
-
}
|
|
461
|
-
function dispatch(node, checked) {
|
|
462
|
-
if (!node) return;
|
|
463
|
-
if ("checked" in node) node.checked = checked;
|
|
464
|
-
node.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
|
465
|
-
node.dispatchEvent(new Event("input", { bubbles: true }));
|
|
466
|
-
node.dispatchEvent(new Event("change", { bubbles: true }));
|
|
467
|
-
if ("checked" in node) node.checked = checked;
|
|
468
|
-
}
|
|
469
|
-
function isDisabled(node) {
|
|
470
|
-
if (!node) return false;
|
|
471
|
-
return !!(node.disabled || (node.classList && node.classList.contains("disabled")) || node.getAttribute("aria-disabled") === "true");
|
|
472
|
-
}
|
|
473
|
-
var form = findForm(${JSON.stringify(options.formSelector ?? "")});
|
|
474
|
-
if (!form) return JSON.stringify({ ok: false, reason: "form_not_found" });
|
|
475
|
-
var submitter = ${JSON.stringify(options.submitSelector ?? "")}
|
|
476
|
-
? document.querySelector(${JSON.stringify(options.submitSelector ?? "")})
|
|
477
|
-
: document.querySelector("[data-submit-next], a.btn-proceed, .btn-proceed, button[type='submit'], input[type='submit']");
|
|
478
|
-
var residentRadio = form.querySelector("input.booking-selection--btn[type='radio'][value='resident'], input[name='booking-selection'][value='resident']");
|
|
479
|
-
var residentCheckbox = form.querySelector("#checkSingapore, input[name='isSingaporean'][type='checkbox']");
|
|
480
|
-
var patched = [];
|
|
481
|
-
if (residentRadio && !residentRadio.checked) {
|
|
482
|
-
dispatch(residentRadio, true);
|
|
483
|
-
patched.push("input[name='booking-selection'][value='resident']");
|
|
484
|
-
}
|
|
485
|
-
if (residentCheckbox && !residentCheckbox.checked) {
|
|
486
|
-
dispatch(residentCheckbox, true);
|
|
487
|
-
patched.push("#checkSingapore");
|
|
488
|
-
}
|
|
489
|
-
var quantityControls = form.querySelector("[data-number-ticket], input[name='quantityTicket']");
|
|
490
|
-
var meta = {
|
|
491
|
-
ok: true,
|
|
492
|
-
form_action: form.getAttribute("action") || "",
|
|
493
|
-
form_method: (form.getAttribute("method") || "GET").toUpperCase(),
|
|
494
|
-
submitter: submitter ? (submitter.getAttribute("name") || submitter.id || submitter.textContent || submitter.tagName || "").trim() : null,
|
|
495
|
-
submit_selector_used: ${JSON.stringify(options.submitSelector ?? null)},
|
|
496
|
-
form_selector_used: ${JSON.stringify(options.formSelector ?? null)},
|
|
497
|
-
prereq_state: { patched: patched, missing: [] },
|
|
498
|
-
};
|
|
499
|
-
if (!quantityControls) {
|
|
500
|
-
meta.ok = false;
|
|
501
|
-
meta.reason = "prereq_state_incomplete";
|
|
502
|
-
meta.prereq_state.missing.push("[data-number-ticket], input[name='quantityTicket']");
|
|
503
|
-
return JSON.stringify(meta);
|
|
504
|
-
}
|
|
505
|
-
if (!submitter) {
|
|
506
|
-
form.submit();
|
|
507
|
-
meta.submit_kind = "native_submit";
|
|
508
|
-
return JSON.stringify(meta);
|
|
509
|
-
}
|
|
510
|
-
if (isDisabled(submitter)) {
|
|
511
|
-
form.submit();
|
|
512
|
-
meta.submit_kind = "native_submit";
|
|
513
|
-
return JSON.stringify(meta);
|
|
514
|
-
}
|
|
515
|
-
submitter.click();
|
|
516
|
-
meta.submit_kind = "click";
|
|
517
|
-
return JSON.stringify(meta);
|
|
518
|
-
})()`;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
function buildMandaiTicketQuantitySubmitExpression(options: BrowseSubmitOptions): string {
|
|
522
|
-
return `(function() {
|
|
523
|
-
function findForm(selector) {
|
|
524
|
-
if (selector) return document.querySelector(selector);
|
|
525
|
-
return document.querySelector("form");
|
|
526
|
-
}
|
|
527
|
-
function dispatch(node, checked) {
|
|
528
|
-
if (!node) return;
|
|
529
|
-
if ("checked" in node) node.checked = checked;
|
|
530
|
-
node.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
|
531
|
-
node.dispatchEvent(new Event("input", { bubbles: true }));
|
|
532
|
-
node.dispatchEvent(new Event("change", { bubbles: true }));
|
|
533
|
-
if ("checked" in node) node.checked = checked;
|
|
534
|
-
}
|
|
535
|
-
function isDisabled(node) {
|
|
536
|
-
if (!node) return false;
|
|
537
|
-
return !!(node.disabled || (node.classList && node.classList.contains("disabled")) || node.getAttribute("aria-disabled") === "true");
|
|
538
|
-
}
|
|
539
|
-
function isLocalResidentQuantity(node) {
|
|
540
|
-
if (!node) return false;
|
|
541
|
-
var name = (node.getAttribute("name") || "").toLowerCase();
|
|
542
|
-
if (name.includes("_local_")) return true;
|
|
543
|
-
var residentsOnly = node.closest(".residents-only, [data-residents-only='true']");
|
|
544
|
-
return !!residentsOnly;
|
|
545
|
-
}
|
|
546
|
-
function setQuantity(node, value) {
|
|
547
|
-
if (!node) return;
|
|
548
|
-
node.disabled = false;
|
|
549
|
-
node.removeAttribute("disabled");
|
|
550
|
-
node.value = String(value);
|
|
551
|
-
node.setAttribute("data-number-ticket", String(value));
|
|
552
|
-
node.dispatchEvent(new Event("input", { bubbles: true }));
|
|
553
|
-
node.dispatchEvent(new Event("change", { bubbles: true }));
|
|
554
|
-
}
|
|
555
|
-
var form = findForm(${JSON.stringify(options.formSelector ?? "")});
|
|
556
|
-
if (!form) return JSON.stringify({ ok: false, reason: "form_not_found" });
|
|
557
|
-
var residentRadio = form.querySelector("input.booking-selection--btn[type='radio'][value='resident'], input[name='booking-selection'][value='resident']");
|
|
558
|
-
var residentCheckbox = form.querySelector("#checkSingapore, input[name='isSingaporean'][type='checkbox']");
|
|
559
|
-
var patched = [];
|
|
560
|
-
if (residentRadio && !residentRadio.checked) {
|
|
561
|
-
dispatch(residentRadio, true);
|
|
562
|
-
patched.push("input[name='booking-selection'][value='resident']");
|
|
563
|
-
}
|
|
564
|
-
if (residentCheckbox && !residentCheckbox.checked) {
|
|
565
|
-
dispatch(residentCheckbox, true);
|
|
566
|
-
patched.push("#checkSingapore");
|
|
567
|
-
}
|
|
568
|
-
var submitter = ${JSON.stringify(options.submitSelector ?? "")}
|
|
569
|
-
? document.querySelector(${JSON.stringify(options.submitSelector ?? "")})
|
|
570
|
-
: document.querySelector("[data-submit-next], a.btn-proceed, .btn-proceed, button[type='submit'], input[type='submit']");
|
|
571
|
-
var quantities = Array.from(form.querySelectorAll("[data-number-ticket], input[name='quantityTicket']"));
|
|
572
|
-
var residentQuantities = quantities.filter(function(node) {
|
|
573
|
-
return isLocalResidentQuantity(node);
|
|
574
|
-
});
|
|
575
|
-
var residentPositives = residentQuantities.filter(function(node) {
|
|
576
|
-
return Number.parseInt(node.value || "0", 10) > 0;
|
|
577
|
-
});
|
|
578
|
-
if (residentPositives.length > 0) {
|
|
579
|
-
residentPositives.forEach(function(node) {
|
|
580
|
-
if (node.disabled || !node.getAttribute("data-number-ticket")) {
|
|
581
|
-
setQuantity(node, Number.parseInt(node.value || "0", 10));
|
|
582
|
-
if (node.id) patched.push("#" + node.id);
|
|
583
|
-
}
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
var positives = quantities.filter(function(node) {
|
|
587
|
-
return Number.parseInt(node.value || "0", 10) > 0;
|
|
588
|
-
});
|
|
589
|
-
var meta = {
|
|
590
|
-
ok: true,
|
|
591
|
-
form_action: form.getAttribute("action") || "",
|
|
592
|
-
form_method: (form.getAttribute("method") || "GET").toUpperCase(),
|
|
593
|
-
submitter: submitter ? (submitter.getAttribute("name") || submitter.id || submitter.textContent || submitter.tagName || "").trim() : null,
|
|
594
|
-
submit_selector_used: ${JSON.stringify(options.submitSelector ?? null)},
|
|
595
|
-
form_selector_used: ${JSON.stringify(options.formSelector ?? null)},
|
|
596
|
-
prereq_state: { patched: patched, missing: [] },
|
|
597
|
-
};
|
|
598
|
-
if (positives.length === 0) {
|
|
599
|
-
meta.ok = false;
|
|
600
|
-
meta.reason = "prereq_state_incomplete";
|
|
601
|
-
meta.prereq_state.missing.push("[data-number-ticket]>0");
|
|
602
|
-
return JSON.stringify(meta);
|
|
603
|
-
}
|
|
604
|
-
if (!submitter) {
|
|
605
|
-
form.submit();
|
|
606
|
-
meta.submit_kind = "native_submit";
|
|
607
|
-
return JSON.stringify(meta);
|
|
608
|
-
}
|
|
609
|
-
if (isDisabled(submitter)) {
|
|
610
|
-
form.submit();
|
|
611
|
-
meta.submit_kind = "native_submit";
|
|
612
|
-
return JSON.stringify(meta);
|
|
613
|
-
}
|
|
614
|
-
submitter.click();
|
|
615
|
-
meta.submit_kind = "click";
|
|
616
|
-
return JSON.stringify(meta);
|
|
617
|
-
})()`;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
function buildMandaiDateSubmitExpression(options: BrowseSubmitOptions): string {
|
|
621
|
-
return `(function() {
|
|
622
|
-
function findForm(selector) {
|
|
623
|
-
if (selector) return document.querySelector(selector);
|
|
624
|
-
return document.querySelector("form");
|
|
625
|
-
}
|
|
626
|
-
function isDisabled(node) {
|
|
627
|
-
if (!node) return false;
|
|
628
|
-
return !!(node.disabled || (node.classList && node.classList.contains("disabled")) || node.getAttribute("aria-disabled") === "true");
|
|
629
|
-
}
|
|
630
|
-
var form = findForm(${JSON.stringify(options.formSelector ?? "")});
|
|
631
|
-
if (!form) return JSON.stringify({ ok: false, reason: "form_not_found" });
|
|
632
|
-
var submitter = ${JSON.stringify(options.submitSelector ?? "")}
|
|
633
|
-
? document.querySelector(${JSON.stringify(options.submitSelector ?? "")})
|
|
634
|
-
: document.querySelector("[data-submit-next], a.btn-proceed, .btn-proceed, button[type='submit'], input[type='submit']");
|
|
635
|
-
var selectedDateCandidates = Array.from(document.querySelectorAll("input[name='selectedDate'], [data-input-date]"));
|
|
636
|
-
var summaryDateCandidates = Array.from(document.querySelectorAll("[data-summary-date], input[name='summaryDate']"));
|
|
637
|
-
var selectedDate = selectedDateCandidates.find(function(node) {
|
|
638
|
-
return typeof node.value === "string" && node.value.trim().length > 0;
|
|
639
|
-
}) || selectedDateCandidates[0] || null;
|
|
640
|
-
var summaryDate = summaryDateCandidates.find(function(node) {
|
|
641
|
-
return typeof node.value === "string" && node.value.trim().length > 0;
|
|
642
|
-
}) || summaryDateCandidates[0] || null;
|
|
643
|
-
var selectedValue = selectedDate && typeof selectedDate.value === "string" ? selectedDate.value.trim() : "";
|
|
644
|
-
var summaryValue = summaryDate && typeof summaryDate.value === "string" ? summaryDate.value.trim() : "";
|
|
645
|
-
var patched = [];
|
|
646
|
-
var meta = {
|
|
647
|
-
ok: true,
|
|
648
|
-
form_action: form.getAttribute("action") || "",
|
|
649
|
-
form_method: (form.getAttribute("method") || "GET").toUpperCase(),
|
|
650
|
-
submitter: submitter ? (submitter.getAttribute("name") || submitter.id || submitter.textContent || submitter.tagName || "").trim() : null,
|
|
651
|
-
submit_selector_used: ${JSON.stringify(options.submitSelector ?? null)},
|
|
652
|
-
form_selector_used: ${JSON.stringify(options.formSelector ?? null)},
|
|
653
|
-
prereq_state: { patched: patched, missing: [] },
|
|
654
|
-
};
|
|
655
|
-
if (!selectedValue && summaryValue && selectedDate && "value" in selectedDate) {
|
|
656
|
-
selectedDate.value = summaryValue;
|
|
657
|
-
selectedDate.dispatchEvent(new Event("input", { bubbles: true }));
|
|
658
|
-
selectedDate.dispatchEvent(new Event("change", { bubbles: true }));
|
|
659
|
-
selectedValue = summaryValue;
|
|
660
|
-
patched.push("input[name='selectedDate']");
|
|
661
|
-
}
|
|
662
|
-
if (!selectedValue) {
|
|
663
|
-
meta.ok = false;
|
|
664
|
-
meta.reason = "prereq_state_incomplete";
|
|
665
|
-
meta.prereq_state.missing.push("selectedDate");
|
|
666
|
-
return JSON.stringify(meta);
|
|
667
|
-
}
|
|
668
|
-
if (!submitter) {
|
|
669
|
-
form.submit();
|
|
670
|
-
meta.submit_kind = "native_submit";
|
|
671
|
-
return JSON.stringify(meta);
|
|
672
|
-
}
|
|
673
|
-
if (isDisabled(submitter)) {
|
|
674
|
-
form.submit();
|
|
675
|
-
meta.submit_kind = "native_submit";
|
|
676
|
-
return JSON.stringify(meta);
|
|
677
|
-
}
|
|
678
|
-
submitter.click();
|
|
679
|
-
meta.submit_kind = "click";
|
|
680
|
-
return JSON.stringify(meta);
|
|
681
|
-
})()`;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
function buildMandaiAddonSubmitExpression(options: BrowseSubmitOptions): string {
|
|
685
|
-
return `(function() {
|
|
686
|
-
function findForm(selector) {
|
|
687
|
-
if (selector) return document.querySelector(selector);
|
|
688
|
-
return document.querySelector("form");
|
|
689
|
-
}
|
|
690
|
-
function isDisabled(node) {
|
|
691
|
-
if (!node) return false;
|
|
692
|
-
return !!(node.disabled || (node.classList && node.classList.contains("disabled")) || node.getAttribute("aria-disabled") === "true");
|
|
693
|
-
}
|
|
694
|
-
var form = findForm(${JSON.stringify(options.formSelector ?? "")});
|
|
695
|
-
if (!form) return JSON.stringify({ ok: false, reason: "form_not_found" });
|
|
696
|
-
var submitter = ${JSON.stringify(options.submitSelector ?? "")} ? document.querySelector(${JSON.stringify(options.submitSelector ?? "")}) : form.querySelector("[data-submit-next], .btn-proceed, button[type='submit'], input[type='submit']");
|
|
697
|
-
var meta = {
|
|
698
|
-
ok: true,
|
|
699
|
-
form_action: form.getAttribute("action") || "",
|
|
700
|
-
form_method: (form.getAttribute("method") || "GET").toUpperCase(),
|
|
701
|
-
submitter: submitter ? (submitter.getAttribute("name") || submitter.id || submitter.textContent || submitter.tagName || "").trim() : null,
|
|
702
|
-
submit_selector_used: ${JSON.stringify(options.submitSelector ?? null)},
|
|
703
|
-
form_selector_used: ${JSON.stringify(options.formSelector ?? null)},
|
|
704
|
-
prereq_state: { patched: [], missing: [] },
|
|
705
|
-
};
|
|
706
|
-
if (!submitter || isDisabled(submitter)) {
|
|
707
|
-
meta.ok = false;
|
|
708
|
-
meta.reason = "prereq_state_incomplete";
|
|
709
|
-
meta.prereq_state.missing.push(${JSON.stringify(options.submitSelector ?? "[data-submit-next]:not(.disabled)")});
|
|
710
|
-
return JSON.stringify(meta);
|
|
711
|
-
}
|
|
712
|
-
submitter.click();
|
|
713
|
-
meta.submit_kind = "click";
|
|
714
|
-
return JSON.stringify(meta);
|
|
715
|
-
})()`;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
function buildSameOriginFetchExpression(options: BrowseSubmitOptions): string {
|
|
719
|
-
return `(async function() {
|
|
720
|
-
function splitPlugins(value) {
|
|
721
|
-
return String(value || "")
|
|
722
|
-
.split(/[\\s,;]+/)
|
|
723
|
-
.map(function(part) { return part.trim(); })
|
|
724
|
-
.filter(Boolean);
|
|
725
|
-
}
|
|
726
|
-
function pluginPath(name) {
|
|
727
|
-
if (/^https?:\\/\\//i.test(name) || name.startsWith("/")) return name;
|
|
728
|
-
return "/etc/designs/wrs/footLibs/js/plugins/" + (name.endsWith(".js") ? name : name + ".js");
|
|
729
|
-
}
|
|
730
|
-
async function bestEffortRehydrate() {
|
|
731
|
-
var modules = Array.from(new Set(
|
|
732
|
-
Array.from(document.querySelectorAll("[data-load-plugins]"))
|
|
733
|
-
.flatMap(function(node) { return splitPlugins(node.getAttribute("data-load-plugins")); })
|
|
734
|
-
));
|
|
735
|
-
if (modules.length === 0) {
|
|
736
|
-
return { attempted: false, loaded: false, nooped: true, reason: "no_plugins", modules: [] };
|
|
737
|
-
}
|
|
738
|
-
if (!window.WRS || typeof window.WRS.require !== "function") {
|
|
739
|
-
return { attempted: false, loaded: false, nooped: true, reason: "missing_wrs_require", modules: modules };
|
|
740
|
-
}
|
|
741
|
-
var requireWrs = window.WRS.require.bind(window.WRS);
|
|
742
|
-
async function loadModules(paths) {
|
|
743
|
-
return await new Promise(function(resolve) {
|
|
744
|
-
var done = false;
|
|
745
|
-
var timer = setTimeout(function() {
|
|
746
|
-
if (done) return;
|
|
747
|
-
done = true;
|
|
748
|
-
resolve({ ok: false, reason: "timeout" });
|
|
749
|
-
}, 1500);
|
|
750
|
-
try {
|
|
751
|
-
requireWrs(paths, function() {
|
|
752
|
-
if (done) return;
|
|
753
|
-
done = true;
|
|
754
|
-
clearTimeout(timer);
|
|
755
|
-
resolve({ ok: true });
|
|
756
|
-
});
|
|
757
|
-
} catch (error) {
|
|
758
|
-
if (done) return;
|
|
759
|
-
done = true;
|
|
760
|
-
clearTimeout(timer);
|
|
761
|
-
resolve({ ok: false, reason: error && error.message ? error.message : String(error) });
|
|
762
|
-
}
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
var configResult = await loadModules(["/etc/designs/wrs/footLibs/js/config.js"]);
|
|
767
|
-
var pluginResult = await loadModules(modules.map(pluginPath));
|
|
768
|
-
for (var i = 0; i < 6; i++) {
|
|
769
|
-
await new Promise(function(resolve) { return setTimeout(resolve, 100); });
|
|
770
|
-
}
|
|
771
|
-
return {
|
|
772
|
-
attempted: true,
|
|
773
|
-
loaded: !!pluginResult.ok,
|
|
774
|
-
nooped: false,
|
|
775
|
-
reason: pluginResult.ok ? undefined : pluginResult.reason,
|
|
776
|
-
config_loaded: !!configResult.ok,
|
|
777
|
-
modules: modules,
|
|
778
|
-
};
|
|
779
|
-
}
|
|
780
|
-
function findForm(selector) {
|
|
781
|
-
if (selector) return document.querySelector(selector);
|
|
782
|
-
var active = document.activeElement;
|
|
783
|
-
if (active && active.closest) {
|
|
784
|
-
var fromActive = active.closest("form");
|
|
785
|
-
if (fromActive) return fromActive;
|
|
786
|
-
}
|
|
787
|
-
return document.querySelector("form");
|
|
788
|
-
}
|
|
789
|
-
function findSubmitter(form, selector) {
|
|
790
|
-
if (!form) return null;
|
|
791
|
-
if (selector) return document.querySelector(selector);
|
|
792
|
-
var active = document.activeElement;
|
|
793
|
-
if (active && form.contains(active) && /^(submit|image)$/i.test(active.getAttribute("type") || "")) return active;
|
|
794
|
-
return form.querySelector('button[type="submit"], input[type="submit"], input[type="image"], button:not([type])');
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
var form = findForm(${JSON.stringify(options.formSelector ?? "")});
|
|
798
|
-
if (!form) return JSON.stringify({ ok: false, reason: "form_not_found" });
|
|
799
|
-
|
|
800
|
-
var submitter = findSubmitter(form, ${JSON.stringify(options.submitSelector ?? "")});
|
|
801
|
-
var method = (form.getAttribute("method") || "GET").toUpperCase();
|
|
802
|
-
var action = form.getAttribute("action") || window.location.href;
|
|
803
|
-
var targetUrl = new URL(action, window.location.href);
|
|
804
|
-
if (targetUrl.origin !== window.location.origin) {
|
|
805
|
-
return JSON.stringify({ ok: false, reason: "cross_origin", url: targetUrl.href });
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
var formData = new FormData(form);
|
|
809
|
-
if (submitter && submitter.name) {
|
|
810
|
-
var submitValue = submitter.value != null ? submitter.value : "";
|
|
811
|
-
if (!formData.has(submitter.name)) formData.append(submitter.name, submitValue);
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
var headers = {};
|
|
815
|
-
var requestUrl = targetUrl.href;
|
|
816
|
-
var body;
|
|
817
|
-
if (method === "GET") {
|
|
818
|
-
var params = new URLSearchParams();
|
|
819
|
-
Array.from(formData.entries()).forEach(function(entry) {
|
|
820
|
-
var value = entry[1];
|
|
821
|
-
if (typeof value === "string") params.append(entry[0], value);
|
|
822
|
-
});
|
|
823
|
-
var query = params.toString();
|
|
824
|
-
if (query) requestUrl += (requestUrl.includes("?") ? "&" : "?") + query;
|
|
825
|
-
} else if ((form.enctype || "").includes("application/x-www-form-urlencoded")) {
|
|
826
|
-
var encoded = new URLSearchParams();
|
|
827
|
-
Array.from(formData.entries()).forEach(function(entry) {
|
|
828
|
-
var value = entry[1];
|
|
829
|
-
if (typeof value === "string") encoded.append(entry[0], value);
|
|
830
|
-
});
|
|
831
|
-
body = encoded.toString();
|
|
832
|
-
headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8";
|
|
833
|
-
} else {
|
|
834
|
-
body = formData;
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
try {
|
|
838
|
-
var response = await fetch(requestUrl, {
|
|
839
|
-
method: method,
|
|
840
|
-
body: method === "GET" ? undefined : body,
|
|
841
|
-
headers: headers,
|
|
842
|
-
credentials: "include",
|
|
843
|
-
redirect: "follow",
|
|
844
|
-
});
|
|
845
|
-
var contentType = response.headers.get("content-type") || "";
|
|
846
|
-
var text = await response.text();
|
|
847
|
-
var finalUrl = response.url || requestUrl;
|
|
848
|
-
if (!/text\\/html|application\\/xhtml\\+xml/i.test(contentType)) {
|
|
849
|
-
return JSON.stringify({
|
|
850
|
-
ok: false,
|
|
851
|
-
reason: "non_html_response",
|
|
852
|
-
status: response.status,
|
|
853
|
-
url: finalUrl,
|
|
854
|
-
content_type: contentType,
|
|
855
|
-
});
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
document.open();
|
|
859
|
-
document.write(text);
|
|
860
|
-
document.close();
|
|
861
|
-
if (finalUrl && finalUrl !== window.location.href) {
|
|
862
|
-
history.replaceState({}, "", finalUrl);
|
|
863
|
-
}
|
|
864
|
-
await new Promise(function(resolve) { return setTimeout(resolve, 50); });
|
|
865
|
-
var rehydrate = await bestEffortRehydrate();
|
|
866
|
-
return JSON.stringify({
|
|
867
|
-
ok: true,
|
|
868
|
-
status: response.status,
|
|
869
|
-
url: finalUrl,
|
|
870
|
-
same_origin_html_rehydrated: true,
|
|
871
|
-
rehydrate: rehydrate,
|
|
872
|
-
});
|
|
873
|
-
} catch (error) {
|
|
874
|
-
return JSON.stringify({
|
|
875
|
-
ok: false,
|
|
876
|
-
reason: error && error.message ? error.message : String(error),
|
|
877
|
-
});
|
|
878
|
-
}
|
|
879
|
-
})()`;
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
function parseJsonString(value: unknown): Record<string, unknown> | null {
|
|
883
|
-
if (typeof value !== "string") return asRecord(value);
|
|
884
|
-
try {
|
|
885
|
-
return asRecord(JSON.parse(value));
|
|
886
|
-
} catch {
|
|
887
|
-
return null;
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
async function settleSubmitDestination(
|
|
892
|
-
client: BrowseSubmitClient,
|
|
893
|
-
tabId: string,
|
|
894
|
-
url: string,
|
|
895
|
-
html: string,
|
|
896
|
-
options: { preferUrlOnly?: boolean } = {},
|
|
897
|
-
): Promise<{ url: string; html: string }> {
|
|
898
|
-
let settledUrl = url;
|
|
899
|
-
let settledHtml = html;
|
|
900
|
-
const deadline = Date.now() + SUBMIT_SETTLE_WINDOW_MS;
|
|
901
|
-
|
|
902
|
-
while (Date.now() < deadline) {
|
|
903
|
-
await sleep(Math.min(SUBMIT_POLL_INTERVAL_MS, Math.max(50, deadline - Date.now())));
|
|
904
|
-
const nextUrl = await client.getCurrentUrl(tabId).catch(() => "");
|
|
905
|
-
if (nextUrl && !nextUrl.startsWith("about:blank")) settledUrl = nextUrl;
|
|
906
|
-
if (!options.preferUrlOnly) {
|
|
907
|
-
const nextHtml = await client.getPageHtml(tabId).catch(() => "");
|
|
908
|
-
if (nextHtml) settledHtml = nextHtml;
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
if (options.preferUrlOnly) {
|
|
913
|
-
const finalHtml = await client.getPageHtml(tabId).catch(() => "");
|
|
914
|
-
if (finalHtml) settledHtml = finalHtml;
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
return { url: settledUrl, html: settledHtml };
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
async function waitForSubmitOutcome(
|
|
921
|
-
client: BrowseSubmitClient,
|
|
922
|
-
tabId: string,
|
|
923
|
-
beforeUrl: string,
|
|
924
|
-
beforeHtml: string,
|
|
925
|
-
options: BrowseSubmitOptions,
|
|
926
|
-
): Promise<{ ok: boolean; url: string; html: string }> {
|
|
927
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_SUBMIT_TIMEOUT_MS;
|
|
928
|
-
const deadline = Date.now() + timeoutMs;
|
|
929
|
-
const waitFor = options.waitFor?.trim();
|
|
930
|
-
const requireUrlTransition = !!waitFor && isUrlWaitHint(waitFor);
|
|
931
|
-
let lastHtml = beforeHtml;
|
|
932
|
-
let lastHtmlPollAt = 0;
|
|
933
|
-
|
|
934
|
-
if (waitFor && !isUrlWaitHint(waitFor)) {
|
|
935
|
-
try {
|
|
936
|
-
const waitResult = await client.waitForSelector(tabId, waitFor, timeoutMs);
|
|
937
|
-
if (waitResult?.status === "found" || waitResult?.status === "ready") {
|
|
938
|
-
const url = await client.getCurrentUrl(tabId).catch(() => beforeUrl);
|
|
939
|
-
const html = await client.getPageHtml(tabId).catch(() => beforeHtml);
|
|
940
|
-
return { ok: true, ...await settleSubmitDestination(client, tabId, url, html) };
|
|
941
|
-
}
|
|
942
|
-
} catch {
|
|
943
|
-
// fall through to polling
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
while (Date.now() < deadline) {
|
|
948
|
-
const url = await client.getCurrentUrl(tabId).catch(() => "");
|
|
949
|
-
|
|
950
|
-
if (waitFor && isUrlWaitHint(waitFor) && url.includes(waitFor)) {
|
|
951
|
-
return {
|
|
952
|
-
ok: true,
|
|
953
|
-
...await settleSubmitDestination(client, tabId, url, lastHtml, { preferUrlOnly: true }),
|
|
954
|
-
};
|
|
955
|
-
}
|
|
956
|
-
if (url && url !== beforeUrl && !url.startsWith("about:blank")) {
|
|
957
|
-
return {
|
|
958
|
-
ok: true,
|
|
959
|
-
...await settleSubmitDestination(client, tabId, url, lastHtml, { preferUrlOnly: requireUrlTransition }),
|
|
960
|
-
};
|
|
961
|
-
}
|
|
962
|
-
if (requireUrlTransition) {
|
|
963
|
-
await sleep(SUBMIT_POLL_INTERVAL_MS);
|
|
964
|
-
continue;
|
|
965
|
-
}
|
|
966
|
-
const now = Date.now();
|
|
967
|
-
let html = lastHtml;
|
|
968
|
-
if (now - lastHtmlPollAt >= SUBMIT_POLL_INTERVAL_MS * 2) {
|
|
969
|
-
const nextHtml = await client.getPageHtml(tabId).catch(() => "");
|
|
970
|
-
if (nextHtml) html = nextHtml;
|
|
971
|
-
lastHtml = html;
|
|
972
|
-
lastHtmlPollAt = now;
|
|
973
|
-
}
|
|
974
|
-
if (hasMeaningfulPageChange(beforeHtml, html)) {
|
|
975
|
-
return { ok: true, ...await settleSubmitDestination(client, tabId, url || beforeUrl, html) };
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
await sleep(SUBMIT_POLL_INTERVAL_MS);
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
return { ok: false, url: beforeUrl, html: beforeHtml };
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
export async function submitBrowseForm(
|
|
985
|
-
deps: BrowseSubmitDeps,
|
|
986
|
-
options: BrowseSubmitOptions = {},
|
|
987
|
-
): Promise<BrowseSubmitResult> {
|
|
988
|
-
const { client, session, flushCapture, restartCapture, rehydratePlugins } = deps;
|
|
989
|
-
const sameOriginFetchFallback = options.sameOriginFetchFallback ?? ENABLE_TRAVERSAL_FETCH_FALLBACK;
|
|
990
|
-
const beforeUrl = await client.getCurrentUrl(session.tabId).catch(() => session.url);
|
|
991
|
-
const beforeHtml = await client.getPageHtml(session.tabId).catch(() => "");
|
|
992
|
-
let pageProbe: BrowseSubmitPageProbe | null = null;
|
|
993
|
-
try {
|
|
994
|
-
pageProbe = parseJsonString(await client.evaluate(session.tabId, buildSubmitPageProbeExpression(options))) as BrowseSubmitPageProbe | null;
|
|
995
|
-
} catch {
|
|
996
|
-
pageProbe = null;
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
let domExpression = buildDomSubmitExpression(options);
|
|
1000
|
-
if (options.assistSiteState) {
|
|
1001
|
-
if (pageProbe?.action && /\/bin\/wrs\/product-selection/i.test(pageProbe.action) && pageProbe.ticketingType === "park") {
|
|
1002
|
-
domExpression = buildMandaiParkSubmitExpression(options);
|
|
1003
|
-
} else if (
|
|
1004
|
-
pageProbe?.action
|
|
1005
|
-
&& /\/bin\/wrs\/datestep\.json/i.test(pageProbe.action)
|
|
1006
|
-
&& pageProbe.step === "3"
|
|
1007
|
-
) {
|
|
1008
|
-
domExpression = buildMandaiDateSubmitExpression(options);
|
|
1009
|
-
} else if (
|
|
1010
|
-
pageProbe?.action
|
|
1011
|
-
&& /\/bin\/wrs\/addon-selection/i.test(pageProbe.action)
|
|
1012
|
-
&& pageProbe.step === "5"
|
|
1013
|
-
) {
|
|
1014
|
-
domExpression = buildMandaiAddonSubmitExpression(options);
|
|
1015
|
-
} else if (
|
|
1016
|
-
pageProbe?.action
|
|
1017
|
-
&& /\/bin\/wrs\/ticket-selection/i.test(pageProbe.action)
|
|
1018
|
-
&& pageProbe.step === "2"
|
|
1019
|
-
&& pageProbe.hasQuantityControls
|
|
1020
|
-
) {
|
|
1021
|
-
domExpression = buildMandaiTicketQuantitySubmitExpression(options);
|
|
1022
|
-
} else if (
|
|
1023
|
-
pageProbe?.action
|
|
1024
|
-
&& /\/bin\/wrs\/ticket-selection/i.test(pageProbe.action)
|
|
1025
|
-
&& pageProbe.step === "2"
|
|
1026
|
-
&& pageProbe.hasResidentGate
|
|
1027
|
-
&& !pageProbe.hasQuantityControls
|
|
1028
|
-
) {
|
|
1029
|
-
domExpression = buildMandaiResidentGateSubmitExpression(options);
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
traceSubmit("page-probe", {
|
|
1033
|
-
before_url: beforeUrl || session.url,
|
|
1034
|
-
page_probe: pageProbe,
|
|
1035
|
-
});
|
|
1036
|
-
|
|
1037
|
-
let submitMeta: Record<string, unknown> | null = null;
|
|
1038
|
-
let submitError: unknown = null;
|
|
1039
|
-
let submitMetaRaw: unknown = null;
|
|
1040
|
-
try {
|
|
1041
|
-
submitMetaRaw = await client.evaluate(session.tabId, domExpression);
|
|
1042
|
-
submitMeta = parseJsonString(submitMetaRaw);
|
|
1043
|
-
} catch (error) {
|
|
1044
|
-
submitError = error;
|
|
1045
|
-
}
|
|
1046
|
-
traceSubmit("dom-meta", {
|
|
1047
|
-
before_url: beforeUrl || session.url,
|
|
1048
|
-
submit_meta: submitMeta,
|
|
1049
|
-
submit_meta_raw_type: submitMetaRaw === null ? "null" : typeof submitMetaRaw,
|
|
1050
|
-
submit_meta_raw_preview: typeof submitMetaRaw === "string" && !submitMeta
|
|
1051
|
-
? submitMetaRaw.slice(0, 240)
|
|
1052
|
-
: undefined,
|
|
1053
|
-
submit_error: submitError instanceof Error ? submitError.message : String(submitError ?? ""),
|
|
1054
|
-
});
|
|
1055
|
-
|
|
1056
|
-
if (!submitMeta?.ok && submitMeta?.reason === "form_not_found") {
|
|
1057
|
-
return {
|
|
1058
|
-
ok: false,
|
|
1059
|
-
url: beforeUrl || session.url,
|
|
1060
|
-
mode: "noop",
|
|
1061
|
-
fallback_used: false,
|
|
1062
|
-
same_origin_html_rehydrated: false,
|
|
1063
|
-
recoverable: false,
|
|
1064
|
-
reason: "form_not_found",
|
|
1065
|
-
submit_meta: submitMeta,
|
|
1066
|
-
};
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
if (!submitMeta?.ok && submitMeta?.reason === "prereq_state_incomplete") {
|
|
1070
|
-
return {
|
|
1071
|
-
ok: false,
|
|
1072
|
-
url: beforeUrl || session.url,
|
|
1073
|
-
mode: "noop",
|
|
1074
|
-
fallback_used: false,
|
|
1075
|
-
same_origin_html_rehydrated: false,
|
|
1076
|
-
recoverable: false,
|
|
1077
|
-
reason: "prereq_state_incomplete",
|
|
1078
|
-
wait_for: options.waitFor,
|
|
1079
|
-
submit_meta: submitMeta,
|
|
1080
|
-
};
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
const domOutcome = await waitForSubmitOutcome(client, session.tabId, beforeUrl, beforeHtml, options);
|
|
1084
|
-
traceSubmit("dom-outcome", {
|
|
1085
|
-
before_url: beforeUrl || session.url,
|
|
1086
|
-
dom_ok: domOutcome.ok,
|
|
1087
|
-
dom_url: domOutcome.url,
|
|
1088
|
-
wait_for: options.waitFor ?? null,
|
|
1089
|
-
});
|
|
1090
|
-
if (domOutcome.ok) {
|
|
1091
|
-
const sameUrl = (domOutcome.url || beforeUrl || session.url) === (beforeUrl || session.url);
|
|
1092
|
-
if (submitMeta == null && sameUrl) {
|
|
1093
|
-
// Unknown submit state + no navigation usually means hidden DOM churn, not a real step transition.
|
|
1094
|
-
} else {
|
|
1095
|
-
session.url = domOutcome.url || beforeUrl || session.url;
|
|
1096
|
-
return {
|
|
1097
|
-
ok: true,
|
|
1098
|
-
url: session.url,
|
|
1099
|
-
mode: "dom",
|
|
1100
|
-
fallback_used: false,
|
|
1101
|
-
same_origin_html_rehydrated: false,
|
|
1102
|
-
wait_for: options.waitFor,
|
|
1103
|
-
submit_meta: submitMeta,
|
|
1104
|
-
capture_sync: null,
|
|
1105
|
-
};
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
if (submitError && !isRecoverableBrowseFailure(submitError) && !sameOriginFetchFallback) {
|
|
1110
|
-
throw submitError;
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
if (!sameOriginFetchFallback) {
|
|
1114
|
-
return {
|
|
1115
|
-
ok: false,
|
|
1116
|
-
url: beforeUrl || session.url,
|
|
1117
|
-
mode: "noop",
|
|
1118
|
-
fallback_used: false,
|
|
1119
|
-
same_origin_html_rehydrated: false,
|
|
1120
|
-
recoverable: !!submitError && isRecoverableBrowseFailure(submitError),
|
|
1121
|
-
reason: submitError instanceof Error ? submitError.message : "submit_failed",
|
|
1122
|
-
submit_meta: submitMeta,
|
|
1123
|
-
};
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
let fallbackPayload: Record<string, unknown> | null = null;
|
|
1127
|
-
let fallbackError: unknown = null;
|
|
1128
|
-
try {
|
|
1129
|
-
fallbackPayload = parseJsonString(await client.evaluate(session.tabId, buildSameOriginFetchExpression(options)));
|
|
1130
|
-
} catch (error) {
|
|
1131
|
-
fallbackError = error;
|
|
1132
|
-
}
|
|
1133
|
-
traceSubmit("fallback", {
|
|
1134
|
-
before_url: beforeUrl || session.url,
|
|
1135
|
-
fallback_payload: fallbackPayload,
|
|
1136
|
-
fallback_error: fallbackError instanceof Error ? fallbackError.message : String(fallbackError ?? ""),
|
|
1137
|
-
});
|
|
1138
|
-
if (fallbackError) {
|
|
1139
|
-
return {
|
|
1140
|
-
ok: false,
|
|
1141
|
-
url: beforeUrl || session.url,
|
|
1142
|
-
mode: "same_origin_fetch",
|
|
1143
|
-
fallback_used: true,
|
|
1144
|
-
same_origin_html_rehydrated: false,
|
|
1145
|
-
recoverable: isRecoverableBrowseFailure(fallbackError),
|
|
1146
|
-
reason: fallbackError instanceof Error ? fallbackError.message : "same_origin_fetch_failed",
|
|
1147
|
-
submit_meta: submitMeta,
|
|
1148
|
-
};
|
|
1149
|
-
}
|
|
1150
|
-
if (!fallbackPayload?.ok) {
|
|
1151
|
-
return {
|
|
1152
|
-
ok: false,
|
|
1153
|
-
url: String(fallbackPayload?.url ?? beforeUrl ?? session.url),
|
|
1154
|
-
mode: "same_origin_fetch",
|
|
1155
|
-
fallback_used: true,
|
|
1156
|
-
same_origin_html_rehydrated: false,
|
|
1157
|
-
recoverable: !!submitError && isRecoverableBrowseFailure(submitError),
|
|
1158
|
-
reason: String(fallbackPayload?.reason ?? "same_origin_fetch_failed"),
|
|
1159
|
-
status: typeof fallbackPayload?.status === "number" ? fallbackPayload.status as number : undefined,
|
|
1160
|
-
submit_meta: submitMeta,
|
|
1161
|
-
};
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
const finalUrl = String(fallbackPayload.url ?? await client.getCurrentUrl(session.tabId).catch(() => beforeUrl));
|
|
1165
|
-
session.url = finalUrl || beforeUrl || session.url;
|
|
1166
|
-
|
|
1167
|
-
let rehydrate = fallbackPayload.rehydrate;
|
|
1168
|
-
if (!rehydrate) {
|
|
1169
|
-
rehydrate = await rehydratePlugins(session.tabId).catch(() => null);
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
return {
|
|
1173
|
-
ok: true,
|
|
1174
|
-
url: session.url,
|
|
1175
|
-
mode: "same_origin_fetch",
|
|
1176
|
-
fallback_used: true,
|
|
1177
|
-
same_origin_html_rehydrated: fallbackPayload.same_origin_html_rehydrated === true,
|
|
1178
|
-
status: typeof fallbackPayload.status === "number" ? fallbackPayload.status as number : undefined,
|
|
1179
|
-
wait_for: options.waitFor,
|
|
1180
|
-
submit_meta: submitMeta,
|
|
1181
|
-
capture_sync: null,
|
|
1182
|
-
rehydrate,
|
|
1183
|
-
};
|
|
1184
|
-
}
|