@decocms/parity 0.1.0 → 0.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/CHANGELOG.md +7 -0
- package/dist/cli.js +474 -146
- package/package.json +85 -85
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.1] — 2026-05-22
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- `cache-coverage`: classify `decoims.com` (deco image proxy) and `assets.decocache.com` (deco edge cache) as first-party so they remain eligible for cache-opportunity reporting instead of being silently skipped as third-party. ([#5](https://github.com/decocms/parity/pull/5))
|
|
15
|
+
- `purchase-journey-flow`: never silently return `pass` when zero comparable steps were evaluated. The check now returns `skipped` when the flow wasn't requested, and `fail` (critical) when the flow was requested but neither side produced a capture or when capture arrays came back empty. Previously a fully broken cand home would still get a green verdict on the purchase-journey check. ([#6](https://github.com/decocms/parity/pull/6))
|
|
16
|
+
|
|
10
17
|
## [0.1.0] — 2026-05-12
|
|
11
18
|
|
|
12
19
|
First public release.
|
package/dist/cli.js
CHANGED
|
@@ -540,7 +540,7 @@ function isThirdParty(url, baseHost) {
|
|
|
540
540
|
return false;
|
|
541
541
|
if (u.hostname.endsWith(`.${cleanedBase}`))
|
|
542
542
|
return false;
|
|
543
|
-
if (u.hostname.includes("vtexassets") || u.hostname.includes("decoassets") || u.hostname.includes("vteximg"))
|
|
543
|
+
if (u.hostname.includes("vtexassets") || u.hostname.includes("decoassets") || u.hostname.includes("vteximg") || u.hostname.includes("decocache") || u.hostname.includes("decoims"))
|
|
544
544
|
return false;
|
|
545
545
|
return true;
|
|
546
546
|
} catch {
|
|
@@ -1118,63 +1118,7 @@ async function capturePage(page, opts) {
|
|
|
1118
1118
|
const overallBudgetMs = opts.fast ? 25000 : 60000;
|
|
1119
1119
|
const deadline = start + overallBudgetMs;
|
|
1120
1120
|
const remaining = () => Math.max(500, deadline - Date.now());
|
|
1121
|
-
|
|
1122
|
-
let finalUrl = opts.url;
|
|
1123
|
-
let xRobotsTag = null;
|
|
1124
|
-
try {
|
|
1125
|
-
response = await page.goto(opts.url, {
|
|
1126
|
-
waitUntil: "domcontentloaded",
|
|
1127
|
-
timeout: Math.min(opts.timeoutMs ?? 30000, remaining())
|
|
1128
|
-
});
|
|
1129
|
-
finalUrl = page.url();
|
|
1130
|
-
if (response) {
|
|
1131
|
-
const headers = response.headers();
|
|
1132
|
-
xRobotsTag = headers["x-robots-tag"] ?? null;
|
|
1133
|
-
}
|
|
1134
|
-
if (opts.fast) {
|
|
1135
|
-
await page.waitForLoadState("networkidle", { timeout: Math.min(4000, remaining()) }).catch(() => {
|
|
1136
|
-
return;
|
|
1137
|
-
});
|
|
1138
|
-
await page.waitForTimeout(Math.min(opts.settleMs ?? 1200, remaining()));
|
|
1139
|
-
} else {
|
|
1140
|
-
await page.waitForLoadState("load", { timeout: Math.min(12000, remaining()) }).catch(() => {
|
|
1141
|
-
return;
|
|
1142
|
-
});
|
|
1143
|
-
await page.waitForLoadState("networkidle", { timeout: Math.min(6000, remaining()) }).catch(() => {
|
|
1144
|
-
return;
|
|
1145
|
-
});
|
|
1146
|
-
await page.waitForTimeout(Math.min(opts.settleMs ?? 2000, remaining()));
|
|
1147
|
-
if (opts.scrollToLoad !== false && remaining() > 3000) {
|
|
1148
|
-
await Promise.race([
|
|
1149
|
-
scrollFullPage(page).catch(() => {
|
|
1150
|
-
return;
|
|
1151
|
-
}),
|
|
1152
|
-
new Promise((resolve) => setTimeout(resolve, Math.min(1e4, remaining())))
|
|
1153
|
-
]);
|
|
1154
|
-
await page.waitForTimeout(Math.min(600, remaining()));
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
} catch (err) {
|
|
1158
|
-
state.console.push({
|
|
1159
|
-
type: "error",
|
|
1160
|
-
text: `[navigation-error] ${err.message}`
|
|
1161
|
-
});
|
|
1162
|
-
}
|
|
1163
|
-
const vitals = await page.evaluate(() => window.__parity_vitals).catch(() => null);
|
|
1164
|
-
if (!opts.skipScreenshot) {
|
|
1165
|
-
await Promise.race([
|
|
1166
|
-
page.screenshot({ path: opts.screenshotPath, fullPage: true, animations: "disabled" }).catch(() => {
|
|
1167
|
-
return;
|
|
1168
|
-
}),
|
|
1169
|
-
new Promise((resolve) => setTimeout(resolve, Math.min(15000, remaining())))
|
|
1170
|
-
]);
|
|
1171
|
-
}
|
|
1172
|
-
const html = await Promise.race([
|
|
1173
|
-
page.content().catch(() => ""),
|
|
1174
|
-
new Promise((resolve) => setTimeout(() => resolve(""), Math.min(5000, remaining())))
|
|
1175
|
-
]);
|
|
1176
|
-
await flushCollectors(state, Math.min(3000, remaining()));
|
|
1177
|
-
return {
|
|
1121
|
+
const buildPartial = () => ({
|
|
1178
1122
|
url: opts.url,
|
|
1179
1123
|
finalUrl,
|
|
1180
1124
|
status: response?.status() ?? 0,
|
|
@@ -1189,7 +1133,83 @@ async function capturePage(page, opts) {
|
|
|
1189
1133
|
harPath: opts.harPath,
|
|
1190
1134
|
tracePath: opts.tracePath,
|
|
1191
1135
|
xRobotsTag
|
|
1136
|
+
});
|
|
1137
|
+
let response = null;
|
|
1138
|
+
let finalUrl = opts.url;
|
|
1139
|
+
let xRobotsTag = null;
|
|
1140
|
+
let vitals = null;
|
|
1141
|
+
let html = "";
|
|
1142
|
+
const inner = async () => {
|
|
1143
|
+
try {
|
|
1144
|
+
response = await page.goto(opts.url, {
|
|
1145
|
+
waitUntil: "domcontentloaded",
|
|
1146
|
+
timeout: Math.min(opts.timeoutMs ?? 30000, remaining())
|
|
1147
|
+
});
|
|
1148
|
+
finalUrl = page.url();
|
|
1149
|
+
if (response) {
|
|
1150
|
+
const headers = response.headers();
|
|
1151
|
+
xRobotsTag = headers["x-robots-tag"] ?? null;
|
|
1152
|
+
}
|
|
1153
|
+
if (opts.fast) {
|
|
1154
|
+
await page.waitForLoadState("networkidle", { timeout: Math.min(4000, remaining()) }).catch(() => {
|
|
1155
|
+
return;
|
|
1156
|
+
});
|
|
1157
|
+
await page.waitForTimeout(Math.min(opts.settleMs ?? 1200, remaining()));
|
|
1158
|
+
} else {
|
|
1159
|
+
await page.waitForLoadState("load", { timeout: Math.min(12000, remaining()) }).catch(() => {
|
|
1160
|
+
return;
|
|
1161
|
+
});
|
|
1162
|
+
await page.waitForLoadState("networkidle", { timeout: Math.min(6000, remaining()) }).catch(() => {
|
|
1163
|
+
return;
|
|
1164
|
+
});
|
|
1165
|
+
await page.waitForTimeout(Math.min(opts.settleMs ?? 2000, remaining()));
|
|
1166
|
+
if (opts.scrollToLoad !== false && remaining() > 3000) {
|
|
1167
|
+
await Promise.race([
|
|
1168
|
+
scrollFullPage(page).catch(() => {
|
|
1169
|
+
return;
|
|
1170
|
+
}),
|
|
1171
|
+
new Promise((resolve) => setTimeout(resolve, Math.min(1e4, remaining())))
|
|
1172
|
+
]);
|
|
1173
|
+
await page.waitForTimeout(Math.min(600, remaining()));
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
} catch (err) {
|
|
1177
|
+
state.console.push({
|
|
1178
|
+
type: "error",
|
|
1179
|
+
text: `[navigation-error] ${err.message}`
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
vitals = await Promise.race([
|
|
1183
|
+
page.evaluate(() => window.__parity_vitals).catch(() => null),
|
|
1184
|
+
new Promise((resolve) => setTimeout(() => resolve(null), Math.min(5000, remaining())))
|
|
1185
|
+
]) ?? null;
|
|
1186
|
+
if (!opts.skipScreenshot) {
|
|
1187
|
+
await Promise.race([
|
|
1188
|
+
page.screenshot({ path: opts.screenshotPath, fullPage: true, animations: "disabled" }).catch(() => {
|
|
1189
|
+
return;
|
|
1190
|
+
}),
|
|
1191
|
+
new Promise((resolve) => setTimeout(resolve, Math.min(15000, remaining())))
|
|
1192
|
+
]);
|
|
1193
|
+
}
|
|
1194
|
+
html = await Promise.race([
|
|
1195
|
+
page.content().catch(() => ""),
|
|
1196
|
+
new Promise((resolve) => setTimeout(() => resolve(""), Math.min(5000, remaining())))
|
|
1197
|
+
]);
|
|
1198
|
+
await flushCollectors(state, Math.min(3000, remaining()));
|
|
1199
|
+
return buildPartial();
|
|
1192
1200
|
};
|
|
1201
|
+
const SAFETY_MARGIN_MS = 1e4;
|
|
1202
|
+
const outerDeadlineMs = overallBudgetMs + SAFETY_MARGIN_MS;
|
|
1203
|
+
return Promise.race([
|
|
1204
|
+
inner(),
|
|
1205
|
+
new Promise((resolve) => setTimeout(() => {
|
|
1206
|
+
state.console.push({
|
|
1207
|
+
type: "error",
|
|
1208
|
+
text: `[capture-timeout] capturePage exceeded ${outerDeadlineMs}ms outer deadline — returning partial capture`
|
|
1209
|
+
});
|
|
1210
|
+
resolve(buildPartial());
|
|
1211
|
+
}, outerDeadlineMs))
|
|
1212
|
+
]);
|
|
1193
1213
|
}
|
|
1194
1214
|
|
|
1195
1215
|
// src/report/render.ts
|
|
@@ -4084,8 +4104,257 @@ Regressões (severity piorou):`));
|
|
|
4084
4104
|
}
|
|
4085
4105
|
}
|
|
4086
4106
|
|
|
4087
|
-
// src/commands/
|
|
4107
|
+
// src/commands/css-trace.ts
|
|
4088
4108
|
import chalk4 from "chalk";
|
|
4109
|
+
async function tracePage(page, url, selector, settleMs) {
|
|
4110
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
4111
|
+
await page.waitForLoadState("load", { timeout: 12000 }).catch(() => {
|
|
4112
|
+
return;
|
|
4113
|
+
});
|
|
4114
|
+
await page.waitForLoadState("networkidle", { timeout: 6000 }).catch(() => {
|
|
4115
|
+
return;
|
|
4116
|
+
});
|
|
4117
|
+
await page.waitForTimeout(settleMs);
|
|
4118
|
+
const cdp = await page.context().newCDPSession(page);
|
|
4119
|
+
await cdp.send("DOM.enable");
|
|
4120
|
+
await cdp.send("CSS.enable");
|
|
4121
|
+
const { root } = await cdp.send("DOM.getDocument", { depth: -1 });
|
|
4122
|
+
const found = await cdp.send("DOM.querySelector", {
|
|
4123
|
+
nodeId: root.nodeId,
|
|
4124
|
+
selector
|
|
4125
|
+
});
|
|
4126
|
+
if (!found.nodeId) {
|
|
4127
|
+
await cdp.detach();
|
|
4128
|
+
return { url, selector, found: false, computed: {}, rules: [] };
|
|
4129
|
+
}
|
|
4130
|
+
const computedResult = await cdp.send("CSS.getComputedStyleForNode", {
|
|
4131
|
+
nodeId: found.nodeId
|
|
4132
|
+
});
|
|
4133
|
+
const computed = {};
|
|
4134
|
+
for (const e of computedResult.computedStyle)
|
|
4135
|
+
computed[e.name] = e.value;
|
|
4136
|
+
const matched = await cdp.send("CSS.getMatchedStylesForNode", {
|
|
4137
|
+
nodeId: found.nodeId
|
|
4138
|
+
});
|
|
4139
|
+
const sheetUrlCache = new Map;
|
|
4140
|
+
async function sheetSource(styleSheetId, origin) {
|
|
4141
|
+
if (!styleSheetId) {
|
|
4142
|
+
if (origin === "user-agent")
|
|
4143
|
+
return "user-agent";
|
|
4144
|
+
return "inline";
|
|
4145
|
+
}
|
|
4146
|
+
if (sheetUrlCache.has(styleSheetId))
|
|
4147
|
+
return sheetUrlCache.get(styleSheetId);
|
|
4148
|
+
try {
|
|
4149
|
+
const header = await cdp.send("CSS.getStyleSheetText", {
|
|
4150
|
+
styleSheetId
|
|
4151
|
+
});
|
|
4152
|
+
const preview = header.text.replace(/\s+/g, " ").trim().slice(0, 100);
|
|
4153
|
+
const label = `stylesheet#${styleSheetId} (${preview})`;
|
|
4154
|
+
sheetUrlCache.set(styleSheetId, label);
|
|
4155
|
+
return label;
|
|
4156
|
+
} catch {
|
|
4157
|
+
const label = `stylesheet#${styleSheetId}`;
|
|
4158
|
+
sheetUrlCache.set(styleSheetId, label);
|
|
4159
|
+
return label;
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
const buildRule = async (m, inheritedFromDistance) => {
|
|
4163
|
+
const r = m.rule;
|
|
4164
|
+
const selectors = r.selectorList.selectors.map((s) => s.text);
|
|
4165
|
+
const matchedSelectors = m.matchingSelectors.map((i) => selectors[i]).filter(Boolean);
|
|
4166
|
+
const sel = matchedSelectors.join(", ") || selectors.join(", ") || "(no selector text)";
|
|
4167
|
+
const source = await sheetSource(r.styleSheetId, r.origin);
|
|
4168
|
+
return {
|
|
4169
|
+
source,
|
|
4170
|
+
selector: sel,
|
|
4171
|
+
properties: (r.style.cssProperties ?? []).filter((p) => p.name && p.value && !p.disabled).map((p) => {
|
|
4172
|
+
const trimmed = p.value.replace(/\s*!important\s*$/i, "").trim();
|
|
4173
|
+
return {
|
|
4174
|
+
name: p.name,
|
|
4175
|
+
value: trimmed || p.value,
|
|
4176
|
+
important: p.important === true || /\s*!important\s*$/i.test(p.value),
|
|
4177
|
+
disabled: p.disabled
|
|
4178
|
+
};
|
|
4179
|
+
}),
|
|
4180
|
+
...inheritedFromDistance !== undefined ? { inheritedFromDistance } : {}
|
|
4181
|
+
};
|
|
4182
|
+
};
|
|
4183
|
+
const rules = [];
|
|
4184
|
+
for (const m of matched.matchedCSSRules ?? []) {
|
|
4185
|
+
rules.push(await buildRule(m));
|
|
4186
|
+
}
|
|
4187
|
+
const inheritedGroups = matched.inherited ?? [];
|
|
4188
|
+
for (let i = 0;i < inheritedGroups.length; i++) {
|
|
4189
|
+
const group = inheritedGroups[i];
|
|
4190
|
+
if (!group?.matchedCSSRules?.length)
|
|
4191
|
+
continue;
|
|
4192
|
+
const distance = i + 1;
|
|
4193
|
+
for (const m of group.matchedCSSRules) {
|
|
4194
|
+
rules.push(await buildRule(m, distance));
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
await cdp.detach();
|
|
4198
|
+
return { url, selector, found: true, computed, rules };
|
|
4199
|
+
}
|
|
4200
|
+
function applyFilter(result, filter) {
|
|
4201
|
+
if (!filter)
|
|
4202
|
+
return result;
|
|
4203
|
+
const wanted = new Set(filter.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean));
|
|
4204
|
+
const computed = {};
|
|
4205
|
+
for (const [k, v] of Object.entries(result.computed)) {
|
|
4206
|
+
if (wanted.has(k.toLowerCase()))
|
|
4207
|
+
computed[k] = v;
|
|
4208
|
+
}
|
|
4209
|
+
const rules = result.rules.map((r) => ({
|
|
4210
|
+
...r,
|
|
4211
|
+
properties: r.properties.filter((p) => wanted.has(p.name.toLowerCase()))
|
|
4212
|
+
})).filter((r) => r.properties.length > 0);
|
|
4213
|
+
return { ...result, computed, rules };
|
|
4214
|
+
}
|
|
4215
|
+
function printResult(result, header) {
|
|
4216
|
+
console.log(chalk4.bold(`
|
|
4217
|
+
${header}`));
|
|
4218
|
+
console.log(chalk4.dim(`URL: ${result.url}`));
|
|
4219
|
+
console.log(chalk4.dim(`Selector: ${result.selector}`));
|
|
4220
|
+
if (!result.found) {
|
|
4221
|
+
console.log(chalk4.red("✖ Element not found"));
|
|
4222
|
+
return;
|
|
4223
|
+
}
|
|
4224
|
+
const computedKeys = Object.keys(result.computed);
|
|
4225
|
+
if (computedKeys.length > 0) {
|
|
4226
|
+
console.log(chalk4.bold(`
|
|
4227
|
+
Computed:`));
|
|
4228
|
+
for (const k of computedKeys) {
|
|
4229
|
+
console.log(` ${chalk4.cyan(k)}: ${result.computed[k]}`);
|
|
4230
|
+
}
|
|
4231
|
+
}
|
|
4232
|
+
if (result.rules.length === 0) {
|
|
4233
|
+
console.log(chalk4.yellow(`
|
|
4234
|
+
No matching rules after filter.`));
|
|
4235
|
+
return;
|
|
4236
|
+
}
|
|
4237
|
+
console.log(chalk4.bold(`
|
|
4238
|
+
Rules (ordered by CDP — most specific last):`));
|
|
4239
|
+
for (const r of result.rules) {
|
|
4240
|
+
const inheritLabel = r.inheritedFromDistance !== undefined ? chalk4.yellow(` ↑ inherited from ancestor (${r.inheritedFromDistance})`) : "";
|
|
4241
|
+
console.log(` ${chalk4.magenta(r.source)}${inheritLabel}`);
|
|
4242
|
+
console.log(` ${chalk4.green(r.selector)} {`);
|
|
4243
|
+
for (const p of r.properties) {
|
|
4244
|
+
const bang = p.important ? chalk4.red(" !important") : "";
|
|
4245
|
+
console.log(` ${p.name}: ${p.value}${bang};`);
|
|
4246
|
+
}
|
|
4247
|
+
console.log(" }");
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
function diffComputed(prod, cand) {
|
|
4251
|
+
const keys = new Set([...Object.keys(prod.computed), ...Object.keys(cand.computed)]);
|
|
4252
|
+
const out = [];
|
|
4253
|
+
for (const k of keys) {
|
|
4254
|
+
const p = prod.computed[k];
|
|
4255
|
+
const c = cand.computed[k];
|
|
4256
|
+
if (p !== c)
|
|
4257
|
+
out.push({ property: k, prod: p, cand: c });
|
|
4258
|
+
}
|
|
4259
|
+
return out.sort((a, b) => a.property.localeCompare(b.property));
|
|
4260
|
+
}
|
|
4261
|
+
function printComparison(prod, cand) {
|
|
4262
|
+
printResult(prod, "── PROD ──────────────────────────────────────────────");
|
|
4263
|
+
printResult(cand, "── CAND ──────────────────────────────────────────────");
|
|
4264
|
+
const diffs = diffComputed(prod, cand);
|
|
4265
|
+
console.log(chalk4.bold(`
|
|
4266
|
+
── DIFF (computed) ───────────────────────────────────`));
|
|
4267
|
+
if (diffs.length === 0) {
|
|
4268
|
+
console.log(chalk4.green(" No computed-style differences in the filtered set."));
|
|
4269
|
+
return;
|
|
4270
|
+
}
|
|
4271
|
+
for (const d of diffs) {
|
|
4272
|
+
console.log(` ${chalk4.cyan(d.property)}`);
|
|
4273
|
+
console.log(` prod: ${chalk4.green(d.prod ?? "(unset)")}`);
|
|
4274
|
+
console.log(` cand: ${chalk4.red(d.cand ?? "(unset)")}`);
|
|
4275
|
+
}
|
|
4276
|
+
}
|
|
4277
|
+
async function cssTraceCommand(opts) {
|
|
4278
|
+
const viewport = opts.viewport ?? "desktop";
|
|
4279
|
+
const settleMs = opts.settleMs ?? 1500;
|
|
4280
|
+
if (!opts.selector) {
|
|
4281
|
+
console.error(chalk4.red("--selector is required"));
|
|
4282
|
+
return 1;
|
|
4283
|
+
}
|
|
4284
|
+
const hasUrl = !!opts.url;
|
|
4285
|
+
const hasProd = !!opts.prod;
|
|
4286
|
+
const hasCand = !!opts.cand;
|
|
4287
|
+
if (hasUrl && (hasProd || hasCand)) {
|
|
4288
|
+
console.error(chalk4.red("--url is mutually exclusive with --prod / --cand. Pass one mode only."));
|
|
4289
|
+
return 1;
|
|
4290
|
+
}
|
|
4291
|
+
if (hasProd && !hasCand || !hasProd && hasCand) {
|
|
4292
|
+
console.error(chalk4.red("Comparison mode needs both --prod and --cand (got only one)."));
|
|
4293
|
+
return 1;
|
|
4294
|
+
}
|
|
4295
|
+
if (!hasUrl && !hasProd && !hasCand) {
|
|
4296
|
+
console.error(chalk4.red("Provide either --url or both --prod and --cand."));
|
|
4297
|
+
return 1;
|
|
4298
|
+
}
|
|
4299
|
+
const isCompare = hasProd && hasCand;
|
|
4300
|
+
let browser = null;
|
|
4301
|
+
try {
|
|
4302
|
+
browser = await launchBrowser({ headless: true });
|
|
4303
|
+
if (isCompare) {
|
|
4304
|
+
const prodCtx = await newContext(browser, { viewport });
|
|
4305
|
+
const candCtx = await newContext(browser, { viewport });
|
|
4306
|
+
const prodPage = await prodCtx.newPage();
|
|
4307
|
+
const candPage = await candCtx.newPage();
|
|
4308
|
+
try {
|
|
4309
|
+
const [prodRes, candRes] = await Promise.all([
|
|
4310
|
+
tracePage(prodPage, opts.prod, opts.selector, settleMs),
|
|
4311
|
+
tracePage(candPage, opts.cand, opts.selector, settleMs)
|
|
4312
|
+
]);
|
|
4313
|
+
const prodF = applyFilter(prodRes, opts.filter);
|
|
4314
|
+
const candF = applyFilter(candRes, opts.filter);
|
|
4315
|
+
if (opts.json) {
|
|
4316
|
+
console.log(JSON.stringify({ prod: prodF, cand: candF, diff: diffComputed(prodF, candF) }, null, 2));
|
|
4317
|
+
} else {
|
|
4318
|
+
printComparison(prodF, candF);
|
|
4319
|
+
}
|
|
4320
|
+
} finally {
|
|
4321
|
+
await prodCtx.close().catch(() => {
|
|
4322
|
+
return;
|
|
4323
|
+
});
|
|
4324
|
+
await candCtx.close().catch(() => {
|
|
4325
|
+
return;
|
|
4326
|
+
});
|
|
4327
|
+
}
|
|
4328
|
+
} else {
|
|
4329
|
+
const ctx = await newContext(browser, { viewport });
|
|
4330
|
+
const page = await ctx.newPage();
|
|
4331
|
+
try {
|
|
4332
|
+
const res = await tracePage(page, opts.url, opts.selector, settleMs);
|
|
4333
|
+
const filtered = applyFilter(res, opts.filter);
|
|
4334
|
+
if (opts.json) {
|
|
4335
|
+
console.log(JSON.stringify(filtered, null, 2));
|
|
4336
|
+
} else {
|
|
4337
|
+
printResult(filtered, "── RESULT ────────────────────────────────────────────");
|
|
4338
|
+
}
|
|
4339
|
+
} finally {
|
|
4340
|
+
await ctx.close().catch(() => {
|
|
4341
|
+
return;
|
|
4342
|
+
});
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4345
|
+
return 0;
|
|
4346
|
+
} catch (err) {
|
|
4347
|
+
console.error(chalk4.red(`Error: ${err.message}`));
|
|
4348
|
+
return 1;
|
|
4349
|
+
} finally {
|
|
4350
|
+
await browser?.close().catch(() => {
|
|
4351
|
+
return;
|
|
4352
|
+
});
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
4355
|
+
|
|
4356
|
+
// src/commands/explain.ts
|
|
4357
|
+
import chalk5 from "chalk";
|
|
4089
4358
|
|
|
4090
4359
|
// src/llm/client.ts
|
|
4091
4360
|
import Anthropic from "@anthropic-ai/sdk";
|
|
@@ -4394,17 +4663,17 @@ Cada issue tem:
|
|
|
4394
4663
|
// src/commands/explain.ts
|
|
4395
4664
|
async function explainCommand(runId, issueId, output) {
|
|
4396
4665
|
if (!isLlmAvailable()) {
|
|
4397
|
-
console.error(
|
|
4666
|
+
console.error(chalk5.red("Nenhuma API key de LLM encontrada — set ANTHROPIC_API_KEY ou OPENROUTER_API_KEY."));
|
|
4398
4667
|
return 1;
|
|
4399
4668
|
}
|
|
4400
4669
|
const run = loadRun(output, runId);
|
|
4401
4670
|
const issue = run.issues.find((i) => i.id === issueId) || run.topIssues.find((i) => i.id === issueId);
|
|
4402
4671
|
if (!issue) {
|
|
4403
|
-
console.error(
|
|
4672
|
+
console.error(chalk5.red(`Issue não encontrada: ${issueId}`));
|
|
4404
4673
|
return 1;
|
|
4405
4674
|
}
|
|
4406
4675
|
const checks = run.checks.filter((c) => c.issues.some((i) => i.id === issueId || i.check === issue.check));
|
|
4407
|
-
console.log(
|
|
4676
|
+
console.log(chalk5.dim(`Consultando LLM…
|
|
4408
4677
|
`));
|
|
4409
4678
|
const text = await callMessage({
|
|
4410
4679
|
systemPrompt: ISSUE_AGGREGATOR_SYSTEM_PROMPT,
|
|
@@ -4425,7 +4694,7 @@ ${JSON.stringify(checks.map((c) => ({ name: c.name, status: c.status, summary: c
|
|
|
4425
4694
|
// src/commands/journey.ts
|
|
4426
4695
|
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
4427
4696
|
import { join as join7 } from "node:path";
|
|
4428
|
-
import
|
|
4697
|
+
import chalk6 from "chalk";
|
|
4429
4698
|
import ora2 from "ora";
|
|
4430
4699
|
|
|
4431
4700
|
// src/llm/pick-plp.ts
|
|
@@ -5655,7 +5924,7 @@ var CRITICAL_STEPS = new Set([
|
|
|
5655
5924
|
async function journeyCommand(opts) {
|
|
5656
5925
|
const viewports = opts.viewports.split(",").map((s) => s.trim()).filter((s) => s === "mobile" || s === "desktop");
|
|
5657
5926
|
if (viewports.length === 0) {
|
|
5658
|
-
console.error(
|
|
5927
|
+
console.error(chalk6.red("Nenhum viewport válido (use mobile,desktop)"));
|
|
5659
5928
|
return 2;
|
|
5660
5929
|
}
|
|
5661
5930
|
const rc = loadParityRc();
|
|
@@ -5665,13 +5934,13 @@ async function journeyCommand(opts) {
|
|
|
5665
5934
|
const runId = newRunId();
|
|
5666
5935
|
const paths = createRunDir(opts.output, runId);
|
|
5667
5936
|
if (!opts.json) {
|
|
5668
|
-
console.log(
|
|
5937
|
+
console.log(chalk6.bold(`
|
|
5669
5938
|
parity journey ${runId}`));
|
|
5670
|
-
console.log(
|
|
5671
|
-
console.log(
|
|
5672
|
-
console.log(
|
|
5939
|
+
console.log(chalk6.dim(` prod: ${opts.prod}`));
|
|
5940
|
+
console.log(chalk6.dim(` cand: ${opts.cand}`));
|
|
5941
|
+
console.log(chalk6.dim(` viewports: ${viewports.join(", ")} · CEP: ${rc.cep}`));
|
|
5673
5942
|
if (isLlmAvailable())
|
|
5674
|
-
console.log(
|
|
5943
|
+
console.log(chalk6.dim(` llm: ${providerLabel()}`));
|
|
5675
5944
|
console.log("");
|
|
5676
5945
|
}
|
|
5677
5946
|
let platform = "custom";
|
|
@@ -5715,15 +5984,15 @@ async function journeyCommand(opts) {
|
|
|
5715
5984
|
const onStepFor = (viewport, side) => (event) => {
|
|
5716
5985
|
if (opts.json)
|
|
5717
5986
|
return;
|
|
5718
|
-
const sideTag = side === "prod" ?
|
|
5719
|
-
const prefix = ` ${
|
|
5987
|
+
const sideTag = side === "prod" ? chalk6.cyan("prod") : chalk6.magenta("cand");
|
|
5988
|
+
const prefix = ` ${chalk6.dim(`[${viewport}/`)}${sideTag}${chalk6.dim("]")}`;
|
|
5720
5989
|
if (event.phase === "start") {
|
|
5721
|
-
console.log(`${prefix} ${
|
|
5990
|
+
console.log(`${prefix} ${chalk6.dim(`${event.index}/${event.total}`)} ▶ ${chalk6.bold(stepLabel(event.name))}`);
|
|
5722
5991
|
} else {
|
|
5723
|
-
const glyph = event.status === "ok" ?
|
|
5724
|
-
const noteText = event.note ?
|
|
5725
|
-
const time =
|
|
5726
|
-
console.log(`${prefix} ${
|
|
5992
|
+
const glyph = event.status === "ok" ? chalk6.green("✓") : event.status === "skipped" ? chalk6.yellow("○") : chalk6.red("✗");
|
|
5993
|
+
const noteText = event.note ? chalk6.dim(` (${event.note})`) : "";
|
|
5994
|
+
const time = chalk6.dim(`${(event.durationMs / 1000).toFixed(1)}s`);
|
|
5995
|
+
console.log(`${prefix} ${chalk6.dim(`${event.index}/${event.total}`)} ${glyph} ${stepLabel(event.name)} ${time}${noteText}`);
|
|
5727
5996
|
}
|
|
5728
5997
|
};
|
|
5729
5998
|
async function runOneSide(browserInstance, viewport, side) {
|
|
@@ -5761,7 +6030,7 @@ async function journeyCommand(opts) {
|
|
|
5761
6030
|
spinner?.succeed("Browser pronto");
|
|
5762
6031
|
for (const viewport of viewports) {
|
|
5763
6032
|
if (!opts.json)
|
|
5764
|
-
console.log(
|
|
6033
|
+
console.log(chalk6.bold(`
|
|
5765
6034
|
── ${viewport} ─────────────────────────────────────────────`));
|
|
5766
6035
|
const [prodCap, candCap] = await Promise.all([
|
|
5767
6036
|
runOneSide(browser, viewport, "prod"),
|
|
@@ -5788,7 +6057,7 @@ async function journeyCommand(opts) {
|
|
|
5788
6057
|
if (opts.junit) {
|
|
5789
6058
|
writeFileSync6(opts.junit, buildJUnit(rows, failures), "utf8");
|
|
5790
6059
|
if (!opts.json)
|
|
5791
|
-
console.log(
|
|
6060
|
+
console.log(chalk6.dim(` → JUnit XML: ${opts.junit}`));
|
|
5792
6061
|
}
|
|
5793
6062
|
return failures.some((f) => f.critical) ? 1 : 0;
|
|
5794
6063
|
} catch (err) {
|
|
@@ -5853,14 +6122,14 @@ function collectFailures(rows) {
|
|
|
5853
6122
|
function printTable(viewports, rows) {
|
|
5854
6123
|
console.log("");
|
|
5855
6124
|
for (const viewport of viewports) {
|
|
5856
|
-
console.log(
|
|
6125
|
+
console.log(chalk6.bold(` [${viewport}]`));
|
|
5857
6126
|
const vrows = rows.filter((r) => r.viewport === viewport);
|
|
5858
6127
|
for (const r of vrows) {
|
|
5859
6128
|
const label = STEP_LABELS[r.name] ?? r.name;
|
|
5860
6129
|
const p = statusGlyph(r.prod?.status);
|
|
5861
6130
|
const c = statusGlyph(r.cand?.status);
|
|
5862
|
-
const status = r.prod?.status === "ok" && r.cand?.status !== "ok" ?
|
|
5863
|
-
const note = r.cand?.status === "failed" || r.cand?.status === "skipped" ?
|
|
6131
|
+
const status = r.prod?.status === "ok" && r.cand?.status !== "ok" ? chalk6.red("FAILED") : "";
|
|
6132
|
+
const note = r.cand?.status === "failed" || r.cand?.status === "skipped" ? chalk6.dim(`(${r.cand?.note ?? r.cand?.status})`) : "";
|
|
5864
6133
|
console.log(` ${label.padEnd(24)} prod ${p} cand ${c} ${status} ${note}`);
|
|
5865
6134
|
}
|
|
5866
6135
|
console.log("");
|
|
@@ -5868,12 +6137,12 @@ function printTable(viewports, rows) {
|
|
|
5868
6137
|
}
|
|
5869
6138
|
function statusGlyph(status) {
|
|
5870
6139
|
if (!status)
|
|
5871
|
-
return
|
|
6140
|
+
return chalk6.dim("—");
|
|
5872
6141
|
if (status === "ok")
|
|
5873
|
-
return
|
|
6142
|
+
return chalk6.green("✓");
|
|
5874
6143
|
if (status === "skipped")
|
|
5875
|
-
return
|
|
5876
|
-
return
|
|
6144
|
+
return chalk6.yellow("○");
|
|
6145
|
+
return chalk6.red("✗");
|
|
5877
6146
|
}
|
|
5878
6147
|
function printSummary2(rows, failures, htmlPath) {
|
|
5879
6148
|
const byViewport = new Map;
|
|
@@ -5884,25 +6153,25 @@ function printSummary2(rows, failures, htmlPath) {
|
|
|
5884
6153
|
v.passed++;
|
|
5885
6154
|
byViewport.set(r.viewport, v);
|
|
5886
6155
|
}
|
|
5887
|
-
console.log(
|
|
6156
|
+
console.log(chalk6.bold(" Summary:"));
|
|
5888
6157
|
for (const [vp, { passed, total }] of byViewport) {
|
|
5889
|
-
const stat = passed === total ?
|
|
6158
|
+
const stat = passed === total ? chalk6.green("✓") : chalk6.yellow(`${passed}/${total}`);
|
|
5890
6159
|
console.log(` ${vp.padEnd(8)} ${stat} steps em cand`);
|
|
5891
6160
|
}
|
|
5892
6161
|
if (failures.length > 0) {
|
|
5893
6162
|
const crit = failures.filter((f) => f.critical).length;
|
|
5894
6163
|
console.log("");
|
|
5895
|
-
console.log(
|
|
6164
|
+
console.log(chalk6.red(` ✗ ${failures.length} step(s) divergent(es) (${crit} crítico)`));
|
|
5896
6165
|
for (const f of failures) {
|
|
5897
|
-
const tag = f.critical ?
|
|
6166
|
+
const tag = f.critical ? chalk6.red("[critical]") : chalk6.yellow("[warn]");
|
|
5898
6167
|
console.log(` ${tag} [${f.viewport}] ${f.step}: ${f.reason}`);
|
|
5899
6168
|
}
|
|
5900
6169
|
} else {
|
|
5901
6170
|
console.log("");
|
|
5902
|
-
console.log(
|
|
6171
|
+
console.log(chalk6.green(" ✓ jornada completa em cand"));
|
|
5903
6172
|
}
|
|
5904
6173
|
console.log("");
|
|
5905
|
-
console.log(
|
|
6174
|
+
console.log(chalk6.dim(` → ${htmlPath}`));
|
|
5906
6175
|
console.log("");
|
|
5907
6176
|
}
|
|
5908
6177
|
function emitGithubAnnotations(failures) {
|
|
@@ -6130,23 +6399,23 @@ function escapeHtml(s) {
|
|
|
6130
6399
|
}
|
|
6131
6400
|
|
|
6132
6401
|
// src/commands/learned.ts
|
|
6133
|
-
import
|
|
6402
|
+
import chalk7 from "chalk";
|
|
6134
6403
|
function learnedStats() {
|
|
6135
6404
|
const lib = loadLearned();
|
|
6136
6405
|
const stats = statsFromLib(lib);
|
|
6137
6406
|
if (stats.platforms.length === 0) {
|
|
6138
|
-
console.log(
|
|
6139
|
-
console.log(
|
|
6407
|
+
console.log(chalk7.dim(`Biblioteca vazia: ${LEARNED_PATH}`));
|
|
6408
|
+
console.log(chalk7.dim("Rode `parity run ...` em algum site para começar a popular."));
|
|
6140
6409
|
return 0;
|
|
6141
6410
|
}
|
|
6142
|
-
console.log(
|
|
6411
|
+
console.log(chalk7.bold(`
|
|
6143
6412
|
learned-selectors stats (${LEARNED_PATH})
|
|
6144
6413
|
`));
|
|
6145
6414
|
for (const p of stats.platforms) {
|
|
6146
|
-
console.log(`${
|
|
6415
|
+
console.log(`${chalk7.cyan(p.platform)}: ${p.activeSelectors} active · ${chalk7.dim(`${p.deprecatedSelectors} deprecated`)}`);
|
|
6147
6416
|
for (const top of p.topByKey) {
|
|
6148
6417
|
const sr = `${(top.successRate * 100).toFixed(0)}%`;
|
|
6149
|
-
console.log(` ${
|
|
6418
|
+
console.log(` ${chalk7.dim(top.key.padEnd(18))} ${chalk7.green(sr.padStart(4))} ${chalk7.dim(`(${top.hosts} hosts)`)} ${top.selector}`);
|
|
6150
6419
|
}
|
|
6151
6420
|
console.log("");
|
|
6152
6421
|
}
|
|
@@ -6156,7 +6425,7 @@ learned-selectors stats (${LEARNED_PATH})
|
|
|
6156
6425
|
// src/commands/vitals.ts
|
|
6157
6426
|
import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
|
|
6158
6427
|
import { join as join8 } from "node:path";
|
|
6159
|
-
import
|
|
6428
|
+
import chalk8 from "chalk";
|
|
6160
6429
|
import ora3 from "ora";
|
|
6161
6430
|
|
|
6162
6431
|
// src/diff/vitals.ts
|
|
@@ -6306,10 +6575,10 @@ async function vitalsCommand(opts) {
|
|
|
6306
6575
|
const runId = newRunId();
|
|
6307
6576
|
const paths = createRunDir(opts.output, runId);
|
|
6308
6577
|
const t0 = Date.now();
|
|
6309
|
-
console.log(
|
|
6578
|
+
console.log(chalk8.bold(`
|
|
6310
6579
|
parity vitals ${runId}`));
|
|
6311
|
-
console.log(
|
|
6312
|
-
console.log(
|
|
6580
|
+
console.log(chalk8.dim(` prod: ${opts.prod}`));
|
|
6581
|
+
console.log(chalk8.dim(` cand: ${opts.cand}`));
|
|
6313
6582
|
const discoverSpinner = ora3("Descobrindo páginas…").start();
|
|
6314
6583
|
const pagePaths = await discoverPagePaths2(opts.prod, opts);
|
|
6315
6584
|
if (pagePaths.length === 0) {
|
|
@@ -6341,7 +6610,7 @@ async function vitalsCommand(opts) {
|
|
|
6341
6610
|
completed++;
|
|
6342
6611
|
const elapsed = ((Date.now() - t0) / 1000).toFixed(0);
|
|
6343
6612
|
const etaSec = completed > 0 ? Math.round((Date.now() - t0) / completed * (total - completed) / 1000) : 0;
|
|
6344
|
-
progress.text = `${completed}/${total} · ${elapsed}s · ETA ${etaSec}s · ${task.side === "prod" ?
|
|
6613
|
+
progress.text = `${completed}/${total} · ${elapsed}s · ETA ${etaSec}s · ${task.side === "prod" ? chalk8.cyan("prod") : chalk8.magenta("cand")} ${task.path} (${task.viewport})`;
|
|
6345
6614
|
}
|
|
6346
6615
|
});
|
|
6347
6616
|
progress.succeed(`${total} capture(s) em ${((Date.now() - t0) / 1000).toFixed(1)}s`);
|
|
@@ -6414,8 +6683,8 @@ async function vitalsCommand(opts) {
|
|
|
6414
6683
|
`, "utf8");
|
|
6415
6684
|
printSummary3(vitalsResult, cacheResult);
|
|
6416
6685
|
console.log("");
|
|
6417
|
-
console.log(
|
|
6418
|
-
console.log(
|
|
6686
|
+
console.log(chalk8.dim(` → ${paths.reportHtml}`));
|
|
6687
|
+
console.log(chalk8.dim(` \uD83D\uDCA1 use 'parity serve ${runId}' pra preview iframe`));
|
|
6419
6688
|
console.log("");
|
|
6420
6689
|
if (opts.open) {
|
|
6421
6690
|
const { default: open } = await import("open");
|
|
@@ -6543,27 +6812,27 @@ function computeVerdict2(checks, issues) {
|
|
|
6543
6812
|
}
|
|
6544
6813
|
function printSummary3(vitals, cache) {
|
|
6545
6814
|
console.log("");
|
|
6546
|
-
console.log(
|
|
6815
|
+
console.log(chalk8.bold(" Summary:"));
|
|
6547
6816
|
console.log(` ${vitals.summary}`);
|
|
6548
6817
|
console.log(` ${cache.summary}`);
|
|
6549
6818
|
}
|
|
6550
6819
|
|
|
6551
6820
|
// src/commands/list.ts
|
|
6552
|
-
import
|
|
6821
|
+
import chalk9 from "chalk";
|
|
6553
6822
|
function listCommand(output) {
|
|
6554
6823
|
const runs = listRuns(output);
|
|
6555
6824
|
if (runs.length === 0) {
|
|
6556
|
-
console.log(
|
|
6825
|
+
console.log(chalk9.dim(`Nenhum run salvo em ${output}`));
|
|
6557
6826
|
return 0;
|
|
6558
6827
|
}
|
|
6559
6828
|
for (const r of runs) {
|
|
6560
6829
|
try {
|
|
6561
6830
|
const run = loadRun(output, r.id);
|
|
6562
6831
|
const v = run.verdict;
|
|
6563
|
-
const status = v.status === "pass" ?
|
|
6564
|
-
console.log(` ${
|
|
6832
|
+
const status = v.status === "pass" ? chalk9.green(v.status) : v.status === "warn" ? chalk9.yellow(v.status) : chalk9.red(v.status);
|
|
6833
|
+
console.log(` ${chalk9.bold(r.id)} ${status} score=${v.score} critical=${v.critical} high=${v.high} ${chalk9.dim(r.timestamp)}`);
|
|
6565
6834
|
} catch {
|
|
6566
|
-
console.log(` ${
|
|
6835
|
+
console.log(` ${chalk9.bold(r.id)} ${chalk9.dim("(report.json ausente)")}`);
|
|
6567
6836
|
}
|
|
6568
6837
|
}
|
|
6569
6838
|
return 0;
|
|
@@ -6571,13 +6840,13 @@ function listCommand(output) {
|
|
|
6571
6840
|
|
|
6572
6841
|
// src/commands/prompt.ts
|
|
6573
6842
|
import { existsSync as existsSync8, writeFileSync as writeFileSync8 } from "node:fs";
|
|
6574
|
-
import
|
|
6843
|
+
import chalk10 from "chalk";
|
|
6575
6844
|
function promptCommand(runId, opts) {
|
|
6576
6845
|
let run;
|
|
6577
6846
|
try {
|
|
6578
6847
|
run = loadRun(opts.output, runId);
|
|
6579
6848
|
} catch (err) {
|
|
6580
|
-
console.error(
|
|
6849
|
+
console.error(chalk10.red(`✖ ${err.message}`));
|
|
6581
6850
|
return 1;
|
|
6582
6851
|
}
|
|
6583
6852
|
const md = buildLlmPrompt(run, {
|
|
@@ -6586,10 +6855,10 @@ function promptCommand(runId, opts) {
|
|
|
6586
6855
|
});
|
|
6587
6856
|
if (opts.out) {
|
|
6588
6857
|
writeFileSync8(opts.out, md, "utf8");
|
|
6589
|
-
console.log(
|
|
6858
|
+
console.log(chalk10.green(`✔ prompt salvo em ${opts.out}`));
|
|
6590
6859
|
if (existsSync8(opts.out)) {
|
|
6591
6860
|
const sizeKb = (md.length / 1024).toFixed(1);
|
|
6592
|
-
console.log(
|
|
6861
|
+
console.log(chalk10.dim(` ${md.length} chars (${sizeKb} KB)`));
|
|
6593
6862
|
}
|
|
6594
6863
|
} else {
|
|
6595
6864
|
process.stdout.write(md);
|
|
@@ -6598,20 +6867,20 @@ function promptCommand(runId, opts) {
|
|
|
6598
6867
|
}
|
|
6599
6868
|
|
|
6600
6869
|
// src/commands/report.ts
|
|
6601
|
-
import
|
|
6870
|
+
import chalk11 from "chalk";
|
|
6602
6871
|
import open from "open";
|
|
6603
6872
|
async function reportCommand(runId, output) {
|
|
6604
6873
|
const paths = getRunPaths(output, runId);
|
|
6605
|
-
console.log(
|
|
6874
|
+
console.log(chalk11.dim(`opening ${paths.reportHtml}`));
|
|
6606
6875
|
await open(paths.reportHtml).catch((err) => {
|
|
6607
|
-
console.error(
|
|
6876
|
+
console.error(chalk11.red(`failed to open: ${err.message}`));
|
|
6608
6877
|
});
|
|
6609
6878
|
return 0;
|
|
6610
6879
|
}
|
|
6611
6880
|
|
|
6612
6881
|
// src/commands/run.ts
|
|
6613
6882
|
import { join as join11 } from "node:path";
|
|
6614
|
-
import
|
|
6883
|
+
import chalk13 from "chalk";
|
|
6615
6884
|
import ora4 from "ora";
|
|
6616
6885
|
|
|
6617
6886
|
// src/checks/http-status.ts
|
|
@@ -7466,11 +7735,49 @@ function purchaseJourneyFlow(ctx) {
|
|
|
7466
7735
|
let totalSteps = 0;
|
|
7467
7736
|
let failedSteps = 0;
|
|
7468
7737
|
let asymmetricSkips = 0;
|
|
7738
|
+
const hasAnyPurchaseFlow = ctx.prodFlows.some((f) => f.flow === "purchase-journey") || ctx.candFlows.some((f) => f.flow === "purchase-journey");
|
|
7739
|
+
if (!hasAnyPurchaseFlow) {
|
|
7740
|
+
return {
|
|
7741
|
+
name: "purchase-journey-flow",
|
|
7742
|
+
status: "skipped",
|
|
7743
|
+
severity: "critical",
|
|
7744
|
+
durationMs: Date.now() - start,
|
|
7745
|
+
summary: "purchase-journey não estava no escopo do run (sem captura de flow)",
|
|
7746
|
+
issues: [],
|
|
7747
|
+
data: { totalSteps: 0, failedSteps: 0, asymmetricSkips: 0 }
|
|
7748
|
+
};
|
|
7749
|
+
}
|
|
7469
7750
|
for (const viewport of ctx.viewports) {
|
|
7470
7751
|
const prodFlow = findFlow(ctx.prodFlows, viewport);
|
|
7471
7752
|
const candFlow = findFlow(ctx.candFlows, viewport);
|
|
7472
|
-
if (!prodFlow || !candFlow)
|
|
7753
|
+
if (!prodFlow || !candFlow) {
|
|
7754
|
+
if (prodFlow && !candFlow) {
|
|
7755
|
+
issues.push({
|
|
7756
|
+
id: `pj:${viewport}:missing-cand-flow`,
|
|
7757
|
+
severity: "critical",
|
|
7758
|
+
category: "functional",
|
|
7759
|
+
check: "purchase-journey-flow",
|
|
7760
|
+
summary: `[${viewport}] cand não produziu captura da purchase-journey (prod produziu) — checkout indisponível ou cliente nunca hidratou`
|
|
7761
|
+
});
|
|
7762
|
+
} else if (!prodFlow && candFlow) {
|
|
7763
|
+
issues.push({
|
|
7764
|
+
id: `pj:${viewport}:missing-prod-flow`,
|
|
7765
|
+
severity: "high",
|
|
7766
|
+
category: "functional",
|
|
7767
|
+
check: "purchase-journey-flow",
|
|
7768
|
+
summary: `[${viewport}] prod não produziu captura da purchase-journey — selectors podem estar desalinhados com a source-of-truth`
|
|
7769
|
+
});
|
|
7770
|
+
} else {
|
|
7771
|
+
issues.push({
|
|
7772
|
+
id: `pj:${viewport}:no-flow-either-side`,
|
|
7773
|
+
severity: "critical",
|
|
7774
|
+
category: "functional",
|
|
7775
|
+
check: "purchase-journey-flow",
|
|
7776
|
+
summary: `[${viewport}] purchase-journey foi requisitada mas não há captura em prod nem cand — flow falhou completamente`
|
|
7777
|
+
});
|
|
7778
|
+
}
|
|
7473
7779
|
continue;
|
|
7780
|
+
}
|
|
7474
7781
|
const prodSteps = indexBy(prodFlow.steps ?? [], (s) => s.name);
|
|
7475
7782
|
const candSteps = indexBy(candFlow.steps ?? [], (s) => s.name);
|
|
7476
7783
|
const allNames = new Set([...prodSteps.keys(), ...candSteps.keys()]);
|
|
@@ -7518,6 +7825,15 @@ function purchaseJourneyFlow(ctx) {
|
|
|
7518
7825
|
} else if (p.status === "skipped" && c.status === "ok") {}
|
|
7519
7826
|
}
|
|
7520
7827
|
}
|
|
7828
|
+
if (totalSteps === 0 && issues.length === 0) {
|
|
7829
|
+
issues.push({
|
|
7830
|
+
id: "pj:zero-steps-evaluated",
|
|
7831
|
+
severity: "critical",
|
|
7832
|
+
category: "functional",
|
|
7833
|
+
check: "purchase-journey-flow",
|
|
7834
|
+
summary: "purchase-journey rodou mas avaliou 0 step(s) — capturas existem mas estão vazias dos dois lados (selectors quebrados ou home não hidratou)"
|
|
7835
|
+
});
|
|
7836
|
+
}
|
|
7521
7837
|
const status = issues.some((i) => i.severity === "critical") ? "fail" : issues.length > 0 ? "warn" : "pass";
|
|
7522
7838
|
return {
|
|
7523
7839
|
name: "purchase-journey-flow",
|
|
@@ -8858,7 +9174,7 @@ cand: ${JSON.stringify(cand)}`,
|
|
|
8858
9174
|
|
|
8859
9175
|
// src/commands/serve.ts
|
|
8860
9176
|
import { existsSync as existsSync11 } from "node:fs";
|
|
8861
|
-
import
|
|
9177
|
+
import chalk12 from "chalk";
|
|
8862
9178
|
|
|
8863
9179
|
// src/server/proxy-server.ts
|
|
8864
9180
|
import { existsSync as existsSync10, readFileSync as readFileSync10, statSync } from "node:fs";
|
|
@@ -9078,16 +9394,16 @@ async function serveRunAndBlock(runDir, opts = {}) {
|
|
|
9078
9394
|
try {
|
|
9079
9395
|
handle = await startProxyServer(runDir, { port: opts.port });
|
|
9080
9396
|
} catch (err) {
|
|
9081
|
-
console.error(
|
|
9397
|
+
console.error(chalk12.red(` ✖ Falha ao iniciar servidor: ${err.message}`));
|
|
9082
9398
|
return 2;
|
|
9083
9399
|
}
|
|
9084
9400
|
const label = opts.label ?? "parity serve";
|
|
9085
9401
|
console.log("");
|
|
9086
|
-
console.log(
|
|
9087
|
-
console.log(` ${
|
|
9088
|
-
console.log(
|
|
9089
|
-
console.log(
|
|
9090
|
-
console.log(
|
|
9402
|
+
console.log(chalk12.bold(` ${label}`));
|
|
9403
|
+
console.log(` ${chalk12.green("●")} ${handle.url}`);
|
|
9404
|
+
console.log(chalk12.dim(` Servindo: ${runDir}`));
|
|
9405
|
+
console.log(chalk12.dim(` Proxy de iframes: ${handle.url}proxy?url=<encoded>`));
|
|
9406
|
+
console.log(chalk12.dim(` Ctrl+C pra parar (2x pra forçar saída).
|
|
9091
9407
|
`));
|
|
9092
9408
|
if (opts.open !== false) {
|
|
9093
9409
|
const { default: open2 } = await import("open");
|
|
@@ -9120,11 +9436,11 @@ async function serveRunAndBlock(runDir, opts = {}) {
|
|
|
9120
9436
|
async function serveCommand(runId, opts) {
|
|
9121
9437
|
const paths = getRunPaths(opts.output, runId);
|
|
9122
9438
|
if (!existsSync11(paths.runDir)) {
|
|
9123
|
-
console.error(
|
|
9439
|
+
console.error(chalk12.red(`✖ Run não encontrado: ${runId} (em ${opts.output})`));
|
|
9124
9440
|
return 1;
|
|
9125
9441
|
}
|
|
9126
9442
|
if (!existsSync11(paths.reportHtml)) {
|
|
9127
|
-
console.error(
|
|
9443
|
+
console.error(chalk12.red(`✖ report.html não existe em ${paths.runDir}`));
|
|
9128
9444
|
return 1;
|
|
9129
9445
|
}
|
|
9130
9446
|
return serveRunAndBlock(paths.runDir, {
|
|
@@ -9184,7 +9500,7 @@ function applyPreset(opts) {
|
|
|
9184
9500
|
async function runCommand(rawOpts) {
|
|
9185
9501
|
const opts = applyPreset(rawOpts);
|
|
9186
9502
|
if (rawOpts.preset) {
|
|
9187
|
-
console.log(
|
|
9503
|
+
console.log(chalk13.dim(` preset: ${rawOpts.preset}`));
|
|
9188
9504
|
}
|
|
9189
9505
|
const flows = opts.flows.split(",").map((s) => s.trim());
|
|
9190
9506
|
const viewports = opts.viewports.split(",").map((s) => s.trim());
|
|
@@ -9194,11 +9510,11 @@ async function runCommand(rawOpts) {
|
|
|
9194
9510
|
const ignore = loadParityIgnore();
|
|
9195
9511
|
const preflight = await preflightCheck(opts.prod, opts.cand);
|
|
9196
9512
|
if (!preflight.ok) {
|
|
9197
|
-
console.error(
|
|
9513
|
+
console.error(chalk13.red(`
|
|
9198
9514
|
✖ pre-flight falhou:`));
|
|
9199
9515
|
for (const err of preflight.errors)
|
|
9200
|
-
console.error(
|
|
9201
|
-
console.error(
|
|
9516
|
+
console.error(chalk13.red(` - ${err}`));
|
|
9517
|
+
console.error(chalk13.dim(`
|
|
9202
9518
|
dica: verifique se as URLs estão corretas e acessíveis`));
|
|
9203
9519
|
return 2;
|
|
9204
9520
|
}
|
|
@@ -9209,7 +9525,7 @@ async function runCommand(rawOpts) {
|
|
|
9209
9525
|
if (prodHomeHtml) {
|
|
9210
9526
|
platform = detectPlatform({ url: opts.prod, html: prodHomeHtml });
|
|
9211
9527
|
if (platform !== "custom") {
|
|
9212
|
-
console.log(
|
|
9528
|
+
console.log(chalk13.dim(` Detected platform: ${platform}`));
|
|
9213
9529
|
}
|
|
9214
9530
|
}
|
|
9215
9531
|
const prodHost = hostOf(opts.prod);
|
|
@@ -9251,11 +9567,11 @@ async function runCommand(rawOpts) {
|
|
|
9251
9567
|
const paths = createRunDir(opts.output, runId);
|
|
9252
9568
|
const startedAt = Date.now();
|
|
9253
9569
|
const timestamp = new Date().toISOString();
|
|
9254
|
-
console.log(
|
|
9570
|
+
console.log(chalk13.bold(`
|
|
9255
9571
|
parity run ${runId}`));
|
|
9256
|
-
console.log(
|
|
9257
|
-
console.log(
|
|
9258
|
-
console.log(
|
|
9572
|
+
console.log(chalk13.dim(` prod: ${opts.prod}`));
|
|
9573
|
+
console.log(chalk13.dim(` cand: ${opts.cand}`));
|
|
9574
|
+
console.log(chalk13.dim(` flows: ${flows.join(", ")} · viewports: ${viewports.join(", ")} · CEP: ${rc.cep}
|
|
9259
9575
|
`));
|
|
9260
9576
|
const spinner = ora4("Launching browser…").start();
|
|
9261
9577
|
let browser = null;
|
|
@@ -9569,7 +9885,7 @@ async function runCommand(rawOpts) {
|
|
|
9569
9885
|
if (opts.ci) {
|
|
9570
9886
|
const blocking = allIssues.filter((i) => failOn.includes(i.severity));
|
|
9571
9887
|
if (blocking.length > 0) {
|
|
9572
|
-
console.log(
|
|
9888
|
+
console.log(chalk13.red(`
|
|
9573
9889
|
✖ ${blocking.length} issue(s) bloqueante(s) [${failOn.join(", ")}] — exit 1`));
|
|
9574
9890
|
return 1;
|
|
9575
9891
|
}
|
|
@@ -9708,26 +10024,26 @@ function hostOf(url) {
|
|
|
9708
10024
|
}
|
|
9709
10025
|
function printSummary4(run, htmlPath, meta) {
|
|
9710
10026
|
const { verdict } = run;
|
|
9711
|
-
const emoji = verdict.status === "pass" ?
|
|
9712
|
-
const score = verdict.status === "pass" ?
|
|
10027
|
+
const emoji = verdict.status === "pass" ? chalk13.green("✔") : verdict.status === "warn" ? chalk13.yellow("⚠") : chalk13.red("✖");
|
|
10028
|
+
const score = verdict.status === "pass" ? chalk13.green(verdict.score) : verdict.status === "warn" ? chalk13.yellow(verdict.score) : chalk13.red(verdict.score);
|
|
9713
10029
|
console.log("");
|
|
9714
10030
|
console.log(` ${emoji} parity ${verdict.status.toUpperCase()} · score ${score}/100`);
|
|
9715
|
-
console.log(` checks: ${
|
|
9716
|
-
console.log(` issues: ${
|
|
10031
|
+
console.log(` checks: ${chalk13.green(verdict.checksPassed)} pass · ${chalk13.red(verdict.checksFailed)} fail · ${chalk13.dim(`${verdict.checksSkipped} skipped`)}`);
|
|
10032
|
+
console.log(` issues: ${chalk13.red(verdict.critical)} critical · ${chalk13.yellow(verdict.high)} high · ${verdict.medium} medium · ${chalk13.dim(`${verdict.low} low`)}`);
|
|
9717
10033
|
if (run.topIssues.length > 0) {
|
|
9718
10034
|
console.log("");
|
|
9719
|
-
console.log(
|
|
10035
|
+
console.log(chalk13.bold(" Top issues:"));
|
|
9720
10036
|
for (const i of run.topIssues.slice(0, 5)) {
|
|
9721
|
-
const sev = i.severity === "critical" ?
|
|
10037
|
+
const sev = i.severity === "critical" ? chalk13.red(`[${i.severity}]`) : i.severity === "high" ? chalk13.yellow(`[${i.severity}]`) : chalk13.dim(`[${i.severity}]`);
|
|
9722
10038
|
console.log(` ${sev} ${i.summary}`);
|
|
9723
10039
|
}
|
|
9724
10040
|
}
|
|
9725
10041
|
if (meta.promotedCount > 0 || meta.deprecatedCount > 0) {
|
|
9726
10042
|
console.log("");
|
|
9727
|
-
console.log(
|
|
10043
|
+
console.log(chalk13.dim(` \uD83D\uDCDA learned-selectors [${meta.platform}]: ${chalk13.green(`+${meta.promotedCount}`)} promoted · ${chalk13.yellow(meta.deprecatedCount)} deprecated`));
|
|
9728
10044
|
}
|
|
9729
10045
|
console.log("");
|
|
9730
|
-
console.log(
|
|
10046
|
+
console.log(chalk13.dim(` → ${htmlPath}`));
|
|
9731
10047
|
console.log("");
|
|
9732
10048
|
}
|
|
9733
10049
|
|
|
@@ -9777,6 +10093,18 @@ program.command("prompt").argument("<runId>", "Run ID").description("Generate a
|
|
|
9777
10093
|
program.command("explain").argument("<runId>", "Run ID").argument("<issueId>", "Issue ID").option("--output <dir>", "Output directory", "./parity-output").description("LLM deep-dive on a specific issue (requires ANTHROPIC_API_KEY)").action(async (runId, issueId, opts) => {
|
|
9778
10094
|
process.exit(await explainCommand(runId, issueId, opts.output));
|
|
9779
10095
|
});
|
|
10096
|
+
program.command("css-trace").description("Inspect which CSS rules (from which stylesheets) are affecting a DOM element. Single URL mode lists every matched rule; --prod + --cand mode diffs computed styles between Fresh and TanStack sides.").option("--url <url>", "Single URL to inspect (mutually exclusive with --prod/--cand)").option("--prod <url>", "Production URL (for comparison mode)").option("--cand <url>", "Candidate URL (for comparison mode)").requiredOption("--selector <sel>", "CSS selector for the target element (e.g. 'html', '[data-aside]', '.drawer-side')").option("--filter <props>", "Comma-separated property names to focus on (e.g. 'scrollbar-gutter,position,width')").option("--viewport <viewport>", "mobile, tablet, or desktop", "desktop").option("--settle <ms>", "Wait this many ms after `load` for hydration", (v) => Number(v), 1500).option("--json", "Output JSON instead of pretty text", false).action(async (opts) => {
|
|
10097
|
+
process.exit(await cssTraceCommand({
|
|
10098
|
+
url: opts.url,
|
|
10099
|
+
prod: opts.prod,
|
|
10100
|
+
cand: opts.cand,
|
|
10101
|
+
selector: opts.selector,
|
|
10102
|
+
filter: opts.filter,
|
|
10103
|
+
viewport: opts.viewport,
|
|
10104
|
+
settleMs: opts.settle,
|
|
10105
|
+
json: opts.json
|
|
10106
|
+
}));
|
|
10107
|
+
});
|
|
9780
10108
|
program.command("learned").description("Inspect the learned-selectors library").addCommand(new Command("stats").description("Print learned-selectors stats per platform").action(() => {
|
|
9781
10109
|
process.exit(learnedStats());
|
|
9782
10110
|
}));
|
package/package.json
CHANGED
|
@@ -1,87 +1,87 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
2
|
+
"name": "@decocms/parity",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "E2E parity validator for site migrations. Compares prod vs cand and reports UI, functional, SEO, visual, and Web Vitals deltas with an LLM-ranked HTML report.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/decocms/parity.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/decocms/parity#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/decocms/parity/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"deco",
|
|
17
|
+
"tanstack",
|
|
18
|
+
"fresh",
|
|
19
|
+
"migration",
|
|
20
|
+
"e2e",
|
|
21
|
+
"playwright",
|
|
22
|
+
"web-vitals",
|
|
23
|
+
"visual-regression",
|
|
24
|
+
"visual-diff",
|
|
25
|
+
"llm",
|
|
26
|
+
"claude",
|
|
27
|
+
"anthropic",
|
|
28
|
+
"cli",
|
|
29
|
+
"parity",
|
|
30
|
+
"regression-testing"
|
|
31
|
+
],
|
|
32
|
+
"bin": {
|
|
33
|
+
"parity": "dist/cli.js"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md",
|
|
38
|
+
"AGENTS.md",
|
|
39
|
+
"CHANGELOG.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"dev": "bun run --hot src/cli.ts",
|
|
44
|
+
"build": "bun build src/cli.ts --target=node --outfile=dist/cli.js --packages=external && bun run scripts/postbuild.ts",
|
|
45
|
+
"check": "tsc --noEmit",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"test:watch": "vitest",
|
|
48
|
+
"test:coverage": "vitest run --coverage",
|
|
49
|
+
"test:e2e": "vitest run tests/e2e",
|
|
50
|
+
"deadcode": "knip",
|
|
51
|
+
"lint": "biome lint .",
|
|
52
|
+
"fmt": "biome format --write .",
|
|
53
|
+
"prepublishOnly": "bun run check && bun run build"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=20",
|
|
57
|
+
"bun": ">=1.1"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@anthropic-ai/sdk": "^0.40.0",
|
|
61
|
+
"chalk": "^5.3.0",
|
|
62
|
+
"cheerio": "^1.0.0",
|
|
63
|
+
"commander": "^12.1.0",
|
|
64
|
+
"open": "^10.1.0",
|
|
65
|
+
"ora": "^8.1.1",
|
|
66
|
+
"pixelmatch": "^6.0.0",
|
|
67
|
+
"playwright": "^1.49.0",
|
|
68
|
+
"pngjs": "^7.0.0",
|
|
69
|
+
"zod": "^3.24.1"
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@biomejs/biome": "^1.9.4",
|
|
73
|
+
"@types/node": "^22.10.0",
|
|
74
|
+
"@types/pixelmatch": "^5.2.6",
|
|
75
|
+
"@types/pngjs": "^6.0.5",
|
|
76
|
+
"@vitest/coverage-v8": "^2.1.8",
|
|
77
|
+
"knip": "^6.13.1",
|
|
78
|
+
"typescript": "^5.7.2",
|
|
79
|
+
"vitest": "^2.1.8"
|
|
80
|
+
},
|
|
81
|
+
"publishConfig": {
|
|
82
|
+
"access": "public"
|
|
83
|
+
},
|
|
84
|
+
"trustedDependencies": [
|
|
85
|
+
"@biomejs/biome"
|
|
86
|
+
]
|
|
87
87
|
}
|