@q-agent/agent 0.1.0 → 0.1.6

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.
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
  /**
3
3
  * Port of `api/app/services/playwright_runner.py`'s config template
4
- * (`_PLAYWRIGHT_CONFIG_TEMPLATE` + `_write_config`) and the sessionStorage
5
- * fixture shim (`_auth_fixtures_ts` + `_apply_auth_fixtures`), reproduced
6
- * faithfully so a spec that runs on the server and one that runs via the
7
- * Local Agent behave identically.
4
+ * (`_PLAYWRIGHT_CONFIG_TEMPLATE` + `_write_config`) and the injected-fixtures
5
+ * shim (`_fixtures_ts` + `_apply_fixtures`), reproduced faithfully so a spec
6
+ * that runs on the server and one that runs via the Local Agent behave
7
+ * identically including always-on DOM capture (raw + distilled).
8
8
  */
9
9
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
10
  if (k2 === undefined) k2 = k;
@@ -41,8 +41,8 @@ var __importStar = (this && this.__importStar) || (function () {
41
41
  })();
42
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
43
  exports.writeConfig = writeConfig;
44
- exports.authFixturesTs = authFixturesTs;
45
- exports.applyAuthFixtures = applyAuthFixtures;
44
+ exports.fixturesTs = fixturesTs;
45
+ exports.applyFixtures = applyFixtures;
46
46
  const fs = __importStar(require("fs"));
47
47
  const path = __importStar(require("path"));
