Skip to main content

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

qapal-2.0.0.tar.gz (200.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

qapal-2.0.0-py3-none-any.whl (161.5 kB view details)

Uploaded Python 3

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

Hashes for qapal-2.0.0.tar.gz
Algorithm Hash digest
SHA256 7c1de21611b95ed6e81be94fec5a42efb01d6d43d4b473096d04f219c99d0e25
MD5 4e6a618df013eb293d5f9021655b435f
BLAKE2b-256 faec04547d461c0a5a0b6a1acf15be0fd9fc22ddcca8327c287178ca7c9adf4c

See more details on using hashes here.

Provenance

The following attestation bundles were made for qapal-2.0.0.tar.gz:

Publisher: ci.yml on ahmadsharabati/QAPAL

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for qapal-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5a0d75a1a20d2d522ff96676eab7d859cc0246433a9cc782ed7211b3d5ff439e
MD5 182f715cfab1443fde8dbbcb2c877fed
BLAKE2b-256 f1a67a3e4182b501464f903a273a6a1efcc911f0643d838442e64386599c6190

See more details on using hashes here.

Provenance

The following attestation bundles were made for qapal-2.0.0-py3-none-any.whl:

Publisher: ci.yml on ahmadsharabati/QAPAL

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page