Site logo
Tác giả
  • avatar Nguyễn Đức Xinh
    Name
    Nguyễn Đức Xinh
    Twitter
Ngày xuất bản
Ngày xuất bản

Playwright: Chụp Screenshot & Đính Kèm vào HTML Report

Playwright: Chụp Screenshot & Đính Kèm vào HTML Report

1. Tổng quan

Playwright cung cấp hai cách để lưu screenshot trong E2E test:

Cách Mô tả Hiện trong report?
page.screenshot({ path }) Lưu file ảnh ra disk Không (phải tự mở file)
testInfo.attach() Đính kèm vào HTML report — hiện trong tab Attachments

Bài này hướng dẫn kết hợp cả hai: lưu file ra disk đính kèm vào report để dễ xem nhất.


2. Cài đặt

pnpm add -D @playwright/test
pnpm add dotenv

Cấu hình playwright.config.ts:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  reporter: 'html',
  use: {
    trace: 'on-first-retry',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
});

3. Chụp screenshot cơ bản

Full page

import { test } from '@playwright/test';

test('chụp full page', async ({ page }) => {
  await page.goto('https://github.com');
  await page.screenshot({
    path: 'screenshots/github-home.png',
    fullPage: true,
  });
});

Chụp một element cụ thể

test('chụp element', async ({ page }) => {
  await page.goto('https://github.com');
  const header = page.locator('header');
  await header.screenshot({ path: 'screenshots/github-header.png' });
});

Chụp theo vùng (clip)

test('chụp theo vùng', async ({ page }) => {
  await page.goto('https://github.com');
  await page.screenshot({
    path: 'screenshots/github-clip.png',
    clip: { x: 0, y: 0, width: 1280, height: 600 },
  });
});

4. Đính kèm screenshot vào HTML Report

Dùng testInfo.attach() để Playwright nhúng ảnh trực tiếp vào HTML report. Screenshot sẽ hiện trong tab Attachments khi chạy npx playwright show-report.

import { test, type TestInfo } from '@playwright/test';
import * as path from 'path';
import * as fs from 'fs';

const SCREENSHOT_DIR = 'src/storage/screenshots';

function ensureDir(dir: string) {
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
  }
}

test('screenshot với report attachment', async ({ page }, testInfo: TestInfo) => {
  await page.goto('https://github.com');

  ensureDir(SCREENSHOT_DIR);
  const screenshotPath = path.join(SCREENSHOT_DIR, 'github-home.png');

  // Bước 1: Lưu file ra disk
  await page.screenshot({ path: screenshotPath, fullPage: true });

  // Bước 2: Đính kèm vào HTML report
  await testInfo.attach('github-home', {
    path: screenshotPath,
    contentType: 'image/png',
  });
});

Lưu ý: testInfo.attach() copy file vào thư mục playwright-report/data/ nên ảnh sẽ không bị mất kể cả khi xóa thư mục screenshots gốc.


5. Thực chiến: Login GitHub & Screenshot billing history

Cấu trúc thư mục

apps/core/
├── .env                          # credentials (không commit)
├── .env.example                  # template
└── tests/
    └── ai-account/
        └── github-login.spec.ts

Cấu hình .env

GITHUB_ACCOUNTS="account1@example.com,password1|account2@example.com,password2"

Format: email,password phân cách bởi | để hỗ trợ nhiều account.

.env.example

GITHUB_ACCOUNTS="account1@kozo-japan.com,xxx|account2@kozo-japan.com,xxx"

Full test file

import { test, type TestInfo } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
import * as dotenv from 'dotenv';

// npx playwright test tests/ai-account/github-login.spec.ts --timeout 60000
// npx playwright test tests/ai-account/github-login.spec.ts --headed --timeout 60000

dotenv.config({ path: path.resolve(__dirname, '../../.env') });

interface GithubAccount {
  email: string;
  password: string;
}

function parseGithubAccounts(): GithubAccount[] {
  const raw = process.env.GITHUB_ACCOUNTS || '';
  return raw
    .split('|')
    .map(entry => entry.trim())
    .filter(Boolean)
    .map(entry => {
      const [email, password] = entry.split(',');
      return { email: email.trim(), password: password.trim() };
    });
}

const ACCOUNTS = parseGithubAccounts();

