tamash-playwright 0.4.0 → 0.6.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.
Files changed (68) hide show
  1. package/.env.example +5 -0
  2. package/README.md +70 -1
  3. package/dist/bindings/index.d.ts.map +1 -1
  4. package/dist/bindings/index.js +3 -0
  5. package/dist/bindings/index.js.map +1 -1
  6. package/dist/bindings/locator.binding.d.ts +4 -3
  7. package/dist/bindings/locator.binding.d.ts.map +1 -1
  8. package/dist/bindings/locator.binding.js +197 -7
  9. package/dist/bindings/locator.binding.js.map +1 -1
  10. package/dist/cli/doctor.d.ts +2 -0
  11. package/dist/cli/doctor.d.ts.map +1 -0
  12. package/dist/cli/doctor.js +154 -0
  13. package/dist/cli/doctor.js.map +1 -0
  14. package/dist/cli/index.d.ts +3 -0
  15. package/dist/cli/index.d.ts.map +1 -0
  16. package/dist/cli/index.js +26 -0
  17. package/dist/cli/index.js.map +1 -0
  18. package/dist/cli/scanActionTimeout.d.ts +17 -0
  19. package/dist/cli/scanActionTimeout.d.ts.map +1 -0
  20. package/dist/cli/scanActionTimeout.js +47 -0
  21. package/dist/cli/scanActionTimeout.js.map +1 -0
  22. package/dist/cli/scanDescribe.d.ts +11 -0
  23. package/dist/cli/scanDescribe.d.ts.map +1 -0
  24. package/dist/cli/scanDescribe.js +84 -0
  25. package/dist/cli/scanDescribe.js.map +1 -0
  26. package/dist/healer/action-recovery.d.ts +14 -0
  27. package/dist/healer/action-recovery.d.ts.map +1 -0
  28. package/dist/healer/action-recovery.js +102 -0
  29. package/dist/healer/action-recovery.js.map +1 -0
  30. package/dist/healer/index.d.ts +10 -2
  31. package/dist/healer/index.d.ts.map +1 -1
  32. package/dist/healer/index.js +297 -72
  33. package/dist/healer/index.js.map +1 -1
  34. package/dist/healer/providers/anthropic.d.ts.map +1 -1
  35. package/dist/healer/providers/anthropic.js +70 -0
  36. package/dist/healer/providers/anthropic.js.map +1 -1
  37. package/dist/healer/providers/gemini.d.ts.map +1 -1
  38. package/dist/healer/providers/gemini.js +99 -0
  39. package/dist/healer/providers/gemini.js.map +1 -1
  40. package/dist/healer/providers/ollama.d.ts.map +1 -1
  41. package/dist/healer/providers/ollama.js +113 -0
  42. package/dist/healer/providers/ollama.js.map +1 -1
  43. package/dist/healer/providers/openai.d.ts.map +1 -1
  44. package/dist/healer/providers/openai.js +99 -0
  45. package/dist/healer/providers/openai.js.map +1 -1
  46. package/dist/healer/providers/prompt.d.ts +7 -1
  47. package/dist/healer/providers/prompt.d.ts.map +1 -1
  48. package/dist/healer/providers/prompt.js +95 -1
  49. package/dist/healer/providers/prompt.js.map +1 -1
  50. package/dist/healer/providers/types.d.ts +36 -0
  51. package/dist/healer/providers/types.d.ts.map +1 -1
  52. package/dist/healer/providers/vision-models.d.ts +3 -0
  53. package/dist/healer/providers/vision-models.d.ts.map +1 -0
  54. package/dist/healer/providers/vision-models.js +40 -0
  55. package/dist/healer/providers/vision-models.js.map +1 -0
  56. package/dist/healer/replay-action.d.ts +3 -0
  57. package/dist/healer/replay-action.d.ts.map +1 -0
  58. package/dist/healer/replay-action.js +83 -0
  59. package/dist/healer/replay-action.js.map +1 -0
  60. package/dist/healer/vision.d.ts +25 -0
  61. package/dist/healer/vision.d.ts.map +1 -0
  62. package/dist/healer/vision.js +75 -0
  63. package/dist/healer/vision.js.map +1 -0
  64. package/dist/index.d.ts +2 -2
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +2 -1
  67. package/dist/index.js.map +1 -1
  68. package/package.json +4 -1
package/.env.example CHANGED
@@ -6,6 +6,11 @@ HEALER_ENABLED=true
6
6
  # a failed action just fails normally, since there's no AI model configured to recover it.
7
7
  HEALER_PROVIDER=ollama
8
8
 
9
+ # Optional, off by default. If a locator gets healed but the action on it still fails (e.g. the
10
+ # element is covered or needs scrolling into view), the AI can pick a recovery tactic (scroll,
11
+ # force, wait, or dispatch the DOM event directly) instead of giving up immediately.
12
+ # HEALER_ACTION_RECOVERY_ENABLED=true
13
+
9
14
  # --- Ollama Cloud (https://ollama.com) ---
10
15
  OLLAMA_MODEL=gpt-oss:120b
11
16
  OLLAMA_API_KEY=
package/README.md CHANGED
@@ -10,6 +10,8 @@ Websites change often. A button gets renamed or moved, and your test can't find
10
10
 
