@tamsensedev/runner 0.1.0 → 0.2.1

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Read this when the project has no Playwright or Cypress tests you can run for the change, but you still need to see the pages: screenshots, console errors, failed requests, horizontal overflow on narrow screens, and optionally an axe scan.
4
4
 
5
- The script below starts the app with the project's own command on a free port, waits for it, opens each page headless at a desktop and a phone width, runs optional steps you write, saves screenshots and a JSON summary to a temp folder, and stops the app and all its child processes at the end.
5
+ The script below starts the app with the project's own command on a free port, waits for it, opens each page headless at a desktop and a phone width, runs optional steps you write, saves screenshots and a JSON summary to a temp folder, and stops the app and all its child processes at the end. In a TAMsense run each page and width is also recorded as a replay for the task, named from `CONFIG.title` and `CONFIG.purpose` (see [recording.md](recording.md)).
6
6
 
7
7
  ## What it needs
8
8
 
@@ -14,7 +14,7 @@ The script below starts the app with the project's own command on a free port, w
14
14
  ## How to use it
15
15
 
16
16
  1. Make a new folder in the OS temp directory (`node -p "require('os').tmpdir()"`), for example `<tmp>/ui-check-<random>`. Never inside the repository.
17
- 2. Write the script there as `ui-check.mjs` and edit only the `CONFIG` block and the `steps` function.
17
+ 2. Write the script there as `ui-check.mjs` and edit only the `CONFIG` block and the `steps` function. Set `title` and `purpose` to what this check is and why ("Feature testing, attempt 2", "Checking the empty orders state after the fix"), and `video: true` when the pages have a canvas, a map, a video player or iframes from another site.
18
18
  3. Run `node <tmp>/ui-check-<random>/ui-check.mjs` with a timeout of a few minutes. It prints the summary; screenshots are in `CONFIG.outDir`.
19
19
  4. Open the screenshots with the Read tool and look at them. Describe in the report what each one shows; the paths alone mean nothing to the reader.
20
20
  5. Delete the folder when the report is written, unless the task asks you to keep artifacts.
@@ -41,13 +41,18 @@ const CONFIG = {
41
41
  widths: [1280, 390],
42
42
  startTimeoutMs: 120_000,
43
43
  navigationTimeoutMs: 30_000,
44
+ title: 'Feature testing',
45
+ purpose: 'Checking the changed screens at desktop and phone width',
46
+ video: false,
44
47
  }
45
48
 
