Oyun Yazarı E2E Testi: Sayfa Nesneleri ve En İyi Uygulamalar
Birim testleri, bireysel işlevlerin doğru şekilde davrandığını doğrular. Entegrasyon testleri hizmetlerin birlikte çalıştığını doğrular. Ancak bir kullanıcı uygulamanıza gerçekten tıkladığında bozulan gezinme akışlarını, JavaScript hidrasyon hatalarını, ağa bağlı işleme hatalarını ve tarayıcılar arası işleme hatalarını da yakalayamaz. Playwright ile yapılan uçtan uca testler bu boşluğu dolduruyor ve Cypress veya Selenium öncüllerinden çok daha güvenilirler.
Oyun Yazarı gerçek Chromium, Firefox ve WebKit tarayıcılarını paralel olarak çalıştırır. Etkileşime geçmeden önce öğelerin görünür ve sabit olmasını otomatik olarak bekler. Arıza durumunda ekran görüntülerini, videoları ve ağ izlerini yakalar. Ve TypeScript öncelikli API'si, sürdürülebilir testler yazmayı bir zevk haline getiriyor. Bu kılavuz, yapılandırmadan sayfa nesnelerine ve Next.js uygulamaları için CI entegrasyonuna kadar her şeyi kapsar.
Önemli Çıkarımlar
- Yalnızca
data-testidseçicilerini kullanın — CSS seçicileri ve metin seçicileri kullanıcı arayüzü değişikliklerinde bozulur- Sayfa Nesne Modeli (POM), seçicileri yeniden kullanılabilir sınıflara ayırır - kullanıcı arayüzü öğesi başına bir konum değişikliği
- Her zaman
await page.goto()+await expect(page).toHaveURL()kullanın — navigasyonun eşzamanlı olarak tamamlandığını asla varsaymayın- Daha hızlı CI çalışmaları için
test.describe.parallel()ve--workersbayrağını kullanın- Testleri
playwright codegenile kaydedin ancak işleme koymadan önce daima sayfa nesnelerini yeniden düzenleyin- Başarısızlık durumunda yapılandırmada
screenshot: 'only-on-failure'aracılığıyla ekran görüntüleri yakalayın@axe-core/playwrightile davranışın yanı sıra erişilebilirliği de test edin- Kesintisiz üçüncü taraf aramaları için sahte API yanıtları; kritik mutlu yollar için gerçek API'yi kullanın
Yapılandırma
// apps/web/playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 30_000,
expect: { timeout: 5_000 },
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 2 : undefined,
reporter: [
['html', { outputFolder: 'playwright-report' }],
['json', { outputFile: 'playwright-report/results.json' }],
...(process.env.CI ? [['github'] as ['github']] : []),
],
use: {
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'mobile-chrome',
use: { ...devices['Pixel 7'] },
},
{
name: 'mobile-safari',
use: { ...devices['iPhone 14'] },
},
],
webServer: {
command: 'pnpm dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});
Dizin Yapısını Test Edin
tests/
public/ # Tests for unauthenticated public pages
homepage.spec.ts
blog.spec.ts
services.spec.ts
sitemap.spec.ts
i18n.spec.ts
seo.spec.ts
admin/ # Tests for authenticated admin pages
contacts.spec.ts
orders.spec.ts
dashboard.spec.ts
fixtures/
auth.fixture.ts # Authentication fixtures
data.fixture.ts # Test data factories
pages/ # Page Object Model classes
HomePage.ts
BlogPage.ts
ContactsPage.ts
LoginPage.ts
helpers/
api.helper.ts # API call helpers for test setup
Sayfa Nesne Modeli
Sayfa Nesne Modeli, sürdürülebilir E2E testleri için en önemli modeldir. Bir seçici değiştiğinde, onu kullanan her testi değil, tek bir yeri güncellersiniz.
// tests/pages/LoginPage.ts
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
readonly forgotPasswordLink: Locator;
constructor(page: Page) {
this.page = page;
// Always use data-testid selectors
this.emailInput = page.getByTestId('login-email');
this.passwordInput = page.getByTestId('login-password');
this.submitButton = page.getByTestId('login-submit');
this.errorMessage = page.getByTestId('login-error');
this.forgotPasswordLink = page.getByTestId('forgot-password-link');
}
async goto() {
await this.page.goto('/auth/login');
await expect(this.page).toHaveURL('/auth/login');
await expect(this.submitButton).toBeVisible();
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async loginAndWaitForRedirect(email: string, password: string, expectedUrl: string) {
await this.login(email, password);
await expect(this.page).toHaveURL(expectedUrl, { timeout: 10_000 });
}
async expectError(message: string) {
await expect(this.errorMessage).toBeVisible();
await expect(this.errorMessage).toContainText(message);
}
}
// tests/pages/ContactsPage.ts
import { Page, Locator, expect } from '@playwright/test';
export class ContactsPage {
readonly page: Page;
readonly addButton: Locator;
readonly searchInput: Locator;
readonly table: Locator;
readonly dialog: Locator;
readonly nameInput: Locator;
readonly emailInput: Locator;
readonly saveButton: Locator;
readonly successToast: Locator;
constructor(page: Page) {
this.page = page;
this.addButton = page.getByTestId('contacts-add-button');
this.searchInput = page.getByTestId('contacts-search');
this.table = page.getByTestId('contacts-table');
this.dialog = page.getByTestId('contact-dialog');
this.nameInput = page.getByTestId('contact-name-input');
this.emailInput = page.getByTestId('contact-email-input');
this.saveButton = page.getByTestId('contact-save-button');
this.successToast = page.getByTestId('toast-success');
}
async goto() {
await this.page.goto('/dashboard/contacts');
await expect(this.table).toBeVisible();
}
async openAddDialog() {
await this.addButton.click();
await expect(this.dialog).toBeVisible();
}
async createContact(name: string, email: string) {
await this.openAddDialog();
await this.nameInput.fill(name);
await this.emailInput.fill(email);
await this.saveButton.click();
await expect(this.successToast).toBeVisible();
}
async searchFor(query: string) {
await this.searchInput.fill(query);
await this.page.waitForTimeout(500); // Debounce delay
}
async getRowCount() {
return this.table.getByRole('row').count();
}
async expectContactInTable(name: string) {
await expect(this.table).toContainText(name);
}
}
Kimlik Doğrulama Armatürleri
// tests/fixtures/auth.fixture.ts
import { test as base, Page } from '@playwright/test';
type AuthFixture = {
adminPage: Page;
userPage: Page;
};
export const test = base.extend<AuthFixture>({
adminPage: async ({ browser }, use) => {
const context = await browser.newContext();
const page = await context.newPage();
// Login via API to bypass UI (faster, more reliable)
const response = await page.request.post('/auth/login', {
data: {
email: process.env.TEST_ADMIN_EMAIL || '[email protected]',
password: process.env.TEST_ADMIN_PASSWORD || 'testpassword123',
},
});
if (!response.ok()) {
throw new Error(`Auth failed: ${response.status()}`);
}
// Cookies are set automatically on the context
await use(page);
await context.close();
},
userPage: async ({ browser }, use) => {
const context = await browser.newContext();
const page = await context.newPage();
await page.request.post('/auth/login', {
data: {
email: process.env.TEST_USER_EMAIL || '[email protected]',
password: process.env.TEST_USER_PASSWORD || 'testpassword123',
},
});
await use(page);
await context.close();
},
});
export { expect } from '@playwright/test';
Test Yazma
// tests/admin/contacts.spec.ts
import { test, expect } from '../fixtures/auth.fixture';
import { ContactsPage } from '../pages/ContactsPage';
test.describe('Contacts Management', () => {
test('admin can create, search, and delete a contact', async ({ adminPage }) => {
const contactsPage = new ContactsPage(adminPage);
const testName = `Test User ${Date.now()}`;
const testEmail = `test-${Date.now()}@example.com`;
await contactsPage.goto();
// Create
await contactsPage.createContact(testName, testEmail);
await contactsPage.expectContactInTable(testName);
// Search
await contactsPage.searchFor(testName);
const rowCount = await contactsPage.getRowCount();
expect(rowCount).toBe(2); // 1 header + 1 result
// Delete
await adminPage.getByTestId(`contact-delete-${testEmail}`).click();
await adminPage.getByTestId('confirm-delete').click();
await expect(contactsPage.successToast).toBeVisible();
await expect(contactsPage.table).not.toContainText(testName);
});
test('shows validation errors for invalid contact data', async ({ adminPage }) => {
const contactsPage = new ContactsPage(adminPage);
await contactsPage.goto();
await contactsPage.openAddDialog();
// Submit without filling required fields
await contactsPage.saveButton.click();
await expect(adminPage.getByTestId('error-name')).toContainText('required');
await expect(adminPage.getByTestId('error-email')).toContainText('required');
});
});
Genel Sayfa Testleri
// tests/public/homepage.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Homepage', () => {
test('renders hero section with correct heading', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/ECOSIRE/);
const hero = page.getByTestId('hero-heading');
await expect(hero).toBeVisible();
await expect(hero).not.toBeEmpty();
});
test('navigation links are all accessible', async ({ page }) => {
await page.goto('/');
const navLinks = page.getByRole('navigation').getByRole('link');
const count = await navLinks.count();
for (let i = 0; i < count; i++) {
const link = navLinks.nth(i);
const href = await link.getAttribute('href');
// All nav links should have valid hrefs
expect(href).toBeTruthy();
expect(href).not.toBe('#');
}
});
test('CTA button links to services page', async ({ page }) => {
await page.goto('/');
await page.getByTestId('hero-cta').click();
await expect(page).toHaveURL(/\/services/);
});
});
i18n Testleri
// tests/public/i18n.spec.ts
import { test, expect } from '@playwright/test';
const testLocales = ['zh', 'es', 'ar', 'fr'];
test.describe('Internationalization', () => {
for (const locale of testLocales) {
test(`/${locale}/ renders with correct lang attribute`, async ({ page }) => {
await page.goto(`/${locale}/`);
const html = page.locator('html');
await expect(html).toHaveAttribute('lang', locale);
});
}
test('Arabic locale sets dir="rtl"', async ({ page }) => {
await page.goto('/ar/');
const html = page.locator('html');
await expect(html).toHaveAttribute('dir', 'rtl');
});
test('language switcher changes locale', async ({ page }) => {
await page.goto('/');
await page.getByTestId('language-switcher').click();
await page.getByTestId('locale-option-fr').click();
await expect(page).toHaveURL('/fr/');
await expect(page.locator('html')).toHaveAttribute('lang', 'fr');
});
test('hreflang tags are present on all pages', async ({ page }) => {
await page.goto('/');
const hreflangs = await page.locator('link[rel="alternate"][hreflang]').all();
// Should have 11 locale hreflangs + x-default
expect(hreflangs.length).toBeGreaterThanOrEqual(12);
});
});
Erişilebilirlik Testleri
// tests/public/accessibility.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test.describe('Accessibility', () => {
const pages = ['/', '/blog', '/services', '/about', '/contact'];
for (const path of pages) {
test(`${path} has no WCAG 2.1 AA violations`, async ({ page }) => {
await page.goto(path);
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
.analyze();
if (results.violations.length > 0) {
const report = results.violations.map((v) =>
`${v.id}: ${v.description} — ${v.nodes.length} element(s)`
).join('\n');
expect.fail(`Accessibility violations on ${path}:\n${report}`);
}
});
}
test('skip link is the first focusable element', async ({ page }) => {
await page.goto('/');
await page.keyboard.press('Tab');
const focused = page.locator(':focus');
await expect(focused).toHaveAttribute('href', '#main-content');
});
});
CI Entegrasyonu
# .github/workflows/ci.yml (E2E section)
e2e-tests:
runs-on: ubuntu-latest
needs: [unit-tests, build]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Install Playwright browsers
run: cd apps/web && npx playwright install --with-deps chromium firefox
- name: Run E2E tests
run: cd apps/web && npx playwright test --project=chromium --project=firefox
env:
PLAYWRIGHT_BASE_URL: ${{ secrets.STAGING_URL }}
TEST_ADMIN_EMAIL: ${{ secrets.TEST_ADMIN_EMAIL }}
TEST_ADMIN_PASSWORD: ${{ secrets.TEST_ADMIN_PASSWORD }}
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: apps/web/playwright-report/
retention-days: 7
Sıkça Sorulan Sorular
data-testid, ARIA rolleri ve metin seçicileri ne zaman kullanmalıyım?
Birincil seçici olarak data-testid'ı tercih edin; kullanıcı arayüzü yeniden tasarımlarından, kopya değişikliklerinden ve yeniden düzenlemelerden kurtulur. Erişilebilirliği aynı anda test etmek için ARIA rol seçicilerini (getByRole('button', { name: 'Submit' })) kullanın. Metin içeriği seçicilerden (getByText('Submit')) kaçının çünkü kopya değiştiğinde bozulurlar. Hiçbir zaman CSS sınıfı seçicilerini kullanmayın; bunlar stil çalışmasıyla değişen uygulama ayrıntılarıdır.
CI'da kaç proje (tarayıcı) çalıştırmalıyım?
Çekme isteklerinde Chromium'u yalnızca CI'yi hızlı tutmak için çalıştırın. Ana dal birleştirmeleri için üç masaüstü tarayıcının tümünü (Chromium, Firefox, WebKit) çalıştırın. Sürüm adayları için mobil görünümler ekleyin. WebKit, Safari'ye özgü düzen hatalarını yakalar; Firefox, Chrome ve Safari'nin gizlediği tuhaflıkları yakalar. Katmanlama, yayınlanmadan önce tarayıcılar arası hataları yakalarken CI dakikalarından tasarruf sağlar.
Zamanlama sorunlarından kaynaklanan hatalı testleri nasıl halledebilirim?
Oyun yazarının otomatik beklemesi çoğu zamanlama sorununu çözecektir; page.waitForTimeout() yerine await expect(locator).toBeVisible() kullanın. Otomatik bekleme yeterli değilse, ağa bağlı durum için page.waitForResponse(), yoğun sayfalar için page.waitForLoadState('networkidle') veya özel koşullar için page.waitForFunction() kullanın. Gerçek arızaları altyapıdaki düzensizliklerden ayırmak için CI'da retries: 2 değerini ayarlayın.
E2E testleri gerçek veritabanını mı yoksa tohumlanmış test veritabanını mı kullanmalı?
E2E testleri için yerleşik bir test veritabanına sahip özel bir hazırlama ortamı kullanın. Üretime karşı asla E2E testlerini çalıştırmayın. CI için, test verilerinin eklendiği yeni bir veritabanıyla Docker Compose yığınını başlatın. Test verilerini, sürüklenebilecek önceden eklenmiş verilere güvenmek yerine beforeAll/afterAll kancalarındaki API çağrıları aracılığıyla oluşturun ve temizleyin.
Playwright'ta dosya indirme işlemlerini nasıl test ederim?
page.waitForDownload() modelini kullanın: const [download] = await Promise.all([page.waitForEvent('download'), page.click('[data-testid="download-button"]')]); const path = await download.path(); expect(path).toBeTruthy();. Ayrıca download.suggestedFilename() öğesini kontrol edebilir ve doğrulama için dosya içeriğini okuyabilirsiniz.
Sonraki Adımlar
Playwright E2E testleri, kullanıcıların uygulamanızı görmeden önceki son güvenlik ağıdır. Sayfa nesneleri, veri testi seçicileri, kimlik doğrulama fikstürleri ve CI entegrasyonuyla doğru bir şekilde yatırım yapıldığında, birim testlerinin kaçırdığı şeyleri yakalar ve size Cuma günleri konuşlandırma konusunda güven verir.
ECOSIRE, genel sayfalarda, yönetici iş akışlarında ve 11 yerel ayarın tamamı için erişilebilirlikte 416'dan fazla test senaryosunu kapsayan 29 Playwright E2E test dosyasını tutar. Kalite kontrollü Next.js uygulamalarını nasıl oluşturduğumuzu öğrenmek için ön uç mühendislik hizmetlerimizi keşfedin.
Yazan
ECOSIRE Research and Development Team
ECOSIRE'da kurumsal düzeyde dijital ürünler geliştiriyor. Odoo entegrasyonları, e-ticaret otomasyonu ve yapay zeka destekli iş çözümleri hakkında içgörüler paylaşıyor.
İlgili Makaleler
AI-Powered Accounting Automation: What Works in 2026
Discover which AI accounting automation tools deliver real ROI in 2026, from bank reconciliation to predictive cash flow, with implementation strategies.
Payroll Processing: Setup, Compliance, and Automation
Complete payroll processing guide covering employee classification, federal and state withholding, payroll taxes, garnishments, automation platforms, and year-end W-2 compliance.
AI Agents for Business Automation: The 2026 Landscape
Explore how AI agents are transforming business automation in 2026, from multi-agent orchestration to practical deployment strategies for enterprise teams.