أساسيات Playwright
Playwright إطار مفتوح المصدر من Microsoft لأتمتة المتصفحات. ومشغّل الاختبارات الخاص به، Playwright Test (@playwright/test)، يقود متصفحات Chromium وFirefox وWebKit، ويعطي كل اختبار سياق متصفح (Browser Context) جديداً، وينتظر حتى يجهز العنصر قبل التفاعل معه، ويعيد محاولة التأكيدات حتى تبلغ الصفحة الحالة المتوقعة. ويُقرأ الاختبار كحالة اختبار يدوية: افتح /checkout، واملأ «Coupon code» بالقيمة SAVE10، وانقر «Apply»، وتوقّع أن يكون إجمالي مصباحين سعر كلٍّ منهما 100.00 SAR هو 180.00 SAR.
لماذا يهم
اختبارات واجهة المستخدم أبطأ طبقات مجموعة الاختبارات وأكثرها هشاشة، وكثير من حالات فشلها سببه التوقيت والحالة المشتركة لا عيوب المنتج. ويزيل Playwright عدداً من هذه الأسباب بتصميمه: فالإجراءات تنتظر، والتأكيدات تعيد المحاولة، وكل اختبار معزول. لكن ذلك لا ينفع إلا إذا استخدمته؛ ففترات الانتظار الثابتة والفحوص التي تقرأ مرة واحدة تبقى متذبذبة في أي أداة.
وكثيراً ما ينضم المختبِرون إلى مجموعة اختبارات Playwright قائمة؛ وقراءة أثر (Trace) اختبار فاشل تفصل بسرعة بين عيب في المنتج وعيب في الاختبار، وتمنح تقرير الخطأ دليلاً دقيقاً.
كيف يعمل Playwright Test
يقرأ الأمر npx playwright test الملف playwright.config.ts ويشغّل ملفات الاختبار في عمليات عمّال (workers) متوازية. ويتسلّم كل اختبار تجهيزات (Fixtures) يطلبها باسمها: فالتجهيزة page علامة تبويب في سياق متصفح جديد (ملف تعريف معزول يشبه نافذة تصفّح خفي جديدة)، فلا تتسرّب ملفات تعريف الارتباط والتخزين بين الاختبارات؛ وrequest ترسل طلبات HTTP، مثلاً لإنشاء بيانات الاختبار؛ وتجهيزاتك الخاصة تأتي من test.extend.
- المحددات (Locators):
getByRole("button", { name: "Apply" })وgetByLabelوgetByTextوgetByTestId. ويُحلّ المحدِّد من جديد في كل استخدام، فيصمد أمام إعادة رسم الصفحة، وهو صارم: الإجراء على محدِّد يطابق عنصرين يفشل بدلاً من أن يختار أحدهما. - الانتظار التلقائي (Auto-waiting): قبل النقر ينتظر Playwright حتى يصبح العنصر مرئياً، ومستقراً (لا يتحرك)، وقادراً على استقبال الأحداث (لا يغطيه عنصر آخر)، ومفعّلاً؛ وقبل
fillحتى يصبح الحقل مرئياً ومفعّلاً وقابلاً للتحرير. - التأكيدات التي تعيد المحاولة (Web-first Assertions): يعيد
await expect(locator).toHaveText("180.00 SAR")المحاولة حتى يطابق النص أو تنتهي مهلة التأكيد، أماexpect(await locator.textContent()).toBe("180.00 SAR")فيقرأ مرة واحدة ويسابق الصفحة. - عارض الأثر (Trace Viewer): كل إجراء مع لقطات DOM قبله وبعده، إضافةً إلى وحدة التحكم (Console) والشبكة والشيفرة المصدرية؛ افتحه أولاً عندما يفشل اختبار لم تشاهده.
- 1قراءة الإعداداتالقيم
baseURLوالمهل والمشاريع وإعادة المحاولة وأدوات التقارير منplaywright.config.tsnpx playwright test - 2مشروع الإعداديسجّل الدخول مرة واحدة ويحفظ ملفات تعريف الارتباط والتخزين المحلي للمشاريع التي تعتمد عليه
playwright/.auth/user.json - 3سياق جديد وتجهيزاتيفتح العامل سياق متصفح جديداً للاختبار ويُعدّ
pageوالتجهيزات الخاصة مثلcheckout - 4المحدِّديصف العنصر؛ ويُبحث عنه من جديد في كل استخدام، ويشترط تطابقاً واحداً
getByRole('button', { name: 'Apply' }) - 5انتظار تلقائي ثم إجراءقبل
click: مرئي، ومستقر، ويستقبل الأحداث، ومفعّل؛ وقبلfill: مرئي، ومفعّل، وقابل للتحريريتكرر: يعيد الفحص حتى يجهز العنصر أو تنتهي مهلة الاختبار
- 6تأكيد يعيد المحاولة
await expect(total).toHaveText('180.00 SAR')يتكرر: يعيد المحاولة حتى ينجح أو تنتهي مهلة التأكيد البالغة 5 ثوانٍ
فرع: ما زال يفشل: يفشل الاختبار؛ وفي CI يُعاد تشغيله ويُسجَّل أثر في أول إعادة محاولةفشل
- 7التفكيكتنظّف التجهيزات ما أنشأته؛ ويُتخلَّص من السياق وملفات تعريف الارتباط والتخزين
- 8التقريرمخرجات
listوتقرير HTML؛ وتُفتح الآثار المسجَّلة عند إعادة المحاولة في عارض الأثرnpx playwright show-report
طوال الخطوات 3–7 مرة لكل اختبار، في عمّال يعملون بالتوازي: لا تنتقل ملفات تعريف الارتباط ولا التخزين إلى الاختبار التالي
التثبيت وبنية المشروع وملف الإعدادات
يضيف الأمر npm init playwright@latest الحزمة @playwright/test، ويكتب playwright.config.ts واختباراً نموذجياً، ويعرض إضافة سير عمل GitHub Actions، وينزّل المتصفحات؛ وعلى جهاز خالٍ منها يثبّت npx playwright install --with-deps المتصفحات ومكتبات النظام التي تحتاجها. ومكان الاختلافات بين البيئات هو ملف الإعدادات، لا الاختبارات أبداً:
use.baseURL: تستدعي الاختباراتpage.goto("/checkout")، ومتغير بيئة واحد ينقل المجموعة بين البيئة المحلية وبيئة ما قبل الإنتاج (staging) وتطبيق المراجعة.timeoutوexpect.timeout: القيمتان الافتراضيتان 30 ثانية لكل اختبار و5 ثوانٍ لكل تأكيد. ارفعهما لسبب مَقيس، لا لإخفاء خطوة بطيئة أبداً.projects: مشروع لكل متصفح أو ملف تعريف جهاز، إضافةً إلى مشروعsetupتذكره المشاريع الأخرى فيdependenciesفيعمل أولاً.retries: صفر محلياً، و1–2 في CI. والاختبار الذي لا ينجح إلا عند إعادة المحاولة يُعلَّم متذبذباً (flaky): فتحقّق منه.reporterوtrace: يطبعlistالتقدّم، ويكتبhtmlالمجلدplaywright-report/، ويسجّل"on-first-retry"أثراً في أول إعادة محاولة لاختبار فاشل.
// shop-e2e/
// package.json "@playwright/test" in devDependencies
// playwright.config.ts this file
// tests/ *.spec.ts, auth.setup.ts, fixtures.ts, files/ (uploads)
// pages/ page objects
// playwright/.auth/ saved sessions: add to .gitignore
// playwright-report/ HTML report (generated)
// test-results/ traces and screenshots (generated)
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 30_000, // whole test, in ms (the default)
expect: { timeout: 5_000 }, // each web-first assertion (the default)
fullyParallel: true, // tests inside one file also run in parallel
forbidOnly: !!process.env.CI, // a forgotten test.only fails the CI run
retries: process.env.CI ? 2 : 0,
reporter: [['list'], ['html', { open: 'never' }]],
use: {
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
dependencies: ['setup'],
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
dependencies: ['setup'],
},
],
});التشغيل وتتبّع الأخطاء ومهمة CI مصغّرة
أوامر Playwright التي تستخدمها يومياً، ثم مهمة CI مصغّرة:
npx playwright test tests/checkout.spec.ts:10يشغّل وحده الاختبار المعرَّف في السطر 10 (أما سطر داخل جسم اختبار فلا يطابق شيئاً)؛ و-g "expired coupon"يرشّح بالعنوان عبر تعبير نمطي (Regular Expression)، و--project=chromiumبالمتصفح.--headedيُظهر المتصفح؛ و--uiيفتح وضع الواجهة (UI Mode) لاختيار الاختبارات وإعادة تشغيلها والتنقّل بين خطواتها.--debugيفتح Playwright Inspector: عامل واحد، ومتصفح ظاهر، وبلا مهلة، ومتوقف قبل أول إجراء. وawait page.pause()يوقف التنفيذ عند سطر تختاره في تشغيل بمتصفح ظاهر.npx playwright show-reportيفتح تقرير HTML، وnpx playwright show-traceيفتح ملفtrace.zip، ولو كان من CI؛ و--trace onيسجّل أثراً لكل اختبار في تشغيل محلي.- في CI: يشغّل المفتاح
container:كل الخطوات داخل صورة Docker، وهي بيئة Linux جاهزة. وتحوي الصورة الرسميةmcr.microsoft.com/playwrightمسبقاً Node.js والمتصفحات ومكتبات النظام التي تحتاجها، فلا تشغّل المهمة إلاnpm ciوالاختبارات. ويجب أن يطابق وسمُ الصورة إصدارَ@playwright/testفيpackage-lock.json. وتقرأsecrets.*وvars.*قيماً محفوظة في إعدادات المستودع، فلا تُكتب أي كلمة مرور في الملف. ويضبط GitHub Actions المتغيرCI=trueفتعمل إعادة المحاولة وforbidOnly(فيُفشلtest.onlyالمنسيُّ التشغيلَ)، ويُرفع التقرير مع آثاره حتى بعد فشل الاختبارات.
# .github/workflows/e2e.yml
name: e2e
on: [pull_request]
jobs:
e2e:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.55.0-noble # = @playwright/test version
options: --user 1001
steps:
- uses: actions/checkout@v7
- run: npm ci # no "playwright install": the image has the browsers
- run: npx playwright test
env:
BASE_URL: ${{ vars.STAGING_URL }}
E2E_EMAIL: ${{ secrets.E2E_EMAIL }}
E2E_PASSWORD: ${{ secrets.E2E_PASSWORD }}
- uses: actions/upload-artifact@v7
if: ${{ !cancelled() }} # also when tests failed
with:
name: playwright-report
path: playwright-report/الجلسات المسجَّل دخولها واعتراض الطلبات ومربعات الحوار وعلامات التبويب ورفع الملفات
تسجيل الدخول عبر النموذج في كل اختبار بطيء، ويُفشل كل شيء إذا تعطّل الدخول. والبديل أن يسجّل مشروع الإعداد (setup) الدخول مرة واحدة ويحفظ ملفات تعريف الارتباط والتخزين المحلي (local storage) عبر storageState؛ ثم تحمّل الاختبارات الملف بـtest.use. ويُحفظ تسجيل الدخول عادةً في ملف تعريف ارتباط أو في التخزين المحلي؛ أما تخزين الجلسة (session storage) فخاص بعلامة تبويب واحدة ولا يُحفظ، فإذا احتفظ التطبيق بتسجيل الدخول فيه لم تبدأ الاختبارات مسجَّلاً دخولها. أبقِ الملف خارج نظام التحكم بالإصدارات، وبيانات الدخول في متغيرات البيئة. والاختبارات التي تتشارك حساباً واحداً يجب ألا تغيّر بيانات تقرؤها اختبارات أخرى، وإلا احتاج كل عامل إلى حسابه الخاص.
يعترض page.route الطلبات المطابقة التي ترسلها الصفحة (لا الطلبات التي يرسلها خادمك): فيجيب route.fulfill بالحالة والمحتوى اللذين تحددهما، ويحاكي route.abort انقطاع الشبكة. سجّله قبل إرسال الطلب. واستبدل باستجابات بديلة (Stubs) ما لا تملكه أو لا تستطيع إحداثه، مثل تعطّل مزوّد خارجي؛ فإن استبدلت كل شيء لم تثبت اختباراتك إلا صحة البدائل.
تُرفض مربعات حوار المتصفح (alert وconfirm وprompt) تلقائياً ما لم تسجّل معالجاً لها، وعندها يجب أن يقبل المعالج كل مربع أو يرفضه؛ أما نافذة HTML المنبثقة داخل الصفحة فليست منها: استخدم معها المحددات. والرابط الذي يفتح علامة تبويب جديدة يطلق الحدث popup: ابدأ انتظاره قبل النقر. ويملأ setInputFiles الحقل <input type="file"> من مسار نسبي إلى مجلد العمل.
// tests/auth.setup.ts: runs once, before the projects that depend on 'setup'
import { test as setup, expect } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
setup('sign in once', async ({ page }) => {
await page.goto('/login');
// process.env values may be undefined; "!" tells TypeScript they are set (CI secrets)
await page.getByLabel('Email').fill(process.env.E2E_EMAIL!);
await page.getByLabel('Password').fill(process.env.E2E_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('heading', { name: 'My account' })).toBeVisible();
await page.context().storageState({ path: authFile }); // cookies + local storage
});
// tests/account.spec.ts: every test in this file starts signed in
import { test, expect } from '@playwright/test';
test.use({ storageState: 'playwright/.auth/user.json' });
test('saved cards: a provider outage is reported', async ({ page }) => {
await page.route('**/api/saved-cards', (route) =>
route.fulfill({ status: 503, json: { error: 'provider unavailable' } }));
await page.goto('/account/payment'); // route registered first
await expect(page.getByRole('alert')).toContainText('try again later');
});
test('terms of use open in a new tab', async ({ page }) => {
await page.goto('/account');
const popupPromise = page.waitForEvent('popup'); // wait before the click
await page.getByRole('link', { name: 'Terms of use' }).click();
const terms = await popupPromise;
await expect(terms).toHaveURL(/terms/);
});
test('profile photo upload shows a preview', async ({ page }) => {
await page.goto('/account/profile');
await page.getByLabel('Profile photo').setInputFiles('tests/files/avatar.png');
await expect(page.getByRole('img', { name: 'Photo preview' })).toBeVisible();
});جرّبها: مولّد استجابات API وهمية جرّبها: مولّد استجابات خطأ وهمية
مثال تطبيقي: اختبارات صفحة الدفع مع كائن صفحة
المتطلب: صفحة الدفع للضيوف تقبل القسائم؛ فالرمز SAVE10 يخصم 10%؛ والرمز المنتهي يُظهر خطأً ويُبقي الإجمالي كما هو؛ وإذا تعطّلت خدمة حساب الشحن تقول الصفحة إن الشحن سيُؤكَّد بالبريد الإلكتروني وتسمح بالطلب مع ذلك؛ وحذف آخر منتج يطلب تأكيداً في مربع حوار confirm من المتصفح. وسعر المصباح المكتبي 100.00 SAR، ويُعرض الشحن منفصلاً لا ضمن الإجمالي.
أربعة اختبارات قصيرة بدلاً من اختبار طويل واحد. لا يعرف الشاشةَ إلا CheckoutPage؛ وتسلّم تجهيزةٌ كل اختبار كائن صفحة جاهزاً؛ ويبني كل اختبار سلة ضيف خاصة به عبر page.request الذي يتشارك ملفات تعريف الارتباط مع الصفحة، فلا ترى الاختبارات المتوازية سلال بعضها. وعندئذٍ يغيّر تغييرُ تسمية حقلٍ سطراً واحداً، لا أربعة اختبارات.
// pages/CheckoutPage.ts: the only file that knows the checkout screen
import type { Locator, Page } from '@playwright/test';
export class CheckoutPage {
readonly couponInput: Locator;
readonly applyButton: Locator;
readonly total: Locator;
readonly alert: Locator;
readonly placeOrder: Locator;
// "private readonly page" also stores the argument as this.page
constructor(private readonly page: Page) {
this.couponInput = page.getByLabel('Coupon code');
this.applyButton = page.getByRole('button', { name: 'Apply' });
this.total = page.getByTestId('order-total');
this.alert = page.getByRole('alert');
this.placeOrder = page.getByRole('button', { name: 'Place order' });
}
// A new guest cart for this test: page.request shares cookies with the page.
async openWith(sku: string, qty: number) {
await this.page.request.post('/api/cart/items', { data: { sku, qty } });
await this.page.goto('/checkout');
}
async applyCoupon(code: string) {
await this.couponInput.fill(code);
await this.applyButton.click();
}
async removeItem(name: string) {
const row = this.page.getByRole('row').filter({ hasText: name });
await row.getByRole('button', { name: 'Remove' }).click();
}
}
// tests/fixtures.ts: a test that asks for "checkout" gets a ready page object
import { test as base } from '@playwright/test';
import { CheckoutPage } from '../pages/CheckoutPage';
export const test = base.extend<{ checkout: CheckoutPage }>({
checkout: async ({ page }, use) => {
await use(new CheckoutPage(page));
},
});
export { expect } from '@playwright/test';
// tests/checkout.spec.ts: desk lamp LAMP-01 costs 100.00 SAR
import { test, expect } from './fixtures';
test('valid coupon takes 10% off', async ({ checkout }) => {
await checkout.openWith('LAMP-01', 2);
await checkout.applyCoupon('SAVE10');
await expect(checkout.total).toHaveText('180.00 SAR');
});
test('expired coupon is refused, total unchanged', async ({ checkout }) => {
await checkout.openWith('LAMP-01', 2);
await checkout.applyCoupon('OLD5');
await expect(checkout.alert).toHaveText('This coupon has expired');
await expect(checkout.total).toHaveText('200.00 SAR');
});
test('shipping quote down: order still possible', async ({ page, checkout }) => {
await page.route('**/api/shipping-quote', (route) =>
route.fulfill({ status: 503, json: { error: 'unavailable' } }));
await checkout.openWith('LAMP-01', 1);
await expect(page.getByText('Shipping will be confirmed by e-mail')).toBeVisible();
await expect(checkout.placeOrder).toBeEnabled();
});
test('last item: confirm, then empty cart', async ({ page, checkout }) => {
await checkout.openWith('LAMP-01', 1);
page.once('dialog', (dialog) => dialog.accept()); // without a handler: dismissed
await checkout.removeItem('Desk lamp');
await expect(page.getByText('Your cart is empty')).toBeVisible();
});أخطاء شائعة
- استدعاء
page.waitForTimeout()أو رفع المهل بدلاً من انتظار شرط. - نسيان
awaitقبل إجراء أو تأكيد، فقد ينجح الاختبار دون أن يتحقق من شيء؛ وقاعدة الفحص@typescript-eslint/no-floating-promisesتكشف ذلك. - التأكيد على قيمة قُرئت مرة واحدة،
expect(await locator.isVisible()).toBe(true)، بدلاً منawait expect(locator).toBeVisible()الذي يعيد المحاولة. - تسجيل الدخول عبر الواجهة في كل اختبار، أو إيداع ملف
storageStateأو كلمة مرور في المستودع. - استبدال كل الطلبات ببدائل، فتظل اختبارات الواجهة تنجح بعد أن تغيّرت الواجهة البرمجية الحقيقية.
- وسم صورة Docker يختلف عن إصدار
@playwright/testالمثبّت، فلا يجد CI المتصفحات المتوقعة.
من منظور ضمان الجودة
يلتقي المختبِرون بـPlaywright عند كتابة اختبارات للقصص الجديدة، ومراجعة طلبات الدمج (Pull Requests) التي تغيّر الاختبارات، وفرز تشغيلات CI الحمراء. وفي المراجعة ابحث عن محددات موجّهة للمستخدم، وتأكيدات تعيد المحاولة، وغياب الانتظار الثابت، وسلوك واحد لكل اختبار، وبيانات ينشئها كل اختبار لنفسه؛ واتفق مع المطورين على قيم data-testid حيث لا يوجد دور (role) أو تسمية ثابتة.
وعندما يفشل CI افتح الأثر قبل إعادة التشغيل: فهو يبيّن هل أساء المنتج التصرف (إجمالي خاطئ في لقطة DOM، أو استجابة 500 في تبويب الشبكة) أم أساء الاختبار (محدِّد أصاب عنصراً خاطئاً). سجّل العنوان والمشروع والإيداع (commit) والأثر في التذكرة، واسأل مبكراً: أي الخدمات يجوز استبدالها ببدائل، وأي الحسابات يجوز أن يستخدمها CI.
تمرين عملي
راجع اختبار Playwright هشاً وأعد كتابته
كتب زميل هذا الاختبار لصفحة الدعم: مستخدم مسجَّل دخوله يرفق إيصالاً، ويرسل تذكرة، ويفتح مركز المساعدة. كان يفشل أحياناً في CI، ومنذ أن صار مركز المساعدة يُفتح في علامة تبويب جديدة صار يفشل في كل مرة. اكتب المشكلات، ثم أعد كتابته وفق هذا الدرس.
import { test, expect } from '@playwright/test';
test('support', async ({ page }) => {
await page.goto('https://staging.shop.example/login');
await page.fill('#email', 'sara.qa@example.com');
await page.fill('#pwd', 'Winter2026');
await page.click('.btn-primary');
await page.waitForTimeout(3000);
await page.goto('https://staging.shop.example/support');
await page.locator('input[type=file]').setInputFiles('C:/Users/sara/receipt.pdf');
await page.fill('#message', 'Charged twice for order 1042');
await page.click('text=Send');
expect(await page.locator('.toast').isVisible()).toBe(true);
await page.click('text=Help centre');
await expect(page).toHaveURL(/help/);
});- علّم الأسطر التي تعتمد على جهاز واحد أو شخص واحد أو آثار جانبية حقيقية أو انتظار ثابت.
- استبدل المحددات بمحددات موجّهة للمستخدم، والفحص الذي يقرأ مرة واحدة بتأكيد يعيد المحاولة.
- قرّر ما الذي ينتقل إلى ملف الإعدادات، وإلى مشروع الإعداد، وإلى استجابة بديلة عبر
page.route. - قسّمه على اختبارات تستخدم كائن صفحة باسم
SupportPage.
إظهار الإجابة النموذجية
المشكلات: عناوين staging مكتوبة كاملة في الشيفرة؛ وتسجيل الدخول داخل الاختبار مع كلمة مرور في الشيفرة؛ وانتظار ثابت مدته 3 ثوانٍ؛ ومحددات مرتبطة بالترميز مثل #email و.btn-primary و.toast؛ ومسار إيصال لا يوجد إلا على حاسوب واحد؛ وفحص isVisible() يقرأ مرة واحدة وكثيراً ما يجري قبل ظهور الإشعار، وهو على الأرجح سبب الفشل المتقطع القديم؛ وتذكرة حقيقية تُنشأ في كل تشغيل؛ وtoHaveURL يفحص الصفحة القديمة مع أن مركز المساعدة يُفتح في علامة تبويب جديدة؛ وثلاثة سلوكيات في اختبار واحد.
إعادة الكتابة: baseURL من ملف الإعدادات؛ وينتقل تسجيل الدخول إلى مشروع الإعداد ببيانات دخول من متغيرات البيئة، وتبدأ الاختبارات مسجَّلاً دخولها عبر test.use({ storageState: "playwright/.auth/user.json" })؛ ويُحذف الانتظار الثابت لأن الإجراءات والتأكيدات تنتظر بنفسها. ويضم كائن SupportPage المحددات getByLabel("Receipt") وgetByLabel("Message") وgetByRole("button", { name: "Send" }). الاختبار الأول يستبدل **/api/tickets باستجابة 201، ويرفق tests/files/receipt.pdf، ويرسل، ويتوقع ظهور getByText("Ticket sent"). والثاني يستبدلها باستجابة 503 ويتحقق من رسالة الخطأ المحددة في المتطلب؛ ويبقى اختبار API منفصل يفحص نقطة النهاية الحقيقية. والثالث ينتظر popup قبل النقر على «Help centre» ويتحقق من toHaveURL(/help/) في الصفحة الجديدة.
أسئلة شائعة
هل يجب أن أستخدم TypeScript؟
لا. يعمل Playwright Test، المشغّل المستخدم في هذا الدرس، مع JavaScript أو TypeScript. ويتوفر Playwright أيضاً للغات Python وJava و.NET، بالمحددات نفسها والانتظار التلقائي والتأكيدات التي تعيد المحاولة، عبر مشغّل الاختبارات المعتاد في تلك اللغة مثل pytest. اختر اللغة التي يستخدمها فريقك أصلاً.
هل أستطيع تسجيل الاختبارات بدلاً من كتابتها؟
نعم، كمسودة. الأمر npx playwright codegen متبوعاً بعنوان URL يفتح متصفحاً ويكتب الشيفرة أثناء نقرك، مفضّلاً المحددات المبنية على الأدوار. ثم قرّر ما الذي تتحقق منه، وانقل الخطوات إلى كائنات الصفحات، واحذف كل ما يربط الاختبار بمجموعة بيانات واحدة.
إذا جعلت إعادة المحاولة تشغيل CI أخضر، فهل الاختبار سليم؟
لا. الاختبار الذي لا ينجح إلا عند إعادة المحاولة يُعلَّم متذبذباً. فإعادة المحاولة تمنع اختباراً واحداً غير مستقر من حجب كل عمليات الدمج، لكن السبب باقٍ: افتح أثر المحاولة الفاشلة، وأصلح الاختبار أو المنتج، وتابعه حتى يُصلَح.
لماذا ينجح الاختبار محلياً ويفشل في CI وحده؟
غالباً لأن CI مختلف: جهاز أبطأ، أو عمّال متوازون يتشاركون البيانات، أو عنوان أساسي أو حساب آخر، أو مشروع متصفح آخر، أو سرّ ناقص. وقبل تغيير أي مهلة افتح الأثر من التقرير المرفوع، وشغّل الاختبار محلياً بالقيمة نفسها لـ--project ومتغيرات البيئة نفسها.
المراجع
- Playwright Documentationالتوثيق الرسمي لـ Playwright، إطار الاختبار الشامل الذي يشغّل Chromium وWebKit وFirefox.
- Best Practices — Playwrightاختبر السلوك الذي يراه المستخدم، واجعل الاختبارات معزولة، وفضّل المحدِّدات المتينة والتأكيدات التي تنتظر، واستخدم التتبّع لتصحيح الأخطاء.
- Auto-waiting — Playwrightفحوص الجاهزية التي يجريها Playwright قبل كل إجراء — مرئي، ومستقر، ومفعّل، ويستقبل الأحداث — والتي تُغني عن معظم الانتظارات المكتوبة يدوياً.
- Locators — Playwrightكيف يعثر Playwright على العناصر، ولماذا تُعاد تحديد المحدِّدات وتنتظر تلقائياً، ولماذا يُوصى بالمحدِّدات المبنية على الدور أولاً.
تدرّب بأدوات متقن
ولّد استجابات API وهمية كاملة (الحالة والترويسات وجسم JSON) لحالات النجاح والقوائم والتفاصيل والأخطاء والنتائج الفارغة.
ولّد استجابات خطأ منظّمة (التحقق والمصادقة وعدم العثور وتحديد المعدّل وخطأ الخادم) بالصيغ الشائعة.
تنسيق YAML والتحقق من صحته بمسافات بادئة وعلامات اقتباس متسقة.
اختبر التعابير النمطية مباشرةً مع إبراز التطابقات ومجموعات الالتقاط ومعاينة الاستبدال.
للتعمّق أكثر
تقدّمك في هذا الموضوع
سجّل الدخول لتتابع تقدّمك
علّم الموضوعات مكتملة، وتابع من حيث توقفت، وافتح امتحانات التخصص.