46
- async function steps(page, { path, width }) {
49
+ async function steps(page, { path, width, mark }) {
47
50
  }
48
51
 
49
52
  const MAX_ITEMS = 20
53
+ const MAX_TITLE = 60
50
54
  const serverOutput = []
55
+ const recorder = process.env.TAMSENSE_RECORDER ? createRequire(import.meta.url)(process.env.TAMSENSE_RECORDER) : null
51
56
 
52
57
  function keepTail(chunk) {
53
58
  serverOutput.push(...String(chunk).split('\n').filter(Boolean))
@@ -162,9 +167,18 @@ async function runAxe(page, axeSource) {
162
167
  }
163
168
  }
164
169
 
170
+ async function openContext(browser, path, width) {
171
+ const options = { viewport: { width, height: 900 } }
172
+ if (!recorder) return browser.newContext(options)
173
+ const where = `, ${path} at ${width}px`
174
+ const title = `${CONFIG.title.slice(0, MAX_TITLE - where.length)}${where}`
175
+ return recorder.newRecordedContext(browser, { title, purpose: CONFIG.purpose, video: CONFIG.video, context: options })
176
+ }
177
+
165
178
  async function checkPage(browser, url, path, width, axeSource) {
166
- const context = await browser.newContext({ viewport: { width, height: 900 } })
179
+ const context = await openContext(browser, path, width)
167
180
  const page = await context.newPage()
181
+ const mark = label => recorder?.mark(page, label)
168
182
  const result = { path, width, consoleErrors: [], pageErrors: [], failedRequests: [], badResponses: [] }
169
183
  const add = (list, item) => { if (list.length < MAX_ITEMS) list.push(item) }
170
184
  page.on('console', message => { if (message.type() === 'error') add(result.consoleErrors, message.text()) })
@@ -175,7 +189,8 @@ async function checkPage(browser, url, path, width, axeSource) {
175
189
  const response = await page.goto(url, { waitUntil: 'load', timeout: CONFIG.navigationTimeoutMs })
176
190
  result.status = response?.status() ?? null
177
191
  await page.waitForLoadState('networkidle', { timeout: 5_000 }).catch(() => {})
178
- result.steps = await steps(page, { path, width })
192
+ await mark(`Opened ${path}`)
193
+ result.steps = await steps(page, { path, width, mark })
179
194
  result.title = await page.title()
180
195
  result.finalUrl = page.url()
181
196
  result.horizontalOverflowPx = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)
@@ -186,7 +201,7 @@ async function checkPage(browser, url, path, width, axeSource) {
186
201
  } catch (error) {
187
202
  result.error = String(error.message).split('\n')[0]
188
203
  } finally {
189
- await context.close()
204
+ result.recording = recorder ? await recorder.closeRecorded(context) : await context.close()
190
205
  }
191
206
  return result
192
207
  }
@@ -236,15 +251,17 @@ main().catch(error => {
236
251
 
237
252
  ## Writing steps
238
253
 
239
- `steps` runs on every page and width before the screenshot. Use it to reach the state the task changed: open the dialog, fill and submit the form, switch the tab. Branch on `path` when pages need different steps. Whatever it returns is saved as `steps` in that page's summary (for example the focus order from a keyboard pass, see the `accessibility` skill). Use the same rules as in tests: role and label locators, web-first waits, no fixed sleeps (see [playwright-patterns.md](playwright-patterns.md)).
254
+ `steps` runs on every page and width before the screenshot. Use it to reach the state the task changed: open the dialog, fill and submit the form, switch the tab. Branch on `path` when pages need different steps. Call `await mark('...')` after each meaningful step: the marks show on the replay's timeline for the people on the task. Whatever it returns is saved as `steps` in that page's summary (for example the focus order from a keyboard pass, see the `accessibility` skill). Use the same rules as in tests: role and label locators, web-first waits, no fixed sleeps (see [playwright-patterns.md](playwright-patterns.md)).
240
255
 
241
256
  ```js
242
- async function steps(page, { path }) {
257
+ async function steps(page, { path, mark }) {
243
258
  if (path !== '/settings') return
244
259
  await page.getByRole('button', { name: 'Edit profile' }).click()
260
+ await mark('Opened the profile form')
245
261
  await page.getByLabel('Display name').fill('A very long display name that should wrap or truncate')
246
262
  await page.getByRole('button', { name: 'Save' }).click()
247
263
  await page.getByRole('status').waitFor()
264
+ await mark('Saved a long display name')
248
265
  }
249
266
  ```
250
267
 
@@ -253,6 +270,7 @@ Steps that write data only touch the local app and whatever local database it us
253
270
  ## Reading the summary
254
271
 
255
272
  - `server`: should be `stopped`. Anything else names a process you must stop yourself before finishing.
273
+ - `recording`: where each page's replay (and video) is. They are attached to the task when the run ends; `attach_replay` and `attach_video` attach one sooner.
256
274
  - `setupError`: the app or the browser did not start. Report it with the last lines of `serverOutputTail` (strip anything that looks like a secret). Do not retry with a different command unless the first one was clearly wrong, and never invent results.
257
275
  - `status` 4xx or 5xx, `pageErrors`, `consoleErrors`: real findings when they come from the changed code; noise from browser extensions does not exist here, but third-party scripts and dev-server warnings do. Say which is which.
258
276
  - `badResponses` and `failedRequests`: API calls the page needed that failed. A 401 or a redirect to a login page means the page needs a signed-in user (see the auth section of [playwright-patterns.md](playwright-patterns.md)).
@@ -1,6 +1,6 @@
1
1
  # Playwright patterns: locators, waiting, flakes, auth, mocking
2
2
 
3
- Read this when you write, fix or review browser tests or the `steps` of a headless check. Examples are Playwright Test in TypeScript; translate to the project's language and runner. If the project uses Cypress, the same ideas apply (see the last section). Check the installed version (`npx playwright --version`) before using an API you are not sure exists in it.
3
+ Read this when you write, fix or review browser tests or the `steps` of a headless check. In a TAMsense run every browser context, also those of the project's tests, is recorded for the task; scripts of your own name their recordings as [recording.md](recording.md) shows. Examples are Playwright Test in TypeScript; translate to the project's language and runner. If the project uses Cypress, the same ideas apply (see the last section). Check the installed version (`npx playwright --version`) before using an API you are not sure exists in it.
4
4
 
5
5
  ## Locators
6
6
 
@@ -0,0 +1,57 @@
1
+ # Recording the browser for the task
2
+
3
+ Read this before any script that opens a browser during a TAMsense run. People on the task watch what your browser did: every page is recorded as a replay (the DOM, clicks and typing, played back in the TAMsense player) and attached to the task when the run ends.
4
+
5
+ ## What happens by itself
6
+
7
+ - When the `TAMSENSE_RECORDER` environment variable is set, every Node process you start loads the TAMsense recorder, and every Playwright browser context is recorded, also the ones opened by the project's own E2E tests. You cannot switch this off, and you do not need to.
8
+ - Requests from the tested site to the TAMsense tracker (`/api/events` with a `?key=` or sent to the TAMsense server) are answered by the recorder and never leave the browser, so your runs do not reach the product's analytics.
9
+ - Password fields are masked in the replay.
10
+ - Recordings you did not attach yourself are attached when the run ends. Unnamed ones get the step name as their title, so name yours.
11
+
12
+ ## What you add in every browser script
13
+
14
+ Load the recorder and open each context through it, with a title and a purpose:
15
+
16
+ ```js
17
+ import { createRequire } from 'node:module'
18
+
19
+ const recorder = process.env.TAMSENSE_RECORDER ? createRequire(import.meta.url)(process.env.TAMSENSE_RECORDER) : null
20
+
21
+ const context = recorder
22
+ ? await recorder.newRecordedContext(browser, {
23
+ title: 'Checkout testing, attempt 2',
24
+ purpose: 'Paying with a saved card after the fix to the total',
25
+ context: { viewport: { width: 1280, height: 900 } },
26
+ })
27
+ : await browser.newContext({ viewport: { width: 1280, height: 900 } })
28
+ const page = await context.newPage()
29
+
30
+ await page.goto(url)
31
+ await recorder?.mark(page, 'Opened checkout')
32
+ await page.getByRole('button', { name: 'Pay' }).click()
33
+ await recorder?.mark(page, 'Paid, the receipt is shown')
34
+
35
+ const recorded = recorder ? await recorder.closeRecorded(context) : await context.close()
36
+ console.log(JSON.stringify(recorded, null, 2))
37
+ ```
38
+
39
+ - `title`: what it shows, at most 60 characters. Number design variants ("Design option 1", "Design option 2") and name test runs by their goal and attempt ("Feature testing", "Second attempt at feature testing"). Never generic names like "screenshot1" or "test".
40
+ - `purpose`: one line, at most 160 characters, on why it was made ("Comparing two layouts for the empty orders state").
41
+ - `mark(page, label)`: call it at each meaningful step (opened a screen, submitted a form, saw the error). Marks show on the replay's timeline, and a click on one jumps there.
42
+ - `closeRecorded(context)`: closes the context, finishes the files and returns where they are. Close every context this way, or at least with `context.close()`, before the script ends.
43
+ - Without `TAMSENSE_RECORDER` (not a TAMsense run) the script works as before.
44
+
45
+ ## When to add a video
46
+
47
+ A replay rebuilds the page from its DOM, so it cannot show pixels drawn by the page itself. Pass `video: true` to `newRecordedContext` as well when the screen has:
48
+
49
+ - a `<canvas>` (charts drawn on canvas, editors, games), a map, or a `<video>` player;
50
+ - iframes from another site (payment forms, embedded widgets);
51
+ - or when someone in the chat asks for a video.
52
+
53
+ `closeRecorded` warns when it saw a canvas, a video or a cross-origin iframe on a page recorded without video. Videos are larger (up to 200 MB each, 5 per run), so keep them short.
54
+
55
+ ## Attaching earlier
56
+
57
+ Everything is attached at the end of the run. To attach a recording sooner, or with a better title, call `attach_replay` (the `.rrweb.ndjson` file) or `attach_video` (the `.webm` file) with the path from `closeRecorded`, a title and a purpose. A run attaches at most 20 replays and 5 videos; the ones you named come first.
package/package.json CHANGED
@@ -1,45 +1,46 @@
1
- {
2
- "name": "@tamsensedev/runner",
3
- "type": "module",
4
- "version": "0.1.0",
5
- "description": "Connects your computer to TAMsense so it can work on your tasks",
6
- "author": "TAMsense",
7
- "license": "UNLICENSED",
8
- "homepage": "https://my.tamsense.com",
9
- "keywords": [
10
- "tamsense",
11
- "claude-code",
12
- "agents",
13
- "tasks"
14
- ],
15
- "publishConfig": {
16
- "access": "public"
17
- },
18
- "bin": {
19
- "tamsense-runner": "./dist/cli.js"
20
- },
21
- "files": [
22
- "dist",
23
- "library"
24
- ],
25
- "engines": {
26
- "node": ">=22"
27
- },
28
- "scripts": {
29
- "build": "tsup",
30
- "dev": "tsup --watch",
31
- "clean": "rimraf dist",
32
- "prepublishOnly": "tsup",
33
- "test": "node --experimental-strip-types --no-warnings --import ./test/resolve.mjs --test \"test/*.test.ts\""
34
- },
35
- "dependencies": {
36
- "@anthropic-ai/claude-agent-sdk": "^0.3.300",
37
- "zod": "^4.0.0"
38
- },
39
- "devDependencies": {
40
- "@types/node": "^22.0.0",
41
- "rimraf": "^6.0.1",
42
- "tsup": "^8.5.0",
43
- "typescript": "^5.5.4"
44
- }
45
- }
1
+ {
2
+ "name": "@tamsensedev/runner",
3
+ "type": "module",
4
+ "version": "0.2.1",
5
+ "description": "Connects your computer to TAMsense so it can work on your tasks",
6
+ "author": "TAMsense",
7
+ "license": "UNLICENSED",
8
+ "homepage": "https://my.tamsense.com",
9
+ "keywords": [
10
+ "tamsense",
11
+ "claude-code",
12
+ "agents",
13
+ "tasks"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "bin": {
19
+ "tamsense-runner": "./dist/cli.js"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "library"
24
+ ],
25
+ "engines": {
26
+ "node": ">=22"
27
+ },
28
+ "scripts": {
29
+ "build": "tsup",
30
+ "dev": "tsup --watch",
31
+ "clean": "rimraf dist",
32
+ "prepublishOnly": "tsup",
33
+ "test": "node --experimental-strip-types --no-warnings --import ./test/resolve.mjs --test \"test/*.test.ts\""
34
+ },
35
+ "dependencies": {
36
+ "@anthropic-ai/claude-agent-sdk": "^0.3.300",
37
+ "rrweb": "2.0.0-alpha.20",
38
+ "zod": "^4.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^22.0.0",
42
+ "rimraf": "^6.0.1",
43
+ "tsup": "^8.5.0",
44
+ "typescript": "^5.5.4"
45
+ }
46
+ }