@ivorycanvas/qamap 0.4.1 → 0.4.2
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 +17 -0
- package/dist/e2e.js +72 -15
- package/dist/e2e.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/architecture.md +1 -1
- package/docs/benchmarking.md +10 -1
- package/docs/commands.md +4 -0
- package/docs/release-validation.md +17 -0
- package/docs/releasing.md +2 -1
- package/docs/roadmap.md +8 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.4.2 - 2026-07-13
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added benchmark contracts for draft readiness, runnable candidates, self-check passes, review-only files, TODO markers, and execution blockers. Public fixtures can now fail CI when QAMap finds the right flow but produces an unusable automation draft.
|
|
10
|
+
- Added deterministic Playwright failure-path compilation when repository evidence exposes a mockable endpoint, a stable action control, and an observable failure message. Generated drafts replace the success route with a 4xx/5xx response, repeat the user action, and assert the failure UI instead of falling back to a body-visible smoke check.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Draft execution readiness now measures whether a generated file can be tried locally from runner, entrypoint, selector, fixture, and self-check evidence. Missing pre-existing validation coverage remains required PR guidance, but no longer falsely claims that the new file cannot execute.
|
|
15
|
+
- A partial fixture match can qualify as a runnable candidate when QAMap generated executable mock scaffolding and every other execution check passes. It still remains a review item before the draft becomes trusted regression evidence.
|
|
16
|
+
- Playwright self-checks now report body-only smoke assertions as domain-assertion warnings. Such files remain tryable, but cannot be called runnable candidates until they assert an observable product outcome.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Draft reports no longer tell users to replace TODO locators when the generated files contain no TODO markers.
|
|
21
|
+
|
|
5
22
|
## 0.4.1 - 2026-07-13
|
|
6
23
|
|
|
7
24
|
### Added
|
package/dist/e2e.js
CHANGED
|
@@ -223,8 +223,8 @@ export async function generateE2eDraft(rootInput, options = {}) {
|
|
|
223
223
|
const todoCount = countTodos(content);
|
|
224
224
|
const selfCheck = evaluateDraftSelfCheck(plan, flow, runner, content, todoCount);
|
|
225
225
|
const actionItems = buildDraftActionItems(plan, flow, runner, validationSummary, promotionGuidance, selfCheck);
|
|
226
|
-
const executionBlockers = draftExecutionBlockers(plan, flow, runner,
|
|
227
|
-
const runnableStatus = draftRunnableStatus(plan, flow, runner,
|
|
226
|
+
const executionBlockers = draftExecutionBlockers(plan, flow, runner, selfCheck);
|
|
227
|
+
const runnableStatus = draftRunnableStatus(plan, flow, runner, executionBlockers, selfCheck);
|
|
228
228
|
const fileDetails = draftFileDetails(flow);
|
|
229
229
|
if (dryRun) {
|
|
230
230
|
files.push({
|
|
@@ -331,7 +331,7 @@ export async function generateE2eDraft(rootInput, options = {}) {
|
|
|
331
331
|
files,
|
|
332
332
|
actionSummary,
|
|
333
333
|
readinessSummary: summarizeDraftReadiness(files, actionSummary),
|
|
334
|
-
nextSteps: buildDraftNextSteps(plan, runner),
|
|
334
|
+
nextSteps: buildDraftNextSteps(plan, runner, files),
|
|
335
335
|
};
|
|
336
336
|
}
|
|
337
337
|
function buildCoverageTargets(kind, files, runner) {
|
|
@@ -1108,7 +1108,8 @@ function draftNeedsAssertionWork(selfCheck) {
|
|
|
1108
1108
|
return true;
|
|
1109
1109
|
}
|
|
1110
1110
|
return selfCheck.checks.some((check) => (check.name === "Unresolved placeholders" && check.status !== "pass") ||
|
|
1111
|
-
(check.name === "TODO comments" && check.status !== "pass")
|
|
1111
|
+
(check.name === "TODO comments" && check.status !== "pass") ||
|
|
1112
|
+
(check.name === "Domain assertions" && check.status !== "pass"));
|
|
1112
1113
|
}
|
|
1113
1114
|
function runnerSetupGap(plan, runner) {
|
|
1114
1115
|
if (runner === "playwright") {
|
|
@@ -5434,7 +5435,7 @@ function draftStability(plan, flow) {
|
|
|
5434
5435
|
}
|
|
5435
5436
|
return "ready";
|
|
5436
5437
|
}
|
|
5437
|
-
function draftExecutionBlockers(plan, flow, runner,
|
|
5438
|
+
function draftExecutionBlockers(plan, flow, runner, selfCheck) {
|
|
5438
5439
|
const verificationOnly = isVerificationOnlyFlow(flow);
|
|
5439
5440
|
const blockers = verificationOnly ? [] : [...plan.executionProfile.blockers];
|
|
5440
5441
|
if (!verificationOnly && runner !== "manual" && flow.entrypoints.length === 0) {
|
|
@@ -5446,15 +5447,14 @@ function draftExecutionBlockers(plan, flow, runner, validationSummary, selfCheck
|
|
|
5446
5447
|
if (!verificationOnly && flow.fixtureReadiness.status === "missing") {
|
|
5447
5448
|
blockers.push(flow.fixtureReadiness.nextActions[0] ?? flow.fixtureReadiness.reason);
|
|
5448
5449
|
}
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
}
|
|
5450
|
+
// Missing validation evidence describes the repository before this draft exists.
|
|
5451
|
+
// Keep it as PR guidance, but do not treat it as proof that the generated file cannot execute.
|
|
5452
5452
|
if (selfCheck?.status === "fail") {
|
|
5453
5453
|
blockers.push(...selfCheck.blockers);
|
|
5454
5454
|
}
|
|
5455
5455
|
return uniqueStrings(blockers).slice(0, 8);
|
|
5456
5456
|
}
|
|
5457
|
-
function draftRunnableStatus(plan, flow, runner,
|
|
5457
|
+
function draftRunnableStatus(plan, flow, runner, executionBlockers, selfCheck) {
|
|
5458
5458
|
if (runner === "manual" || isVerificationOnlyFlow(flow)) {
|
|
5459
5459
|
return "review-only";
|
|
5460
5460
|
}
|
|
@@ -5462,9 +5462,8 @@ function draftRunnableStatus(plan, flow, runner, validationSummary, executionBlo
|
|
|
5462
5462
|
return "review-only";
|
|
5463
5463
|
}
|
|
5464
5464
|
if (executionBlockers.length === 0 &&
|
|
5465
|
-
validationSummary.blockingGapCount === 0 &&
|
|
5466
5465
|
flow.missingTestability.length === 0 &&
|
|
5467
|
-
|
|
5466
|
+
flow.fixtureReadiness.status !== "missing" &&
|
|
5468
5467
|
selfCheck?.status === "pass") {
|
|
5469
5468
|
return "runnable-candidate";
|
|
5470
5469
|
}
|
|
@@ -5502,6 +5501,10 @@ function evaluatePlaywrightDraftSelfCheck(plan, flow, content, todoCount) {
|
|
|
5502
5501
|
checks.push(draftSelfCheckItem("TODO comments", todoCount === 0 ? "pass" : "warning", todoCount === 0
|
|
5503
5502
|
? "No TODO comments remain in the generated draft."
|
|
5504
5503
|
: `${todoCount} TODO marker${todoCount === 1 ? "" : "s"} remain for reviewer follow-up.`));
|
|
5504
|
+
const weakSmokeAssertions = content.match(/expect\(page\.locator\(["']body["']\)\)\.toBeVisible\(\)/g)?.length ?? 0;
|
|
5505
|
+
checks.push(draftSelfCheckItem("Domain assertions", weakSmokeAssertions === 0 ? "pass" : "warning", weakSmokeAssertions === 0
|
|
5506
|
+
? "The draft does not rely on body-only smoke assertions."
|
|
5507
|
+
: `${weakSmokeAssertions} body-only smoke assertion${weakSmokeAssertions === 1 ? "" : "s"} must be replaced with observable domain outcomes.`));
|
|
5505
5508
|
checks.push(draftSelfCheckItem("Execution profile", plan.executionProfile.confidence === "low" || plan.executionProfile.blockers.length > 0 ? "warning" : "pass", plan.executionProfile.confidence === "low" || plan.executionProfile.blockers.length > 0
|
|
5506
5509
|
? "Runner setup, base URL, launch command, or config evidence is incomplete."
|
|
5507
5510
|
: "Runner setup evidence is strong enough for a local execution attempt."));
|
|
@@ -5846,8 +5849,10 @@ function buildPlaywrightDraft(plan, flow, addedDiffText = {}) {
|
|
|
5846
5849
|
for (const step of draftExecutableSteps(flow, "playwright")) {
|
|
5847
5850
|
const manifestCheck = manifestCheckForDraftStep(flow, step);
|
|
5848
5851
|
const manifestBody = manifestCheck ? playwrightActionForManifestCheck(manifestCheck, step) : undefined;
|
|
5849
|
-
const
|
|
5852
|
+
const failureBody = manifestBody ? undefined : playwrightFailureActionForStep(flow, step);
|
|
5853
|
+
const selector = manifestBody || failureBody ? undefined : takeSelectorForStep(selectorQueue, step);
|
|
5850
5854
|
const body = manifestBody ??
|
|
5855
|
+
failureBody ??
|
|
5851
5856
|
(selector
|
|
5852
5857
|
? playwrightActionForStep(selector, playwrightLocator(selector), step)
|
|
5853
5858
|
: playwrightFallbackActionForStep(step));
|
|
@@ -6833,19 +6838,24 @@ function formatDraftActionKindSummary(summary) {
|
|
|
6833
6838
|
.map((item) => `${item.kind} ${item.total} (${item.required} required, ${item.recommended} recommended)`)
|
|
6834
6839
|
.join(", ");
|
|
6835
6840
|
}
|
|
6836
|
-
function buildDraftNextSteps(plan, runner) {
|
|
6841
|
+
function buildDraftNextSteps(plan, runner, files) {
|
|
6837
6842
|
const steps = [];
|
|
6843
|
+
const hasTodos = files.some((file) => (file.todoCount ?? 0) > 0);
|
|
6838
6844
|
if (runner === "maestro") {
|
|
6839
6845
|
steps.push(plan.executionProfile.appId
|
|
6840
6846
|
? `Use app id ${plan.executionProfile.appId} or export APP_ID before running Maestro.`
|
|
6841
6847
|
: "Replace ${APP_ID} or export APP_ID before running Maestro.");
|
|
6842
|
-
|
|
6848
|
+
if (hasTodos) {
|
|
6849
|
+
steps.push("Replace TODO text selectors with visible copy, testID, or accessibilityLabel selectors.");
|
|
6850
|
+
}
|
|
6843
6851
|
steps.push(plan.executionProfile.startCommand
|
|
6844
6852
|
? `Launch the app with \`${plan.executionProfile.startCommand}\`, then run \`${plan.executionProfile.testCommand ?? "maestro test .maestro"}\`.`
|
|
6845
6853
|
: `Run the app with the launch command that matches your simulator or device, then run \`${plan.executionProfile.testCommand ?? "maestro test .maestro"}\`.`);
|
|
6846
6854
|
}
|
|
6847
6855
|
else if (runner === "playwright") {
|
|
6848
|
-
|
|
6856
|
+
if (hasTodos) {
|
|
6857
|
+
steps.push("Replace TODO locators with role, text, or data-testid locators from the app.");
|
|
6858
|
+
}
|
|
6849
6859
|
if (plan.executionProfile.blockers.some((blocker) => /No Playwright config/i.test(blocker))) {
|
|
6850
6860
|
steps.push(playwrightConfigGuidance(plan.executionProfile));
|
|
6851
6861
|
}
|
|
@@ -7124,6 +7134,53 @@ function playwrightActionForStep(selector, locator, step) {
|
|
|
7124
7134
|
body.push(`await ${locator}.click();`);
|
|
7125
7135
|
return body;
|
|
7126
7136
|
}
|
|
7137
|
+
function playwrightFailureActionForStep(flow, step) {
|
|
7138
|
+
if (!isFailurePathStep(step)) {
|
|
7139
|
+
return undefined;
|
|
7140
|
+
}
|
|
7141
|
+
const endpoint = playwrightFailureMockEndpoint(flow);
|
|
7142
|
+
const actionSelector = flow.selectors.find((selector) => !isInputSelector(selector) && selectorCanDriveInteraction(selector));
|
|
7143
|
+
const outcomeSelector = flow.selectors.find((selector) => selectorCanSupportAssertion(selector) && isFailureOutcomeText(selector.value));
|
|
7144
|
+
if (!endpoint || !actionSelector || !outcomeSelector) {
|
|
7145
|
+
return undefined;
|
|
7146
|
+
}
|
|
7147
|
+
const routePattern = playwrightMockRoutePattern(endpoint);
|
|
7148
|
+
const status = failureResponseStatus(step);
|
|
7149
|
+
return [
|
|
7150
|
+
`// Step intent: ${step}`,
|
|
7151
|
+
`await page.unroute("${quoteJs(routePattern)}");`,
|
|
7152
|
+
`await page.route("${quoteJs(routePattern)}", async (route) => {`,
|
|
7153
|
+
" await route.fulfill({",
|
|
7154
|
+
` status: ${status},`,
|
|
7155
|
+
' contentType: "application/json",',
|
|
7156
|
+
' body: JSON.stringify({ error: "QAMap simulated failure" }),',
|
|
7157
|
+
" });",
|
|
7158
|
+
"});",
|
|
7159
|
+
`await ${playwrightLocator(actionSelector)}.click();`,
|
|
7160
|
+
`await expect(${playwrightLocator(outcomeSelector)}).toBeVisible();`,
|
|
7161
|
+
];
|
|
7162
|
+
}
|
|
7163
|
+
function playwrightFailureMockEndpoint(flow) {
|
|
7164
|
+
const observedEndpoints = observedEndpointsForFlow(flow);
|
|
7165
|
+
return flow.fixtureReadiness.apiEndpoints.find((endpoint) => !endpointMatchesAny(endpoint, observedEndpoints));
|
|
7166
|
+
}
|
|
7167
|
+
function isFailurePathStep(step) {
|
|
7168
|
+
return /\b(?:blocked|declined|denied|empty|error|failed|failure|invalid|rejected|timeout|unauthori[sz]ed)\b/i.test(step) ||
|
|
7169
|
+
/(?:오류|실패|거절|권한|잘못|할 수 없)/.test(step);
|
|
7170
|
+
}
|
|
7171
|
+
function isFailureOutcomeText(value) {
|
|
7172
|
+
return /\b(?:cannot|could not|declined|denied|error|failed|failure|invalid|rejected|try again|unauthori[sz]ed|unavailable)\b/i.test(value) ||
|
|
7173
|
+
/(?:오류|실패|거절|권한|잘못|할 수 없)/.test(value);
|
|
7174
|
+
}
|
|
7175
|
+
function failureResponseStatus(step) {
|
|
7176
|
+
if (/\b(?:denied|unauthori[sz]ed)\b/i.test(step) || /권한/.test(step)) {
|
|
7177
|
+
return 401;
|
|
7178
|
+
}
|
|
7179
|
+
if (/\b(?:invalid|validation)\b/i.test(step) || /잘못/.test(step)) {
|
|
7180
|
+
return 422;
|
|
7181
|
+
}
|
|
7182
|
+
return 500;
|
|
7183
|
+
}
|
|
7127
7184
|
function manifestCheckForDraftStep(flow, step) {
|
|
7128
7185
|
const checks = flow.manifestCheckMatches ?? [];
|
|
7129
7186
|
const stepKey = normalizeManifestStepKey(step);
|