tamash-playwright 0.6.1 → 0.7.0-beta.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/README.md +156 -0
- package/dist/cli/applyHeals.d.ts +15 -0
- package/dist/cli/applyHeals.d.ts.map +1 -0
- package/dist/cli/applyHeals.js +293 -0
- package/dist/cli/applyHeals.js.map +1 -0
- package/dist/cli/index.js +7 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/healer/heal-log.d.ts +21 -0
- package/dist/healer/heal-log.d.ts.map +1 -0
- package/dist/healer/heal-log.js +163 -0
- package/dist/healer/heal-log.js.map +1 -0
- package/dist/healer/index.d.ts.map +1 -1
- package/dist/healer/index.js +68 -2
- package/dist/healer/index.js.map +1 -1
- package/package.json +2 -1
- package/usage.md +360 -0
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@ Websites change often. A button gets renamed or moved, and your test can't find
|
|
|
12
12
|
|
|
13
13
|
**Want to see it working before you set it up yourself?** Clone the sample repo — [github.com/qtpsudhakarproducts/tamash-playwright-typescript-playwright](https://github.com/qtpsudhakarproducts/tamash-playwright-typescript-playwright) — a full worked example with both a plain-locator test and a Page Object Model test, an intentionally broken selector, and step-by-step setup instructions.
|
|
14
14
|
|
|
15
|
+
**Looking for the complete reference** — caching, `apply-heals`, sharded-CI and PR automation, every env var and CLI flag? See [usage.md](usage.md).
|
|
16
|
+
|
|
15
17
|
Here are the detailed steps to use this package.
|
|
16
18
|
|
|
17
19
|
## Step 1: Install it
|
|
@@ -184,6 +186,160 @@ Sometimes an element has nothing useful to match on by text — an icon-only but
|
|
|
184
186
|
|
|
185
187
|
Occasionally a locator heals correctly — the AI found the right element — but the *action* on it still fails, e.g. it's covered by an overlay or needs scrolling into view first. Set `HEALER_ACTION_RECOVERY_ENABLED=true` to let the AI pick a recovery tactic from a fixed, safe set (scroll into view, retry bypassing actionability checks, wait briefly and retry, or dispatch the DOM event directly) before giving up. It's off by default since it's a second, more speculative layer of intervention beyond selector healing — the AI only ever picks from that fixed menu, it never decides how to interact with the page on its own.
|
|
186
188
|
|
|
189
|
+
## Not paying for the same heal twice
|
|
190
|
+
|
|
191
|
+
Once a locator heals successfully, `tamash-playwright` remembers the fix in `.tamash-playwright/heals.jsonl` — the same file `apply-heals` reads (see below). The next time that exact locator breaks the same way, it tries the previously-confirmed selector *first*, with no ARIA snapshot and no AI call. Only if that no longer works (the page changed again) does it fall through to a fresh snapshot-and-AI-call, exactly as before — so there's no correctness risk in trying the cached selector, only a cost/time saving when it still works.
|
|
192
|
+
|
|
193
|
+
This persists across runs, not just within one — since it reads from disk rather than an in-memory cache, it also helps the case that costs the most in practice: the same broken selector otherwise getting healed by AI on every single CI run until someone gets around to running `apply-heals`. You'll see it in the console line as `provider=cache` with no token count, instead of the real provider name:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
[self-healer] tests/sampletest.spec.ts:13 — locator.fill "User Name Textbox (placeholder "xyz")" -> HEALED [provider=cache, vision=no, actionRecovery=no, suggested="role:textbox:Username"] — locator.fill: Timeout 8000ms exceeded.
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
A cache hit doesn't re-log itself (there's nothing new to record), so the log doesn't grow just from repeated confirmations of the same already-known fix — it only grows when a *new* AI call produces a fresh suggestion.
|
|
200
|
+
|
|
201
|
+
## Making a heal permanent: `apply-heals`
|
|
202
|
+
|
|
203
|
+
Runtime healing (including the caching above) never touches your source code — the original locator stays broken in your test file or Page Object forever, healed at runtime on every run, until you fix it yourself. `apply-heals` closes that loop: it rewrites the original broken locator to the selector that actually worked, so the next run doesn't need healing — from cache or otherwise — at all.
|
|
204
|
+
|
|
205
|
+
```sh
|
|
206
|
+
npx playwright test # heals at runtime, and records what it healed
|
|
207
|
+
npx tamash-playwright apply-heals --dry-run # preview the source changes it would make
|
|
208
|
+
npx tamash-playwright apply-heals # write them
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
[FIX] src/pages/loginpage.ts:11
|
|
213
|
+
- .locator('input[name="username1"]')
|
|
214
|
+
+ .getByRole("textbox", { name: "Username" })
|
|
215
|
+
|
|
216
|
+
1 fix(es) applied to 1 file(s), 0 skipped.
|
|
217
|
+
Review the changes (e.g. `git diff`) before committing.
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
A few things worth knowing:
|
|
221
|
+
|
|
222
|
+
- **Nothing is applied automatically.** `apply-heals` is a separate, deliberate command — a test run never edits your source on its own.
|
|
223
|
+
- **Only real selector fixes are eligible.** A heal only qualifies if it's text/ARIA-based (not the screenshot-based vision fallback, which has no reusable source form — see below) and the locator itself was actually replaced (not an action-recovery heal, where the original locator was already correct and only the action needed help).
|
|
224
|
+
- **Only the matched call is touched.** `.describe('...')` and everything else on the line is left exactly as written; only the `.locator(...)`/`.getByRole(...)`/etc. call itself is replaced.
|
|
225
|
+
- **Always review the diff before committing** — this rewrites your source files, so treat it like any other automated code change: check `git diff`, run your tests again, and commit deliberately.
|
|
226
|
+
|
|
227
|
+
Every run also writes a before/after report to `.tamash-playwright/` — `apply-heals-report.json` (machine-readable) and `apply-heals-report.md` (human-readable, one section per fix with the exact before/after code). This is what feeds the PR body in the CI pattern below, so a reviewer sees the actual change without digging through logs — worth knowing about even if you only ever run this locally, since it's a persisted record of every fix beyond whatever's still in your scrollback.
|
|
228
|
+
|
|
229
|
+
Those two filenames always mean "the latest run" — each run overwrites them. Since `.tamash-playwright/` is gitignored (never committed), that would otherwise mean a second run erases all trace of the first. To keep a real history, every run *also* archives a timestamped copy of both reports, plus the raw `heals.jsonl` that produced them, under `.tamash-playwright/history/` — nothing in there is ever overwritten or deleted by a later run, so it's safe to just leave it accumulating, or clean it out yourself whenever you want.
|
|
230
|
+
|
|
231
|
+
### Running `apply-heals` in CI (sharded or not)
|
|
232
|
+
|
|
233
|
+
`apply-heals` never touches git itself — in CI, that means the fix only exists in that job's ephemeral checkout unless something turns it into a real, reviewable change. The recommended shape: a separate job that runs *after* your test job(s), downloads whatever got healed, and opens a PR rather than pushing straight to a branch.
|
|
234
|
+
|
|
235
|
+
If your suite runs sharded (`--shard=N/M` across multiple CI machines), each shard only sees its own slice of what got healed — `.tamash-playwright/heals.jsonl` ends up fragmented, one partial file per shard. `--logs-dir` is built for exactly this: point it at a directory containing any number of `heals.jsonl` files, nested however you like, and it merges all of them before planning fixes (deduplicating by keeping only the newest entry per file:line, so two shards healing the same line is harmless):
|
|
236
|
+
|
|
237
|
+
```sh
|
|
238
|
+
npx tamash-playwright apply-heals --logs-dir shard-logs
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
A GitHub Actions example — each test job uploads its own log as an artifact; a separate job merges them, applies the fixes on a fresh branch, **re-runs the suite against just those fixes to prove they actually work**, and only then opens a PR (labeled with whether verification passed, either way):
|
|
242
|
+
|
|
243
|
+
```yaml
|
|
244
|
+
jobs:
|
|
245
|
+
test:
|
|
246
|
+
# ...your existing test job(s), sharded or not...
|
|
247
|
+
steps:
|
|
248
|
+
- run: npx playwright test
|
|
249
|
+
- uses: actions/upload-artifact@v4
|
|
250
|
+
if: ${{ !cancelled() }}
|
|
251
|
+
with:
|
|
252
|
+
name: heals-log-${{ strategy.job-index }}
|
|
253
|
+
path: .tamash-playwright/heals.jsonl
|
|
254
|
+
if-no-files-found: ignore
|
|
255
|
+
|
|
256
|
+
apply-heals:
|
|
257
|
+
needs: test
|
|
258
|
+
if: ${{ !cancelled() && github.event_name == 'push' }} # not pull_request — see note below
|
|
259
|
+
runs-on: ubuntu-latest
|
|
260
|
+
permissions:
|
|
261
|
+
contents: write
|
|
262
|
+
pull-requests: write
|
|
263
|
+
steps:
|
|
264
|
+
- uses: actions/checkout@v4
|
|
265
|
+
- uses: actions/setup-node@v4
|
|
266
|
+
with: { node-version: lts/* }
|
|
267
|
+
- run: npm ci
|
|
268
|
+
|
|
269
|
+
- uses: actions/download-artifact@v4
|
|
270
|
+
with:
|
|
271
|
+
pattern: heals-log-*
|
|
272
|
+
path: shard-logs
|
|
273
|
+
continue-on-error: true # no artifact at all when nothing needed healing — the common case
|
|
274
|
+
- run: npx tamash-playwright apply-heals --logs-dir shard-logs
|
|
275
|
+
|
|
276
|
+
- name: Check whether any fixes were applied
|
|
277
|
+
id: check
|
|
278
|
+
run: echo "changed=$(git diff --quiet || echo true)" >> "$GITHUB_OUTPUT"
|
|
279
|
+
|
|
280
|
+
# HEALER_ENABLED=false here is deliberate: this proves the *written* fix works standalone —
|
|
281
|
+
# leaving healing on could let a still-broken selector get silently re-healed at runtime
|
|
282
|
+
# again, reporting green without ever proving the applied source fix was actually correct.
|
|
283
|
+
- name: Verify the healed selectors work on their own
|
|
284
|
+
id: verify
|
|
285
|
+
if: steps.check.outputs.changed == 'true'
|
|
286
|
+
run: npx playwright test
|
|
287
|
+
continue-on-error: true
|
|
288
|
+
env:
|
|
289
|
+
HEALER_ENABLED: false
|
|
290
|
+
|
|
291
|
+
# Captured via the step's own id so Compose PR body can link straight to it — otherwise it's
|
|
292
|
+
# uploaded correctly but nobody reviewing the PR would know it exists.
|
|
293
|
+
- name: Upload verification report
|
|
294
|
+
id: upload-verification-report
|
|
295
|
+
if: steps.check.outputs.changed == 'true' && !cancelled()
|
|
296
|
+
uses: actions/upload-artifact@v4
|
|
297
|
+
with:
|
|
298
|
+
name: apply-heals-verification-report
|
|
299
|
+
path: playwright-report/
|
|
300
|
+
|
|
301
|
+
# apply-heals already wrote .tamash-playwright/apply-heals-report.md with a before/after per
|
|
302
|
+
# fix — this prepends the verification result so the PR body is one linked story (what
|
|
303
|
+
# broke, what changed, whether it's proven to work) instead of three things to go find.
|
|
304
|
+
- name: Compose PR body
|
|
305
|
+
if: steps.check.outputs.changed == 'true'
|
|
306
|
+
run: |
|
|
307
|
+
{
|
|
308
|
+
echo "Auto-generated by \`tamash-playwright apply-heals\` after self-healing kicked in during CI."
|
|
309
|
+
echo ""
|
|
310
|
+
if [ "${{ steps.verify.outcome }}" = "success" ]; then
|
|
311
|
+
echo "**Verification run (healing disabled): ✅ passed.**"
|
|
312
|
+
else
|
|
313
|
+
echo "**Verification run (healing disabled): ⚠️ FAILED — review carefully before merging.**"
|
|
314
|
+
fi
|
|
315
|
+
echo "[Full test execution report](${{ steps.upload-verification-report.outputs.artifact-url }})"
|
|
316
|
+
echo ""
|
|
317
|
+
cat .tamash-playwright/apply-heals-report.md
|
|
318
|
+
} > .tamash-playwright/pr-body.md
|
|
319
|
+
|
|
320
|
+
- name: Open PR with healed selectors
|
|
321
|
+
if: steps.check.outputs.changed == 'true'
|
|
322
|
+
uses: peter-evans/create-pull-request@v6
|
|
323
|
+
with:
|
|
324
|
+
commit-message: "fix: apply self-healed selectors from CI"
|
|
325
|
+
title: "Apply self-healed selectors (verification ${{ steps.verify.outcome == 'success' && 'passed' || 'FAILED' }})"
|
|
326
|
+
body-path: .tamash-playwright/pr-body.md
|
|
327
|
+
branch: tamash-playwright/apply-heals
|
|
328
|
+
delete-branch: true
|
|
329
|
+
|
|
330
|
+
- name: Fail the job if verification didn't pass
|
|
331
|
+
if: steps.check.outputs.changed == 'true' && steps.verify.outcome != 'success'
|
|
332
|
+
run: exit 1 # PR is still opened above for review — this just keeps CI status honest
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
One more thing worth flagging if you copy this: `.tamash-playwright/` needs to be in your `.gitignore`. `peter-evans/create-pull-request` commits whatever differs from `HEAD` in the working tree — without the ignore, the report/log files themselves would get swept into the PR alongside the actual source fix.
|
|
336
|
+
|
|
337
|
+
A few choices worth calling out:
|
|
338
|
+
|
|
339
|
+
- **Gated on `push`, not `pull_request`.** A `pull_request` run from a fork gets a read-only `GITHUB_TOKEN` (so it couldn't open a PR anyway), and "open a PR to fix this still-open PR" isn't a sensible flow regardless. Running after merges to `main`/`master` avoids both problems.
|
|
340
|
+
- **The PR is opened either way**, verification passed or failed — a failed verification is still worth a human's attention (maybe the fix is right and something else was flaky); it just gets an honest label instead of a silent false-positive. The final step fails the *job* itself when verification fails, so CI status stays truthful even though the PR still exists for review.
|
|
341
|
+
- [`peter-evans/create-pull-request`](https://github.com/peter-evans/create-pull-request) is a no-op if there's nothing to commit, so the job is safe to run on every push — it only ever opens a PR when there's an actual fix to review, and reuses the same branch/PR on subsequent runs rather than piling up duplicates.
|
|
342
|
+
|
|
187
343
|
## Checking what actually happened
|
|
188
344
|
|
|
189
345
|
Every healing attempt — whether it succeeded or not — shows up in Playwright's own HTML report (`npx playwright show-report`), no separate report to check:
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type HealLogEntry } from '../healer/heal-log';
|
|
2
|
+
export type FixOutcome = {
|
|
3
|
+
file: string;
|
|
4
|
+
line: number;
|
|
5
|
+
before: string;
|
|
6
|
+
after: string;
|
|
7
|
+
applied: boolean;
|
|
8
|
+
reason?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function planFixes(cwd?: string, rawEntries?: HealLogEntry[]): {
|
|
11
|
+
outcomes: FixOutcome[];
|
|
12
|
+
fileContents: Map<string, string>;
|
|
13
|
+
};
|
|
14
|
+
export declare function runApplyHeals(args?: string[]): Promise<void>;
|
|
15
|
+
//# sourceMappingURL=applyHeals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"applyHeals.d.ts","sourceRoot":"","sources":["../../src/cli/applyHeals.ts"],"names":[],"mappings":"AAEA,OAAO,EAAwF,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAiH7I,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAqBF,wBAAgB,SAAS,CAAC,GAAG,GAAE,MAAsB,EAAE,UAAU,CAAC,EAAE,YAAY,EAAE,GAAG;IAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;IAAC,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CA2DjJ;AA2ED,wBAAsB,aAAa,CAAC,IAAI,GAAE,MAAM,EAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CA+DtE"}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.planFixes = planFixes;
|
|
7
|
+
exports.runApplyHeals = runApplyHeals;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const heal_log_1 = require("../healer/heal-log");
|
|
11
|
+
const LOCATOR_FACTORY_METHODS = ['locator', 'getByRole', 'getByLabel', 'getByPlaceholder', 'getByText', 'getByAltText', 'getByTitle', 'getByTestId'];
|
|
12
|
+
const FACTORY_METHOD_PATTERN = new RegExp(`\\.(${LOCATOR_FACTORY_METHODS.join('|')})\\(`);
|
|
13
|
+
function generateReplacementCall(suggestion) {
|
|
14
|
+
switch (suggestion.strategy) {
|
|
15
|
+
case 'role':
|
|
16
|
+
return suggestion.name
|
|
17
|
+
? `getByRole(${JSON.stringify(suggestion.role)}, { name: ${JSON.stringify(suggestion.name)} })`
|
|
18
|
+
: `getByRole(${JSON.stringify(suggestion.role)})`;
|
|
19
|
+
case 'text':
|
|
20
|
+
return `getByText(${JSON.stringify(suggestion.text)})`;
|
|
21
|
+
case 'testId':
|
|
22
|
+
return `getByTestId(${JSON.stringify(suggestion.testId)})`;
|
|
23
|
+
case 'label':
|
|
24
|
+
return `getByLabel(${JSON.stringify(suggestion.label)})`;
|
|
25
|
+
case 'placeholder':
|
|
26
|
+
return `getByPlaceholder(${JSON.stringify(suggestion.placeholder)})`;
|
|
27
|
+
case 'css':
|
|
28
|
+
return `locator(${JSON.stringify(suggestion.css)})`;
|
|
29
|
+
default:
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Finds the exact [dotIndex, callEnd) range of a locator-factory call (`.locator(...)`,
|
|
34
|
+
// `.getByRole(...)`, etc.) starting on `targetLine` (1-based) — the exact call site captured at
|
|
35
|
+
// heal time via resolveCallerLocation in bindings/locator.binding.ts. Deliberately not a real
|
|
36
|
+
// parser: a small string/paren-balance scanner anchored at a known line, in the same "good enough
|
|
37
|
+
// for a human to review, not an exhaustive linter" spirit as scanDescribe.ts and
|
|
38
|
+
// scanActionTimeout.ts — and it means this file has zero dependency on whichever TypeScript
|
|
39
|
+
// version (if any) happens to be resolvable in the consumer's own node_modules.
|
|
40
|
+
// `/` starts a regex literal (e.g. `getByRole('button', { name: /submit/i })` — a completely
|
|
41
|
+
// normal Playwright pattern) unless the preceding significant character means it's actually
|
|
42
|
+
// division, which can't happen in a locator argument list except in wildly unrealistic
|
|
43
|
+
// expressions. Standard division-vs-regex disambiguation heuristic: division only follows
|
|
44
|
+
// something that evaluates to a value — an identifier, number, or a closing bracket/paren/quote.
|
|
45
|
+
function isRegexLiteralStart(content, index) {
|
|
46
|
+
let j = index - 1;
|
|
47
|
+
while (j >= 0 && /\s/.test(content[j])) {
|
|
48
|
+
j--;
|
|
49
|
+
}
|
|
50
|
+
if (j < 0) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return !/[\w$)\]`'"]/.test(content[j]);
|
|
54
|
+
}
|
|
55
|
+
function findFactoryCallOnLine(content, targetLine) {
|
|
56
|
+
const lines = content.split('\n');
|
|
57
|
+
if (targetLine < 1 || targetLine > lines.length) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
let lineStartOffset = 0;
|
|
61
|
+
for (let i = 0; i < targetLine - 1; i++) {
|
|
62
|
+
lineStartOffset += lines[i].length + 1; // +1 for the '\n' each split() consumed
|
|
63
|
+
}
|
|
64
|
+
const match = lines[targetLine - 1].match(FACTORY_METHOD_PATTERN);
|
|
65
|
+
if (!match || match.index === undefined) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const dotIndex = lineStartOffset + match.index;
|
|
69
|
+
const openParenIndex = dotIndex + match[0].length - 1;
|
|
70
|
+
let depth = 1;
|
|
71
|
+
let i = openParenIndex + 1;
|
|
72
|
+
// Tracks whichever of `"`, `'`, `` ` ``, or `/` (regex) we're currently inside — parens found
|
|
73
|
+
// while this is set (e.g. an escaped `\(` inside a regex literal, or any paren at all inside a
|
|
74
|
+
// string) don't affect depth, since they're not real call-argument boundaries.
|
|
75
|
+
let inString = null;
|
|
76
|
+
while (i < content.length && depth > 0) {
|
|
77
|
+
const ch = content[i];
|
|
78
|
+
if (inString) {
|
|
79
|
+
if (ch === '\\') {
|
|
80
|
+
i += 2;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (ch === inString) {
|
|
84
|
+
inString = null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
88
|
+
inString = ch;
|
|
89
|
+
}
|
|
90
|
+
else if (ch === '/' && isRegexLiteralStart(content, i)) {
|
|
91
|
+
inString = '/';
|
|
92
|
+
}
|
|
93
|
+
else if (ch === '(') {
|
|
94
|
+
depth++;
|
|
95
|
+
}
|
|
96
|
+
else if (ch === ')') {
|
|
97
|
+
depth--;
|
|
98
|
+
}
|
|
99
|
+
i++;
|
|
100
|
+
}
|
|
101
|
+
if (depth !== 0) {
|
|
102
|
+
// Unbalanced, or the argument list contains something this scanner can't safely track — bail
|
|
103
|
+
// out rather than guess at a range that might be wrong.
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
return { dotIndex, callEnd: i };
|
|
107
|
+
}
|
|
108
|
+
// Only the most recent entry per file:line survives — later runs supersede earlier ones, and
|
|
109
|
+
// there's nothing useful about applying the same location twice.
|
|
110
|
+
function latestPerLocation(entries) {
|
|
111
|
+
const byKey = new Map();
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
const key = `${entry.file}:${entry.line}`;
|
|
114
|
+
const existing = byKey.get(key);
|
|
115
|
+
if (!existing || entry.timestamp > existing.timestamp) {
|
|
116
|
+
byKey.set(key, entry);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return [...byKey.values()];
|
|
120
|
+
}
|
|
121
|
+
// Computes every fix and the resulting file contents, but never touches disk — the CLI layer
|
|
122
|
+
// decides whether to actually write based on --dry-run. Only the matched `.locator(...)`/
|
|
123
|
+
// `.getByX(...)` text itself is replaced; the receiver (`page`, `this.page`, `container`, ...) and
|
|
124
|
+
// any subsequent chained calls like `.describe(...)` are left completely untouched since they sit
|
|
125
|
+
// outside the replaced range entirely.
|
|
126
|
+
function planFixes(cwd = process.cwd(), rawEntries) {
|
|
127
|
+
const entries = latestPerLocation(rawEntries ?? (0, heal_log_1.readHealLog)(cwd));
|
|
128
|
+
const byFile = new Map();
|
|
129
|
+
for (const entry of entries) {
|
|
130
|
+
const list = byFile.get(entry.file) ?? [];
|
|
131
|
+
list.push(entry);
|
|
132
|
+
byFile.set(entry.file, list);
|
|
133
|
+
}
|
|
134
|
+
const outcomes = [];
|
|
135
|
+
const fileContents = new Map();
|
|
136
|
+
for (const [relativeFile, fileEntries] of byFile) {
|
|
137
|
+
const fullPath = path_1.default.resolve(cwd, relativeFile);
|
|
138
|
+
if (!fs_1.default.existsSync(fullPath)) {
|
|
139
|
+
for (const entry of fileEntries) {
|
|
140
|
+
outcomes.push({ file: relativeFile, line: entry.line, before: '', after: '', applied: false, reason: 'File no longer exists.' });
|
|
141
|
+
}
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
let content = fs_1.default.readFileSync(fullPath, 'utf-8');
|
|
145
|
+
let changed = false;
|
|
146
|
+
// Bottom-to-top: replacing a later line first means an earlier line's own offsets are never
|
|
147
|
+
// invalidated by a change below it.
|
|
148
|
+
const sortedEntries = [...fileEntries].sort((a, b) => b.line - a.line);
|
|
149
|
+
for (const entry of sortedEntries) {
|
|
150
|
+
const range = findFactoryCallOnLine(content, entry.line);
|
|
151
|
+
const replacement = range ? generateReplacementCall(entry.suggestion) : undefined;
|
|
152
|
+
if (!range || !replacement) {
|
|
153
|
+
outcomes.push({
|
|
154
|
+
file: relativeFile,
|
|
155
|
+
line: entry.line,
|
|
156
|
+
before: '',
|
|
157
|
+
after: '',
|
|
158
|
+
applied: false,
|
|
159
|
+
reason: !range
|
|
160
|
+
? 'Could not find the original locator call on this line — the file may have changed since this heal was recorded.'
|
|
161
|
+
: `Unsupported suggestion strategy "${entry.suggestion.strategy}".`,
|
|
162
|
+
});
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const before = content.slice(range.dotIndex, range.callEnd);
|
|
166
|
+
const after = `.${replacement}`;
|
|
167
|
+
content = content.slice(0, range.dotIndex) + after + content.slice(range.callEnd);
|
|
168
|
+
changed = true;
|
|
169
|
+
outcomes.push({ file: relativeFile, line: entry.line, before, after, applied: true });
|
|
170
|
+
}
|
|
171
|
+
if (changed) {
|
|
172
|
+
fileContents.set(fullPath, content);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { outcomes, fileContents };
|
|
176
|
+
}
|
|
177
|
+
const REPORT_DIR = '.tamash-playwright';
|
|
178
|
+
// Filesystem-safe version of an ISO timestamp (`:` isn't valid in a Windows filename) — shared
|
|
179
|
+
// across the report files and the archived heal-log for one run, so all three end up correlated
|
|
180
|
+
// by the same label.
|
|
181
|
+
function timestampLabel() {
|
|
182
|
+
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
183
|
+
}
|
|
184
|
+
// Persists the before/after of every fix (applied or skipped) as both a machine-readable JSON
|
|
185
|
+
// report and a human-readable markdown one — the latter is written specifically so a CI job can
|
|
186
|
+
// hand its contents straight to a PR body (see the "Running apply-heals in CI" README section),
|
|
187
|
+
// so a reviewer sees exactly what changed without having to dig through CI logs or a separate
|
|
188
|
+
// artifact.
|
|
189
|
+
//
|
|
190
|
+
// Written twice: once at a stable filename (what CI and anything else automated should read —
|
|
191
|
+
// always "the latest run"), and once archived under history/<timestamp>.* — otherwise a second
|
|
192
|
+
// apply-heals run silently erases the first's report with no trace, and since .tamash-playwright/
|
|
193
|
+
// is gitignored, that's not recoverable from git either.
|
|
194
|
+
function writeReports(outcomes, dryRun, cwd, label) {
|
|
195
|
+
const dir = path_1.default.join(cwd, REPORT_DIR);
|
|
196
|
+
const historyDir = path_1.default.join(dir, 'history');
|
|
197
|
+
if (!fs_1.default.existsSync(historyDir)) {
|
|
198
|
+
fs_1.default.mkdirSync(historyDir, { recursive: true });
|
|
199
|
+
}
|
|
200
|
+
const applied = outcomes.filter((o) => o.applied);
|
|
201
|
+
const skipped = outcomes.filter((o) => !o.applied);
|
|
202
|
+
const timestamp = new Date().toISOString();
|
|
203
|
+
const jsonContent = JSON.stringify({ timestamp, dryRun, applied: applied.length, skipped: skipped.length, fixes: outcomes }, null, 2);
|
|
204
|
+
const jsonPath = path_1.default.join(dir, 'apply-heals-report.json');
|
|
205
|
+
fs_1.default.writeFileSync(jsonPath, jsonContent, 'utf-8');
|
|
206
|
+
fs_1.default.writeFileSync(path_1.default.join(historyDir, `${label}.apply-heals-report.json`), jsonContent, 'utf-8');
|
|
207
|
+
const lines = [
|
|
208
|
+
'# Self-healing fixes',
|
|
209
|
+
'',
|
|
210
|
+
`Generated ${timestamp} by \`tamash-playwright apply-heals\`${dryRun ? ' (--dry-run, nothing written)' : ''}.`,
|
|
211
|
+
'',
|
|
212
|
+
];
|
|
213
|
+
if (applied.length > 0) {
|
|
214
|
+
lines.push(`## Applied (${applied.length})`, '');
|
|
215
|
+
for (const fix of applied) {
|
|
216
|
+
lines.push(`### \`${fix.file}:${fix.line}\``, '', '**Before:**', '```ts', fix.before, '```', '', '**After:**', '```ts', fix.after, '```', '');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (skipped.length > 0) {
|
|
220
|
+
lines.push(`## Skipped (${skipped.length})`, '');
|
|
221
|
+
for (const fix of skipped) {
|
|
222
|
+
lines.push(`- \`${fix.file}:${fix.line}\` — ${fix.reason}`);
|
|
223
|
+
}
|
|
224
|
+
lines.push('');
|
|
225
|
+
}
|
|
226
|
+
const markdownContent = lines.join('\n');
|
|
227
|
+
const markdownPath = path_1.default.join(dir, 'apply-heals-report.md');
|
|
228
|
+
fs_1.default.writeFileSync(markdownPath, markdownContent, 'utf-8');
|
|
229
|
+
fs_1.default.writeFileSync(path_1.default.join(historyDir, `${label}.apply-heals-report.md`), markdownContent, 'utf-8');
|
|
230
|
+
return { jsonPath, markdownPath };
|
|
231
|
+
}
|
|
232
|
+
function parseArgs(args) {
|
|
233
|
+
const logsDirIndex = args.indexOf('--logs-dir');
|
|
234
|
+
return {
|
|
235
|
+
dryRun: args.includes('--dry-run'),
|
|
236
|
+
logsDir: logsDirIndex !== -1 ? args[logsDirIndex + 1] : undefined,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
async function runApplyHeals(args = []) {
|
|
240
|
+
const { dryRun, logsDir } = parseArgs(args);
|
|
241
|
+
console.log('tamash-playwright apply-heals\n');
|
|
242
|
+
// --logs-dir supports the sharded-CI pattern: a merge job downloads every shard's heals.jsonl
|
|
243
|
+
// artifact into one directory and points here instead of relying on a single local log file.
|
|
244
|
+
const rawEntries = logsDir ? (0, heal_log_1.readHealLogsFromDir)(path_1.default.resolve(process.cwd(), logsDir)) : undefined;
|
|
245
|
+
const { outcomes, fileContents } = planFixes(process.cwd(), rawEntries);
|
|
246
|
+
if (outcomes.length === 0) {
|
|
247
|
+
console.log(logsDir ? `No eligible heals found under ${logsDir}.` : 'No eligible heals found in .tamash-playwright/heals.jsonl.');
|
|
248
|
+
console.log('(Only text/ARIA-based heals with a known source location are eligible — run your tests first.)');
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const applied = outcomes.filter((o) => o.applied);
|
|
252
|
+
const skipped = outcomes.filter((o) => !o.applied);
|
|
253
|
+
for (const outcome of applied) {
|
|
254
|
+
console.log(`${dryRun ? '[WOULD FIX]' : '[FIX]'} ${outcome.file}:${outcome.line}`);
|
|
255
|
+
console.log(` - ${outcome.before}`);
|
|
256
|
+
console.log(` + ${outcome.after}`);
|
|
257
|
+
}
|
|
258
|
+
for (const outcome of skipped) {
|
|
259
|
+
console.log(`[SKIP] ${outcome.file}:${outcome.line} — ${outcome.reason}`);
|
|
260
|
+
}
|
|
261
|
+
console.log('');
|
|
262
|
+
const label = timestampLabel();
|
|
263
|
+
const { markdownPath } = writeReports(outcomes, dryRun, process.cwd(), label);
|
|
264
|
+
console.log(`Report written to ${path_1.default.relative(process.cwd(), markdownPath)} (and the matching .json).`);
|
|
265
|
+
console.log(`Archived under .tamash-playwright/history/${label}.* — kept even after the next run.`);
|
|
266
|
+
if (dryRun) {
|
|
267
|
+
console.log(`${applied.length} fix(es) would be applied, ${skipped.length} skipped. Re-run without --dry-run to write them.`);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
for (const [fullPath, content] of fileContents) {
|
|
271
|
+
fs_1.default.writeFileSync(fullPath, content, 'utf-8');
|
|
272
|
+
}
|
|
273
|
+
console.log(`${applied.length} fix(es) applied to ${fileContents.size} file(s), ${skipped.length} skipped.`);
|
|
274
|
+
console.log('Review the changes (e.g. `git diff`) before committing.');
|
|
275
|
+
// Applied entries are now baked into source — clearing avoids re-applying (harmlessly, since a
|
|
276
|
+
// second pass would just fail to find the old call text, but noisily) or double-counting them if
|
|
277
|
+
// apply-heals is run again before the next test run produces new entries. Archived first (using
|
|
278
|
+
// the same label as this run's reports, so all three correlate by filename) so clearing doesn't
|
|
279
|
+
// also erase the only record of what heals.jsonl actually contained.
|
|
280
|
+
//
|
|
281
|
+
// --logs-dir means the entries came from downloaded shard artifacts, not this checkout's own
|
|
282
|
+
// local heals.jsonl (there usually isn't one — this job never ran tests itself), so
|
|
283
|
+
// archiveHealLog would silently find nothing to copy. archiveMergedEntries writes what was
|
|
284
|
+
// actually merged instead, so the CI path gets the same raw-log archival as the local one.
|
|
285
|
+
if (logsDir) {
|
|
286
|
+
(0, heal_log_1.archiveMergedEntries)(rawEntries ?? [], label, process.cwd());
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
(0, heal_log_1.archiveHealLog)(label, process.cwd());
|
|
290
|
+
}
|
|
291
|
+
(0, heal_log_1.clearHealLog)();
|
|
292
|
+
}
|
|
293
|
+
//# sourceMappingURL=applyHeals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"applyHeals.js","sourceRoot":"","sources":["../../src/cli/applyHeals.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/index.js
CHANGED
|
@@ -2,20 +2,25 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
const doctor_1 = require("./doctor");
|
|
5
|
+
const applyHeals_1 = require("./applyHeals");
|
|
6
|
+
const USAGE = 'Usage: tamash-playwright doctor [--dir <path>] | apply-heals [--dry-run] [--logs-dir <path>]';
|
|
5
7
|
async function main() {
|
|
6
8
|
const [, , command, ...rest] = process.argv;
|
|
7
9
|
switch (command) {
|
|
8
10
|
case 'doctor':
|
|
9
11
|
await (0, doctor_1.runDoctor)(rest);
|
|
10
12
|
break;
|
|
13
|
+
case 'apply-heals':
|
|
14
|
+
await (0, applyHeals_1.runApplyHeals)(rest);
|
|
15
|
+
break;
|
|
11
16
|
case undefined:
|
|
12
17
|
case '--help':
|
|
13
18
|
case '-h':
|
|
14
|
-
console.log(
|
|
19
|
+
console.log(USAGE);
|
|
15
20
|
break;
|
|
16
21
|
default:
|
|
17
22
|
console.log(`Unknown command: ${command}`);
|
|
18
|
-
console.log(
|
|
23
|
+
console.log(USAGE);
|
|
19
24
|
process.exitCode = 1;
|
|
20
25
|
}
|
|
21
26
|
}
|
package/dist/cli/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";;;AACA,qCAAqC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";;;AACA,qCAAqC;AACrC,6CAA6C;AAE7C,MAAM,KAAK,GAAG,8FAA8F,CAAC;AAE7G,KAAK,UAAU,IAAI;IACjB,MAAM,CAAC,EAAE,AAAD,EAAG,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAE5C,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,QAAQ;YACX,MAAM,IAAA,kBAAS,EAAC,IAAI,CAAC,CAAC;YACtB,MAAM;QACR,KAAK,aAAa;YAChB,MAAM,IAAA,0BAAa,EAAC,IAAI,CAAC,CAAC;YAC1B,MAAM;QACR,KAAK,SAAS,CAAC;QACf,KAAK,QAAQ,CAAC;QACd,KAAK,IAAI;YACP,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACnB,MAAM;QACR;YACE,OAAO,CAAC,GAAG,CAAC,oBAAoB,OAAO,EAAE,CAAC,CAAC;YAC3C,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACnB,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;IACzB,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACrB,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SelectorSuggestion } from './providers/types';
|
|
2
|
+
export type HealLogEntry = {
|
|
3
|
+
timestamp: string;
|
|
4
|
+
file: string;
|
|
5
|
+
line: number;
|
|
6
|
+
action: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
suggestion: SelectorSuggestion;
|
|
9
|
+
};
|
|
10
|
+
export declare function parseSourceLocation(sourceLocation: string): {
|
|
11
|
+
file: string;
|
|
12
|
+
line: number;
|
|
13
|
+
} | undefined;
|
|
14
|
+
export declare function appendHealLogEntry(entry: HealLogEntry, cwd?: string): void;
|
|
15
|
+
export declare function readHealLog(cwd?: string): HealLogEntry[];
|
|
16
|
+
export declare function readHealLogsFromDir(dir: string): HealLogEntry[];
|
|
17
|
+
export declare function findCachedSuggestion(sourceLocation: string, cwd?: string): SelectorSuggestion | undefined;
|
|
18
|
+
export declare function archiveHealLog(label: string, cwd?: string): void;
|
|
19
|
+
export declare function archiveMergedEntries(entries: HealLogEntry[], label: string, cwd?: string): void;
|
|
20
|
+
export declare function clearHealLog(cwd?: string): void;
|
|
21
|
+
//# sourceMappingURL=heal-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heal-log.d.ts","sourceRoot":"","sources":["../../src/healer/heal-log.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAO5D,MAAM,MAAM,YAAY,GAAG;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,kBAAkB,CAAC;CAChC,CAAC;AAYF,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAYtG;AAKD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,GAAE,MAAsB,GAAG,IAAI,CAUzF;AAmBD,wBAAgB,WAAW,CAAC,GAAG,GAAE,MAAsB,GAAG,YAAY,EAAE,CAEvE;AAOD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,EAAE,CAe/D;AAQD,wBAAgB,oBAAoB,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,GAAE,MAAsB,GAAG,kBAAkB,GAAG,SAAS,CAiBxH;AAOD,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,GAAE,MAAsB,GAAG,IAAI,CAc/E;AAOD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,GAAE,MAAsB,GAAG,IAAI,CAc9G;AAED,wBAAgB,YAAY,CAAC,GAAG,GAAE,MAAsB,GAAG,IAAI,CAM9D"}
|