Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 31 additions & 0 deletions demo/README.md
Original file line number Diff line number Diff line change
@@ -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)
```
9 changes: 9 additions & 0 deletions demo/heal.cast
Original file line number Diff line number Diff line change
@@ -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"]
Binary file added demo/heal.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions demo/heal.sh
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions demo/login.spec.js
Original file line number Diff line number Diff line change
@@ -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();
});
21 changes: 21 additions & 0 deletions demo/record.sh
Original file line number Diff line number Diff line change
@@ -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" <<INNER
printf '\033[38;5;213m\$ \033[1;38;5;51m9l heal login.spec.js\033[0m\n\n'
sleep 0.8
DEMO_URL=http://localhost:8137 $NINE heal --yes login.spec.js
sleep 1.2
INNER
asciinema record -c "bash $runner" --overwrite --idle-time-limit 1.2 heal.cast
agg --font-size 22 --theme asciinema heal.cast heal.gif
rm -f "$runner"
echo "wrote heal.gif"
20 changes: 20 additions & 0 deletions demo/site/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Acme — Login</title>
<style>
body{font-family:system-ui;background:#0b0f1a;color:#e6e9ef;display:grid;place-items:center;height:100vh;margin:0}
.card{background:#141a2b;padding:32px 40px;border-radius:14px;box-shadow:0 8px 40px #0006;width:280px}
h1{font-size:18px;margin:0 0 18px} input{width:100%;padding:10px;margin:6px 0;border-radius:8px;border:1px solid #2a3350;background:#0e1524;color:#e6e9ef;box-sizing:border-box}
button{width:100%;padding:11px;margin-top:12px;border:0;border-radius:8px;background:#7c5cff;color:#fff;font-weight:600;cursor:pointer}
.ok{margin-top:14px;color:#3ddc84;display:none}
</style></head>
<body>
<div class="card">
<h1>Sign in to Acme</h1>
<input id="email" placeholder="Email" value="ada@acme.dev">
<input id="password" type="password" placeholder="Password" value="hunter2">
<!-- The button copy is "Sign in" (lowercase i). The test looks for "Sign In". -->
<button id="submit" onclick="document.querySelector('.ok').style.display='block'">Sign in</button>
<div class="ok" role="status">✅ Signed in</div>
</div>
</body></html>
Loading