@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 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, validationSummary, selfCheck);
227
- const runnableStatus = draftRunnableStatus(plan, flow, runner, validationSummary, executionBlockers, selfCheck);
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, validationSummary, selfCheck) {
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
- if (validationSummary.blockingGapCount > 0) {
5450
- blockers.push(`${validationSummary.blockingGapCount} blocking validation gap${validationSummary.blockingGapCount === 1 ? "" : "s"} ${validationSummary.blockingGapCount === 1 ? "remains" : "remain"}.`);
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, validationSummary, executionBlockers, selfCheck) {
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
- (flow.fixtureReadiness.status === "ready" || flow.fixtureReadiness.status === "not-needed") &&
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 selector = manifestBody ? undefined : takeSelectorForStep(selectorQueue, step);
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
- steps.push("Replace TODO text selectors with visible copy, testID, or accessibilityLabel selectors.");
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
- steps.push("Replace TODO locators with role, text, or data-testid locators from the app.");
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);