أساسيات 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) والشبكة والشيفرة المصدرية؛ افتحه أولاً عندما يفشل اختبار لم تشاهده.
تشغيل واحد لـPlaywright: الإعدادات ومشروع الإعداد أولاً، ثم لكل اختبار سياق جديد ومحدِّد وإجراء بانتظار تلقائي وتأكيد يعيد المحاولة وتفكيك، وفي النهاية تقرير فيه آثار الاختبارات الفاشلة التي أُعيدت محاولتها.
  1. 1قراءة الإعداداتالقيم baseURL والمهل والمشاريع وإعادة المحاولة وأدوات التقارير من playwright.config.tsnpx playwright test
  2. 2مشروع الإعداديسجّل الدخول مرة واحدة ويحفظ ملفات تعريف الارتباط والتخزين المحلي للمشاريع التي تعتمد عليهplaywright/.auth/user.json
  3. 3سياق جديد وتجهيزاتيفتح العامل سياق متصفح جديداً للاختبار ويُعدّ page والتجهيزات الخاصة مثل checkout
  4. 4المحدِّديصف العنصر؛ ويُبحث عنه من جديد في كل استخدام، ويشترط تطابقاً واحداًgetByRole('button', { name: 'Apply' })
  5. 5انتظار تلقائي ثم إجراءقبل click: مرئي، ومستقر، ويستقبل الأحداث، ومفعّل؛ وقبل fill: مرئي، ومفعّل، وقابل للتحرير

    يتكرر: يعيد الفحص حتى يجهز العنصر أو تنتهي مهلة الاختبار

  6. 6تأكيد يعيد المحاولةawait expect(total).toHaveText('180.00 SAR')

    يتكرر: يعيد المحاولة حتى ينجح أو تنتهي مهلة التأكيد البالغة 5 ثوانٍ

    فرع: ما زال يفشل: يفشل الاختبار؛ وفي CI يُعاد تشغيله ويُسجَّل أثر في أول إعادة محاولةفشل

  7. 7التفكيكتنظّف التجهيزات ما أنشأته؛ ويُتخلَّص من السياق وملفات تعريف الارتباط والتخزين
  8. 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/

جرّبها: منسّق YAML جرّبها: أداة اختبار Regex

الجلسات المسجَّل دخولها واعتراض الطلبات ومربعات الحوار وعلامات التبويب ورفع الملفات

تسجيل الدخول عبر النموذج في كل اختبار بطيء، ويُفشل كل شيء إذا تعطّل الدخول. والبديل أن يسجّل مشروع الإعداد (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/);
});
الخطوات
  1. علّم الأسطر التي تعتمد على جهاز واحد أو شخص واحد أو آثار جانبية حقيقية أو انتظار ثابت.
  2. استبدل المحددات بمحددات موجّهة للمستخدم، والفحص الذي يقرأ مرة واحدة بتأكيد يعيد المحاولة.
  3. قرّر ما الذي ينتقل إلى ملف الإعدادات، وإلى مشروع الإعداد، وإلى استجابة بديلة عبر page.route.
  4. قسّمه على اختبارات تستخدم كائن صفحة باسم 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 DocumentationMicrosoft (Playwright)التوثيق الرسمي لـ Playwright، إطار الاختبار الشامل الذي يشغّل Chromium وWebKit وFirefox.
  • Best Practices — PlaywrightMicrosoft (Playwright)اختبر السلوك الذي يراه المستخدم، واجعل الاختبارات معزولة، وفضّل المحدِّدات المتينة والتأكيدات التي تنتظر، واستخدم التتبّع لتصحيح الأخطاء.
  • Auto-waiting — PlaywrightMicrosoft (Playwright)فحوص الجاهزية التي يجريها Playwright قبل كل إجراء — مرئي، ومستقر، ومفعّل، ويستقبل الأحداث — والتي تُغني عن معظم الانتظارات المكتوبة يدوياً.
  • Locators — PlaywrightMicrosoft (Playwright)كيف يعثر Playwright على العناصر، ولماذا تُعاد تحديد المحدِّدات وتنتظر تلقائياً، ولماذا يُوصى بالمحدِّدات المبنية على الدور أولاً.

تدرّب بأدوات متقن

ولّد استجابات API وهمية كاملة (الحالة والترويسات وجسم JSON) لحالات النجاح والقوائم والتفاصيل والأخطاء والنتائج الفارغة.

المحاكاة

ولّد استجابات خطأ منظّمة (التحقق والمصادقة وعدم العثور وتحديد المعدّل وخطأ الخادم) بالصيغ الشائعة.

المحاكاة

تنسيق YAML والتحقق من صحته بمسافات بادئة وعلامات اقتباس متسقة.

التنسيق

اختبر التعابير النمطية مباشرةً مع إبراز التطابقات ومجموعات الالتقاط ومعاينة الاستبدال.

أدوات مساعدة

للتعمّق أكثر

تقدّمك في هذا الموضوع

جزء من مسار تعلم ضمان الجودة