const SCREENSHOT_DIR = path.join(__dirname, '../../src/storage/ai-account/screenshots');

function ensureDir(dir: string) {
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
  }
}

for (const account of ACCOUNTS) {
  test(`Github login - ${account.email}`, async ({ page }, testInfo: TestInfo) => {
    console.log(`\nLogging in as: ${account.email}`);

    // Bước 1: Truy cập trang login
    await page.goto('https://github.com/login');
    await page.waitForSelector('#login_field', { timeout: 15000 });

    // Bước 2: Điền credentials
    await page.fill('#login_field', account.email);
    await page.fill('#password', account.password);
    await page.click('[name="commit"]');

    // Bước 3: Chờ redirect ra khỏi /login (login thành công)
    await page.waitForURL(url => !url.toString().includes('/login'), { timeout: 30000 });
    console.log(`Login successful for: ${account.email}`);

    // Bước 4: Navigate đến billing history
    await page.goto('https://github.com/account/billing/history');
    await page.waitForTimeout(2000);
    console.log(`Navigated to billing history for: ${account.email}`);

    // Bước 5: Lưu screenshot ra disk
    ensureDir(SCREENSHOT_DIR);
    const safeEmail = account.email.replace(/[^a-zA-Z0-9]/g, '_');
    const screenshotPath = path.join(SCREENSHOT_DIR, `billing-history_${safeEmail}.png`);
    await page.screenshot({ path: screenshotPath, fullPage: true });
    console.log(`Screenshot saved: ${screenshotPath}`);

    // Bước 6: Đính kèm vào Playwright HTML report
    await testInfo.attach(`billing-history_${safeEmail}`, {
      path: screenshotPath,
      contentType: 'image/png',
    });
  });
}

6. Chạy test & xem report

# Chạy headless (mặc định)
npx playwright test tests/ai-account/github-login.spec.ts --timeout 60000

# Chạy có browser hiện lên (dễ debug)
npx playwright test tests/ai-account/github-login.spec.ts --headed --timeout 60000

# Chạy 1 account cụ thể bằng grep
npx playwright test tests/ai-account/github-login.spec.ts -g "account1@example.com" --timeout 60000

# Mở HTML report
npx playwright show-report

Sau khi chạy xong, mở report và click vào test case → tab Attachments sẽ hiện screenshot:

Tests
└── Github login - account1@example.com  ✓ passed
    └── Attachments
        └── billing-history_account1_example_com.png  [xem ảnh]

7. Các tùy chọn screenshot nâng cao

Tắt animations khi chụp

await page.screenshot({
  path: 'screenshot.png',
  fullPage: true,
  animations: 'disabled', // tắt CSS animations để ảnh không bị blur
});

Chụp với scale cao hơn (retina)

await page.screenshot({
  path: 'screenshot@2x.png',
  scale: 'css', // hoặc 'device' cho retina
});

Attach buffer trực tiếp (không cần lưu file)

test('attach buffer', async ({ page }, testInfo: TestInfo) => {
  await page.goto('https://github.com');

  const buffer = await page.screenshot({ fullPage: true });

  await testInfo.attach('github-home', {
    body: buffer,
    contentType: 'image/png',
  });
});

Chụp nhiều bước trong cùng 1 test

test('multi-step screenshot', async ({ page }, testInfo: TestInfo) => {
  await page.goto('https://github.com/login');
  await testInfo.attach('step-1-login-page', {
    body: await page.screenshot(),
    contentType: 'image/png',
  });

  await page.fill('#login_field', 'user@example.com');
  await page.fill('#password', 'password');
  await page.click('[name="commit"]');

  await page.waitForURL(url => !url.toString().includes('/login'));
  await testInfo.attach('step-2-after-login', {
    body: await page.screenshot(),
    contentType: 'image/png',
  });
});

Tóm tắt

Mục tiêu Giải pháp
Lưu ảnh ra disk page.screenshot({ path: '...' })
Hiện ảnh trong HTML report testInfo.attach(name, { path, contentType })
Attach không cần file testInfo.attach(name, { body: buffer, contentType })
Chụp full page { fullPage: true }
Chụp element locator.screenshot()
Tắt animations { animations: 'disabled' }
Xem report npx playwright show-report