48
48
  const PLAYWRIGHT_CONFIG_TEMPLATE = `import { defineConfig } from '@playwright/test';
@@ -83,14 +83,37 @@ function writeConfig(specDir, workers, headless, baseUrl = "", storageState = ""
83
83
  fs.writeFileSync(path.join(specDir, "playwright.config.ts"), content, "utf-8");
84
84
  }
85
85
  /**
86
- * TypeScript for a generated `fixtures.ts` that replays sessionStorage
87
- * port of `_auth_fixtures_ts`.
86
+ * TypeScript for a generated `fixtures.ts` that captures the page DOM after
87
+ * every test, and optionally replays a captured sessionStorage — port of
88
+ * `_fixtures_ts`.
89
+ *
90
+ * The module re-exports Playwright's `test` extended with:
91
+ * - an `{auto: true}` fixture that, after each test, best-effort attaches the
92
+ * live page's raw HTML (`qagent-dom-raw`) and a distilled inventory of the
93
+ * page's interactable elements (`qagent-dom-distilled`) via `testInfo.attach`,
94
+ * so self-heal can ground on the real DOM. Wrapped in try/catch so it never
95
+ * fails a test.
96
+ * - (only when `replaySession`) a `context` override that restores the captured
97
+ * `sessionStorage` (MSAL/SPA tokens) for the current origin before app code runs.
88
98
  *
89
99
  * @param sessionFile Absolute path to the captured `sessionStorage.json`;
90
- * embedded as a JSON-encoded string literal so the fixture reads it at
91
- * runtime.
100
+ * embedded as a JSON-encoded string literal, read at runtime only when
101
+ * `replaySession` is set.
102
+ * @param replaySession Whether to inject the sessionStorage-replay `context`
103
+ * override (DOM capture is always injected regardless).
92
104
  */
93
- function authFixturesTs(sessionFile) {
105
+ function fixturesTs(sessionFile, replaySession) {
106
+ const contextFixture = replaySession
107
+ ? " context: async ({ context }, use) => {\n" +
108
+ " await context.addInitScript((sessions: Record<string, Record<string, string>>) => {\n" +
109
+ " try {\n" +
110
+ " const entries = sessions[location.origin];\n" +
111
+ " if (entries) for (const k in entries) window.sessionStorage.setItem(k, entries[k]);\n" +
112
+ " } catch {}\n" +
113
+ " }, SESSIONS);\n" +
114
+ " await use(context);\n" +
115
+ " },\n"
116
+ : "";
94
117
  return ("import { test as base, expect } from '@playwright/test';\n" +
95
118
  "import * as fs from 'fs';\n" +
96
119
  "\n" +
@@ -98,47 +121,71 @@ function authFixturesTs(sessionFile) {
98
121
  "let SESSIONS: Record<string, Record<string, string>> = {};\n" +
99
122
  "try { SESSIONS = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8')); } catch {}\n" +
100
123
  "\n" +
101
- "export const test = base.extend({\n" +
102
- " context: async ({ context }, use) => {\n" +
103
- " await context.addInitScript((sessions: Record<string, Record<string, string>>) => {\n" +
104
- " try {\n" +
105
- " const entries = sessions[location.origin];\n" +
106
- " if (entries) for (const k in entries) window.sessionStorage.setItem(k, entries[k]);\n" +
107
- " } catch {}\n" +
108
- " }, SESSIONS);\n" +
109
- " await use(context);\n" +
110
- " },\n" +
124
+ "export const test = base.extend<{ _domCapture: void }>({\n" +
125
+ contextFixture +
126
+ " // Always-on DOM capture: after each test, snapshot the live page so the\n" +
127
+ " // runner (evidence) and self-heal loop (real selectors) can use it.\n" +
128
+ " _domCapture: [async ({ page }, use, testInfo) => {\n" +
129
+ " await use();\n" +
130
+ " try {\n" +
131
+ " const raw = await page.content();\n" +
132
+ " const rawPath = testInfo.outputPath('qagent-dom-raw.html');\n" +
133
+ " fs.writeFileSync(rawPath, raw, 'utf-8');\n" +
134
+ " await testInfo.attach('qagent-dom-raw', { path: rawPath, contentType: 'text/html' });\n" +
135
+ " } catch {}\n" +
136
+ " try {\n" +
137
+ " const snapshot = await page.evaluate(() => {\n" +
138
+ " const SEL = 'a,button,input,select,textarea,[role],[data-testid],[data-test],[id]';\n" +
139
+ " const elements = Array.from(document.querySelectorAll(SEL)).slice(0, 400).map((node) => {\n" +
140
+ " const el = node as HTMLElement;\n" +
141
+ " const text = (el.innerText || '').trim().slice(0, 80);\n" +
142
+ " return {\n" +
143
+ " tag: el.tagName.toLowerCase(),\n" +
144
+ " role: el.getAttribute('role') || undefined,\n" +
145
+ " testId: el.getAttribute('data-testid') || el.getAttribute('data-test') || undefined,\n" +
146
+ " id: el.id || undefined,\n" +
147
+ " name: el.getAttribute('name') || undefined,\n" +
148
+ " text: text || undefined,\n" +
149
+ " placeholder: el.getAttribute('placeholder') || undefined,\n" +
150
+ " type: el.getAttribute('type') || undefined,\n" +
151
+ " };\n" +
152
+ " });\n" +
153
+ " return { path: location.pathname, url: location.href, elements };\n" +
154
+ " });\n" +
155
+ " const distilledPath = testInfo.outputPath('qagent-dom-distilled.json');\n" +
156
+ " fs.writeFileSync(distilledPath, JSON.stringify(snapshot), 'utf-8');\n" +
157
+ " await testInfo.attach('qagent-dom-distilled', { path: distilledPath, contentType: 'application/json' });\n" +
158
+ " } catch {}\n" +
159
+ " }, { auto: true }],\n" +
111
160
  "});\n" +
112
161
  "\n" +
113
162
  "export { expect };\n" +
114
163
  "export type * from '@playwright/test';\n");
115
164
  }
116
165
  /**
117
- * Rewrite each spec's Playwright import to use (or stop using) the
118
- * sessionStorage fixture — port of `_apply_auth_fixtures`. Only the module
119
- * specifier is touched.
166
+ * Point each spec's Playwright import at the generated `fixtures.ts` and write
167
+ * it — port of `_apply_fixtures`. Only the module specifier is touched.
168
+ *
169
+ * DOM capture is always on, so fixtures are ALWAYS injected (unlike the previous
170
+ * auth-only behavior): each spec's `'@playwright/test'` import is rewritten to
171
+ * `'./fixtures'` and `fixtures.ts` is (re)written every run. `replaySession` only
172
+ * controls whether the generated module also replays the captured sessionStorage.
120
173
  *
121
- * The Python version globs `*.spec.ts` in `specDir`; the agent already
122
- * knows exactly which spec filenames it wrote for this job, so
123
- * `specFilenames` is passed explicitly instead of re-scanning the
124
- * directory (equivalent behavior, no extra glob dependency).
174
+ * The Python version globs `*.spec.ts` in `specDir`; the agent already knows
175
+ * exactly which spec filenames it wrote for this job, so `specFilenames` is
176
+ * passed explicitly instead of re-scanning the directory.
125
177
  *
126
178
  * @param specDir The job's local workdir containing the spec files.
127
179
  * @param specFilenames The spec filenames written into `specDir` for this job.
128
180
  * @param sessionFile Absolute path to the `sessionStorage.json` snapshot embedded
129
181
  * in the generated `fixtures.ts`.
130
- * @param enabled Whether sessionStorage replay is active for this run.
182
+ * @param replaySession Whether sessionStorage replay is active for this run.
131
183
  */
132
- function applyAuthFixtures(specDir, specFilenames, sessionFile, enabled) {
133
- const replacements = enabled
134
- ? [
135
- ["'@playwright/test'", "'./fixtures'"],
136
- ['"@playwright/test"', '"./fixtures"'],
137
- ]
138
- : [
139
- ["'./fixtures'", "'@playwright/test'"],
140
- ['"./fixtures"', '"@playwright/test"'],
141
- ];
184
+ function applyFixtures(specDir, specFilenames, sessionFile, replaySession) {
185
+ const replacements = [
186
+ ["'@playwright/test'", "'./fixtures'"],
187
+ ['"@playwright/test"', '"./fixtures"'],
188
+ ];
142
189
  for (const filename of specFilenames) {
143
190
  const specPath = path.join(specDir, filename);
144
191
  let text;
@@ -156,7 +203,5 @@ function applyAuthFixtures(specDir, specFilenames, sessionFile, enabled) {
156
203
  fs.writeFileSync(specPath, newText, "utf-8");
157
204
  }
158
205
  }
159
- if (enabled) {
160
- fs.writeFileSync(path.join(specDir, "fixtures.ts"), authFixturesTs(sessionFile), "utf-8");
161
- }
206
+ fs.writeFileSync(path.join(specDir, "fixtures.ts"), fixturesTs(sessionFile, replaySession), "utf-8");
162
207
  }
@@ -22,6 +22,10 @@ const ATTACHMENT_KIND_MAP = {
22
22
  screenshot: "screenshot",
23
23
  video: "video",
24
24
  trace: "trace",
25
+ // DOM captured by the injected fixtures (see playwrightConfig.fixturesTs):
26
+ // raw page HTML + a distilled interactable-element inventory.
27
+ "qagent-dom-raw": "dom",
28
+ "qagent-dom-distilled": "dom-distilled",
25
29
  };
26
30
  /* eslint-enable @typescript-eslint/no-explicit-any */
27
31
  /**