Skip to main content

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)

Source distribution for qapal 2.0.0
File Size Uploaded Provenance
qapal-2.0.0.tar.gz 200.1 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for qapal 2.0.0
File Interpreter ABI Platform
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Release history Release notifications | RSS feed

This release

2.0.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page