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
Metadata
Release files for qapal 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| qapal-2.0.0.tar.gz | 200.1 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| qapal-2.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 361.6 kB
Release files / qapal-2.0.0.tar.gz
| Download URL | qapal-2.0.0.tar.gz |
|---|---|
| Size | 200.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7c1de21611b95ed6e81be94fec5a42efb01d6d43d4b473096d04f219c99d0e25
|
|
BLAKE2b-256 checksum How to use checksums |
faec04547d461c0a5a0b6a1acf15be0fd9fc22ddcca8327c287178ca7c9adf4c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 17, 2026.
Transparency logRelease files / qapal-2.0.0-py3-none-any.whl
| Download URL | qapal-2.0.0-py3-none-any.whl |
|---|---|
| Size | 161.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5a0d75a1a20d2d522ff96676eab7d859cc0246433a9cc782ed7211b3d5ff439e
|
|
BLAKE2b-256 checksum How to use checksums |
f1a67a3e4182b501464f903a273a6a1efcc911f0643d838442e64386599c6190
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 17, 2026.
Transparency logSecurity for qapal 2.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for qapal-2.0.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 17, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.