11
11
  `tamash-playwright` fixes this automatically. When a test can't find an element, it asks an AI model to find it on the current page and tries again. If it succeeds, your test keeps going. If not, it fails normally, just like before.
12
12
 
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
+
13
15
  Here are the detailed steps to use this package.
14
16
 
15
17
  ## Step 1: Install it
@@ -37,6 +39,9 @@ HEALER_ENABLED=true
37
39
  # Pick one: ollama | openai | anthropic | gemini
38
40
  HEALER_PROVIDER=ollama
39
41
 
42
+ # Optional, off by default — see "Action recovery" below.
43
+ # HEALER_ACTION_RECOVERY_ENABLED=true
44
+
40
45
  # --- Ollama Cloud (https://ollama.com) ---
41
46
  OLLAMA_MODEL=gpt-oss:120b
42
47
  OLLAMA_API_KEY=
@@ -74,7 +79,41 @@ OLLAMA_API_KEY=paste_your_key_here
74
79
 
75
80
  That's all you need — no other variables required.
76
81
 
77
- ## Step 3: Use it in your tests
82
+ ### Important: set `actionTimeout` in your `playwright.config.ts`
83
+
84
+ By default, Playwright lets a broken locator retry silently for your *entire* test timeout before it ever throws an error — which means self-healing never gets a turn at all, since it only kicks in once an action actually fails. Set `actionTimeout` to something well below your test timeout so a broken locator fails fast, leaving real time for healing to run:
85
+
86
+ ```ts
87
+ export default defineConfig({
88
+ timeout: 60000, // your overall test timeout
89
+ use: {
90
+ actionTimeout: 8000, // must be comfortably less than the test timeout above
91
+ },
92
+ });
93
+ ```
94
+
95
+ Without this, healing attempts will show `stage=no_snapshot` in the console and never recover anything — not because healing failed, but because it never had time to run before the whole test was torn down.
96
+
97
+ ## Step 3: Check your setup
98
+
99
+ Run the built-in doctor command to confirm everything's wired up correctly before you rely on it:
100
+
101
+ ```sh
102
+ npx tamash-playwright doctor
103
+ ```
104
+
105
+ It checks:
106
+
107
+ 1. **AI connectivity** — confirms `HEALER_ENABLED`/`HEALER_PROVIDER` are set correctly and actually calls your configured provider to make sure the API key and model work.
108
+ 2. **`actionTimeout` configuration** — checks your `playwright.config.ts` for an `actionTimeout` set well below your test `timeout` (see above); flags it if missing or too close to the test timeout, since that silently starves self-healing of any time to run.
109
+ 3. **Action recovery status** — whether `HEALER_ACTION_RECOVERY_ENABLED` is on (see below).
110
+ 4. **Vision capability** — whether your configured model is expected to support the screenshot-based fallback (see below), based on its name.
111
+ 5. **Missing `.describe()` labels** — scans your test files (`tests/` by default, or pass `--dir <path>`) for locators that don't have a `.describe('...')` label, and flags the ones most worth fixing (raw CSS/XPath selectors first).
112
+ 6. **Locators written directly in test files** — flags any locator defined inline in a test rather than inside a Page Object class, which is a Playwright best practice regardless of self-healing: it keeps tests readable and means a UI change only needs a fix in one place.
113
+
114
+ If it finds issues, the fastest fix is to open the project in an AI coding assistant (Claude Code, Cursor, GitHub Copilot, etc.) and ask it to address what it flagged — add `.describe()` calls, or extract locators into Page Object classes. You can also add a standing rule to that assistant's instructions/skill file (e.g. `CLAUDE.md`, `.cursor/rules`, `.github/copilot-instructions.md`) so it follows both practices automatically on any new test code going forward.
115
+
116
+ ## Step 4: Use it in your tests
78
117
 
79
118
  Change one line at the top of your test file — everything else about how you write tests stays exactly the same:
80
119
 
