Testy samoleczenia z dramatopisarzem i sztuczną inteligencją: zmniejszają łuszczenie się o 70%
Najbardziej frustrującym problemem w utrzymaniu pakietów E2E jest testy wytrzymałościowe dla Drobne zmiany w interfejsie użytkownika: zmień identyfikator, przesuń przycisk, zmień nazwę klasy CSS i nagle codziennie 30 testów kończy się niepowodzeniem. Deweloper, który wprowadził zmianę musi ręcznie poprawiać każdy test – jest to powtarzalna praca, która pochłania wiele godzin sprintu i budzi ogólną niechęć do testów E2E.
I próba samoleczenia używają sztucznej inteligencji do automatycznego wykrywania selektora już nie działa i znajdź nowy poprawny element w bieżącym interfejsie użytkownika, aktualizując test bez interwencji człowieka. Dramaturg w 2026 roku zintegrował tę umiejętność poprzez swoje inteligentne lokalizatory oraz integracja z Playwright MCP.
Czego się nauczysz
- Dlaczego testy się psują i najczęstsze wzorce niepowodzeń
- Lokalizatory dramatopisarzy: natywna strategia antykruchości
- Konfiguracja samoleczenia z Playwright MCP i Healenium
- Strategie lokalizatorów w kolejności priorytetu: ARIA > identyfikator testu > CSS
- Wdrożenie spersonalizowanego systemu leczenia
- Metryki: jak mierzyć redukcję łuszczenia się
Dlaczego testy E2E się psują: analiza przyczyn
Przed wdrożeniem leczenia musisz zrozumieć przyczyny załamania. Analiza jedna na 10 000 niepowodzenie testu w CI z 2025 r. ujawnia następujący rozkład:
Causa di rottura test E2E | Frequenza | Self-healing efficace?
-------------------------------- |-----------|----------------------
Selettore CSS/XPath cambiato | 38% | SI - alta probabilita
ID elemento rinominato | 22% | SI - cerca per testo/ARIA
Testo elemento cambiato | 15% | Parziale - dipende dal match
Timeout (lentezza server/UI) | 12% | NO - problema di infra
Logica di business cambiata | 8% | NO - richiede test update
Race condition | 5% | Parziale - retry aiuta
60–75% awarii należy do kategorii „nieprawidłowy selektor” — to jest właśnie problem które rozwiązuje samoleczenie. Pozostałe 25-40% wymaga interwencji człowieka ze względu na zachowanie oczekiwane i rzeczywiście zmienione.
Lokalizatory dramaturgów: Fundacja Anti-Fragile
Zanim porozmawiamy o samonaprawiającej się sztucznej inteligencji, ważne jest, aby zrozumieć, że Playwright ma ją natywnie wbudowaną Solidne strategie lokalizacji, które zapobiegają wielu awariom:
// STRATEGIA PRIORITA LOCATOR — dal piu robusto al piu fragile
// 1. ARIA roles (raccomandato — semantica stabile)
await page.getByRole('button', { name: 'Accedi' }).click();
await page.getByRole('textbox', { name: 'Email' }).fill('test@example.com');
await page.getByRole('heading', { name: 'Dashboard' });
// 2. Text content (buono per elementi con testo stabile)
await page.getByText('Conferma prenotazione').click();
await page.getByLabel('Password').fill('secret');
await page.getByPlaceholder('Cerca...').fill('query');
// 3. Test ID (ottimo controllo, richiede attributo nel codice)
// Nel componente: <button data-testid="submit-btn">
await page.getByTestId('submit-btn').click();
// 4. CSS selector (evitare dove possibile)
// FRAGILE — si rompe con refactoring CSS
await page.locator('.btn-primary.submit').click(); // EVITARE
// 5. XPath (evitare — dipende dalla struttura DOM)
// MOLTO FRAGILE
await page.locator('//button[@class="btn submit"]').click(); // EVITARE
// Playwright auto-wait: non serve waitFor() nella maggior parte dei casi
// Playwright attende automaticamente che l'elemento sia visible, enabled, stable
await page.getByRole('button', { name: 'Salva' }).click();
// Equivalente a:
// await page.waitForSelector('button:text("Salva")', { state: 'visible' });
// await page.click('button:text("Salva")');
Wdrażaj samoleczenie za pomocą dramaturga i sztucznej inteligencji
Istnieją trzy podejścia do samoleczenia z Playwright w 2026 roku:
Podejście 1: Healenium (otwarte oprogramowanie)
Healenium to biblioteka typu open source, która integruje się z Selenium i Playwright w celu dostarczenia alternatywne selektory, gdy oryginalny zawiedzie.
// playwright.config.ts con healing personalizzato
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
reporter: [
['html'],
['./reporters/healing-reporter.ts'] // reporter custom per tracciare healing events
],
use: {
actionTimeout: 10000,
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
});
// Self-healing locator wrapper personalizzato
// src/helpers/healing-locator.ts
import { Page, Locator } from '@playwright/test';
interface HealingResult {
original: string;
healed: string | null;
strategy: string;
confidence: number;
}
export class HealingPage {
private page: Page;
private healingLog: HealingResult[] = [];
constructor(page: Page) {
this.page = page;
}
/**
* Locator con fallback intelligente.
* Strategia: primary selector -> fallback selectors -> ARIA -> testo
*/
async smartLocator(
primarySelector: string,
fallbacks: string[] = [],
contextHint?: string
): Promise {
// Prova il selettore primario
const primaryLocator = this.page.locator(primarySelector);
const isPrimaryVisible = await primaryLocator.isVisible().catch(() => false);
if (isPrimaryVisible) {
return primaryLocator;
}
// Prova i fallback nell'ordine
for (const fallback of fallbacks) {
const fallbackLocator = this.page.locator(fallback);
const isFallbackVisible = await fallbackLocator.isVisible().catch(() => false);
if (isFallbackVisible) {
this.healingLog.push({
original: primarySelector,
healed: fallback,
strategy: 'explicit-fallback',
confidence: 0.9
});
console.log(`[HEALING] Healed: ${primarySelector} -> ${fallback}`);
return fallbackLocator;
}
}
// Fallback ARIA: cerca per ruolo e nome/hint
if (contextHint) {
const ariaLocator = this.page.getByRole('button', { name: contextHint })
.or(this.page.getByRole('link', { name: contextHint }))
.or(this.page.getByText(contextHint));
const isAriaVisible = await ariaLocator.isVisible().catch(() => false);
if (isAriaVisible) {
this.healingLog.push({
original: primarySelector,
healed: `aria:${contextHint}`,
strategy: 'aria-healing',
confidence: 0.75
});
return ariaLocator;
}
}
// Se nessun fallback funziona, lancia errore con contesto
throw new Error(
`[HEALING FAILED] Cannot locate element.\n` +
`Original: ${primarySelector}\n` +
`Tried fallbacks: ${fallbacks.join(', ')}\n` +
`Context hint: ${contextHint ?? 'none'}`
);
}
getHealingLog(): HealingResult[] {
return this.healingLog;
}
}
Podejście 2: Dramaturg MCP w leczeniu AI
Playwright MCP (Model Context Protocol) umożliwia LLM bezpośrednią interakcję z przeglądarka. Otwiera to zaawansowane możliwości: w przypadku niepowodzenia testu agent AI może przeprowadzić analizę bieżący DOM i zaproponuj właściwy selektor.
// Integrazione Playwright MCP per healing assistito da AI
// scripts/heal-failing-tests.ts
import { OpenAI } from 'openai';
import * as fs from 'fs';
const openai = new OpenAI();
interface FailedLocator {
selector: string;
lastKnownHTML: string;
currentPageHTML: string;
testFile: string;
lineNumber: number;
}
async function healSelector(failed: FailedLocator): Promise {
const prompt = `Sei un esperto di Playwright E2E testing.
Un test sta fallendo perche il selettore CSS non trova piu l'elemento.
Selettore originale (NON funzionante): ${failed.selector}
HTML dell'elemento quando il test funzionava:
${failed.lastKnownHTML}
HTML corrente della pagina (sezione rilevante):
${failed.currentPageHTML}
Analizza le differenze e suggerisci il nuovo selettore Playwright piu robusto.
Priorita: getByRole > getByTestId > getByText > locator con CSS specifico.
Rispondi con SOLO il nuovo selettore, senza spiegazioni.
Esempio di formato valido: page.getByRole('button', { name: 'Accedi' })`;
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: prompt }],
temperature: 0,
max_tokens: 150
});
return response.choices[0]?.message?.content?.trim() ?? null;
}
// Processa il report di failure di Playwright
async function processPlaywrightFailures(reportPath: string): Promise {
const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
for (const suite of report.suites) {
for (const test of suite.tests) {
if (test.status === 'failed' && test.error?.message?.includes('locator')) {
console.log(`Processing failure in: ${test.title}`);
// Analizza e propone healing...
}
}
}
}
Praktyczne wdrożenie: zestaw testów antykruchych
// tests/checkout.spec.ts — test scritto per resistere ai cambiamenti
import { test, expect } from '@playwright/test';
test.describe('Processo di checkout', () => {
test('utente completa un ordine con successo', async ({ page }) => {
await page.goto('/shop');
// USA getByRole — stabile anche se cambiano classi CSS
await page.getByRole('button', { name: 'Aggiungi al carrello' }).first().click();
// USA getByRole per navigazione — non dipende da URL
await page.getByRole('link', { name: 'Vai al carrello' }).click();
// Attendi conferma visuale prima di procedere
await expect(page.getByRole('heading', { name: 'Il tuo carrello' })).toBeVisible();
// Compila form con getByLabel — stabile rispetto a ristrutturazioni
await page.getByLabel('Nome completo').fill('Mario Rossi');
await page.getByLabel('Indirizzo email').fill('mario@example.com');
await page.getByLabel('Numero carta').fill('4242424242424242');
await page.getByLabel('Data scadenza').fill('12/28');
await page.getByLabel('CVV').fill('123');
// Usa data-testid per elementi critici senza testo ovvio
await page.getByTestId('place-order-btn').click();
// Assert su ARIA per conferma — non su testo esatto che puo cambiare
await expect(page.getByRole('alert', { name: /ordine confermato/i }))
.toBeVisible({ timeout: 10000 });
// Assert numero ordine con regex — robusto rispetto al formato
const orderNumber = page.getByText(/Ordine #\d+/);
await expect(orderNumber).toBeVisible();
});
});
Metryki: jak mierzyć poprawę
// reporters/flakiness-reporter.ts — traccia la stabilita dei test
import { Reporter, TestCase, TestResult } from '@playwright/test/reporter';
import * as fs from 'fs';
class FlakinessReporter implements Reporter {
private results: Map<string, { passes: number; failures: number }> = new Map();
onTestEnd(test: TestCase, result: TestResult): void {
const key = test.titlePath().join(' > ');
const existing = this.results.get(key) ?? { passes: 0, failures: 0 };
if (result.status === 'passed') {
existing.passes++;
} else if (result.status === 'failed') {
existing.failures++;
}
this.results.set(key, existing);
}
onEnd(): void {
const report: Record<string, unknown> = {};
this.results.forEach((stats, testName) => {
const total = stats.passes + stats.failures;
const flakinessRate = total > 0 ? stats.failures / total : 0;
if (flakinessRate > 0) {
report[testName] = {
...stats,
total,
flakinessRate: (flakinessRate * 100).toFixed(2) + '%',
status: flakinessRate < 0.05 ? 'acceptable' : 'needs-attention'
};
}
});
fs.writeFileSync('flakiness-report.json', JSON.stringify(report, null, 2));
console.log(`\nFlakiness Report saved. Tests with issues: ${Object.keys(report).length}`);
}
}
export default FlakinessReporter;
Docelowa stabilność dla pakietu E2E
- Stopień łuszczenia się < 0,5%: akceptowalny w produkcji
- 0,5% - 2%: strefa uwagi, analizuje przyczyny
- > 2%: Wymagane natychmiastowe działanie — podważa zaufanie do pakietu
- Cel z samoleczeniem: 60-70% redukcja błędów selektora nieważny; łuszczenie się całkowite poniżej 0,3%
Wnioski
Testy samonaprawiające się są rozwiązaniem najbardziej frustrującego problemu testowania E2E. Droga najskuteczniejsza w 2026 roku jest kombinacja: wykorzystania natywnych lokalizatorów Playwright (getByRole, getByTestId) jako profilaktyka podstawowa i dodaj warstwę leczenia AI dla pozostałych przypadków.
Inwestycja szybko się zwraca: zespół, który spędza 4 godziny w każdym sprincie na naprawie zepsutych testów odzyskać ten czas całkowicie w ciągu pierwszego miesiąca samoleczenia.







