Locator intelligence engine for Playwright — analyze, fix, and heal test selectors
Project description
QAPAL
Locator intelligence engine for Playwright. Analyze, fix, and heal broken test selectors — automatically.
pip install qapal
playwright install chromium
The Problem
Playwright tests break when selectors go stale. A renamed CSS class, a removed data-testid, a changed button label — and your entire CI pipeline goes red. Teams spend hours manually hunting down which selector broke and what to replace it with.
The Solution
QAPAL probes your selectors against the live page, scores them by resilience, and replaces weak ones with validated alternatives. No AI in the loop at runtime. Pure locator resolution + scoring.
# Find every weak selector in your test suite
qapal analyze tests/ --url https://staging.myapp.com
# Auto-fix them
qapal fix tests/ --url https://staging.myapp.com --apply
# CI self-healing: tests fail -> QAPAL patches -> opens a PR
qapal heal --test-results results.json --url https://staging.myapp.com --pr
Quick Start
Analyze selector health
$ qapal analyze tests/login.spec.ts --url https://myapp.com/login
File Line Type Value Found Grade
-------------------------------------------------------------------------------------------------
login.spec.ts 9 placeholder Email address YES [A - 0.83]
login.spec.ts 10 placeholder Password YES [A - 0.83]
login.spec.ts 12 css .btn-submit YES [B - 0.70]
login.spec.ts 15 testid nonexistent NO [F - 0.00]
--- Summary ---
Total: 4 | Strong: 2 | Weak: 1 | Broken: 1
Grades: A (>0.8) rock-solid, B (>0.6) acceptable, C (>0.4) fragile, D/F replace immediately.
Fix weak selectors
$ qapal fix tests/login.spec.ts --url https://myapp.com/login --dry-run
Found 1 selector replacement(s):
login.spec.ts:12 page.locator(".btn-submit") -> page.getByRole("button", { name: "Sign In" })
[A - 0.88] Replaced css with role (confidence: 0.88)
--- Diff Preview (--dry-run) ---
--- a/login.spec.ts
+++ b/login.spec.ts
@@ -9,7 +9,7 @@
await page.getByPlaceholder('Email address').fill('user@test.com');
await page.getByPlaceholder('Password').fill('secret');
- await page.locator('.btn-submit').click();
+ await page.getByRole('button', { name: 'Sign In' }).click();
Happy with it? Apply:
qapal fix tests/login.spec.ts --url https://myapp.com/login --apply
Or send a PR directly:
qapal fix tests/ --url https://myapp.com --pr
Probe a single selector
$ qapal probe "page.getByTestId('email')" --url https://myapp.com/login
Selector: page.getByTestId('email')
Type: testid
Value: email
Probing https://myapp.com/login...
Found: YES
Count: 1
Visible: True
Enabled: True
In viewport: True
Confidence: [A - 0.95]
Strategy: testid
Generate a test scaffold
$ qapal generate --url https://myapp.com/login --language python
Probing https://myapp.com/login...
Discovered 6 interactive elements.
Scaffold written to: tests/generated/test_login.py
Output:
"""Auto-generated scaffold by QAPAL"""
from playwright.sync_api import Page, expect
# === Validated elements on https://myapp.com/login ===
#
# Textbox "Email" -> page.get_by_test_id("email") [A - 0.95]
# Textbox "Password" -> page.get_by_test_id("password") [A - 0.95]
# Button "Sign In" -> page.get_by_role("button", name=...) [A - 0.88]
# Link "Forgot password" -> page.get_by_role("link", name=...) [B - 0.72]
def test_login(page: Page):
page.goto("https://myapp.com/login", wait_until="domcontentloaded")
# TODO: Write your test logic using the validated selectors above
pass
CI Self-Healing
# In your CI pipeline, after tests fail:
qapal heal --test-results results.json --url $STAGING_URL --pr
QAPAL reads the failure report, finds which selectors broke, probes for working alternatives, patches the files, and opens a PR.
How It Works
QAPAL has a 4-step locator resolution chain:
1. DB chain lookup (cached selectors from previous crawls)
2. Primary selector (the one in your test file)
3. Fallback selector (testid-prefix matching, OR-locator for testid variants)
4. AI rediscovery (one-shot AI call using accessibility snapshot -- optional)
Scoring Model
Each selector gets a confidence score (0.0 - 1.0) based on weighted factors:
| Factor | Weight | What it measures |
|---|---|---|
| Strategy | 35% | testid > role > text > css |
| Uniqueness | 30% | Does it match exactly 1 element? |
| Visibility | 15% | Is the element visible and in viewport? |
| Interactability | 10% | Is the element enabled? |
| History | 10% | Past success/failure rate |
Strategy scores:
| Strategy | Score | Why |
|---|---|---|
testid |
1.0 | Explicit test contract, never changes accidentally |
id |
0.9 | Stable but may conflict |
role |
0.8 | Semantic, accessible, resilient |
aria-label |
0.75 | Good but may be localized |
label |
0.7 | Tied to form structure |
placeholder |
0.65 | Can change with UX copy |
text |
0.5 | Fragile to copy changes |
css |
0.3 | Breaks on any style refactor |
xpath |
0.2 | Breaks on any DOM change |
CLI Reference
qapal analyze <files> --url <url> [--format table|json|github]
qapal fix <files> --url <url> [--dry-run|--apply|--pr] [--min-confidence 0.8]
qapal generate --url <url> [--output dir] [--language python|typescript]
qapal probe "<sel>" --url <url>
qapal heal --test-results <json> --url <url> [--pr]
Global Options
| Flag | Description |
|---|---|
--headless |
Run browser headlessly (default) |
--headed |
Show browser window |
--device |
Playwright device preset (e.g. "iPhone 12") |
--credentials-file |
JSON file with login credentials |
--timeout |
Action timeout in ms (default: 10000) |
--db-path |
Path to locator DB (default: locators.json) |
GitHub Actions Output
qapal analyze tests/ --url $STAGING_URL --format github
Outputs GitHub-compatible annotations:
::error file=tests/login.spec.ts,line=19::Broken selector: page.getByTestId('nonexistent') - element not found
::warning file=tests/login.spec.ts,line=12::Weak selector: page.locator('.btn') (confidence: 0.30)
GitHub Action
Add to your CI workflow:
name: Playwright Tests + QAPAL Healing
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: |
pip install qapal[ai]
playwright install chromium --with-deps
- name: Run Playwright tests
id: tests
continue-on-error: true
run: |
pytest tests/ --json-report --json-report-file=results.json
- name: QAPAL Analyze
if: always()
run: |
qapal analyze tests/ --url ${{ vars.STAGING_URL }} --format github
- name: QAPAL Heal (on failure)
if: steps.tests.outcome == 'failure'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
qapal heal --test-results results.json --url ${{ vars.STAGING_URL }} --pr
Authentication
For apps behind login, provide a credentials file:
{
"url": "https://myapp.com/login",
"username": "test@example.com",
"password": "testpass123",
"username_field": "email",
"password_field": "password",
"submit_button": "sign-in"
}
qapal analyze tests/ --url https://myapp.com/dashboard --credentials-file creds.json
Python + TypeScript
QAPAL parses both languages:
Python (pytest-playwright):
page.get_by_test_id("email")
page.get_by_role("button", name="Submit")
page.locator(".css-selector")
TypeScript (@playwright/test):
page.getByTestId('email')
page.getByRole('button', { name: 'Submit' })
page.locator('.css-selector')
Fixes are generated in the correct language for each file.
Installation
pip install qapal
playwright install chromium
Optional extras
pip install "qapal[ai]" # AI rediscovery (anthropic + openai)
pip install "qapal[all]" # Everything
From source
git clone https://github.com/ahmadsharabati/QAPAL.git
cd QAPAL
pip install -e ".[dev]"
playwright install chromium
Environment Variables
| Variable | Default | Description |
|---|---|---|
QAPAL_HEADLESS |
true |
Run browser headlessly |
QAPAL_DB_PATH |
locators.json |
Locator database path |
QAPAL_ACTION_TIMEOUT |
10000 |
Timeout per action (ms) |
QAPAL_AI_REDISCOVERY |
true |
Enable AI fallback for missing locators |
QAPAL_AI_PROVIDER |
anthropic |
AI provider: anthropic, openai, grok |
ANTHROPIC_API_KEY |
- | Required for AI rediscovery with Claude |
OPENAI_API_KEY |
- | Required for AI rediscovery with GPT-4 |
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file qapal-2.0.0.tar.gz.
File metadata
- Download URL: qapal-2.0.0.tar.gz
- Upload date:
- Size: 200.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7c1de21611b95ed6e81be94fec5a42efb01d6d43d4b473096d04f219c99d0e25
|
|
| MD5 |
4e6a618df013eb293d5f9021655b435f
|
|
| BLAKE2b-256 |
faec04547d461c0a5a0b6a1acf15be0fd9fc22ddcca8327c287178ca7c9adf4c
|
Provenance
The following attestation bundles were made for qapal-2.0.0.tar.gz:
Publisher:
ci.yml on ahmadsharabati/QAPAL
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
qapal-2.0.0.tar.gz -
Subject digest:
7c1de21611b95ed6e81be94fec5a42efb01d6d43d4b473096d04f219c99d0e25 - Sigstore transparency entry: 1117546789
- Sigstore integration time:
-
Permalink:
ahmadsharabati/QAPAL@8ddcd1a710c259cb32f7c5170b2a771065f4e500 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/ahmadsharabati
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@8ddcd1a710c259cb32f7c5170b2a771065f4e500 -
Trigger Event:
push
-
Statement type:
File details
Details for the file qapal-2.0.0-py3-none-any.whl.
File metadata
- Download URL: qapal-2.0.0-py3-none-any.whl
- Upload date:
- Size: 161.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5a0d75a1a20d2d522ff96676eab7d859cc0246433a9cc782ed7211b3d5ff439e
|
|
| MD5 |
182f715cfab1443fde8dbbcb2c877fed
|
|
| BLAKE2b-256 |
f1a67a3e4182b501464f903a273a6a1efcc911f0643d838442e64386599c6190
|
Provenance
The following attestation bundles were made for qapal-2.0.0-py3-none-any.whl:
Publisher:
ci.yml on ahmadsharabati/QAPAL
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
qapal-2.0.0-py3-none-any.whl -
Subject digest:
5a0d75a1a20d2d522ff96676eab7d859cc0246433a9cc782ed7211b3d5ff439e - Sigstore transparency entry: 1117546859
- Sigstore integration time:
-
Permalink:
ahmadsharabati/QAPAL@8ddcd1a710c259cb32f7c5170b2a771065f4e500 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/ahmadsharabati
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@8ddcd1a710c259cb32f7c5170b2a771065f4e500 -
Trigger Event:
push
-
Statement type: