@decocms/parity 0.4.0 → 0.5.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/CHANGELOG.md +25 -0
- package/dist/cli.js +972 -360
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,21 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
var __create = Object.create;
|
|
4
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
5
|
-
var __defProp = Object.defineProperty;
|
|
6
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __toESM = (mod, isNodeMode, target) => {
|
|
9
|
-
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
10
|
-
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
11
|
-
for (let key of __getOwnPropNames(mod))
|
|
12
|
-
if (!__hasOwnProp.call(to, key))
|
|
13
|
-
__defProp(to, key, {
|
|
14
|
-
get: () => mod[key],
|
|
15
|
-
enumerable: true
|
|
16
|
-
});
|
|
17
|
-
return to;
|
|
18
|
-
};
|
|
19
3
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
20
4
|
|
|
21
5
|
// src/cli.ts
|
|
@@ -302,7 +286,13 @@ var ParityRc = z.object({
|
|
|
302
286
|
minicartTrigger: z.string().optional(),
|
|
303
287
|
cepInputPdp: z.string().optional(),
|
|
304
288
|
cepInputCart: z.string().optional(),
|
|
305
|
-
checkoutButton: z.string().optional()
|
|
289
|
+
checkoutButton: z.string().optional(),
|
|
290
|
+
sizeSwatch: z.string().optional(),
|
|
291
|
+
colorSwatch: z.string().optional(),
|
|
292
|
+
variantRow: z.string().optional(),
|
|
293
|
+
quantityIncrement: z.string().optional(),
|
|
294
|
+
quantityInput: z.string().optional(),
|
|
295
|
+
minicartCount: z.string().optional()
|
|
306
296
|
}).default({}),
|
|
307
297
|
skipSteps: z.array(z.string()).default([])
|
|
308
298
|
});
|
|
@@ -4469,6 +4459,23 @@ async function callAnthropicTool(params) {
|
|
|
4469
4459
|
return null;
|
|
4470
4460
|
}
|
|
4471
4461
|
async function callOpenRouterTool(params) {
|
|
4462
|
+
const baseTokens = params.maxTokens ?? 2000;
|
|
4463
|
+
const totalBudget = params.timeoutMs ?? defaultTimeout(params);
|
|
4464
|
+
const start = Date.now();
|
|
4465
|
+
for (let attempt = 1;attempt <= 2; attempt++) {
|
|
4466
|
+
const isRetry = attempt > 1;
|
|
4467
|
+
const remaining = totalBudget - (Date.now() - start);
|
|
4468
|
+
if (remaining <= 2000 && isRetry) {
|
|
4469
|
+
return null;
|
|
4470
|
+
}
|
|
4471
|
+
const tokens = isRetry ? Math.max(baseTokens * 2, 4000) : baseTokens;
|
|
4472
|
+
const result = await openRouterToolOnce(params, tokens, isRetry, Math.max(remaining, 2000));
|
|
4473
|
+
if (result !== undefined)
|
|
4474
|
+
return result;
|
|
4475
|
+
}
|
|
4476
|
+
return null;
|
|
4477
|
+
}
|
|
4478
|
+
async function openRouterToolOnce(params, maxTokens, isRetry, attemptTimeoutMs) {
|
|
4472
4479
|
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
4473
4480
|
if (!apiKey)
|
|
4474
4481
|
return null;
|
|
@@ -4481,8 +4488,7 @@ async function callOpenRouterTool(params) {
|
|
|
4481
4488
|
}
|
|
4482
4489
|
});
|
|
4483
4490
|
}
|
|
4484
|
-
const
|
|
4485
|
-
const { signal, clear } = makeTimeoutSignal(timeoutMs);
|
|
4491
|
+
const { signal, clear } = makeTimeoutSignal(attemptTimeoutMs);
|
|
4486
4492
|
try {
|
|
4487
4493
|
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
4488
4494
|
method: "POST",
|
|
@@ -4495,7 +4501,7 @@ async function callOpenRouterTool(params) {
|
|
|
4495
4501
|
},
|
|
4496
4502
|
body: JSON.stringify({
|
|
4497
4503
|
model: LLM_MODEL_OPENROUTER,
|
|
4498
|
-
max_tokens:
|
|
4504
|
+
max_tokens: maxTokens,
|
|
4499
4505
|
messages: [
|
|
4500
4506
|
{ role: "system", content: params.systemPrompt },
|
|
4501
4507
|
{ role: "user", content: userContent }
|
|
@@ -4516,6 +4522,8 @@ async function callOpenRouterTool(params) {
|
|
|
4516
4522
|
if (!response.ok) {
|
|
4517
4523
|
const txt = await response.text().catch(() => "");
|
|
4518
4524
|
console.error(`[llm-openrouter] HTTP ${response.status}: ${txt.slice(0, 200)}`);
|
|
4525
|
+
if (response.status >= 500 || response.status === 429)
|
|
4526
|
+
return;
|
|
4519
4527
|
return null;
|
|
4520
4528
|
}
|
|
4521
4529
|
const json = await response.json();
|
|
@@ -4531,8 +4539,12 @@ async function callOpenRouterTool(params) {
|
|
|
4531
4539
|
return JSON.parse(repaired);
|
|
4532
4540
|
} catch {}
|
|
4533
4541
|
}
|
|
4534
|
-
|
|
4535
|
-
|
|
4542
|
+
if (isRetry) {
|
|
4543
|
+
console.error(`[llm-openrouter] failed to parse tool arguments after retry (raw len=${raw.length}, head=${JSON.stringify(raw.slice(0, 80))})`);
|
|
4544
|
+
return null;
|
|
4545
|
+
}
|
|
4546
|
+
console.error(`[llm-openrouter] parse error, retrying with more tokens (raw len=${raw.length}, head=${JSON.stringify(raw.slice(0, 80))})`);
|
|
4547
|
+
return;
|
|
4536
4548
|
}
|
|
4537
4549
|
}
|
|
4538
4550
|
const content = json.choices?.[0]?.message?.content;
|
|
@@ -4541,12 +4553,17 @@ async function callOpenRouterTool(params) {
|
|
|
4541
4553
|
return JSON.parse(content);
|
|
4542
4554
|
} catch {}
|
|
4543
4555
|
}
|
|
4556
|
+
return null;
|
|
4544
4557
|
} catch (err) {
|
|
4545
|
-
|
|
4558
|
+
const msg = err.message;
|
|
4559
|
+
console.error(`[llm-openrouter] failed: ${msg}`);
|
|
4560
|
+
if (!isRetry && (msg.includes("ECONN") || msg.includes("aborted") || msg.includes("fetch"))) {
|
|
4561
|
+
return;
|
|
4562
|
+
}
|
|
4563
|
+
return null;
|
|
4546
4564
|
} finally {
|
|
4547
4565
|
clear();
|
|
4548
4566
|
}
|
|
4549
|
-
return null;
|
|
4550
4567
|
}
|
|
4551
4568
|
function tryRepairJson(raw) {
|
|
4552
4569
|
let s = raw.trim();
|
|
@@ -4962,7 +4979,13 @@ var SelectorKey = z2.enum([
|
|
|
4962
4979
|
"minicartTrigger",
|
|
4963
4980
|
"cepInputPdp",
|
|
4964
4981
|
"cepInputCart",
|
|
4965
|
-
"checkoutButton"
|
|
4982
|
+
"checkoutButton",
|
|
4983
|
+
"sizeSwatch",
|
|
4984
|
+
"colorSwatch",
|
|
4985
|
+
"variantRow",
|
|
4986
|
+
"quantityIncrement",
|
|
4987
|
+
"quantityInput",
|
|
4988
|
+
"minicartCount"
|
|
4966
4989
|
]);
|
|
4967
4990
|
var SelectorEntry = z2.object({
|
|
4968
4991
|
selector: z2.string(),
|
|
@@ -5156,8 +5179,80 @@ var DEFAULT_SELECTORS = {
|
|
|
5156
5179
|
"button:has-text('Finalizar compra')",
|
|
5157
5180
|
"a:has-text('Ir para o checkout')",
|
|
5158
5181
|
"button:has-text('Ir para o checkout')",
|
|
5182
|
+
"[role='dialog'] a:has-text('Finalizar')",
|
|
5183
|
+
"[role='dialog'] button:has-text('Finalizar')",
|
|
5184
|
+
"[data-minicart] a:has-text('Finalizar')",
|
|
5185
|
+
"[data-minicart] button:has-text('Finalizar')",
|
|
5186
|
+
"[data-cart-drawer] a:has-text('Finalizar')",
|
|
5187
|
+
"[data-cart-drawer] button:has-text('Finalizar')",
|
|
5159
5188
|
"a:has-text('Checkout')",
|
|
5160
|
-
"[data-checkout-button]"
|
|
5189
|
+
"[data-checkout-button]",
|
|
5190
|
+
"a:text-is('Finalizar')",
|
|
5191
|
+
"button:text-is('Finalizar')"
|
|
5192
|
+
],
|
|
5193
|
+
sizeSwatch: [
|
|
5194
|
+
"[data-size]:not([disabled]):not([aria-disabled='true']):not(.unavailable):not(.sold-out)",
|
|
5195
|
+
"[data-variant-size]:not([disabled]):not([aria-disabled='true']):not(.unavailable)",
|
|
5196
|
+
"button[aria-label*='tamanho' i]:not([aria-disabled='true']):not([disabled])",
|
|
5197
|
+
"button[aria-label*='size' i]:not([aria-disabled='true']):not([disabled])",
|
|
5198
|
+
"[data-testid='size-selector'] button:not([disabled]):not(.unavailable)",
|
|
5199
|
+
".size-selector button:not([disabled]):not(.unavailable):not(.sold-out)",
|
|
5200
|
+
"[class*='size'] button:not([disabled]):not(.unavailable)",
|
|
5201
|
+
"label:has(input[name*='size' i]:not([disabled])) "
|
|
5202
|
+
],
|
|
5203
|
+
colorSwatch: [
|
|
5204
|
+
"[data-color]:not([disabled]):not([aria-disabled='true']):not(.unavailable):not(.sold-out)",
|
|
5205
|
+
"[data-variant-color]:not([disabled]):not([aria-disabled='true']):not(.unavailable)",
|
|
5206
|
+
"button[aria-label*='cor' i]:not([aria-disabled='true']):not([disabled])",
|
|
5207
|
+
"button[aria-label*='color' i]:not([aria-disabled='true']):not([disabled])",
|
|
5208
|
+
"[data-testid='color-selector'] button:not([disabled]):not(.unavailable)",
|
|
5209
|
+
".color-swatch:not(.unavailable):not(.sold-out)",
|
|
5210
|
+
"[class*='color'] button:not([disabled]):not(.unavailable)"
|
|
5211
|
+
],
|
|
5212
|
+
variantRow: [
|
|
5213
|
+
"[data-sku-row]",
|
|
5214
|
+
"[data-variant-row]",
|
|
5215
|
+
"tr[data-variant]",
|
|
5216
|
+
"tr:has([aria-label*='quantidade' i])",
|
|
5217
|
+
"tbody tr:has(input[type='number'])"
|
|
5218
|
+
],
|
|
5219
|
+
quantityIncrement: [
|
|
5220
|
+
"[data-qty-plus]",
|
|
5221
|
+
"[data-quantity-plus]",
|
|
5222
|
+
"button[aria-label*='aumentar' i]",
|
|
5223
|
+
"button[aria-label*='increase' i]",
|
|
5224
|
+
"button[aria-label*='increment' i]",
|
|
5225
|
+
"button:has-text('+')",
|
|
5226
|
+
"[class*='qty'] button:has-text('+')"
|
|
5227
|
+
],
|
|
5228
|
+
quantityInput: [
|
|
5229
|
+
"input[type='number'][min='0'][value='0']",
|
|
5230
|
+
"input[id*='quantity-input' i]",
|
|
5231
|
+
"input[name*='qty' i]",
|
|
5232
|
+
"input[name*='quantity' i]",
|
|
5233
|
+
"input[name*='quantidade' i]",
|
|
5234
|
+
"input[aria-label*='quantidade' i]",
|
|
5235
|
+
"[data-qty-input]",
|
|
5236
|
+
"input[type='number'][min='0']",
|
|
5237
|
+
"input[type='number'][min='1']"
|
|
5238
|
+
],
|
|
5239
|
+
minicartCount: [
|
|
5240
|
+
"[data-minicart-count]",
|
|
5241
|
+
"[data-cart-count]",
|
|
5242
|
+
".cart-count",
|
|
5243
|
+
".minicart__count",
|
|
5244
|
+
"[class*='cart-count']",
|
|
5245
|
+
"[class*='CartCount']",
|
|
5246
|
+
"[aria-label*='itens' i][role='status']",
|
|
5247
|
+
"header [data-minicart-trigger] [class*='badge']",
|
|
5248
|
+
"header [aria-label*='cart' i] [class*='count']",
|
|
5249
|
+
"header [aria-label*='sacola' i] [class*='badge']",
|
|
5250
|
+
"header [aria-label*='sacola' i] [class*='count']",
|
|
5251
|
+
"header a[href*='/cart'] [class*='count']",
|
|
5252
|
+
"header a[href*='/checkout'] [class*='count']",
|
|
5253
|
+
"[data-fs-cart-icon] + [class*='count']",
|
|
5254
|
+
"[class*='Minicart'] [class*='count']",
|
|
5255
|
+
"[class*='minicart'] [class*='count']"
|
|
5161
5256
|
]
|
|
5162
5257
|
};
|
|
5163
5258
|
function selectorsFor(key, ctxOrRc = {}) {
|
|
@@ -5182,7 +5277,7 @@ function isResolutionContext(v) {
|
|
|
5182
5277
|
}
|
|
5183
5278
|
|
|
5184
5279
|
// src/engine/flows.ts
|
|
5185
|
-
var PURCHASE_JOURNEY_TOTAL_STEPS =
|
|
5280
|
+
var PURCHASE_JOURNEY_TOTAL_STEPS = 9;
|
|
5186
5281
|
var DEBUG_PARITY2 = process.env.DEBUG_PARITY === "1" || process.env.DEBUG_PARITY === "true";
|
|
5187
5282
|
var DEBUG_START2 = Date.now();
|
|
5188
5283
|
function dlog2(ctx, msg) {
|
|
@@ -5192,6 +5287,41 @@ function dlog2(ctx, msg) {
|
|
|
5192
5287
|
process.stderr.write(`[+${elapsed}s ${ctx.viewport}/${ctx.side}] ${msg}
|
|
5193
5288
|
`);
|
|
5194
5289
|
}
|
|
5290
|
+
function withCap(p, capMs, fallback) {
|
|
5291
|
+
return Promise.race([
|
|
5292
|
+
p.catch(() => fallback),
|
|
5293
|
+
new Promise((resolve) => setTimeout(() => resolve(fallback), capMs))
|
|
5294
|
+
]);
|
|
5295
|
+
}
|
|
5296
|
+
var VARIANT_REQUIRED_TEXT_PATTERNS = [
|
|
5297
|
+
/selecione um produto/i,
|
|
5298
|
+
/selecione um tamanho/i,
|
|
5299
|
+
/selecione uma cor/i,
|
|
5300
|
+
/selecione uma op[cç][aã]o/i,
|
|
5301
|
+
/select a size/i,
|
|
5302
|
+
/select a color/i,
|
|
5303
|
+
/select an option/i,
|
|
5304
|
+
/choose an option/i,
|
|
5305
|
+
/please select/i,
|
|
5306
|
+
/select size/i,
|
|
5307
|
+
/select color/i
|
|
5308
|
+
];
|
|
5309
|
+
var ADD_TO_CART_ERROR_PATTERNS = [
|
|
5310
|
+
...VARIANT_REQUIRED_TEXT_PATTERNS,
|
|
5311
|
+
/estoque esgotado/i,
|
|
5312
|
+
/out of stock/i,
|
|
5313
|
+
/indispon[ií]vel/i,
|
|
5314
|
+
/unavailable/i
|
|
5315
|
+
];
|
|
5316
|
+
var ADD_TO_CART_SUCCESS_PATTERNS = [
|
|
5317
|
+
/produto adicionado/i,
|
|
5318
|
+
/adicionado ao carrinho/i,
|
|
5319
|
+
/adicionado [aà]\s+sacola/i,
|
|
5320
|
+
/added to cart/i,
|
|
5321
|
+
/added to bag/i,
|
|
5322
|
+
/item added/i,
|
|
5323
|
+
/successfully added/i
|
|
5324
|
+
];
|
|
5195
5325
|
function selFor(ctx, key) {
|
|
5196
5326
|
return selectorsFor(key, { rc: ctx.rc, learned: ctx.learned, platform: ctx.platform });
|
|
5197
5327
|
}
|
|
@@ -5199,7 +5329,7 @@ var FLOW_DEADLINE_MS = {
|
|
|
5199
5329
|
homepage: 90000,
|
|
5200
5330
|
plp: 180000,
|
|
5201
5331
|
pdp: 240000,
|
|
5202
|
-
"purchase-journey":
|
|
5332
|
+
"purchase-journey": 420000
|
|
5203
5333
|
};
|
|
5204
5334
|
async function runFlow(flow, ctx) {
|
|
5205
5335
|
const start = Date.now();
|
|
@@ -5236,7 +5366,7 @@ async function runFlow(flow, ctx) {
|
|
|
5236
5366
|
status: "failed",
|
|
5237
5367
|
durationMs: deadlineMs,
|
|
5238
5368
|
screenshotPath: "",
|
|
5239
|
-
actionDescription: `[flow-timeout] flow "${flow}" excedeu ${deadlineMs}ms — captura abortada pela safety net externa, ${pages.length} page(s) fechada(s) para liberar o contexto
|
|
5369
|
+
actionDescription: `[flow-timeout] flow "${flow}" excedeu ${deadlineMs}ms — captura abortada pela safety net externa, ${pages.length} page(s) fechada(s) para liberar o contexto.`
|
|
5240
5370
|
}
|
|
5241
5371
|
], start));
|
|
5242
5372
|
const CLOSE_CAP_MS = 5000;
|
|
@@ -5244,7 +5374,7 @@ async function runFlow(flow, ctx) {
|
|
|
5244
5374
|
p.close().catch(() => {
|
|
5245
5375
|
return;
|
|
5246
5376
|
}),
|
|
5247
|
-
new Promise((
|
|
5377
|
+
new Promise((resolveClose) => setTimeout(resolveClose, CLOSE_CAP_MS))
|
|
5248
5378
|
]);
|
|
5249
5379
|
cleanup = Promise.allSettled(pages.map(cappedClose));
|
|
5250
5380
|
}, deadlineMs);
|
|
@@ -5352,7 +5482,7 @@ async function flowPurchaseJourney(ctx) {
|
|
|
5352
5482
|
const pages = [];
|
|
5353
5483
|
const steps = [];
|
|
5354
5484
|
const page = await ctx.ctx.newPage();
|
|
5355
|
-
|
|
5485
|
+
const budget = { remaining: ctx.recoveryBudget ?? 5 };
|
|
5356
5486
|
const total = PURCHASE_JOURNEY_TOTAL_STEPS;
|
|
5357
5487
|
const reportStart = (idx, name) => {
|
|
5358
5488
|
ctx.onStep?.({ phase: "start", name, index: idx, total });
|
|
@@ -5362,15 +5492,12 @@ async function flowPurchaseJourney(ctx) {
|
|
|
5362
5492
|
};
|
|
5363
5493
|
try {
|
|
5364
5494
|
reportStart(1, "visit-home");
|
|
5365
|
-
dlog2(ctx, `step 1 visit-home: capturePage(${ctx.baseUrl}) start (scrollToLoad=false)`);
|
|
5366
5495
|
const homeCap = await capturePage(page, {
|
|
5367
5496
|
url: ctx.baseUrl,
|
|
5368
5497
|
side: ctx.side,
|
|
5369
5498
|
viewport: ctx.viewport,
|
|
5370
|
-
screenshotPath: screenshotPath(ctx, "pj-1-home")
|
|
5371
|
-
scrollToLoad: false
|
|
5499
|
+
screenshotPath: screenshotPath(ctx, "pj-1-home")
|
|
5372
5500
|
});
|
|
5373
|
-
dlog2(ctx, `step 1 visit-home: capturePage done status=${homeCap.status}`);
|
|
5374
5501
|
pages.push(homeCap);
|
|
5375
5502
|
const step1Status = homeCap.status >= 200 && homeCap.status < 400 ? "ok" : "failed";
|
|
5376
5503
|
steps.push({
|
|
@@ -5389,24 +5516,19 @@ async function flowPurchaseJourney(ctx) {
|
|
|
5389
5516
|
return { pages, steps };
|
|
5390
5517
|
}
|
|
5391
5518
|
reportStart(2, "navigate-plp");
|
|
5392
|
-
dlog2(ctx, `step 2 navigate-plp: findCategoryUrl start (hint=${ctx.rc.plpUrlHint ?? "—"})`);
|
|
5393
5519
|
const plpHit = ctx.rc.plpUrlHint ? { url: new URL(ctx.rc.plpUrlHint, ctx.baseUrl).toString(), selector: "__hint__" } : await findCategoryUrl(page, ctx);
|
|
5394
|
-
dlog2(ctx, `step 2 navigate-plp: findCategoryUrl done → ${plpHit?.url ?? "null"}`);
|
|
5395
5520
|
if (!plpHit) {
|
|
5396
5521
|
steps.push(makeSkipStep(2, "navigate-plp", ctx, "no category link found"));
|
|
5397
5522
|
reportEnd(2, "navigate-plp", "skipped", 0, "no category link found");
|
|
5398
5523
|
return { pages, steps };
|
|
5399
5524
|
}
|
|
5400
5525
|
const t2 = Date.now();
|
|
5401
|
-
dlog2(ctx, `step 2 navigate-plp: capturePage(${plpHit.url}) start (scrollToLoad=false)`);
|
|
5402
5526
|
const plpCap = await capturePage(page, {
|
|
5403
5527
|
url: plpHit.url,
|
|
5404
5528
|
side: ctx.side,
|
|
5405
5529
|
viewport: ctx.viewport,
|
|
5406
|
-
screenshotPath: screenshotPath(ctx, "pj-2-plp")
|
|
5407
|
-
scrollToLoad: false
|
|
5530
|
+
screenshotPath: screenshotPath(ctx, "pj-2-plp")
|
|
5408
5531
|
});
|
|
5409
|
-
dlog2(ctx, `step 2 navigate-plp: capturePage done status=${plpCap.status}`);
|
|
5410
5532
|
pages.push(plpCap);
|
|
5411
5533
|
const step2Status = plpCap.status >= 200 && plpCap.status < 400 ? "ok" : "failed";
|
|
5412
5534
|
steps.push({
|
|
@@ -5425,9 +5547,35 @@ async function flowPurchaseJourney(ctx) {
|
|
|
5425
5547
|
steps[steps.length - 1].actionDescription = `Navegou pra categoria \`${plpHit.url}\` (via \`${plpHit.selector}\`)`;
|
|
5426
5548
|
steps[steps.length - 1].beforeUrl = ctx.baseUrl;
|
|
5427
5549
|
reportStart(3, "enter-pdp");
|
|
5428
|
-
|
|
5550
|
+
let pdpHit = await findProductUrl(page, ctx);
|
|
5551
|
+
let pdpRecoveredByLlm = false;
|
|
5552
|
+
if (!pdpHit && budget.remaining > 0) {
|
|
5553
|
+
const html = await page.content().catch(() => "");
|
|
5554
|
+
if (html) {
|
|
5555
|
+
const suggestion = await suggestRecovery({
|
|
5556
|
+
stepName: "enter-pdp",
|
|
5557
|
+
intendedAction: "Encontrar um link <a> que leve para a página de detalhes (PDP) de algum produto listado na PLP atual",
|
|
5558
|
+
html,
|
|
5559
|
+
alreadyTried: selFor(ctx, "productCard")
|
|
5560
|
+
});
|
|
5561
|
+
if (suggestion) {
|
|
5562
|
+
budget.remaining--;
|
|
5563
|
+
try {
|
|
5564
|
+
const el = page.locator(suggestion.selector).first();
|
|
5565
|
+
if (await el.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
5566
|
+
const href = await el.getAttribute("href");
|
|
5567
|
+
if (href) {
|
|
5568
|
+
const abs = new URL(href, page.url()).toString();
|
|
5569
|
+
pdpHit = { url: abs, selector: suggestion.selector };
|
|
5570
|
+
pdpRecoveredByLlm = true;
|
|
5571
|
+
}
|
|
5572
|
+
}
|
|
5573
|
+
} catch {}
|
|
5574
|
+
}
|
|
5575
|
+
}
|
|
5576
|
+
}
|
|
5429
5577
|
if (!pdpHit) {
|
|
5430
|
-
steps.push(makeSkipStep(3, "enter-pdp", ctx, "no product card found"));
|
|
5578
|
+
steps.push(makeSkipStep(3, "enter-pdp", ctx, "no product card found (recovery exhausted)"));
|
|
5431
5579
|
reportEnd(3, "enter-pdp", "skipped", 0, "no product card found");
|
|
5432
5580
|
return { pages, steps };
|
|
5433
5581
|
}
|
|
@@ -5436,8 +5584,7 @@ async function flowPurchaseJourney(ctx) {
|
|
|
5436
5584
|
url: pdpHit.url,
|
|
5437
5585
|
side: ctx.side,
|
|
5438
5586
|
viewport: ctx.viewport,
|
|
5439
|
-
screenshotPath: screenshotPath(ctx, "pj-3-pdp")
|
|
5440
|
-
scrollToLoad: false
|
|
5587
|
+
screenshotPath: screenshotPath(ctx, "pj-3-pdp")
|
|
5441
5588
|
});
|
|
5442
5589
|
pages.push(pdpCap);
|
|
5443
5590
|
const step3Status = pdpCap.status >= 200 && pdpCap.status < 400 ? "ok" : "failed";
|
|
@@ -5451,10 +5598,11 @@ async function flowPurchaseJourney(ctx) {
|
|
|
5451
5598
|
url: pdpCap.finalUrl,
|
|
5452
5599
|
screenshotPath: pdpCap.screenshotPath,
|
|
5453
5600
|
selectorKey: "productCard",
|
|
5454
|
-
usedSelector: pdpHit.selector
|
|
5601
|
+
usedSelector: pdpHit.selector,
|
|
5602
|
+
recoveredByLlm: pdpRecoveredByLlm || undefined
|
|
5455
5603
|
});
|
|
5456
5604
|
reportEnd(3, "enter-pdp", step3Status, Date.now() - t3);
|
|
5457
|
-
steps[steps.length - 1].actionDescription = `Abriu PDP \`${pdpHit.url}\` (via \`${pdpHit.selector}
|
|
5605
|
+
steps[steps.length - 1].actionDescription = `Abriu PDP \`${pdpHit.url}\` (via \`${pdpHit.selector}\`${pdpRecoveredByLlm ? " — recovery LLM" : ""})`;
|
|
5458
5606
|
steps[steps.length - 1].beforeUrl = plpHit.url;
|
|
5459
5607
|
const expectedProductTitle = await extractProductTitle(page);
|
|
5460
5608
|
dlog2(ctx, `step 3 enter-pdp: extracted product title → ${expectedProductTitle ? `"${expectedProductTitle.slice(0, 60)}"` : "null"}`);
|
|
@@ -5464,115 +5612,211 @@ async function flowPurchaseJourney(ctx) {
|
|
|
5464
5612
|
productTitle: expectedProductTitle
|
|
5465
5613
|
};
|
|
5466
5614
|
}
|
|
5467
|
-
reportStart(4, "
|
|
5615
|
+
reportStart(4, "select-variant");
|
|
5616
|
+
const t4 = Date.now();
|
|
5617
|
+
const beforeUrl4 = page.url();
|
|
5618
|
+
const spBefore4 = screenshotPath(ctx, "pj-4-select-variant-before");
|
|
5619
|
+
await page.screenshot({ path: spBefore4, fullPage: false }).catch(() => {
|
|
5620
|
+
return;
|
|
5621
|
+
});
|
|
5622
|
+
const variantResult = await selectVariant(page, ctx);
|
|
5623
|
+
let variantLlmAction = null;
|
|
5624
|
+
if (variantResult.actions.length === 0 && variantResult.variantRequired && budget.remaining > 0) {
|
|
5625
|
+
variantLlmAction = await attemptStepAction({
|
|
5626
|
+
page,
|
|
5627
|
+
ctx,
|
|
5628
|
+
stepName: "select-variant",
|
|
5629
|
+
intendedAction: "Selecionar uma variante disponível (tamanho, cor, sabor) ou clicar no botão '+' para incrementar a quantidade da primeira variante listada na PDP. Não clique no botão de comprar.",
|
|
5630
|
+
selectorKey: "quantityIncrement",
|
|
5631
|
+
action: "click",
|
|
5632
|
+
recoveryBudget: budget
|
|
5633
|
+
});
|
|
5634
|
+
}
|
|
5635
|
+
const sp4 = screenshotPath(ctx, "pj-4-select-variant");
|
|
5636
|
+
await page.screenshot({ path: sp4, fullPage: false }).catch(() => {
|
|
5637
|
+
return;
|
|
5638
|
+
});
|
|
5639
|
+
if (variantResult.actions.length > 0 || variantLlmAction?.performed) {
|
|
5640
|
+
const llmDesc = variantLlmAction?.performed ? `Recovery LLM: ${variantLlmAction.action} em \`${variantLlmAction.selector}\`` : null;
|
|
5641
|
+
const desc = variantResult.actions.length > 0 ? variantResult.actions.join("; ") : llmDesc ?? "(variante selecionada)";
|
|
5642
|
+
steps.push({
|
|
5643
|
+
step: 4,
|
|
5644
|
+
name: "select-variant",
|
|
5645
|
+
side: ctx.side,
|
|
5646
|
+
viewport: ctx.viewport,
|
|
5647
|
+
status: "ok",
|
|
5648
|
+
durationMs: Date.now() - t4,
|
|
5649
|
+
url: page.url(),
|
|
5650
|
+
screenshotPath: sp4,
|
|
5651
|
+
screenshotBeforePath: spBefore4,
|
|
5652
|
+
beforeUrl: beforeUrl4,
|
|
5653
|
+
actionDescription: llmDesc && variantResult.actions.length > 0 ? `${desc}; ${llmDesc}` : desc,
|
|
5654
|
+
selectorKey: variantLlmAction?.performed ? "quantityIncrement" : variantResult.primarySelectorKey,
|
|
5655
|
+
usedSelector: variantLlmAction?.performed ? variantLlmAction.selector : variantResult.primarySelector,
|
|
5656
|
+
recoveredByLlm: variantLlmAction?.recoveredByLlm || undefined,
|
|
5657
|
+
detail: { actions: variantResult.actions, llmRecovery: !!variantLlmAction?.performed }
|
|
5658
|
+
});
|
|
5659
|
+
reportEnd(4, "select-variant", "ok", Date.now() - t4);
|
|
5660
|
+
} else {
|
|
5661
|
+
const skipNote = variantResult.variantRequired ? "variant required by store but no selectors matched (LLM recovery also failed)" : "no variant selectors found on PDP (assuming single-SKU product)";
|
|
5662
|
+
steps.push(makeSkipStep(4, "select-variant", ctx, skipNote));
|
|
5663
|
+
reportEnd(4, "select-variant", "skipped", 0, skipNote);
|
|
5664
|
+
}
|
|
5665
|
+
reportStart(5, "shipping-calc-pdp");
|
|
5468
5666
|
let cepInputPdp = await firstVisible(page, selFor(ctx, "cepInputPdp"));
|
|
5469
|
-
let
|
|
5470
|
-
if (!cepInputPdp &&
|
|
5471
|
-
const
|
|
5472
|
-
if (
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5667
|
+
let cepPdpRecoveredByLlm = false;
|
|
5668
|
+
if (!cepInputPdp && budget.remaining > 0) {
|
|
5669
|
+
const html = await page.content().catch(() => "");
|
|
5670
|
+
if (html) {
|
|
5671
|
+
const suggestion = await suggestRecovery({
|
|
5672
|
+
stepName: "shipping-calc-pdp",
|
|
5673
|
+
intendedAction: "Encontre o <input> de CEP (código postal de ENDEREÇO de entrega) na PDP, usado pra CALCULAR FRETE — placeholder normalmente é 'CEP', 'Digite seu CEP', 'Cód. postal', 'Postal code', aria-label='CEP' ou name='zip'/'cep'/'postal'. **NÃO** retorne o input de CUPOM de desconto / código promocional / 'Digite o cupom' / 'Insira código' — esses são pra desconto, não pra frete.",
|
|
5674
|
+
html,
|
|
5675
|
+
alreadyTried: selFor(ctx, "cepInputPdp")
|
|
5676
|
+
});
|
|
5677
|
+
if (suggestion) {
|
|
5678
|
+
budget.remaining--;
|
|
5679
|
+
const el = page.locator(suggestion.selector).first();
|
|
5680
|
+
if (await el.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
5681
|
+
cepInputPdp = suggestion.selector;
|
|
5682
|
+
cepPdpRecoveredByLlm = true;
|
|
5683
|
+
}
|
|
5684
|
+
}
|
|
5476
5685
|
}
|
|
5477
5686
|
}
|
|
5478
5687
|
if (cepInputPdp) {
|
|
5479
|
-
const
|
|
5480
|
-
const
|
|
5481
|
-
const
|
|
5482
|
-
await page.screenshot({ path:
|
|
5688
|
+
const t5 = Date.now();
|
|
5689
|
+
const beforeUrl5 = page.url();
|
|
5690
|
+
const spBefore5 = screenshotPath(ctx, "pj-5-shipping-pdp-before");
|
|
5691
|
+
await page.screenshot({ path: spBefore5, fullPage: false }).catch(() => {
|
|
5483
5692
|
return;
|
|
5484
5693
|
});
|
|
5485
5694
|
const ok = await fillCep(page, cepInputPdp, ctx.rc.cep);
|
|
5486
|
-
const sp = screenshotPath(ctx, "pj-
|
|
5695
|
+
const sp = screenshotPath(ctx, "pj-5-shipping-pdp");
|
|
5487
5696
|
await page.screenshot({ path: sp, fullPage: false }).catch(() => {
|
|
5488
5697
|
return;
|
|
5489
5698
|
});
|
|
5490
|
-
const
|
|
5699
|
+
const step5Status = ok ? "ok" : "failed";
|
|
5491
5700
|
steps.push({
|
|
5492
|
-
step:
|
|
5701
|
+
step: 5,
|
|
5493
5702
|
name: "shipping-calc-pdp",
|
|
5494
5703
|
side: ctx.side,
|
|
5495
5704
|
viewport: ctx.viewport,
|
|
5496
|
-
status:
|
|
5497
|
-
durationMs: Date.now() -
|
|
5705
|
+
status: step5Status,
|
|
5706
|
+
durationMs: Date.now() - t5,
|
|
5498
5707
|
url: page.url(),
|
|
5499
5708
|
screenshotPath: sp,
|
|
5500
|
-
screenshotBeforePath:
|
|
5501
|
-
beforeUrl:
|
|
5502
|
-
actionDescription: `Preencheu CEP '${ctx.rc.cep}' no input \`${cepInputPdp}
|
|
5709
|
+
screenshotBeforePath: spBefore5,
|
|
5710
|
+
beforeUrl: beforeUrl5,
|
|
5711
|
+
actionDescription: `Preencheu CEP '${ctx.rc.cep}' no input \`${cepInputPdp}\`${cepPdpRecoveredByLlm ? " (via recovery LLM)" : ""}`,
|
|
5503
5712
|
detail: { cepUsed: ctx.rc.cep },
|
|
5504
5713
|
selectorKey: "cepInputPdp",
|
|
5505
5714
|
usedSelector: cepInputPdp,
|
|
5506
|
-
recoveredByLlm:
|
|
5715
|
+
recoveredByLlm: cepPdpRecoveredByLlm || undefined
|
|
5507
5716
|
});
|
|
5508
|
-
reportEnd(
|
|
5717
|
+
reportEnd(5, "shipping-calc-pdp", step5Status, Date.now() - t5);
|
|
5509
5718
|
} else {
|
|
5510
|
-
steps.push(makeSkipStep(
|
|
5511
|
-
reportEnd(
|
|
5719
|
+
steps.push(makeSkipStep(5, "shipping-calc-pdp", ctx, "no CEP input on PDP (recovery exhausted)"));
|
|
5720
|
+
reportEnd(5, "shipping-calc-pdp", "skipped", 0, "no CEP input on PDP");
|
|
5512
5721
|
}
|
|
5513
|
-
reportStart(
|
|
5722
|
+
reportStart(6, "add-to-cart");
|
|
5514
5723
|
let buyHit = await firstVisibleLocator(page, selFor(ctx, "buyButton"));
|
|
5515
5724
|
let buyRecovered = false;
|
|
5516
|
-
if (!buyHit &&
|
|
5725
|
+
if (!buyHit && budget.remaining > 0) {
|
|
5517
5726
|
const recovery = await attemptRecovery(page, ctx, "add-to-cart", "Clicar no botão de comprar/adicionar ao carrinho", selFor(ctx, "buyButton"));
|
|
5518
5727
|
if (recovery) {
|
|
5519
5728
|
buyHit = recovery;
|
|
5520
5729
|
buyRecovered = true;
|
|
5521
|
-
|
|
5730
|
+
budget.remaining--;
|
|
5522
5731
|
}
|
|
5523
5732
|
}
|
|
5524
5733
|
if (!buyHit) {
|
|
5525
|
-
steps.push(makeSkipStep(
|
|
5526
|
-
reportEnd(
|
|
5734
|
+
steps.push(makeSkipStep(6, "add-to-cart", ctx, "no buy button found (recovery exhausted)"));
|
|
5735
|
+
reportEnd(6, "add-to-cart", "skipped", 0, "no buy button found");
|
|
5527
5736
|
return { pages, steps };
|
|
5528
5737
|
}
|
|
5529
|
-
const
|
|
5530
|
-
const
|
|
5531
|
-
const
|
|
5532
|
-
await page.screenshot({ path:
|
|
5738
|
+
const t6 = Date.now();
|
|
5739
|
+
const beforeUrl6 = page.url();
|
|
5740
|
+
const spBefore6 = screenshotPath(ctx, "pj-6-add-cart-before");
|
|
5741
|
+
await page.screenshot({ path: spBefore6, fullPage: false }).catch(() => {
|
|
5533
5742
|
return;
|
|
5534
5743
|
});
|
|
5744
|
+
const cartCountBefore = await readCartCount(page, ctx);
|
|
5535
5745
|
const buyText = await buyHit.locator.innerText().catch(() => "");
|
|
5536
5746
|
await buyHit.locator.click({ timeout: 5000 }).catch(() => {
|
|
5537
5747
|
return;
|
|
5538
5748
|
});
|
|
5539
|
-
await page
|
|
5540
|
-
|
|
5541
|
-
|
|
5749
|
+
let validation = await validateAddToCart(page, ctx, cartCountBefore, beforeUrl6);
|
|
5750
|
+
let variantRetryNote;
|
|
5751
|
+
if (validation.status === "failed" && validation.errorText && VARIANT_REQUIRED_TEXT_PATTERNS.some((re) => re.test(validation.errorText))) {
|
|
5752
|
+
const retryHit = await findAndIncrementZeroQtyInput(page, ctx);
|
|
5753
|
+
if (retryHit) {
|
|
5754
|
+
variantRetryNote = `Retry: ${retryHit.description}`;
|
|
5755
|
+
} else if (budget.remaining > 0) {
|
|
5756
|
+
const llmRetry = await attemptStepAction({
|
|
5757
|
+
page,
|
|
5758
|
+
ctx,
|
|
5759
|
+
stepName: "add-to-cart-retry-variant",
|
|
5760
|
+
intendedAction: "A loja rejeitou o add-to-cart com a mensagem 'Selecione um produto/tamanho/cor'. Encontre o picker de variante visível e selecione UMA opção (clicar num swatch, em '+' ao lado de uma quantidade '0', em um botão de tamanho). Não clique no botão de comprar.",
|
|
5761
|
+
selectorKey: "quantityIncrement",
|
|
5762
|
+
action: "click",
|
|
5763
|
+
recoveryBudget: budget
|
|
5764
|
+
});
|
|
5765
|
+
if (llmRetry.performed) {
|
|
5766
|
+
variantRetryNote = `Retry via LLM: ${llmRetry.action} em \`${llmRetry.selector}\``;
|
|
5767
|
+
}
|
|
5768
|
+
}
|
|
5769
|
+
if (variantRetryNote) {
|
|
5770
|
+
await page.waitForTimeout(500);
|
|
5771
|
+
await buyHit.locator.click({ timeout: 5000 }).catch(() => {
|
|
5772
|
+
return;
|
|
5773
|
+
});
|
|
5774
|
+
validation = await validateAddToCart(page, ctx, cartCountBefore, beforeUrl6);
|
|
5775
|
+
}
|
|
5776
|
+
}
|
|
5777
|
+
const sp6 = screenshotPath(ctx, "pj-6-add-cart");
|
|
5778
|
+
await page.screenshot({ path: sp6, fullPage: false }).catch(() => {
|
|
5542
5779
|
return;
|
|
5543
5780
|
});
|
|
5781
|
+
const buyActionDesc = `Clicou no botão${buyText ? ` '${buyText.slice(0, 40).trim()}'` : ""} (\`${buyHit.selector}\`)${buyRecovered ? " — selector veio de recovery LLM" : ""}`;
|
|
5782
|
+
const fullActionDesc = variantRetryNote ? `${buyActionDesc} — ${variantRetryNote} — ${validation.note}` : `${buyActionDesc} — ${validation.note}`;
|
|
5544
5783
|
steps.push({
|
|
5545
|
-
step:
|
|
5784
|
+
step: 6,
|
|
5546
5785
|
name: "add-to-cart",
|
|
5547
5786
|
side: ctx.side,
|
|
5548
5787
|
viewport: ctx.viewport,
|
|
5549
|
-
status:
|
|
5550
|
-
durationMs: Date.now() -
|
|
5788
|
+
status: validation.status,
|
|
5789
|
+
durationMs: Date.now() - t6,
|
|
5551
5790
|
url: page.url(),
|
|
5552
|
-
screenshotPath:
|
|
5553
|
-
screenshotBeforePath:
|
|
5554
|
-
beforeUrl:
|
|
5555
|
-
actionDescription:
|
|
5791
|
+
screenshotPath: sp6,
|
|
5792
|
+
screenshotBeforePath: spBefore6,
|
|
5793
|
+
beforeUrl: beforeUrl6,
|
|
5794
|
+
actionDescription: fullActionDesc,
|
|
5556
5795
|
selectorKey: "buyButton",
|
|
5557
5796
|
usedSelector: buyHit.selector,
|
|
5558
|
-
recoveredByLlm: buyRecovered || undefined
|
|
5797
|
+
recoveredByLlm: buyRecovered || undefined,
|
|
5798
|
+
note: validation.status === "ok" ? undefined : validation.note,
|
|
5799
|
+
detail: { signal: validation.signal, errorText: validation.errorText, variantRetry: variantRetryNote }
|
|
5559
5800
|
});
|
|
5560
|
-
reportEnd(
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5801
|
+
reportEnd(6, "add-to-cart", validation.status, Date.now() - t6, validation.status === "ok" ? undefined : validation.note);
|
|
5802
|
+
if (validation.status === "failed") {
|
|
5803
|
+
return { pages, steps };
|
|
5804
|
+
}
|
|
5805
|
+
reportStart(7, "open-minicart");
|
|
5806
|
+
const t7 = Date.now();
|
|
5807
|
+
const beforeUrl7 = page.url();
|
|
5808
|
+
const spBefore7 = screenshotPath(ctx, "pj-7-minicart-before");
|
|
5809
|
+
await page.screenshot({ path: spBefore7, fullPage: false }).catch(() => {
|
|
5566
5810
|
return;
|
|
5567
5811
|
});
|
|
5568
5812
|
let miniHit = await firstVisibleLocator(page, selFor(ctx, "minicartTrigger"));
|
|
5569
5813
|
let miniRecovered = false;
|
|
5570
|
-
if (!miniHit &&
|
|
5814
|
+
if (!miniHit && budget.remaining > 0) {
|
|
5571
5815
|
const recovery = await attemptRecovery(page, ctx, "open-minicart", "Abrir o minicart/drawer do carrinho", selFor(ctx, "minicartTrigger"));
|
|
5572
5816
|
if (recovery) {
|
|
5573
5817
|
miniHit = recovery;
|
|
5574
5818
|
miniRecovered = true;
|
|
5575
|
-
|
|
5819
|
+
budget.remaining--;
|
|
5576
5820
|
}
|
|
5577
5821
|
}
|
|
5578
5822
|
let cartOpenMethod = "failed";
|
|
@@ -5585,281 +5829,235 @@ async function flowPurchaseJourney(ctx) {
|
|
|
5585
5829
|
const alreadyOpen = await isCartRevealed(page, expectedProductTitle);
|
|
5586
5830
|
if (alreadyOpen) {
|
|
5587
5831
|
cartOpenMethod = "already-open";
|
|
5588
|
-
dlog2(ctx, `step
|
|
5832
|
+
dlog2(ctx, `step 7 open-minicart: no trigger, drawer already visible (${alreadyOpen})`);
|
|
5589
5833
|
}
|
|
5590
5834
|
}
|
|
5591
|
-
let
|
|
5835
|
+
let step7Validation;
|
|
5592
5836
|
if (expectedProductTitle && cartOpenMethod !== "failed") {
|
|
5593
5837
|
const v = await validateCartContainsTitle(page, expectedProductTitle, ctx);
|
|
5594
5838
|
let reasonText;
|
|
5595
5839
|
if (!v.found) {
|
|
5596
5840
|
if (v.observedTitles.length === 0) {
|
|
5597
5841
|
const emptyBanner = await detectEmptyCartBanner(page);
|
|
5598
|
-
reasonText = emptyBanner ? `cart genuinely empty —
|
|
5842
|
+
reasonText = emptyBanner ? `cart genuinely empty — add-to-cart didn't persist. Banner: "${emptyBanner}"` : "no cart line items visible (selectors may not match markup)";
|
|
5599
5843
|
} else {
|
|
5600
5844
|
reasonText = `expected title not found among ${v.observedTitles.length} observed`;
|
|
5601
5845
|
}
|
|
5602
5846
|
}
|
|
5603
|
-
|
|
5847
|
+
step7Validation = {
|
|
5604
5848
|
expectedTitle: expectedProductTitle,
|
|
5605
5849
|
found: v.found,
|
|
5606
5850
|
method: v.method,
|
|
5607
5851
|
observedTitles: v.observedTitles.slice(0, 8),
|
|
5608
5852
|
reason: reasonText
|
|
5609
5853
|
};
|
|
5610
|
-
dlog2(ctx, `step
|
|
5611
|
-
} else if (!expectedProductTitle) {
|
|
5612
|
-
dlog2(ctx, "step 6 open-minicart: skipping validation — no PDP title captured");
|
|
5613
|
-
} else {
|
|
5614
|
-
dlog2(ctx, "step 6 open-minicart: skipping validation — cart UI not revealed");
|
|
5854
|
+
dlog2(ctx, `step 7 open-minicart: validation → found=${v.found} (${v.method})${reasonText ? ` — ${reasonText.slice(0, 80)}` : ""}`);
|
|
5615
5855
|
}
|
|
5616
|
-
const
|
|
5617
|
-
await page.screenshot({ path:
|
|
5856
|
+
const sp7 = screenshotPath(ctx, "pj-7-minicart");
|
|
5857
|
+
await page.screenshot({ path: sp7, fullPage: false }).catch(() => {
|
|
5618
5858
|
return;
|
|
5619
5859
|
});
|
|
5620
|
-
const
|
|
5860
|
+
const step7Status = cartOpenMethod === "failed" ? "failed" : step7Validation && !step7Validation.found ? "failed" : "ok";
|
|
5621
5861
|
steps.push({
|
|
5622
|
-
step:
|
|
5862
|
+
step: 7,
|
|
5623
5863
|
name: "open-minicart",
|
|
5624
5864
|
side: ctx.side,
|
|
5625
5865
|
viewport: ctx.viewport,
|
|
5626
|
-
status:
|
|
5627
|
-
durationMs: Date.now() -
|
|
5866
|
+
status: step7Status,
|
|
5867
|
+
durationMs: Date.now() - t7,
|
|
5628
5868
|
url: page.url(),
|
|
5629
|
-
screenshotPath:
|
|
5630
|
-
screenshotBeforePath:
|
|
5631
|
-
beforeUrl:
|
|
5869
|
+
screenshotPath: sp7,
|
|
5870
|
+
screenshotBeforePath: spBefore7,
|
|
5871
|
+
beforeUrl: beforeUrl7,
|
|
5632
5872
|
cartOpenMethod,
|
|
5633
|
-
cartValidation:
|
|
5634
|
-
actionDescription: miniHit ? `Abriu minicart via ${cartOpenMethod}${miniText ? ` em '${miniText.slice(0, 30).trim()}'` : ""} (\`${miniHit.selector}\`)${miniRecovered ? " — selector via recovery LLM" : ""}${
|
|
5873
|
+
cartValidation: step7Validation,
|
|
5874
|
+
actionDescription: miniHit ? `Abriu minicart via ${cartOpenMethod}${miniText ? ` em '${miniText.slice(0, 30).trim()}'` : ""} (\`${miniHit.selector}\`)${miniRecovered ? " — selector via recovery LLM" : ""}${step7Validation ? step7Validation.found ? " ✓ produto encontrado no cart" : ` ✗ produto ESPERADO não encontrado (${step7Validation.observedTitles?.length ?? 0} itens observados)` : ""}` : cartOpenMethod === "already-open" ? "Minicart já estava aberto após add-to-cart" : "Minicart não pôde ser aberto",
|
|
5635
5875
|
selectorKey: miniHit ? "minicartTrigger" : undefined,
|
|
5636
5876
|
usedSelector: miniHit?.selector,
|
|
5637
5877
|
recoveredByLlm: miniRecovered || undefined
|
|
5638
5878
|
});
|
|
5639
|
-
reportEnd(
|
|
5640
|
-
reportStart(
|
|
5879
|
+
reportEnd(7, "open-minicart", step7Status, Date.now() - t7);
|
|
5880
|
+
reportStart(8, "shipping-calc-cart");
|
|
5641
5881
|
let cepInputCart = await firstVisible(page, selFor(ctx, "cepInputCart"));
|
|
5642
|
-
let
|
|
5643
|
-
if (!cepInputCart &&
|
|
5644
|
-
const
|
|
5645
|
-
if (
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5882
|
+
let cepCartRecoveredByLlm = false;
|
|
5883
|
+
if (!cepInputCart && budget.remaining > 0) {
|
|
5884
|
+
const html = await page.content().catch(() => "");
|
|
5885
|
+
if (html) {
|
|
5886
|
+
const suggestion = await suggestRecovery({
|
|
5887
|
+
stepName: "shipping-calc-cart",
|
|
5888
|
+
intendedAction: "Encontre o <input> de CEP (código postal de ENDEREÇO de entrega) dentro do minicart drawer ou da página de carrinho — usado pra CALCULAR FRETE antes do checkout. Placeholder normalmente é 'CEP', 'Digite seu CEP', 'Cód. postal', 'Postal code', aria-label='CEP' ou name='zip'/'cep'/'postal'. **NÃO** retorne o input de CUPOM de desconto / 'Digite o cupom' / código promocional — esses são pra desconto, não pra frete. Também NÃO retorne campo de e-mail/newsletter.",
|
|
5889
|
+
html,
|
|
5890
|
+
alreadyTried: selFor(ctx, "cepInputCart")
|
|
5891
|
+
});
|
|
5892
|
+
if (suggestion) {
|
|
5893
|
+
budget.remaining--;
|
|
5894
|
+
const el = page.locator(suggestion.selector).first();
|
|
5895
|
+
if (await el.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
5896
|
+
cepInputCart = suggestion.selector;
|
|
5897
|
+
cepCartRecoveredByLlm = true;
|
|
5898
|
+
}
|
|
5899
|
+
}
|
|
5649
5900
|
}
|
|
5650
5901
|
}
|
|
5651
5902
|
if (cepInputCart) {
|
|
5652
|
-
const
|
|
5653
|
-
const
|
|
5654
|
-
const
|
|
5655
|
-
await page.screenshot({ path:
|
|
5903
|
+
const t8 = Date.now();
|
|
5904
|
+
const beforeUrl8 = page.url();
|
|
5905
|
+
const spBefore8 = screenshotPath(ctx, "pj-8-shipping-cart-before");
|
|
5906
|
+
await page.screenshot({ path: spBefore8, fullPage: false }).catch(() => {
|
|
5656
5907
|
return;
|
|
5657
5908
|
});
|
|
5658
5909
|
const ok = await fillCep(page, cepInputCart, ctx.rc.cep);
|
|
5659
|
-
const
|
|
5660
|
-
await page.screenshot({ path:
|
|
5910
|
+
const sp8 = screenshotPath(ctx, "pj-8-shipping-cart");
|
|
5911
|
+
await page.screenshot({ path: sp8, fullPage: false }).catch(() => {
|
|
5661
5912
|
return;
|
|
5662
5913
|
});
|
|
5663
|
-
const
|
|
5914
|
+
const step8Status = ok ? "ok" : "failed";
|
|
5664
5915
|
steps.push({
|
|
5665
|
-
step:
|
|
5916
|
+
step: 8,
|
|
5666
5917
|
name: "shipping-calc-cart",
|
|
5667
5918
|
side: ctx.side,
|
|
5668
5919
|
viewport: ctx.viewport,
|
|
5669
|
-
status:
|
|
5670
|
-
durationMs: Date.now() -
|
|
5920
|
+
status: step8Status,
|
|
5921
|
+
durationMs: Date.now() - t8,
|
|
5671
5922
|
url: page.url(),
|
|
5672
|
-
screenshotPath:
|
|
5673
|
-
screenshotBeforePath:
|
|
5674
|
-
beforeUrl:
|
|
5675
|
-
actionDescription: `Preencheu CEP '${ctx.rc.cep}' no carrinho (\`${cepInputCart}\`)${
|
|
5923
|
+
screenshotPath: sp8,
|
|
5924
|
+
screenshotBeforePath: spBefore8,
|
|
5925
|
+
beforeUrl: beforeUrl8,
|
|
5926
|
+
actionDescription: `Preencheu CEP '${ctx.rc.cep}' no carrinho (\`${cepInputCart}\`)${cepCartRecoveredByLlm ? " — recovery LLM" : ""}`,
|
|
5676
5927
|
detail: { cepUsed: ctx.rc.cep },
|
|
5677
5928
|
selectorKey: "cepInputCart",
|
|
5678
5929
|
usedSelector: cepInputCart,
|
|
5679
|
-
recoveredByLlm:
|
|
5930
|
+
recoveredByLlm: cepCartRecoveredByLlm || undefined
|
|
5680
5931
|
});
|
|
5681
|
-
reportEnd(
|
|
5932
|
+
reportEnd(8, "shipping-calc-cart", step8Status, Date.now() - t8);
|
|
5682
5933
|
} else {
|
|
5683
|
-
steps.push(makeSkipStep(
|
|
5684
|
-
reportEnd(
|
|
5685
|
-
}
|
|
5686
|
-
reportStart(
|
|
5687
|
-
const
|
|
5688
|
-
const
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
dlog2(ctx, "step 8 go-checkout: drawer appears closed, re-clicking minicart trigger");
|
|
5702
|
-
const reTrigger = await firstVisibleLocator(page, selFor(ctx, "minicartTrigger"));
|
|
5703
|
-
if (reTrigger) {
|
|
5704
|
-
await reTrigger.locator.click({ timeout: 3000 }).catch(() => {
|
|
5705
|
-
return;
|
|
5706
|
-
});
|
|
5707
|
-
await page.waitForTimeout(1500);
|
|
5708
|
-
dlog2(ctx, `step 8 go-checkout: re-opened drawer via ${reTrigger.selector}`);
|
|
5709
|
-
} else {
|
|
5710
|
-
dlog2(ctx, "step 8 go-checkout: minicartTrigger not found — drawer state unknown");
|
|
5711
|
-
}
|
|
5712
|
-
} else {
|
|
5713
|
-
dlog2(ctx, `step 8 go-checkout: drawer still open (matched ${drawerStillOpen})`);
|
|
5934
|
+
steps.push(makeSkipStep(8, "shipping-calc-cart", ctx, "no CEP input in cart (recovery exhausted)"));
|
|
5935
|
+
reportEnd(8, "shipping-calc-cart", "skipped", 0, "no CEP input in cart");
|
|
5936
|
+
}
|
|
5937
|
+
reportStart(9, "go-checkout");
|
|
5938
|
+
const t9 = Date.now();
|
|
5939
|
+
const beforeUrl9 = page.url();
|
|
5940
|
+
if (/\/(checkout|carrinho)(\/|$|\?)/i.test(beforeUrl9)) {
|
|
5941
|
+
await waitForCartHydration(page);
|
|
5942
|
+
let step9EarlyValidation;
|
|
5943
|
+
if (expectedProductTitle) {
|
|
5944
|
+
const v = await validateCartContainsTitle(page, expectedProductTitle, ctx);
|
|
5945
|
+
step9EarlyValidation = {
|
|
5946
|
+
expectedTitle: expectedProductTitle,
|
|
5947
|
+
found: v.found,
|
|
5948
|
+
method: v.method,
|
|
5949
|
+
observedTitles: v.observedTitles.slice(0, 8),
|
|
5950
|
+
reason: v.found ? undefined : `expected title not found among ${v.observedTitles.length} observed on checkout`
|
|
5951
|
+
};
|
|
5714
5952
|
}
|
|
5953
|
+
const sp9early = screenshotPath(ctx, "pj-9-checkout-reached");
|
|
5954
|
+
await page.screenshot({ path: sp9early, fullPage: false }).catch(() => {
|
|
5955
|
+
return;
|
|
5956
|
+
});
|
|
5957
|
+
const earlyStatus = step9EarlyValidation && !step9EarlyValidation.found ? "failed" : "ok";
|
|
5958
|
+
steps.push({
|
|
5959
|
+
step: 9,
|
|
5960
|
+
name: "go-checkout",
|
|
5961
|
+
side: ctx.side,
|
|
5962
|
+
viewport: ctx.viewport,
|
|
5963
|
+
status: earlyStatus,
|
|
5964
|
+
durationMs: Date.now() - t9,
|
|
5965
|
+
url: page.url(),
|
|
5966
|
+
screenshotPath: sp9early,
|
|
5967
|
+
cartValidation: step9EarlyValidation,
|
|
5968
|
+
actionDescription: `Já estava em \`${beforeUrl9}\` (minicart navegou direto para checkout)${step9EarlyValidation ? step9EarlyValidation.found ? " ✓ produto persiste no checkout" : ` ✗ produto ESPERADO ausente (${step9EarlyValidation.observedTitles?.length ?? 0} observados)` : ""}`,
|
|
5969
|
+
detail: { reachedVia: "minicart-direct-link" }
|
|
5970
|
+
});
|
|
5971
|
+
reportEnd(9, "go-checkout", earlyStatus, Date.now() - t9);
|
|
5972
|
+
return { pages, steps };
|
|
5715
5973
|
}
|
|
5716
|
-
|
|
5717
|
-
"button:has-text('Continuar para pagamento')",
|
|
5718
|
-
"a:has-text('Continuar para pagamento')",
|
|
5719
|
-
"button:has-text('Ir para o pagamento')",
|
|
5720
|
-
"a:has-text('Ir para o pagamento')",
|
|
5721
|
-
"button:has-text('Continuar')",
|
|
5722
|
-
"a:has-text('Continuar')",
|
|
5723
|
-
"button:has-text('Avançar')",
|
|
5724
|
-
"a:has-text('Avançar')",
|
|
5725
|
-
"button:has-text('Próxima etapa')",
|
|
5726
|
-
"a:has-text('Próxima etapa')",
|
|
5727
|
-
"button:has-text('Confirmar')",
|
|
5728
|
-
"button:has-text('Finalizar')",
|
|
5729
|
-
"a:has-text('Finalizar')",
|
|
5730
|
-
"#cart-to-orderform",
|
|
5731
|
-
"#btn-go-to-payment",
|
|
5732
|
-
".btn-go-to-payment",
|
|
5733
|
-
"a.btn-place-order",
|
|
5734
|
-
"a.orange-btn",
|
|
5735
|
-
".cart-links-bottom a",
|
|
5736
|
-
"button[type='submit'][class*='checkout' i]",
|
|
5737
|
-
"button[type='submit'][class*='continue' i]",
|
|
5738
|
-
"[data-checkout-next]",
|
|
5739
|
-
"[data-fs-cart-checkout-button]"
|
|
5740
|
-
] : [];
|
|
5741
|
-
const baseSelectors = selFor(ctx, "checkoutButton");
|
|
5742
|
-
const checkoutSelectors = [...nextStepSelectors, ...baseSelectors];
|
|
5743
|
-
dlog2(ctx, `step 8 go-checkout: firstVisibleLocator on ${checkoutSelectors.length} selectors${alreadyInCheckoutPage ? ` (${nextStepSelectors.length} next-step prefixed)` : ""}`);
|
|
5744
|
-
let checkoutHit = await firstVisibleLocator(page, checkoutSelectors);
|
|
5745
|
-
dlog2(ctx, `step 8 go-checkout: default match → ${checkoutHit ? `selector=${checkoutHit.selector}` : "null"}`);
|
|
5974
|
+
let checkoutHit = await firstVisibleLocator(page, selFor(ctx, "checkoutButton"));
|
|
5746
5975
|
let checkoutRecovered = false;
|
|
5747
|
-
if (!checkoutHit &&
|
|
5748
|
-
const
|
|
5749
|
-
dlog2(ctx, `step 8 go-checkout: defaults missed, calling attemptRecovery (budget=${recoveryBudget}, mode=${alreadyInCheckoutPage ? "advance-checkout" : "drawer-finalize"})`);
|
|
5750
|
-
const recovery = await attemptRecovery(page, ctx, "go-checkout", recoveryIntent, checkoutSelectors);
|
|
5751
|
-
dlog2(ctx, `step 8 go-checkout: LLM recovery → ${recovery ? `selector=${recovery.selector}` : "null"}`);
|
|
5976
|
+
if (!checkoutHit && budget.remaining > 0) {
|
|
5977
|
+
const recovery = await attemptRecovery(page, ctx, "go-checkout", "Clicar no botão 'Finalizar compra' / 'Ir para o checkout' / 'Finalizar'", selFor(ctx, "checkoutButton"));
|
|
5752
5978
|
if (recovery) {
|
|
5753
5979
|
checkoutHit = recovery;
|
|
5754
5980
|
checkoutRecovered = true;
|
|
5755
|
-
|
|
5981
|
+
budget.remaining--;
|
|
5756
5982
|
}
|
|
5757
5983
|
}
|
|
5758
5984
|
if (!checkoutHit) {
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
reportEnd(8, "go-checkout", "skipped", 0, "no checkout button found");
|
|
5985
|
+
steps.push(makeSkipStep(9, "go-checkout", ctx, "no checkout button found (recovery exhausted)"));
|
|
5986
|
+
reportEnd(9, "go-checkout", "skipped", 0, "no checkout button found");
|
|
5762
5987
|
return { pages, steps };
|
|
5763
5988
|
}
|
|
5764
|
-
const
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5989
|
+
const spBefore9 = screenshotPath(ctx, "pj-9-checkout-before");
|
|
5990
|
+
await page.screenshot({ path: spBefore9, fullPage: false }).catch(() => {
|
|
5991
|
+
return;
|
|
5992
|
+
});
|
|
5993
|
+
let usedSelector = checkoutHit.selector;
|
|
5994
|
+
let clickedText = await checkoutHit.locator.innerText().catch(() => "");
|
|
5995
|
+
let reachedCheckout = false;
|
|
5996
|
+
let attempt = 0;
|
|
5997
|
+
const triedSelectors = new Set([usedSelector]);
|
|
5998
|
+
while (attempt < 3) {
|
|
5999
|
+
attempt++;
|
|
5772
6000
|
await Promise.all([
|
|
5773
|
-
page.waitForURL(
|
|
6001
|
+
page.waitForURL(/\/(checkout|carrinho)/i, { timeout: 1e4 }).catch(() => {
|
|
5774
6002
|
return;
|
|
5775
6003
|
}),
|
|
5776
|
-
|
|
6004
|
+
checkoutHit.locator.click({ timeout: 5000 }).catch(() => {
|
|
5777
6005
|
return;
|
|
5778
6006
|
})
|
|
5779
6007
|
]);
|
|
5780
6008
|
await page.waitForTimeout(1500);
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
let
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
dlog2(ctx, `step 8 go-checkout: attempt 1 result — finalUrl=${result.url} reached=${reachedCheckout} clicked='${clickedText.slice(0, 40).trim()}'`);
|
|
5802
|
-
if (!reachedCheckout && recoveryBudget > 0) {
|
|
5803
|
-
dlog2(ctx, `step 8 go-checkout: attempt 1 missed target — calling attemptRecovery for retry (budget=${recoveryBudget}, mode=${alreadyInCheckoutPage ? "advance-checkout" : "drawer-finalize"})`);
|
|
5804
|
-
const retryIntent = alreadyInCheckoutPage ? `Já estou no checkout, em ${beforeUrl8}. Cliquei em '${clickedText.slice(0, 40).trim()}' (selector \`${usedSelector}\`), mas a URL não mudou ou foi pra ${result.url}. Achar o botão que avança pra próxima etapa do checkout (Continuar para pagamento, Avançar, Próxima etapa, Confirmar, etc).` : `Cliquei em '${clickedText.slice(0, 40).trim()}' (selector \`${usedSelector}\`), mas a URL ficou em ${result.url} e não foi pra /checkout. Achar o botão que de fato navega pra /checkout neste cart/minicart aberto.`;
|
|
5805
|
-
const retrySuggestion = await attemptRecovery(page, ctx, "go-checkout-retry", retryIntent, [usedSelector, ...checkoutSelectors]);
|
|
5806
|
-
dlog2(ctx, `step 8 go-checkout: retry LLM suggestion → ${retrySuggestion ? `selector=${retrySuggestion.selector}` : "null"}`);
|
|
5807
|
-
if (retrySuggestion) {
|
|
5808
|
-
recoveryBudget--;
|
|
5809
|
-
checkoutRecovered = true;
|
|
5810
|
-
attempt++;
|
|
5811
|
-
dlog2(ctx, `step 8 go-checkout: tryCheckoutClick attempt 2 — selector=${retrySuggestion.selector}`);
|
|
5812
|
-
const retryResult = await tryCheckoutClick(retrySuggestion, attempt);
|
|
5813
|
-
result = retryResult;
|
|
5814
|
-
usedSelector = retrySuggestion.selector;
|
|
5815
|
-
clickedText = retryResult.clickedText;
|
|
5816
|
-
reachedCheckout = isReachedCheckout(retryResult.url);
|
|
5817
|
-
dlog2(ctx, `step 8 go-checkout: attempt 2 result — finalUrl=${retryResult.url} reached=${reachedCheckout} clicked='${retryResult.clickedText.slice(0, 40).trim()}'`);
|
|
5818
|
-
}
|
|
5819
|
-
} else if (!reachedCheckout) {
|
|
5820
|
-
dlog2(ctx, `step 8 go-checkout: not retrying — recoveryBudget=${recoveryBudget}`);
|
|
5821
|
-
}
|
|
5822
|
-
let step8Validation;
|
|
5823
|
-
if (expectedProductTitle && reachedCheckout) {
|
|
5824
|
-
await page.waitForTimeout(1500);
|
|
6009
|
+
reachedCheckout = /\/(checkout|carrinho)(\/|$|\?)/i.test(page.url());
|
|
6010
|
+
if (reachedCheckout)
|
|
6011
|
+
break;
|
|
6012
|
+
if (budget.remaining <= 0)
|
|
6013
|
+
break;
|
|
6014
|
+
dlog2(ctx, `step 9 go-checkout: click on \`${usedSelector}\` didn't navigate (attempt ${attempt}). URL still ${page.url()}. Asking LLM for another selector.`);
|
|
6015
|
+
const triedList = Array.from(triedSelectors);
|
|
6016
|
+
const recovery = await attemptRecovery(page, ctx, "go-checkout", `O click anterior em \`${usedSelector}\` não navegou para /checkout (URL continua em ${page.url()}). Encontre OUTRO selector — o botão real de finalizar compra deve ser visível agora (no drawer aberto ou na página). Não retorne ${triedList.join(", ")} novamente.`, triedList);
|
|
6017
|
+
if (!recovery)
|
|
6018
|
+
break;
|
|
6019
|
+
checkoutHit = recovery;
|
|
6020
|
+
checkoutRecovered = true;
|
|
6021
|
+
usedSelector = recovery.selector;
|
|
6022
|
+
triedSelectors.add(usedSelector);
|
|
6023
|
+
clickedText = await recovery.locator.innerText().catch(() => "");
|
|
6024
|
+
budget.remaining--;
|
|
6025
|
+
}
|
|
6026
|
+
let step9Validation;
|
|
6027
|
+
if (reachedCheckout && expectedProductTitle) {
|
|
6028
|
+
await waitForCartHydration(page);
|
|
5825
6029
|
const v = await validateCartContainsTitle(page, expectedProductTitle, ctx);
|
|
5826
|
-
|
|
5827
|
-
if (!v.found) {
|
|
5828
|
-
if (v.observedTitles.length === 0) {
|
|
5829
|
-
const emptyBanner = await detectEmptyCartBanner(page);
|
|
5830
|
-
reasonText = emptyBanner ? `checkout shows empty cart banner — upstream session lost. Banner: "${emptyBanner}"` : "checkout page has no visible line items (selectors may not match markup)";
|
|
5831
|
-
} else {
|
|
5832
|
-
reasonText = `expected title not found among ${v.observedTitles.length} observed on checkout`;
|
|
5833
|
-
}
|
|
5834
|
-
}
|
|
5835
|
-
step8Validation = {
|
|
6030
|
+
step9Validation = {
|
|
5836
6031
|
expectedTitle: expectedProductTitle,
|
|
5837
6032
|
found: v.found,
|
|
5838
6033
|
method: v.method,
|
|
5839
6034
|
observedTitles: v.observedTitles.slice(0, 8),
|
|
5840
|
-
reason:
|
|
6035
|
+
reason: v.found ? undefined : `expected title not found among ${v.observedTitles.length} observed on checkout`
|
|
5841
6036
|
};
|
|
5842
|
-
dlog2(ctx, `step 8 go-checkout: checkout validation → found=${v.found} (${v.method})${reasonText ? ` — ${reasonText.slice(0, 80)}` : ""}`);
|
|
5843
6037
|
}
|
|
5844
|
-
const
|
|
6038
|
+
const sp9 = screenshotPath(ctx, "pj-9-checkout-reached");
|
|
6039
|
+
await page.screenshot({ path: sp9, fullPage: false }).catch(() => {
|
|
6040
|
+
return;
|
|
6041
|
+
});
|
|
6042
|
+
const step9Status = !reachedCheckout ? "failed" : step9Validation && !step9Validation.found ? "failed" : "ok";
|
|
5845
6043
|
steps.push({
|
|
5846
|
-
step:
|
|
6044
|
+
step: 9,
|
|
5847
6045
|
name: "go-checkout",
|
|
5848
6046
|
side: ctx.side,
|
|
5849
6047
|
viewport: ctx.viewport,
|
|
5850
|
-
status:
|
|
5851
|
-
durationMs: Date.now() -
|
|
5852
|
-
url:
|
|
5853
|
-
screenshotPath:
|
|
5854
|
-
screenshotBeforePath:
|
|
5855
|
-
beforeUrl:
|
|
5856
|
-
cartValidation:
|
|
5857
|
-
actionDescription: `Clicou em${clickedText ? ` '${clickedText.slice(0, 30).trim()}'` : ""} (\`${usedSelector}\`); URL final: ${
|
|
6048
|
+
status: step9Status,
|
|
6049
|
+
durationMs: Date.now() - t9,
|
|
6050
|
+
url: page.url(),
|
|
6051
|
+
screenshotPath: sp9,
|
|
6052
|
+
screenshotBeforePath: spBefore9,
|
|
6053
|
+
beforeUrl: beforeUrl9,
|
|
6054
|
+
cartValidation: step9Validation,
|
|
6055
|
+
actionDescription: `Clicou em${clickedText ? ` '${clickedText.slice(0, 30).trim()}'` : ""} (\`${usedSelector}\`); URL final: ${page.url()}${reachedCheckout ? " ✓ atingiu checkout" : " ✗ não foi pra checkout"}${attempt > 1 ? ` (após ${attempt} tentativas com recovery LLM)` : ""}${step9Validation ? step9Validation.found ? " ✓ produto persiste no checkout" : ` ✗ produto ESPERADO ausente no checkout (${step9Validation.observedTitles?.length ?? 0} observados)` : ""}`,
|
|
5858
6056
|
selectorKey: "checkoutButton",
|
|
5859
6057
|
usedSelector,
|
|
5860
6058
|
recoveredByLlm: checkoutRecovered || undefined
|
|
5861
6059
|
});
|
|
5862
|
-
reportEnd(
|
|
6060
|
+
reportEnd(9, "go-checkout", step9Status, Date.now() - t9);
|
|
5863
6061
|
return { pages, steps };
|
|
5864
6062
|
} finally {
|
|
5865
6063
|
await page.close();
|
|
@@ -5879,15 +6077,10 @@ function makeSkipStep(step, name, ctx, note) {
|
|
|
5879
6077
|
}
|
|
5880
6078
|
async function findCategoryUrl(page, ctx) {
|
|
5881
6079
|
const selectors = selFor(ctx, "categoryLink");
|
|
5882
|
-
dlog2(ctx, ` findCategoryUrl: collectCandidateLinks(${selectors.length} selectors) start`);
|
|
5883
|
-
const t0 = Date.now();
|
|
5884
6080
|
const candidates = await collectCandidateLinks(page, selectors, 12);
|
|
5885
|
-
dlog2(ctx, ` findCategoryUrl: collectCandidateLinks done in ${Date.now() - t0}ms → ${candidates.length} candidates`);
|
|
5886
6081
|
if (candidates.length === 0)
|
|
5887
6082
|
return null;
|
|
5888
|
-
dlog2(ctx, " findCategoryUrl: pickCategoryLink LLM call start");
|
|
5889
6083
|
const picked = await pickCategoryLink(candidates.map((c) => ({ text: c.text, href: c.href })));
|
|
5890
|
-
dlog2(ctx, ` findCategoryUrl: pickCategoryLink LLM done → ${picked?.href ?? "null"}`);
|
|
5891
6084
|
if (!picked)
|
|
5892
6085
|
return null;
|
|
5893
6086
|
const original = candidates.find((c) => c.href === picked.href);
|
|
@@ -5936,34 +6129,20 @@ async function firstVisibleLocator(page, selectors) {
|
|
|
5936
6129
|
}
|
|
5937
6130
|
return null;
|
|
5938
6131
|
}
|
|
5939
|
-
var COLLECT_CANDIDATES_BUDGET_MS = 15000;
|
|
5940
|
-
function withCap(p, capMs, fallback) {
|
|
5941
|
-
return Promise.race([
|
|
5942
|
-
p.catch(() => fallback),
|
|
5943
|
-
new Promise((resolve) => setTimeout(() => resolve(fallback), capMs))
|
|
5944
|
-
]);
|
|
5945
|
-
}
|
|
5946
6132
|
async function collectCandidateLinks(page, selectors, limit = 12) {
|
|
5947
6133
|
const out = [];
|
|
5948
6134
|
const seenHrefs = new Set;
|
|
5949
|
-
const deadline = Date.now() + COLLECT_CANDIDATES_BUDGET_MS;
|
|
5950
|
-
const expired = () => Date.now() >= deadline;
|
|
5951
6135
|
for (const sel of selectors) {
|
|
5952
6136
|
if (out.length >= limit)
|
|
5953
6137
|
break;
|
|
5954
|
-
if (expired())
|
|
5955
|
-
break;
|
|
5956
6138
|
try {
|
|
5957
6139
|
const elements = page.locator(sel);
|
|
5958
|
-
const count = await
|
|
6140
|
+
const count = await elements.count();
|
|
5959
6141
|
for (let i = 0;i < count && out.length < limit; i++) {
|
|
5960
|
-
if (expired())
|
|
5961
|
-
break;
|
|
5962
6142
|
const el = elements.nth(i);
|
|
5963
|
-
|
|
5964
|
-
if (!visible)
|
|
6143
|
+
if (!await el.isVisible({ timeout: 250 }).catch(() => false))
|
|
5965
6144
|
continue;
|
|
5966
|
-
const href = await
|
|
6145
|
+
const href = await el.getAttribute("href").catch(() => null);
|
|
5967
6146
|
if (!href)
|
|
5968
6147
|
continue;
|
|
5969
6148
|
let abs = href;
|
|
@@ -5975,7 +6154,7 @@ async function collectCandidateLinks(page, selectors, limit = 12) {
|
|
|
5975
6154
|
if (seenHrefs.has(abs))
|
|
5976
6155
|
continue;
|
|
5977
6156
|
seenHrefs.add(abs);
|
|
5978
|
-
const text = (await
|
|
6157
|
+
const text = (await el.innerText().catch(() => "")).slice(0, 60).trim();
|
|
5979
6158
|
out.push({ text, href: abs, selector: sel });
|
|
5980
6159
|
}
|
|
5981
6160
|
} catch {}
|
|
@@ -5994,6 +6173,360 @@ async function fillCep(page, selector, cep) {
|
|
|
5994
6173
|
return false;
|
|
5995
6174
|
}
|
|
5996
6175
|
}
|
|
6176
|
+
async function selectVariant(page, ctx) {
|
|
6177
|
+
const actions = [];
|
|
6178
|
+
let primarySelectorKey;
|
|
6179
|
+
let primarySelector;
|
|
6180
|
+
const trackPrimary = (key, sel) => {
|
|
6181
|
+
if (!primarySelectorKey) {
|
|
6182
|
+
primarySelectorKey = key;
|
|
6183
|
+
primarySelector = sel;
|
|
6184
|
+
}
|
|
6185
|
+
};
|
|
6186
|
+
try {
|
|
6187
|
+
const rowSelectors = selFor(ctx, "variantRow");
|
|
6188
|
+
const incrementSelectors = selFor(ctx, "quantityIncrement");
|
|
6189
|
+
rowLoop:
|
|
6190
|
+
for (const rowSel of rowSelectors) {
|
|
6191
|
+
const rows = page.locator(rowSel);
|
|
6192
|
+
const count = await rows.count().catch(() => 0);
|
|
6193
|
+
for (let i = 0;i < Math.min(count, 5); i++) {
|
|
6194
|
+
const row = rows.nth(i);
|
|
6195
|
+
if (!await row.isVisible({ timeout: 500 }).catch(() => false))
|
|
6196
|
+
continue;
|
|
6197
|
+
const rowText = (await row.innerText().catch(() => "")).replace(/\s+/g, " ").trim();
|
|
6198
|
+
for (const incSel of incrementSelectors) {
|
|
6199
|
+
const plus = row.locator(incSel).first();
|
|
6200
|
+
if (!await plus.isVisible({ timeout: 500 }).catch(() => false))
|
|
6201
|
+
continue;
|
|
6202
|
+
if (await plus.isDisabled().catch(() => false))
|
|
6203
|
+
continue;
|
|
6204
|
+
await plus.click({ timeout: 2000 }).catch(() => {
|
|
6205
|
+
return;
|
|
6206
|
+
});
|
|
6207
|
+
await page.waitForTimeout(400);
|
|
6208
|
+
actions.push(`Incrementou quantidade da variante \`${rowSel}\`[${i}]${rowText ? ` (${rowText.slice(0, 40)})` : ""} via \`${incSel}\``);
|
|
6209
|
+
trackPrimary("variantRow", rowSel);
|
|
6210
|
+
break rowLoop;
|
|
6211
|
+
}
|
|
6212
|
+
}
|
|
6213
|
+
}
|
|
6214
|
+
} catch {}
|
|
6215
|
+
if (actions.length === 0) {
|
|
6216
|
+
const sizeHit = await firstVisibleLocator(page, selFor(ctx, "sizeSwatch"));
|
|
6217
|
+
if (sizeHit && !await sizeHit.locator.isDisabled().catch(() => false)) {
|
|
6218
|
+
const sizeText = (await sizeHit.locator.innerText().catch(() => "")).slice(0, 20).trim();
|
|
6219
|
+
await sizeHit.locator.click({ timeout: 2000 }).catch(() => {
|
|
6220
|
+
return;
|
|
6221
|
+
});
|
|
6222
|
+
await page.waitForTimeout(400);
|
|
6223
|
+
actions.push(`Selecionou tamanho${sizeText ? ` '${sizeText}'` : ""} (\`${sizeHit.selector}\`)`);
|
|
6224
|
+
trackPrimary("sizeSwatch", sizeHit.selector);
|
|
6225
|
+
}
|
|
6226
|
+
}
|
|
6227
|
+
const colorHit = await firstVisibleLocator(page, selFor(ctx, "colorSwatch"));
|
|
6228
|
+
if (colorHit && !await colorHit.locator.isDisabled().catch(() => false)) {
|
|
6229
|
+
const colorText = (await colorHit.locator.innerText().catch(() => "")).slice(0, 20).trim();
|
|
6230
|
+
const colorLabel = colorText || await colorHit.locator.getAttribute("aria-label").catch(() => null) || "";
|
|
6231
|
+
await colorHit.locator.click({ timeout: 2000 }).catch(() => {
|
|
6232
|
+
return;
|
|
6233
|
+
});
|
|
6234
|
+
await page.waitForTimeout(400);
|
|
6235
|
+
actions.push(`Selecionou cor${colorLabel ? ` '${colorLabel}'` : ""} (\`${colorHit.selector}\`)`);
|
|
6236
|
+
trackPrimary("colorSwatch", colorHit.selector);
|
|
6237
|
+
}
|
|
6238
|
+
if (actions.length === 0) {
|
|
6239
|
+
await scrollPageInChunks(page);
|
|
6240
|
+
const result = await findAndIncrementZeroQtyInput(page, ctx);
|
|
6241
|
+
if (result) {
|
|
6242
|
+
actions.push(result.description);
|
|
6243
|
+
trackPrimary(result.selectorKey, result.usedSelector);
|
|
6244
|
+
}
|
|
6245
|
+
}
|
|
6246
|
+
let variantRequired = false;
|
|
6247
|
+
try {
|
|
6248
|
+
const bodyText = await page.locator("body").innerText({ timeout: 1000 }).catch(() => "");
|
|
6249
|
+
variantRequired = VARIANT_REQUIRED_TEXT_PATTERNS.some((re) => re.test(bodyText));
|
|
6250
|
+
} catch {}
|
|
6251
|
+
return { actions, primarySelectorKey, primarySelector, variantRequired };
|
|
6252
|
+
}
|
|
6253
|
+
async function scrollPageInChunks(page) {
|
|
6254
|
+
await page.evaluate(async () => {
|
|
6255
|
+
const height = document.body.scrollHeight;
|
|
6256
|
+
const stops = [0.2, 0.4, 0.6, 0.8, 1, 0.4];
|
|
6257
|
+
for (const frac of stops) {
|
|
6258
|
+
window.scrollTo({ top: height * frac, behavior: "instant" });
|
|
6259
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
6260
|
+
}
|
|
6261
|
+
window.scrollTo({ top: 0, behavior: "instant" });
|
|
6262
|
+
}).catch(() => {
|
|
6263
|
+
return;
|
|
6264
|
+
});
|
|
6265
|
+
await page.waitForTimeout(1200);
|
|
6266
|
+
}
|
|
6267
|
+
async function findAndIncrementZeroQtyInput(page, ctx) {
|
|
6268
|
+
const qtySelectors = selFor(ctx, "quantityInput");
|
|
6269
|
+
const scopeSelectors = [
|
|
6270
|
+
"[data-manifest-key*='ProductDetails' i]",
|
|
6271
|
+
"[data-manifest-key*='Product/' i]",
|
|
6272
|
+
"main"
|
|
6273
|
+
];
|
|
6274
|
+
const scopeRoots = [];
|
|
6275
|
+
for (const sel of scopeSelectors) {
|
|
6276
|
+
const cand = page.locator(sel).first();
|
|
6277
|
+
if (await cand.count().then((n) => n > 0).catch(() => false)) {
|
|
6278
|
+
scopeRoots.push({ root: cand, tag: "main" });
|
|
6279
|
+
break;
|
|
6280
|
+
}
|
|
6281
|
+
}
|
|
6282
|
+
scopeRoots.push({ root: null, tag: "page" });
|
|
6283
|
+
for (const { root, tag } of scopeRoots) {
|
|
6284
|
+
for (const sel of qtySelectors) {
|
|
6285
|
+
const all = root ? root.locator(sel) : page.locator(sel);
|
|
6286
|
+
const count = await all.count().catch(() => 0);
|
|
6287
|
+
for (let i = 0;i < Math.min(count, 20); i++) {
|
|
6288
|
+
const el = all.nth(i);
|
|
6289
|
+
if (!await el.isVisible({ timeout: 300 }).catch(() => false))
|
|
6290
|
+
continue;
|
|
6291
|
+
const value = (await el.inputValue().catch(() => "")).trim();
|
|
6292
|
+
if (value !== "" && value !== "0")
|
|
6293
|
+
continue;
|
|
6294
|
+
const plus = await findPlusButtonNear(el);
|
|
6295
|
+
if (plus) {
|
|
6296
|
+
if (await plus.isDisabled().catch(() => false))
|
|
6297
|
+
continue;
|
|
6298
|
+
await plus.click({ timeout: 2000 }).catch(() => {
|
|
6299
|
+
return;
|
|
6300
|
+
});
|
|
6301
|
+
await page.waitForTimeout(500);
|
|
6302
|
+
return {
|
|
6303
|
+
description: `Incrementou variante via botão '+' próximo ao input \`${sel}\` (scope=${tag}, índice ${i})`,
|
|
6304
|
+
selectorKey: "quantityInput",
|
|
6305
|
+
usedSelector: sel
|
|
6306
|
+
};
|
|
6307
|
+
}
|
|
6308
|
+
await el.fill("1", { timeout: 1500 }).catch(() => {
|
|
6309
|
+
return;
|
|
6310
|
+
});
|
|
6311
|
+
await el.dispatchEvent("input").catch(() => {
|
|
6312
|
+
return;
|
|
6313
|
+
});
|
|
6314
|
+
await el.dispatchEvent("change").catch(() => {
|
|
6315
|
+
return;
|
|
6316
|
+
});
|
|
6317
|
+
await page.waitForTimeout(400);
|
|
6318
|
+
return {
|
|
6319
|
+
description: `Ajustou quantidade do input \`${sel}\` para 1 (scope=${tag}, índice ${i})`,
|
|
6320
|
+
selectorKey: "quantityInput",
|
|
6321
|
+
usedSelector: sel
|
|
6322
|
+
};
|
|
6323
|
+
}
|
|
6324
|
+
}
|
|
6325
|
+
}
|
|
6326
|
+
return null;
|
|
6327
|
+
}
|
|
6328
|
+
async function findPlusButtonNear(input) {
|
|
6329
|
+
const ancestorXPath = "xpath=ancestor::*[self::li or self::tr or self::form or self::fieldset or @data-sku-row or @data-variant-row][1]//button[normalize-space(.)='+']";
|
|
6330
|
+
const inAncestor = input.locator(ancestorXPath).first();
|
|
6331
|
+
if (await inAncestor.isVisible({ timeout: 600 }).catch(() => false)) {
|
|
6332
|
+
return inAncestor;
|
|
6333
|
+
}
|
|
6334
|
+
for (let depth = 1;depth <= 6; depth++) {
|
|
6335
|
+
const climb = `xpath=ancestor::*[${depth}]//button[normalize-space(.)='+']`;
|
|
6336
|
+
const cand = input.locator(climb).first();
|
|
6337
|
+
if (await cand.isVisible({ timeout: 300 }).catch(() => false)) {
|
|
6338
|
+
return cand;
|
|
6339
|
+
}
|
|
6340
|
+
}
|
|
6341
|
+
const sibling = input.locator("xpath=following-sibling::button[1]").first();
|
|
6342
|
+
if (await sibling.isVisible({ timeout: 300 }).catch(() => false)) {
|
|
6343
|
+
const text = (await sibling.innerText().catch(() => "")).trim();
|
|
6344
|
+
if (text === "+")
|
|
6345
|
+
return sibling;
|
|
6346
|
+
}
|
|
6347
|
+
return null;
|
|
6348
|
+
}
|
|
6349
|
+
async function readCartCount(page, ctx) {
|
|
6350
|
+
const selectors = selFor(ctx, "minicartCount");
|
|
6351
|
+
for (const sel of selectors) {
|
|
6352
|
+
try {
|
|
6353
|
+
const el = page.locator(sel).first();
|
|
6354
|
+
if (!await el.isVisible({ timeout: 500 }).catch(() => false))
|
|
6355
|
+
continue;
|
|
6356
|
+
const raw = (await el.innerText().catch(() => "")).trim();
|
|
6357
|
+
const match = raw.match(/\d+/);
|
|
6358
|
+
if (match)
|
|
6359
|
+
return Number.parseInt(match[0], 10);
|
|
6360
|
+
} catch {}
|
|
6361
|
+
}
|
|
6362
|
+
return 0;
|
|
6363
|
+
}
|
|
6364
|
+
async function validateAddToCart(page, ctx, cartCountBefore, beforeUrl) {
|
|
6365
|
+
const deadline = Date.now() + 3000;
|
|
6366
|
+
let lastErrorText;
|
|
6367
|
+
while (Date.now() < deadline) {
|
|
6368
|
+
const currentUrl = page.url();
|
|
6369
|
+
if (currentUrl !== beforeUrl && /\/(cart|carrinho|checkout)(\/|$|\?)/i.test(currentUrl)) {
|
|
6370
|
+
return {
|
|
6371
|
+
status: "ok",
|
|
6372
|
+
signal: "url-changed",
|
|
6373
|
+
note: `URL mudou para \`${currentUrl}\` (carrinho/checkout)`
|
|
6374
|
+
};
|
|
6375
|
+
}
|
|
6376
|
+
const cartCountNow = await readCartCount(page, ctx);
|
|
6377
|
+
if (cartCountNow > cartCountBefore) {
|
|
6378
|
+
return {
|
|
6379
|
+
status: "ok",
|
|
6380
|
+
signal: "count-increased",
|
|
6381
|
+
note: `Contador do minicart foi de ${cartCountBefore} para ${cartCountNow}`
|
|
6382
|
+
};
|
|
6383
|
+
}
|
|
6384
|
+
const drawerSelectors = [
|
|
6385
|
+
"[role='dialog']",
|
|
6386
|
+
"[data-minicart][aria-expanded='true']",
|
|
6387
|
+
"[data-cart-drawer].open",
|
|
6388
|
+
"[data-minicart-drawer]:not([hidden])",
|
|
6389
|
+
".minicart--open",
|
|
6390
|
+
".cart-drawer--open"
|
|
6391
|
+
];
|
|
6392
|
+
for (const sel of drawerSelectors) {
|
|
6393
|
+
const el = page.locator(sel).first();
|
|
6394
|
+
if (await el.isVisible({ timeout: 200 }).catch(() => false)) {
|
|
6395
|
+
return {
|
|
6396
|
+
status: "ok",
|
|
6397
|
+
signal: "drawer-open",
|
|
6398
|
+
note: `Drawer/modal do carrinho abriu (\`${sel}\`)`
|
|
6399
|
+
};
|
|
6400
|
+
}
|
|
6401
|
+
}
|
|
6402
|
+
try {
|
|
6403
|
+
const bodyText = await page.locator("body").innerText({ timeout: 500 }).catch(() => "");
|
|
6404
|
+
for (const re of ADD_TO_CART_SUCCESS_PATTERNS) {
|
|
6405
|
+
const match = bodyText.match(re);
|
|
6406
|
+
if (match) {
|
|
6407
|
+
return {
|
|
6408
|
+
status: "ok",
|
|
6409
|
+
signal: "success-toast",
|
|
6410
|
+
note: `Toast de sucesso visível: '${match[0]}'`
|
|
6411
|
+
};
|
|
6412
|
+
}
|
|
6413
|
+
}
|
|
6414
|
+
for (const re of ADD_TO_CART_ERROR_PATTERNS) {
|
|
6415
|
+
const match = bodyText.match(re);
|
|
6416
|
+
if (match) {
|
|
6417
|
+
lastErrorText = match[0];
|
|
6418
|
+
break;
|
|
6419
|
+
}
|
|
6420
|
+
}
|
|
6421
|
+
} catch {}
|
|
6422
|
+
await page.waitForTimeout(250);
|
|
6423
|
+
}
|
|
6424
|
+
if (lastErrorText) {
|
|
6425
|
+
return {
|
|
6426
|
+
status: "failed",
|
|
6427
|
+
signal: "error-text",
|
|
6428
|
+
note: `add-to-cart silenciosamente falhou: '${lastErrorText}' visível na página`,
|
|
6429
|
+
errorText: lastErrorText
|
|
6430
|
+
};
|
|
6431
|
+
}
|
|
6432
|
+
return {
|
|
6433
|
+
status: "failed",
|
|
6434
|
+
signal: "no-signal",
|
|
6435
|
+
note: "add-to-cart sem confirmação visível (minicart não atualizou, sem drawer, sem mudança de URL)"
|
|
6436
|
+
};
|
|
6437
|
+
}
|
|
6438
|
+
async function attemptRecovery(page, _ctx, stepName, intendedAction, alreadyTried) {
|
|
6439
|
+
let html = "";
|
|
6440
|
+
try {
|
|
6441
|
+
html = await page.content();
|
|
6442
|
+
} catch {
|
|
6443
|
+
return null;
|
|
6444
|
+
}
|
|
6445
|
+
const suggestion = await suggestRecovery({ stepName, intendedAction, html, alreadyTried });
|
|
6446
|
+
if (!suggestion)
|
|
6447
|
+
return null;
|
|
6448
|
+
try {
|
|
6449
|
+
const el = page.locator(suggestion.selector).first();
|
|
6450
|
+
if (await el.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
6451
|
+
return { locator: el, selector: suggestion.selector };
|
|
6452
|
+
}
|
|
6453
|
+
} catch {}
|
|
6454
|
+
return null;
|
|
6455
|
+
}
|
|
6456
|
+
async function attemptStepAction(args) {
|
|
6457
|
+
const { page, ctx, stepName, intendedAction, selectorKey, action, value, recoveryBudget } = args;
|
|
6458
|
+
const selectors = selFor(ctx, selectorKey);
|
|
6459
|
+
for (const sel of selectors) {
|
|
6460
|
+
try {
|
|
6461
|
+
const el = page.locator(sel).first();
|
|
6462
|
+
if (!await el.isVisible({ timeout: 800 }).catch(() => false))
|
|
6463
|
+
continue;
|
|
6464
|
+
const ok = await performAction(el, action, value);
|
|
6465
|
+
if (ok) {
|
|
6466
|
+
return { performed: true, selector: sel, action, recoveredByLlm: false };
|
|
6467
|
+
}
|
|
6468
|
+
} catch {}
|
|
6469
|
+
}
|
|
6470
|
+
if (recoveryBudget.remaining <= 0) {
|
|
6471
|
+
return { performed: false, selector: "", action, recoveredByLlm: false };
|
|
6472
|
+
}
|
|
6473
|
+
let html = "";
|
|
6474
|
+
try {
|
|
6475
|
+
html = await page.content();
|
|
6476
|
+
} catch {
|
|
6477
|
+
return { performed: false, selector: "", action, recoveredByLlm: false };
|
|
6478
|
+
}
|
|
6479
|
+
const suggestion = await suggestRecovery({
|
|
6480
|
+
stepName,
|
|
6481
|
+
intendedAction,
|
|
6482
|
+
html,
|
|
6483
|
+
alreadyTried: selectors
|
|
6484
|
+
});
|
|
6485
|
+
if (!suggestion) {
|
|
6486
|
+
return { performed: false, selector: "", action, recoveredByLlm: false };
|
|
6487
|
+
}
|
|
6488
|
+
recoveryBudget.remaining--;
|
|
6489
|
+
const llmAction = suggestion.action ?? action;
|
|
6490
|
+
const llmValue = suggestion.value ?? value;
|
|
6491
|
+
try {
|
|
6492
|
+
const el = page.locator(suggestion.selector).first();
|
|
6493
|
+
if (!await el.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
6494
|
+
return { performed: false, selector: "", action: llmAction, recoveredByLlm: false };
|
|
6495
|
+
}
|
|
6496
|
+
const ok = await performAction(el, llmAction, llmValue);
|
|
6497
|
+
if (ok) {
|
|
6498
|
+
return {
|
|
6499
|
+
performed: true,
|
|
6500
|
+
selector: suggestion.selector,
|
|
6501
|
+
action: llmAction,
|
|
6502
|
+
recoveredByLlm: true
|
|
6503
|
+
};
|
|
6504
|
+
}
|
|
6505
|
+
} catch {}
|
|
6506
|
+
return { performed: false, selector: "", action: llmAction, recoveredByLlm: false };
|
|
6507
|
+
}
|
|
6508
|
+
async function performAction(el, action, value) {
|
|
6509
|
+
try {
|
|
6510
|
+
if (action === "click") {
|
|
6511
|
+
await el.click({ timeout: 3000 });
|
|
6512
|
+
return true;
|
|
6513
|
+
}
|
|
6514
|
+
if (action === "fill") {
|
|
6515
|
+
await el.fill(value ?? "", { timeout: 3000 });
|
|
6516
|
+
await el.press("Enter", { timeout: 1500 }).catch(() => {
|
|
6517
|
+
return;
|
|
6518
|
+
});
|
|
6519
|
+
return true;
|
|
6520
|
+
}
|
|
6521
|
+
if (action === "press") {
|
|
6522
|
+
await el.press(value ?? "Enter", { timeout: 1500 });
|
|
6523
|
+
return true;
|
|
6524
|
+
}
|
|
6525
|
+
} catch {
|
|
6526
|
+
return false;
|
|
6527
|
+
}
|
|
6528
|
+
return false;
|
|
6529
|
+
}
|
|
5997
6530
|
var GENERIC_TITLE_PATTERNS = [
|
|
5998
6531
|
/^p[áa]gina de produto/i,
|
|
5999
6532
|
/^product page/i,
|
|
@@ -6246,7 +6779,7 @@ async function openMinicart(page, trigger, ctx, expectedProductTitle) {
|
|
|
6246
6779
|
}
|
|
6247
6780
|
})();
|
|
6248
6781
|
if (targetUrl) {
|
|
6249
|
-
dlog2(ctx, ` openMinicart: all interactive strategies failed
|
|
6782
|
+
dlog2(ctx, ` openMinicart: all interactive strategies failed; navigating directly to ${targetUrl}`);
|
|
6250
6783
|
await page.goto(targetUrl, { waitUntil: "load", timeout: 15000 }).catch(() => {
|
|
6251
6784
|
return;
|
|
6252
6785
|
});
|
|
@@ -6345,7 +6878,7 @@ async function validateCartContainsTitle(page, expectedTitle, ctx) {
|
|
|
6345
6878
|
await page.waitForTimeout(2000);
|
|
6346
6879
|
observed = await sweepTitles();
|
|
6347
6880
|
}
|
|
6348
|
-
dlog2(ctx, ` validateCartContainsTitle: observed ${observed.length} titles
|
|
6881
|
+
dlog2(ctx, ` validateCartContainsTitle: observed ${observed.length} titles`);
|
|
6349
6882
|
if (observed.length === 0) {
|
|
6350
6883
|
return { found: false, observedTitles: [], method: "none" };
|
|
6351
6884
|
}
|
|
@@ -6374,24 +6907,6 @@ async function detectEmptyCartBanner(page) {
|
|
|
6374
6907
|
}
|
|
6375
6908
|
return null;
|
|
6376
6909
|
}
|
|
6377
|
-
async function attemptRecovery(page, _ctx, stepName, intendedAction, alreadyTried) {
|
|
6378
|
-
let html = "";
|
|
6379
|
-
try {
|
|
6380
|
-
html = await page.content();
|
|
6381
|
-
} catch {
|
|
6382
|
-
return null;
|
|
6383
|
-
}
|
|
6384
|
-
const suggestion = await suggestRecovery({ stepName, intendedAction, html, alreadyTried });
|
|
6385
|
-
if (!suggestion)
|
|
6386
|
-
return null;
|
|
6387
|
-
try {
|
|
6388
|
-
const el = page.locator(suggestion.selector).first();
|
|
6389
|
-
if (await el.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
6390
|
-
return { locator: el, selector: suggestion.selector };
|
|
6391
|
-
}
|
|
6392
|
-
} catch {}
|
|
6393
|
-
return null;
|
|
6394
|
-
}
|
|
6395
6910
|
|
|
6396
6911
|
// src/ignore/parser.ts
|
|
6397
6912
|
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
|
|
@@ -6501,6 +7016,51 @@ function detectFromHtml(html) {
|
|
|
6501
7016
|
return "custom";
|
|
6502
7017
|
}
|
|
6503
7018
|
|
|
7019
|
+
// src/learned/promote.ts
|
|
7020
|
+
function promoteStepsFromFlow(learned, platform, host, flow) {
|
|
7021
|
+
let promoted = 0;
|
|
7022
|
+
let deprecated = 0;
|
|
7023
|
+
let recorded = 0;
|
|
7024
|
+
for (const step of flow.steps ?? []) {
|
|
7025
|
+
if (!step.selectorKey || !step.usedSelector)
|
|
7026
|
+
continue;
|
|
7027
|
+
if (!isReusableSelector(step.usedSelector))
|
|
7028
|
+
continue;
|
|
7029
|
+
const parsed = SelectorKey.safeParse(step.selectorKey);
|
|
7030
|
+
if (!parsed.success)
|
|
7031
|
+
continue;
|
|
7032
|
+
const key = parsed.data;
|
|
7033
|
+
if (step.recoveredByLlm) {
|
|
7034
|
+
promoteFromLlm(learned, platform, key, step.usedSelector, host);
|
|
7035
|
+
promoted++;
|
|
7036
|
+
} else if (step.status === "ok") {
|
|
7037
|
+
recordSuccess(learned, platform, key, step.usedSelector, host);
|
|
7038
|
+
recorded++;
|
|
7039
|
+
} else if (step.status === "failed") {
|
|
7040
|
+
const wasDeprecated = isAlreadyDeprecated(learned, platform, key, step.usedSelector);
|
|
7041
|
+
const after = recordFailure(learned, platform, key, step.usedSelector, host);
|
|
7042
|
+
if (!wasDeprecated && after?.deprecated)
|
|
7043
|
+
deprecated++;
|
|
7044
|
+
}
|
|
7045
|
+
}
|
|
7046
|
+
return { promoted, deprecated, recorded };
|
|
7047
|
+
}
|
|
7048
|
+
function isReusableSelector(s) {
|
|
7049
|
+
if (s.includes("→"))
|
|
7050
|
+
return false;
|
|
7051
|
+
if (s.includes(" ~ "))
|
|
7052
|
+
return false;
|
|
7053
|
+
return s.trim().length > 0;
|
|
7054
|
+
}
|
|
7055
|
+
function isAlreadyDeprecated(lib, platform, key, selector) {
|
|
7056
|
+
const platformEntries = lib.platforms[platform];
|
|
7057
|
+
if (!platformEntries)
|
|
7058
|
+
return false;
|
|
7059
|
+
const entries = platformEntries[key] ?? [];
|
|
7060
|
+
const entry = entries.find((e) => e.selector === selector);
|
|
7061
|
+
return !!entry?.deprecated;
|
|
7062
|
+
}
|
|
7063
|
+
|
|
6504
7064
|
// src/llm/discover-selectors.ts
|
|
6505
7065
|
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
6506
7066
|
import { join as join6 } from "node:path";
|
|
@@ -6526,26 +7086,26 @@ var DISCOVER_SELECTORS_TOOL = {
|
|
|
6526
7086
|
},
|
|
6527
7087
|
minicart_trigger: {
|
|
6528
7088
|
type: "string",
|
|
6529
|
-
description:
|
|
7089
|
+
description: `CSS selector for the CART ICON / SACOLA icon in the HEADER that opens the mini-cart drawer or navigates to the cart page when clicked (e.g. '[data-minicart-trigger]', or 'header a[aria-label*="sacola" i]', or 'header a[href*="cart"]'). This is the icon ALWAYS visible in the page header.`
|
|
6530
7090
|
},
|
|
6531
7091
|
cep_input_pdp: {
|
|
6532
7092
|
type: "string",
|
|
6533
|
-
description: "CSS selector for the CEP / zipcode <input> on a PDP. Empty string if the PDP has no shipping calculator."
|
|
7093
|
+
description: "CSS selector for the CEP / zipcode <input> on a PDP — the input where the customer types their ADDRESS POSTAL CODE to calculate shipping. Placeholder is typically 'CEP', 'Digite seu CEP', 'Cód. postal'. Do NOT return a coupon input ('Digite o cupom') or email/newsletter field. Empty string if the PDP has no shipping calculator."
|
|
6534
7094
|
},
|
|
6535
7095
|
cep_input_cart: {
|
|
6536
7096
|
type: "string",
|
|
6537
|
-
description: "CSS selector for the CEP / zipcode <input> inside the mini-cart
|
|
7097
|
+
description: "CSS selector for the CEP / zipcode <input> inside the mini-cart drawer or cart page — for shipping calculation. Same rules: ADDRESS postal code, NOT coupon, NOT email. Empty string if cart has no shipping calculator."
|
|
6538
7098
|
},
|
|
6539
7099
|
checkout_button: {
|
|
6540
7100
|
type: "string",
|
|
6541
|
-
description:
|
|
7101
|
+
description: "CSS selector for the FINAL 'Go to checkout' / 'Finalizar compra' / 'Finalizar' button INSIDE the mini-cart drawer or cart page — the one user clicks AFTER reviewing cart items to proceed to checkout/payment. **IMPORTANT**: this is DIFFERENT from `minicart_trigger` (which is the cart ICON in the header). The checkout button is typically a big colored button at the bottom of the cart drawer or cart page. If you cannot see it on the home page (which is common — it's only rendered after add-to-cart), return EMPTY STRING. NEVER return the same selector as `minicart_trigger`."
|
|
6542
7102
|
},
|
|
6543
7103
|
reasoning: {
|
|
6544
7104
|
type: "string",
|
|
6545
7105
|
description: "1-2 sentence rationale describing what was inferred from the markup."
|
|
6546
7106
|
}
|
|
6547
7107
|
},
|
|
6548
|
-
required: ["category_link", "product_card", "buy_button", "minicart_trigger"
|
|
7108
|
+
required: ["category_link", "product_card", "buy_button", "minicart_trigger"]
|
|
6549
7109
|
}
|
|
6550
7110
|
};
|
|
6551
7111
|
var SYSTEM_PROMPT = `
|
|
@@ -6582,7 +7142,17 @@ REGRAS:
|
|
|
6582
7142
|
4. Para "cep_input_pdp" e "cep_input_cart": retorne string vazia se não existir esse recurso na plataforma.
|
|
6583
7143
|
Não invente.
|
|
6584
7144
|
|
|
6585
|
-
5.
|
|
7145
|
+
5. **NUNCA confunda \`minicart_trigger\` (ícone do carrinho no HEADER) com \`checkout_button\`
|
|
7146
|
+
(botão "Finalizar" DENTRO do drawer ou na página de cart).** Se você só vê o ícone do
|
|
7147
|
+
carrinho na home e não consegue ver o botão de checkout (caso comum — ele só renderiza
|
|
7148
|
+
depois de add-to-cart), retorne STRING VAZIA pra \`checkout_button\`. NUNCA retorne o
|
|
7149
|
+
mesmo seletor para os dois — isso quebra o fluxo de teste.
|
|
7150
|
+
|
|
7151
|
+
6. Para CEP: \`cep_input_pdp\` e \`cep_input_cart\` são pra ENDEREÇO (CEP / Postal Code).
|
|
7152
|
+
NÃO confunda com input de CUPOM ('Digite o cupom', 'Insira código', 'Promo code') —
|
|
7153
|
+
cupom é desconto, não frete.
|
|
7154
|
+
|
|
7155
|
+
7. Sempre teste mentalmente: "este seletor pegaria o elemento certo na home E também em PDPs/cart?"
|
|
6586
7156
|
|
|
6587
7157
|
Responda SEMPRE via tool_use report_selectors. Não escreva texto livre fora da tool call.
|
|
6588
7158
|
`.trim();
|
|
@@ -6656,12 +7226,24 @@ ${compacted}
|
|
|
6656
7226
|
cepInputCart: emptyToUndef(input.cep_input_cart),
|
|
6657
7227
|
checkoutButton: emptyToUndef(input.checkout_button)
|
|
6658
7228
|
};
|
|
7229
|
+
if (selectors.checkoutButton && selectors.minicartTrigger && selectorsLikelyConflict(selectors.checkoutButton, selectors.minicartTrigger)) {
|
|
7230
|
+
console.warn(`[discover-selectors] checkout_button == minicart_trigger (\`${selectors.checkoutButton}\`); dropping to avoid step-9 misclick`);
|
|
7231
|
+
selectors.checkoutButton = undefined;
|
|
7232
|
+
}
|
|
6659
7233
|
if (!existsSync6(cacheDir))
|
|
6660
7234
|
mkdirSync4(cacheDir, { recursive: true });
|
|
6661
7235
|
writeFileSync5(cachePath, `${JSON.stringify(selectors, null, 2)}
|
|
6662
7236
|
`, "utf8");
|
|
6663
7237
|
return selectors;
|
|
6664
7238
|
}
|
|
7239
|
+
function selectorsLikelyConflict(a, b) {
|
|
7240
|
+
if (a === b)
|
|
7241
|
+
return true;
|
|
7242
|
+
const norm = (s) => s.replace(/\s+/g, "").toLowerCase();
|
|
7243
|
+
const na = norm(a);
|
|
7244
|
+
const nb = norm(b);
|
|
7245
|
+
return na.includes(nb) || nb.includes(na);
|
|
7246
|
+
}
|
|
6665
7247
|
function emptyToUndef(v) {
|
|
6666
7248
|
if (v == null)
|
|
6667
7249
|
return;
|
|
@@ -6681,11 +7263,12 @@ var STEP_LABELS = {
|
|
|
6681
7263
|
"visit-home": "1. visit-home",
|
|
6682
7264
|
"navigate-plp": "2. navigate-plp",
|
|
6683
7265
|
"enter-pdp": "3. enter-pdp",
|
|
6684
|
-
"
|
|
6685
|
-
"
|
|
6686
|
-
"
|
|
6687
|
-
"
|
|
6688
|
-
"
|
|
7266
|
+
"select-variant": "4. select-variant",
|
|
7267
|
+
"shipping-calc-pdp": "5. shipping-calc-pdp",
|
|
7268
|
+
"add-to-cart": "6. add-to-cart",
|
|
7269
|
+
"open-minicart": "7. open-minicart",
|
|
7270
|
+
"shipping-calc-cart": "8. shipping-calc-cart",
|
|
7271
|
+
"go-checkout": "9. go-checkout"
|
|
6689
7272
|
};
|
|
6690
7273
|
var CRITICAL_STEPS = new Set([
|
|
6691
7274
|
"visit-home",
|
|
@@ -6814,6 +7397,39 @@ async function journeyCommand(opts) {
|
|
|
6814
7397
|
}
|
|
6815
7398
|
if (!opts.json)
|
|
6816
7399
|
console.log("");
|
|
7400
|
+
const learnedBefore = JSON.stringify(learned);
|
|
7401
|
+
const prodHost = safeHost2(opts.prod);
|
|
7402
|
+
let totalPromoted = 0;
|
|
7403
|
+
let totalDeprecated = 0;
|
|
7404
|
+
let totalRecorded = 0;
|
|
7405
|
+
for (const cap of flowCaptures) {
|
|
7406
|
+
if (cap.side !== "prod")
|
|
7407
|
+
continue;
|
|
7408
|
+
const result = promoteStepsFromFlow(learned, platform, prodHost, cap);
|
|
7409
|
+
totalPromoted += result.promoted;
|
|
7410
|
+
totalDeprecated += result.deprecated;
|
|
7411
|
+
totalRecorded += result.recorded;
|
|
7412
|
+
}
|
|
7413
|
+
if (JSON.stringify(learned) !== learnedBefore) {
|
|
7414
|
+
try {
|
|
7415
|
+
saveLearned(learned);
|
|
7416
|
+
if (!opts.json) {
|
|
7417
|
+
const bits = [];
|
|
7418
|
+
if (totalPromoted > 0)
|
|
7419
|
+
bits.push(`${totalPromoted} promovido(s) via LLM`);
|
|
7420
|
+
if (totalRecorded > 0)
|
|
7421
|
+
bits.push(`${totalRecorded} reforçado(s)`);
|
|
7422
|
+
if (totalDeprecated > 0)
|
|
7423
|
+
bits.push(`${totalDeprecated} depreciado(s)`);
|
|
7424
|
+
if (bits.length > 0)
|
|
7425
|
+
console.log(chalk6.dim(` learned-selectors atualizado: ${bits.join(", ")}`));
|
|
7426
|
+
}
|
|
7427
|
+
} catch (err) {
|
|
7428
|
+
if (!opts.json) {
|
|
7429
|
+
console.warn(chalk6.yellow(` warn: failed to persist learned-selectors: ${err.message}`));
|
|
7430
|
+
}
|
|
7431
|
+
}
|
|
7432
|
+
}
|
|
6817
7433
|
const rows = buildRows(flowCaptures, viewports);
|
|
6818
7434
|
const failures = collectFailures(rows);
|
|
6819
7435
|
writeRunReportJson(paths.runDir, buildJourneyRun(opts, runId, flowCaptures, failures));
|
|
@@ -7171,6 +7787,13 @@ function renderSummaryBlock(rows, failures) {
|
|
|
7171
7787
|
function escapeHtml(s) {
|
|
7172
7788
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
7173
7789
|
}
|
|
7790
|
+
function safeHost2(url) {
|
|
7791
|
+
try {
|
|
7792
|
+
return new URL(url).hostname;
|
|
7793
|
+
} catch {
|
|
7794
|
+
return url;
|
|
7795
|
+
}
|
|
7796
|
+
}
|
|
7174
7797
|
|
|
7175
7798
|
// src/commands/learned.ts
|
|
7176
7799
|
import chalk7 from "chalk";
|
|
@@ -8489,6 +9112,7 @@ var STEP_LABELS2 = {
|
|
|
8489
9112
|
"visit-home": "Visitar home",
|
|
8490
9113
|
"navigate-plp": "Navegar para categoria (PLP)",
|
|
8491
9114
|
"enter-pdp": "Entrar em PDP",
|
|
9115
|
+
"select-variant": "Selecionar variante (tamanho/cor/quantidade)",
|
|
8492
9116
|
"shipping-calc-pdp": "Cálculo de frete na PDP",
|
|
8493
9117
|
"add-to-cart": "Adicionar ao carrinho",
|
|
8494
9118
|
"open-minicart": "Abrir minicart",
|
|
@@ -10383,21 +11007,9 @@ async function runCommand(rawOpts) {
|
|
|
10383
11007
|
for (const p of cap.pages)
|
|
10384
11008
|
allPageCaptures.push(p);
|
|
10385
11009
|
if (opts.learn !== false) {
|
|
10386
|
-
|
|
10387
|
-
|
|
10388
|
-
|
|
10389
|
-
const key = step.selectorKey;
|
|
10390
|
-
if (step.recoveredByLlm) {
|
|
10391
|
-
promoteFromLlm(learned, platform, key, step.usedSelector, prodHost);
|
|
10392
|
-
promotedCount++;
|
|
10393
|
-
} else if (step.status === "ok") {
|
|
10394
|
-
recordSuccess(learned, platform, key, step.usedSelector, prodHost);
|
|
10395
|
-
} else if (step.status === "failed") {
|
|
10396
|
-
const before = recordFailure(learned, platform, key, step.usedSelector, prodHost);
|
|
10397
|
-
if (before?.deprecated)
|
|
10398
|
-
deprecatedCount++;
|
|
10399
|
-
}
|
|
10400
|
-
}
|
|
11010
|
+
const result = promoteStepsFromFlow(learned, platform, prodHost, cap);
|
|
11011
|
+
promotedCount += result.promoted;
|
|
11012
|
+
deprecatedCount += result.deprecated;
|
|
10401
11013
|
}
|
|
10402
11014
|
}
|
|
10403
11015
|
await stopTracing(ctx, tracePath).catch(() => {
|