diff --git a/README.md b/README.md index ee19407..178ce80 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ **Your tests have nine lives.** Self-healing QA for the coding-agent era, by [QualityMax](https://qualitymax.io). +![9lives healing a broken Playwright selector β€” offline, in seconds](demo/heal.gif) + +> A coding agent renamed a button, the test went red, `9l heal` read the live page, fixed the locator, re-ran it green, and showed the diff β€” no API key. [**Run the demo yourself β†’**](demo/) + Your coding agent shipped a change and your Playwright test went red? Don't rewrite it β€” resurrect it: ```bash diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000..afde46d --- /dev/null +++ b/demo/README.md @@ -0,0 +1,31 @@ +# 🐾 9lives demo β€” watch a test heal itself + +A coding agent renamed a button from **β€œSign In”** to **β€œSign in”**, and the +Playwright test went red. `9l heal` reads the live page, finds the element under +its new label, patches the spec, re-runs it green, and shows you the diff β€” all +**offline** (Tier 1, no API key). + +![9lives healing a broken selector](heal.gif) + +## Run it yourself (one command) + +```bash +pip install 9lives # or: uvx --from 9lives 9l ... +cd demo && ./heal.sh +``` + +`heal.sh` serves the tiny local page in [`site/`](site/index.html), then runs +`9l heal login.spec.js`. The spec ships **broken** on purpose: + +```js +await page.locator("text='Sign In'").click(); // page now says "Sign in" +``` + +You'll watch it become `text='Sign in'` and pass. `git checkout -- login.spec.js` +resets it to broken so you can run the demo again. + +## Regenerate the GIF + +```bash +./record.sh # asciinema + agg β†’ heal.gif (deps: asciinema, agg, 9l, node) +``` diff --git a/demo/heal.cast b/demo/heal.cast new file mode 100644 index 0000000..1aa0570 --- /dev/null +++ b/demo/heal.cast @@ -0,0 +1,9 @@ +{"version":3,"term":{"cols":80,"rows":24},"timestamp":1783634686,"idle_time_limit":1.2,"command":"bash /var/folders/nk/63t416311tx1dg1p62835pjr0000gn/T/tmp.5p4eFse3jD","env":{"SHELL":"/bin/zsh"}} +[0.119, "o", "\u001b[38;5;213m$ \u001b[1;38;5;51m9l heal login.spec.js\u001b[0m\r\n\r\n"] +[1.203, "o", "🐾 run 1/3: login.spec.js …\r\n"] +[39.687, "o", " failure: locator_not_found (selector: text='Sign In') β†’ tier1_auto\r\n"] +[0.003, "o", " tier1_auto: Replaced selector 'text='Sign In'' with 'text='Sign in'' (confidence 85%)\r\n"] +[0.000, "o", "🐾 run 2/3: login.spec.js …\r\n"] +[9.023, "o", "\r\n🐾 healed! (+1 / -1)\r\n\r\n--- a/login.spec.js\r\n+++ b/login.spec.js\r\n@@ -2,8 +2,8 @@\r\n \r\n // This test breaks the way real tests break: a coding agent tweaked the button\r\n // copy from \"Sign In\" to \"Sign in\", so this exact-text selector went stale.\r\n test('user can sign in', async ({ page }) => {\r\n await page.goto(process.env.DEMO_URL || 'http://localhost:8137');\r\n- await page.locator(\"text='Sign In'\").click(); // page now says \"Sign in\"\r\n+ await page.locator(\"text='Sign in'\").click(); // page now says \"Sign in\"\r\n await expect(page.locator('.ok')).toBeVisible();\r\n });\r\n\r\n"] +[0.000, "o", "🐾 applied. Your test came back to life.\r\n"] +[1.258, "x", "0"] diff --git a/demo/heal.gif b/demo/heal.gif new file mode 100644 index 0000000..3b4fd29 Binary files /dev/null and b/demo/heal.gif differ diff --git a/demo/heal.sh b/demo/heal.sh new file mode 100755 index 0000000..d749724 --- /dev/null +++ b/demo/heal.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# 🐾 9lives demo β€” break a selector, watch it heal itself. No API key needed +# (Tier 1 is fully offline). Requires: 9l on PATH (pip install 9lives) + Node.js. +set -euo pipefail +cd "$(dirname "$0")" +git checkout -- login.spec.js 2>/dev/null || true # reset to the "broken" state +python3 -m http.server 8137 --directory site >/dev/null 2>&1 & SRV=$! +trap 'kill "$SRV" 2>/dev/null || true' EXIT +sleep 1 +DEMO_URL="http://localhost:8137" 9l heal login.spec.js diff --git a/demo/login.spec.js b/demo/login.spec.js new file mode 100644 index 0000000..75481e0 --- /dev/null +++ b/demo/login.spec.js @@ -0,0 +1,9 @@ +const { test, expect } = require('@playwright/test'); + +// This test breaks the way real tests break: a coding agent tweaked the button +// copy from "Sign In" to "Sign in", so this exact-text selector went stale. +test('user can sign in', async ({ page }) => { + await page.goto(process.env.DEMO_URL || 'http://localhost:8137'); + await page.locator("text='Sign in'").click(); // page now says "Sign in" + await expect(page.locator('.ok')).toBeVisible(); +}); diff --git a/demo/record.sh b/demo/record.sh new file mode 100755 index 0000000..d996d70 --- /dev/null +++ b/demo/record.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Regenerate heal.gif from the demo using asciinema + agg. +# deps: asciinema, agg, 9l (or NINE="python3 -m ninelives.cli"), node +set -euo pipefail +cd "$(dirname "$0")" +NINE="${NINE:-9l}" +git checkout -- login.spec.js 2>/dev/null || true +python3 -m http.server 8137 --directory site >/dev/null 2>&1 & SRV=$! +trap 'kill "$SRV" 2>/dev/null || true' EXIT +sleep 1 +runner="$(mktemp)" +cat > "$runner" < + +Acme β€” Login + + +
+

Sign in to Acme

+ + + + +
βœ… Signed in
+
+