@@ -129,6 +168,36 @@ test('login test using CSS Selectors', async ({ page }) => {
129
168
 
130
169
  This step is optional, but recommended — without it, the healer has to guess purely from a broken CSS selector, which gives it a lot less to work with.
131
170
 
171
+ ## What else it heals — no extra setup needed
172
+
173
+ Beyond a single broken `click`/`fill`/`getByRole` on the main page, all of this works automatically once you've done Steps 1–2:
174
+
175
+ - **Popups and extra tabs.** A page opened via `context.newPage()`, `window.open`, or a `target="_blank"` link is just as healing-aware as your main `page` — no manual wrapping needed.
176
+ - **Elements inside `<iframe>`s.** `page.frameLocator('#my-iframe')` and anything chained off it heals the same way, scoped correctly to the iframe's own document.
177
+ - **Most of the Playwright API surface**, not just clicks and fills — `check`, `selectOption`, `dragTo`, `dispatchEvent`, read methods like `textContent`/`getAttribute`/`isChecked`, `screenshot`, and more. Methods that can't be safely healed by guessing a replacement element (`dragTo`, `drop`) are still reported honestly on failure, they're just never silently retried with a different element.
178
+
179
+ ## When text alone isn't enough: vision fallback
180
+
181
+ Sometimes an element has nothing useful to match on by text — an icon-only button with no label, or several visually distinct elements that all look identical in the accessibility tree. If your configured model supports image input (e.g. `gpt-4o`, `claude-haiku-4-5`, `gemini-2.0-flash`), `tamash-playwright` automatically falls back to a screenshot-based search after the normal text-based attempt fails — no separate setup, it just uses the same provider and API key from Step 2. Run `npx tamash-playwright doctor` to check whether your configured model is expected to support this.
182
+
183
+ ## Action recovery (optional)
184
+
185
+ 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
+
187
+ ## Checking what actually happened
188
+
189
+ 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:
190
+
191
+ - An annotation on the test summarizing what happened, e.g. `Recovered using ollama:gpt-oss:120b (role:button:Submit)`.
192
+ - A JSON attachment with the full detail: which provider was used, whether the vision or action-recovery fallback was involved, the AI's suggested selector, token cost, and — if it didn't heal — which stage it stopped at (e.g. `ai_declined`, `replay_failed`).
193
+ - Exactly where in your own code the locator was created — a test file or a Page Object class, whichever it really is — so you know which line to go fix even if you never look at the healing report again.
194
+
195
+ The same detail is also printed to the console as it happens, one line per attempt:
196
+
197
+ ```
198
+ [self-healer] src/pages/loginpage.ts:11 — locator.fill "Username Textbox" -> HEALED [provider=ollama:gpt-oss:120b, vision=no, actionRecovery=no, suggested="role:textbox:Username", 620 tokens (489 input + 131 output)] — locator.fill: Timeout 8000ms exceeded.
199
+ ```
200
+
132
201
  ## License
133
202
 
134
203
  Free to use, including commercially. The source code may not be copied, modified, redistributed, or resold without prior written permission. See the LICENSE file included in this package for the full terms.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bindings/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,IAAI,EAAE,KAAK,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAGvE,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,EAAE,IAAI,CAAC;IAChB,UAAU,EAAE,KAAK,CAAC;CACnB,CAAC;AAEF,eAAO,MAAM,IAAI,0PAWf,CAAC;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bindings/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,IAAI,EAAE,KAAK,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAGvE,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,EAAE,IAAI,CAAC;IAChB,UAAU,EAAE,KAAK,CAAC;CACnB,CAAC;AAEF,eAAO,MAAM,IAAI,0PAcf,CAAC;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC"}
@@ -4,6 +4,9 @@ exports.expect = exports.test = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const locator_binding_1 = require("./locator.binding");
6
6
  exports.test = test_1.test.extend({
7
+ context: async ({ context }, use) => {
8
+ await use((0, locator_binding_1.bindContext)(context));
9
+ },
7
10
  page: async ({ page }, use) => {
8
11
  await use((0, locator_binding_1.bindPageActions)(page));
9
12
  },
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/bindings/index.ts"],"names":[],"mappings":";;;AAAA,2CAAuE;AACvE,uDAAsE;AAOzD,QAAA,IAAI,GAAG,WAAI,CAAC,MAAM,CAAkB;IAC/C,IAAI,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,EAAE;QAC5B,MAAM,GAAG,CAAC,IAAA,iCAAe,EAAC,IAAI,CAAC,CAAC,CAAC;IACnC,CAAC;IACD,SAAS,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,EAAE;QACjC,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC;IAClB,CAAC;IACD,UAAU,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,EAAE;QAClC,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC/B,MAAM,GAAG,CAAC,IAAA,kCAAgB,EAAC,KAAK,CAAC,CAAC,CAAC;IACrC,CAAC;CACF,CAAC,CAAC;AAEH,yCAA0C;AAAjC,8FAAA,MAAM,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/bindings/index.ts"],"names":[],"mappings":";;;AAAA,2CAAuE;AACvE,uDAAmF;AAOtE,QAAA,IAAI,GAAG,WAAI,CAAC,MAAM,CAAkB;IAC/C,OAAO,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,EAAE;QAClC,MAAM,GAAG,CAAC,IAAA,6BAAW,EAAC,OAAO,CAAC,CAAC,CAAC;IAClC,CAAC;IACD,IAAI,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,EAAE;QAC5B,MAAM,GAAG,CAAC,IAAA,iCAAe,EAAC,IAAI,CAAC,CAAC,CAAC;IACnC,CAAC;IACD,SAAS,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,EAAE;QACjC,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC;IAClB,CAAC;IACD,UAAU,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,EAAE;QAClC,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC/B,MAAM,GAAG,CAAC,IAAA,kCAAgB,EAAC,KAAK,CAAC,CAAC,CAAC;IACrC,CAAC;CACF,CAAC,CAAC;AAEH,yCAA0C;AAAjC,8FAAA,MAAM,OAAA"}
@@ -1,8 +1,9 @@
1
- import type { Frame, Locator, Page } from '@playwright/test';
2
- type BindingKind = 'locator' | 'page' | 'frame';
3
- export declare function bindTargetActions<T extends Locator | Page | Frame>(target: T, kind?: BindingKind, description?: string): T;
1
+ import type { BrowserContext, Frame, FrameLocator, Locator, Page } from '@playwright/test';
2
+ type BindingKind = 'locator' | 'page' | 'frame' | 'frameLocator';
3
+ export declare function bindTargetActions<T extends Locator | Page | Frame | FrameLocator>(target: T, kind?: BindingKind, description?: string, frameScope?: FrameLocator, selector?: string, callSite?: Error): T;
4
4
  export declare function bindLocatorActions(locator: Locator): Locator;
5
5
  export declare function bindPageActions(page: Page): Page;
6
6
  export declare function bindFrameActions(frame: Frame): Frame;
7
+ export declare function bindContext(context: BrowserContext): BrowserContext;
7
8
  export {};
8
9
  //# sourceMappingURL=locator.binding.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"locator.binding.d.ts","sourceRoot":"","sources":["../../src/bindings/locator.binding.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAsC7D,KAAK,WAAW,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC;AA6BhD,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,OAAO,GAAG,IAAI,GAAG,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,GAAE,WAAuB,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,CAAC,CAwErI;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAE5D;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAEhD;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,CAEpD"}
1
+ {"version":3,"file":"locator.binding.d.ts","sourceRoot":"","sources":["../../src/bindings/locator.binding.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AA2E3F,KAAK,WAAW,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,cAAc,CAAC;AA8EjE,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,OAAO,GAAG,IAAI,GAAG,KAAK,GAAG,YAAY,EAC/E,MAAM,EAAE,CAAC,EACT,IAAI,GAAE,WAAuB,EAC7B,WAAW,CAAC,EAAE,MAAM,EAKpB,UAAU,CAAC,EAAE,YAAY,EAMzB,QAAQ,CAAC,EAAE,MAAM,EAKjB,QAAQ,CAAC,EAAE,KAAK,GACf,CAAC,CA2HH;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAE5D;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAEhD;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,CAEpD;AAYD,wBAAgB,WAAW,CAAC,OAAO,EAAE,cAAc,GAAG,cAAc,CAmCnE"}
@@ -1,9 +1,14 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.bindTargetActions = bindTargetActions;
4
7
  exports.bindLocatorActions = bindLocatorActions;
5
8
  exports.bindPageActions = bindPageActions;
6
9
  exports.bindFrameActions = bindFrameActions;
10
+ exports.bindContext = bindContext;
11
+ const path_1 = __importDefault(require("path"));
7
12
  const healer_1 = require("../healer");
8
13
  const ACTIONS = [
9
14
  'click',
@@ -18,8 +23,40 @@ const ACTIONS = [
18
23
  'selectOption',
19
24
  'setInputFiles',
20
25
  'dragTo',
26
+ // `drop` validates its `{files, data}` payload *before* ever touching the locator (confirmed:
27
+ // an empty payload throws "At least one of files or data must be provided" instantly, with no
28
+ // "Timeout ... exceeded" resolving-the-locator phase at all), so like `dragTo` it's caught and
29
+ // reported here but excluded from replayAction — a two-sided drag/drop pairing can't be safely
30
+ // healed by guessing a substitute locator for just one side of it.
31
+ 'drop',
21
32
  'focus',
22
33
  'blur',
34
+ 'type',
35
+ 'pressSequentially',
36
+ 'setChecked',
37
+ 'scrollIntoViewIfNeeded',
38
+ 'dispatchEvent',
39
+ 'waitFor',
40
+ // Read methods below don't act on the page — they raise when the locator can't be resolved
41
+ // (confirmed empirically: they throw the same "Timeout ... exceeded" shape as click/fill do
42
+ // against a zero-match locator), so a broken selector here is just as healable as a broken
43
+ // selector on a click. `isVisible`/`isHidden`/`count`/`all`/`allInnerTexts`/`allTextContents`
44
+ // are deliberately excluded — confirmed they degrade gracefully (false/0/[]) instead of
45
+ // throwing, so there's no exception here for the healer to ever catch.
46
+ 'textContent',
47
+ 'innerText',
48
+ 'innerHTML',
49
+ 'getAttribute',
50
+ 'inputValue',
51
+ 'isChecked',
52
+ 'isEnabled',
53
+ 'isDisabled',
54
+ 'isEditable',
55
+ 'selectText',
56
+ 'screenshot',
57
+ 'boundingBox',
58
+ 'elementHandle',
59
+ 'ariaSnapshot',
23
60
  ];
24
61
  const LOCATOR_FACTORY_METHODS = [
25
62
  'locator',
@@ -31,6 +68,10 @@ const LOCATOR_FACTORY_METHODS = [
31
68
  'getByTitle',
32
69
  'getByTestId',
33
70
  ];
71
+ // Handled separately from LOCATOR_FACTORY_METHODS: frameLocator() returns a FrameLocator, not a
72
+ // Locator, so it needs its own BindingKind and its own frameScope propagation (see below) rather
73
+ // than being folded into the generic factory-method branch.
74
+ const FRAME_LOCATOR_METHOD = 'frameLocator';
34
75
  // Methods that return a *new* Locator derived from the current one. Playwright's own
35
76
  // implementations are immutable (they don't mutate `target`), so if we don't intercept these
36
77
  // and re-wrap the result, the returned Locator silently loses its healing Proxy. This mattered
@@ -58,23 +99,111 @@ function describeFactoryCall(prop, args) {
58
99
  return `testid "${String(first)}"`;
59
100
  case 'locator':
60
101
  return `selector "${String(first)}"`;
102
+ case 'frameLocator':
103
+ return `iframe "${String(first)}"`;
61
104
  default:
62
105
  return undefined;
63
106
  }
64
107
  }
65
- function bindTargetActions(target, kind = 'locator', description) {
66
- return new Proxy(target, {
108
+ // Resolves the real, human-meaningful call site of a locator — the line in the consumer's own
109
+ // test file or Page Object that actually wrote `.locator(...)`/`.getByRole(...)`/etc. — from an
110
+ // Error captured at that exact call. Only ever invoked on a genuine failure (see the ACTIONS
111
+ // branch below), never on every locator creation: constructing an Error is cheap, but reading
112
+ // `.stack` is not (V8 computes it lazily on first access), so this cost is deliberately deferred
113
+ // until it's actually needed.
114
+ //
115
+ // Filters by checking whether a frame's file path contains "locator.binding" rather than an exact
116
+ // path/`__filename` comparison — verified empirically that this matters: Node resolves stack
117
+ // frames through this package's own source maps, so a frame belonging to this file shows up as
118
+ // `.../src/bindings/locator.binding.ts` in the formatted stack string even when the code actually
119
+ // executing is the compiled `dist/bindings/locator.binding.js` — an exact `__filename` compare
120
+ // would silently never match and filter nothing out.
121
+ function resolveCallerLocation(callSite) {
122
+ const stack = callSite.stack;
123
+ if (!stack) {
124
+ return undefined;
125
+ }
126
+ for (const line of stack.split('\n')) {
127
+ if (line.includes('locator.binding')) {
128
+ continue;
129
+ }
130
+ const match = line.match(/^\s*at\s+(?:.*\s+\()?(.+):(\d+):(\d+)\)?\s*$/);
131
+ if (!match) {
132
+ continue;
133
+ }
134
+ const [, filePath, lineNumber] = match;
135
+ return `${path_1.default.relative(process.cwd(), filePath)}:${lineNumber}`;
136
+ }
137
+ return undefined;
138
+ }
139
+ // Marks every proxy this module creates so a target that's already wrapped is never wrapped
140
+ // again. This matters concretely for the primary test `page`: Playwright's own built-in `page`
141
+ // fixture is implemented as `context.newPage()`, and since bindContext() (below) already wraps
142
+ // whatever newPage() returns, the fixture override in bindings/index.ts receives an
143
+ // already-wrapped page as its `page` dependency. Without this check, wrapping it a second time
144
+ // would nest two independent healing layers around the same actions — every failed action would
145
+ // then be caught and reported twice (the inner layer reports, rethrows; the outer layer catches
146
+ // that rethrow and reports again), even though only one real action ever ran. Keying by the
147
+ // object actually passed in (rather than tracking raw targets) is what makes this work: the
148
+ // second call receives the *proxy* from the first call, not the original raw object, so a
149
+ // raw-object cache would miss it.
150
+ const knownProxies = new WeakSet();
151
+ function bindTargetActions(target, kind = 'locator', description,
152
+ // The owning FrameLocator, when `target` (or anything derived from it via chaining/factory
153
+ // calls) lives inside an <iframe> — threaded through to healActionFailure so the healer
154
+ // snapshots and rebuilds a replacement locator *inside that iframe*, not the outer page where
155
+ // the element doesn't exist. ariaSnapshot() never descends into iframes on its own.
156
+ frameScope,
157
+ // The selector text captured once at the factory call (e.g. `selector "#foo"`), tracked
158
+ // independently of `description` so a later `.describe("Submit button")` overrides only the
159
+ // human-readable label without erasing what the original selector actually was — both are worth
160
+ // showing the AI/report, since "Submit button (selector \"#foo\")" is more useful for debugging
161
+ // a bad selector than either alone.
162
+ selector,
163
+ // An Error captured at the factory call that created this Locator — its birthplace, not
164
+ // wherever it's later chained/acted on. Threaded through exactly like `selector`: freshly
165
+ // captured on every factory call, propagated unchanged through CHAIN_METHODS. Only ever turned
166
+ // into an actual file:line (via resolveCallerLocation) on a genuine failure, not eagerly here.
167
+ callSite) {
168
+ if (knownProxies.has(target)) {
169
+ return target;
170
+ }
171
+ const proxy = new Proxy(target, {
67
172
  get(target, prop, receiver) {
68
173
  if (typeof prop === 'string') {
69
174
  const targetObject = target;
70
- if (kind !== 'locator' && LOCATOR_FACTORY_METHODS.includes(prop)) {
175
+ if (prop === FRAME_LOCATOR_METHOD) {
176
+ return (...args) => {
177
+ const factory = targetObject[prop];
178
+ if (typeof factory !== 'function') {
179
+ throw new Error(`frameLocator() is not available on this ${kind}.`);
180
+ }
181
+ const childFrameLocator = factory.apply(target, args);
182
+ const frameSelector = describeFactoryCall(prop, args);
183
+ // The FrameLocator itself becomes the frameScope for everything resolved through it.
184
+ return bindTargetActions(childFrameLocator, 'frameLocator', frameSelector, childFrameLocator, frameSelector);
185
+ };
186
+ }
187
+ // Previously gated on `kind !== 'locator'`, which meant chaining a factory method off an
188
+ // already-obtained Locator (e.g. `container.getByRole('button')`, a common pattern for
189
+ // scoping a search) silently returned a raw, unwrapped Locator with no healing at all.
190
+ // Locator/Page/Frame/FrameLocator all expose these methods, so there's no kind this
191
+ // should be excluded for.
192
+ if (LOCATOR_FACTORY_METHODS.includes(prop)) {
71
193
  return (...args) => {
72
194
  const factory = targetObject[prop];
73
195
  if (typeof factory !== 'function') {
74
196
  throw new Error(`The locator factory "${String(prop)}" is not available on this ${kind}.`);
75
197
  }
76
198
  const locator = factory.apply(target, args);
77
- return bindTargetActions(locator, 'locator', describeFactoryCall(prop, args));
199
+ const newSelector = describeFactoryCall(prop, args);
200
+ // Captured fresh at every factory call, same as newSelector — this call is this
201
+ // Locator's birthplace, regardless of where `target` itself came from.
202
+ const newCallSite = new Error();
203
+ // description starts out equal to the selector text (nothing else to show yet); a
204
+ // later `.describe()` call overrides description independently, without touching
205
+ // `newSelector` — see the CHAIN_METHODS branch below.
206
+ return bindTargetActions(locator, 'locator', newSelector, frameScope, newSelector, newCallSite);
78
207
  };
79
208
  }
80
209
  if (kind === 'locator' && CHAIN_METHODS.includes(prop)) {
@@ -85,7 +214,11 @@ function bindTargetActions(target, kind = 'locator', description) {
85
214
  }
86
215
  const result = chainMethod.apply(target, args);
87
216
  const nextDescription = prop === 'describe' ? String(args[0] ?? '') : description;
88
- return bindTargetActions(result, 'locator', nextDescription);
217
+ // `selector` and `callSite` are deliberately NOT touched here, for any chain method
218
+ // including `describe()` — they always stay the original factory-call selector/site,
219
+ // so a human description added later doesn't erase where this Locator actually came
220
+ // from.
221
+ return bindTargetActions(result, 'locator', nextDescription, frameScope, selector, callSite);
89
222
  };
90
223
  }
91
224
  if (ACTIONS.includes(prop)) {
@@ -104,13 +237,27 @@ function bindTargetActions(target, kind = 'locator', description) {
104
237
  return await target[prop](...callArgs);
105
238
  }
106
239
  catch (error) {
240
+ // description defaults to the selector text at the factory call, so the two are
241
+ // identical until `.describe()` overrides description — only combine them once
242
+ // they've actually diverged, so a plain (never-described) locator still reports a
243
+ // single clean "selector \"#foo\"" instead of a redundant "selector \"#foo\"
244
+ // (selector \"#foo\")".
245
+ const effectiveDescription = description && selector && description !== selector
246
+ ? `${description} (${selector})`
247
+ : (description ?? selector ?? `${kind} action ${String(prop)}`);
107
248
  const healing = await (0, healer_1.healActionFailure)({
108
249
  action: String(prop),
109
250
  kind,
110
- description: description ?? `${kind} action ${String(prop)}`,
251
+ description: effectiveDescription,
111
252
  error,
112
- target,
253
+ // FrameLocator never reaches this branch (it has no ACTIONS methods, only the
254
+ // locator-factory ones handled above), so this narrowing is safe.
255
+ target: target,
113
256
  callArgs,
257
+ frameScope,
258
+ // Only resolved now, on a genuine failure — not at every locator creation. See
259
+ // resolveCallerLocation's own comment for why.
260
+ sourceLocation: callSite ? resolveCallerLocation(callSite) : undefined,
114
261
  });
115
262
  // NOTE: most Playwright actions (click/fill/check/hover/...) resolve to `undefined`
116
263
  // on success, so `healing.recovered` alone is the correct success signal here —
@@ -128,6 +275,8 @@ function bindTargetActions(target, kind = 'locator', description) {
128
275
  return typeof value === 'function' ? value.bind(target) : value;
129
276
  },
130
277
  });
278
+ knownProxies.add(proxy);
279
+ return proxy;
131
280
  }
132
281
  function bindLocatorActions(locator) {
133
282
  return bindTargetActions(locator, 'locator');
@@ -138,4 +287,45 @@ function bindPageActions(page) {
138
287
  function bindFrameActions(frame) {
139
288
  return bindTargetActions(frame, 'frame');
140
289
  }
290
+ // A second/third page (a popup, a manually-opened tab, target="_blank" navigation) is just as
291
+ // much "what the test did" as the primary page — but unlike Python's monkey-patching approach
292
+ // (where mutating the real instance means every reference to it sees the patch, however it was
293
+ // obtained), a Proxy only affects code that actually holds a reference to *that* Proxy. Passively
294
+ // listening via `context.on('page', ...)` and discarding the result would never reach the test:
295
+ // `const popup = await context.newPage()` returns Playwright's own raw object directly, bypassing
296
+ // any side-effecting listener entirely. So this wraps the *methods that hand pages back to the
297
+ // caller* instead — newPage(), pages(), and on('page'/'popup', handler) (rewriting the handler so
298
+ // whatever it's called with is the wrapped page, not the raw one) — which is what actually gets
299
+ // the healing-aware Proxy into the test's hands regardless of how it obtained the new page.
300
+ function bindContext(context) {
301
+ if (knownProxies.has(context)) {
302
+ return context;
303
+ }
304
+ const proxy = new Proxy(context, {
305
+ get(target, prop, receiver) {
306
+ if (prop === 'newPage') {
307
+ return async (...args) => {
308
+ const page = await target.newPage(...args);
309
+ return bindPageActions(page);
310
+ };
311
+ }
312
+ if (prop === 'pages') {
313
+ return () => target.pages().map((page) => bindPageActions(page));
314
+ }
315
+ if (prop === 'on' || prop === 'once') {
316
+ return (event, handler) => {
317
+ if (event === 'page' || event === 'popup') {
318
+ const wrappedHandler = (page) => handler(bindPageActions(page));
319
+ return target[prop](event, wrappedHandler);
320
+ }
321
+ return target[prop](event, handler);
322
+ };
323
+ }
324
+ const value = Reflect.get(target, prop, receiver);
325
+ return typeof value === 'function' ? value.bind(target) : value;
326
+ },
327
+ });
328
+ knownProxies.add(proxy);
329
+ return proxy;
330
+ }
141
331
  //# sourceMappingURL=locator.binding.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"locator.binding.js","sourceRoot":"","sources":["../../src/bindings/locator.binding.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"locator.binding.js","sourceRoot":"","sources":["../../src/bindings/locator.binding.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function runDoctor(args?: string[]): Promise<void>;
2
+ //# sourceMappingURL=doctor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":"AAqJA,wBAAsB,SAAS,CAAC,IAAI,GAAE,MAAM,EAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBlE"}
@@ -0,0 +1,154 @@
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.runDoctor = runDoctor;
7
+ const path_1 = __importDefault(require("path"));
8
+ const dotenv_1 = __importDefault(require("dotenv"));
9
+ const providers_1 = require("../healer/providers");
10
+ const healer_1 = require("../healer");
11
+ const scanDescribe_1 = require("./scanDescribe");
12
+ const scanActionTimeout_1 = require("./scanActionTimeout");
13
+ dotenv_1.default.config({ path: path_1.default.resolve(process.cwd(), '.env') });
14
+ const CONNECTIVITY_TIMEOUT_MS = 15000;
15
+ const CONNECTIVITY_SNAPSHOT = '- generic "tamash-playwright doctor check":\n - button "OK"';
16
+ async function checkProviderConnectivity() {
17
+ console.log(`HEALER_ENABLED: ${process.env.HEALER_ENABLED ?? '(unset — defaults to true)'}`);
18
+ if (!(0, healer_1.isHealingEnabled)()) {
19
+ console.log(' Healing is currently OFF. Set HEALER_ENABLED=true (or remove the line) to turn it back on.');
20
+ return;
21
+ }
22
+ console.log(`HEALER_ACTION_RECOVERY_ENABLED: ${process.env.HEALER_ACTION_RECOVERY_ENABLED ?? '(unset — defaults to false)'}`);
23
+ if ((0, healer_1.isActionRecoveryEnabled)()) {
24
+ console.log(' Action recovery is ON — if a locator heals but the action on it still fails, the AI may retry');
25
+ console.log(' it (scroll into view, force, wait, or dispatch the DOM event directly) before giving up.');
26
+ }
27
+ else {
28
+ console.log(' Action recovery is OFF (opt-in, off by default). Set HEALER_ACTION_RECOVERY_ENABLED=true to turn it on.');
29
+ }
30
+ console.log('');
31
+ const providerName = process.env.HEALER_PROVIDER;
32
+ if (!providerName) {
33
+ console.log('[WARN] HEALER_PROVIDER is not set. No AI provider is configured, so self-healing will never');
34
+ console.log(' recover a failed action — it will just fail normally.');
35
+ console.log(' Set HEALER_PROVIDER (ollama | openai | anthropic | gemini) plus its API key/model in .env.');
36
+ return;
37
+ }
38
+ const provider = (0, providers_1.getHealProvider)();
39
+ if (!provider) {
40
+ console.log(`[FAIL] HEALER_PROVIDER=${providerName}, but its required model/API key env vars are missing.`);
41
+ console.log(' Check the matching section for it in your .env file.');
42
+ return;
43
+ }
44
+ // Based on a model-name heuristic (see providers/vision-models.ts), not a live check — kept
45
+ // that way deliberately so `doctor` doesn't spend an extra API call confirming something the
46
+ // name pattern already answers; if the heuristic is ever wrong for a given model, that surfaces
47
+ // the first time healing actually needs the vision fallback, not here.
48
+ if (provider.supportsVision) {
49
+ console.log(`Vision-capable model: yes — ${provider.name} may be used for the screenshot-based fallback when text-based healing fails.`);
50
+ }
51
+ else {
52
+ console.log(`Vision-capable model: no — ${provider.name} does not look vision-capable by name, so the screenshot-based fallback will never be attempted.`);
53
+ }
54
+ console.log(`Testing connectivity to ${provider.name}...`);
55
+ const result = await provider.suggestSelector({
56
+ action: 'click',
57
+ description: 'tamash-playwright doctor connectivity check',
58
+ ariaSnapshot: CONNECTIVITY_SNAPSHOT,
59
+ timeoutMs: CONNECTIVITY_TIMEOUT_MS,
60
+ });
61
+ if (result) {
62
+ console.log(`[OK] Connected to ${provider.name} successfully.`);
63
+ }
64
+ else {
65
+ console.log(`[FAIL] Could not get a valid response from ${provider.name}.`);
66
+ console.log(' Check the warning above (if any), your API key, model name, and network connection.');
67
+ }
68
+ }
69
+ function checkDescribeUsage(occurrences) {
70
+ const missingDescribe = occurrences.filter((o) => !o.hasDescribe);
71
+ if (missingDescribe.length === 0) {
72
+ console.log('[OK] No locators found without a .describe() label.');
73
+ return;
74
+ }
75
+ console.log(`[WARN] Found ${missingDescribe.length} locator(s) without .describe():\n`);
76
+ for (const finding of missingDescribe) {
77
+ const tag = finding.priority === 'high' ? '[HIGH]' : '[LOW] ';
78
+ const relativeFile = path_1.default.relative(process.cwd(), finding.file);
79
+ console.log(` ${tag} ${relativeFile}:${finding.line} ${finding.snippet}`);
80
+ }
81
+ console.log('');
82
+ console.log('Recommendation: add .describe(\'...\') to these locators — especially the [HIGH] ones (raw CSS/XPath');
83
+ console.log('selectors), since those give the self-healer the least to go on if they break. Example:');
84
+ console.log('');
85
+ console.log(' page.locator(\'input[name="username"]\').describe(\'Username Textbox\')');
86
+ console.log('');
87
+ console.log('Fastest way to fix these: open this project in an AI coding assistant (Claude Code, Cursor, GitHub');
88
+ console.log('Copilot, etc.) and ask it to add .describe() to the locators flagged above.');
89
+ console.log('');
90
+ console.log('To make this automatic going forward, add a standing rule to your AI IDE\'s instructions/skill file');
91
+ console.log('(e.g. CLAUDE.md, .cursor/rules, .github/copilot-instructions.md) along the lines of:');
92
+ console.log(' "Whenever creating or editing a Playwright locator, chain .describe(\'...\') onto it with a short,');
93
+ console.log(' human-readable label of what the element is."');
94
+ }
95
+ // Any locator this scan finds lives in a *.spec.ts/*.test.ts file by construction (that's the
96
+ // glob it scans), which means it's written directly into the test rather than inside a Page
97
+ // Object — hence this fires whenever there's at least one occurrence, regardless of .describe().
98
+ function checkPageObjectUsage(occurrences) {
99
+ if (occurrences.length === 0) {
100
+ return;
101
+ }
102
+ const fileCount = new Set(occurrences.map((o) => o.file)).size;
103
+ console.log('');
104
+ console.log(`[INFO] Found ${occurrences.length} locator(s) defined directly inside ${fileCount} test file(s).`);
105
+ console.log(' Best practice: don\'t write locators straight into your tests — move them into Page Object');
106
+ console.log(' classes instead (e.g. a pages/LoginPage.ts with a login() method that internally holds the');
107
+ console.log(' page.locator(...) calls). Tests then call page-object methods, not raw locators. This keeps');
108
+ console.log(' tests readable and means a UI change only needs a fix in one place.');
109
+ }
110
+ function checkActionTimeoutSetup() {
111
+ const result = (0, scanActionTimeout_1.checkActionTimeout)();
112
+ switch (result.status) {
113
+ case 'no_config':
114
+ console.log('[INFO] Could not find a playwright.config.(ts|js|mjs|cjs) in the current directory to check.');
115
+ console.log(' Make sure `actionTimeout` (inside `use: {...}`) is set well below your test `timeout` —');
116
+ console.log(' without it, a broken locator can retry silently for the entire test timeout, leaving no');
117
+ console.log(' time for self-healing to run at all.');
118
+ return;
119
+ case 'not_set':
120
+ console.log(`[WARN] No \`actionTimeout\` found in ${result.configFile}.`);
121
+ console.log(' Without it, Playwright lets a broken locator retry for your whole test timeout before');
122
+ console.log(' ever throwing — self-healing never gets a turn. Add this to your `use: {...}` block:');
123
+ console.log(' actionTimeout: 8000, // comfortably less than your test timeout');
124
+ return;
125
+ case 'too_close':
126
+ console.log(`[WARN] ${result.configFile} sets actionTimeout: ${result.actionTimeout} >= timeout: ${result.testTimeout}.`);
127
+ console.log(' actionTimeout must be well below the overall test timeout, or self-healing still won\'t');
128
+ console.log(' have any time left to run once an action fails.');
129
+ return;
130
+ case 'ok':
131
+ console.log(`[OK] ${result.configFile} sets actionTimeout: ${result.actionTimeout}ms — self-healing has room to run.`);
132
+ return;
133
+ }
134
+ }
135
+ function parseTestDir(args) {
136
+ const flagIndex = args.indexOf('--dir');
137
+ const value = flagIndex !== -1 ? args[flagIndex + 1] : undefined;
138
+ return path_1.default.resolve(process.cwd(), value ?? 'tests');
139
+ }
140
+ async function runDoctor(args = []) {
141
+ console.log('tamash-playwright doctor\n');
142
+ await checkProviderConnectivity();
143
+ console.log('');
144
+ checkActionTimeoutSetup();
145
+ console.log('');
146
+ const testDir = parseTestDir(args);
147
+ const relativeDir = path_1.default.relative(process.cwd(), testDir) || testDir;
148
+ console.log(`Scanning ${relativeDir} for locators...`);
149
+ console.log('(static text scan, not a real parser — review these before acting on them)\n');
150
+ const occurrences = (0, scanDescribe_1.scanTestDirectory)(testDir);
151
+ checkDescribeUsage(occurrences);
152
+ checkPageObjectUsage(occurrences);
153
+ }
154
+ //# sourceMappingURL=doctor.js.map