Why Capture Visual Evidence?

When a test fails, the most common question is “what did the screen look like when it failed?” A stack trace tells you which assertion failed, but a screenshot shows the actual state of the application at that moment. Perhaps the element was hidden behind a modal. Perhaps a loading spinner was still visible. Perhaps the page showed an unexpected error message. Screenshots answer these questions instantly.

Video recording goes further — it shows the entire sequence of events leading to the failure. You can watch the test navigate, click, type, and see exactly where things went wrong.

Screenshot Capture

Playwright

// Screenshot on failure (built-in)
// playwright.config.ts
export default defineConfig({
  use: {
    screenshot: 'only-on-failure', // 'on', 'off', or 'only-on-failure'
  },
});

// Manual screenshot in test
test('checkout flow', async ({ page }) => {
  await page.goto('/cart');
  await page.screenshot({ path: 'screenshots/cart.png' });

  await page.click('#checkout');
  await page.screenshot({ path: 'screenshots/checkout.png', fullPage: true });
});

// Element screenshot
const header = page.locator('header');
await header.screenshot({ path: 'screenshots/header.png' });

Cypress

// Cypress captures screenshots on failure by default
// cypress.config.js
module.exports = defineConfig({
  screenshotOnRunFailure: true,
  screenshotsFolder: 'cypress/screenshots',
});

// Manual screenshot
cy.screenshot('cart-page');
cy.get('.header').screenshot('header-component');

Selenium

// Manual screenshot
File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
FileUtils.copyFile(screenshot, new File("screenshots/failure.png"));

// In TestNG listener for automatic capture on failure
@Override
public void onTestFailure(ITestResult result) {
    WebDriver driver = getDriverFromResult(result);
    File src = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
    String path = "screenshots/" + result.getName() + "_" + System.currentTimeMillis() + ".png";
    FileUtils.copyFile(src, new File(path));
}

Video Recording

Playwright

// playwright.config.ts
export default defineConfig({
  use: {
    video: 'retain-on-failure', // 'on', 'off', 'on-first-retry', 'retain-on-failure'
  },
});
// Videos saved to test-results/ directory

Cypress

// cypress.config.js
module.exports = defineConfig({
  video: true,            // Record all test runs
  videoCompression: 32,   // Compression quality (0-51)
  videosFolder: 'cypress/videos',
});
// Videos are saved per spec file

Playwright Trace Viewer

Playwright’s trace viewer captures far more than video — it records DOM snapshots, network requests, console logs, and action details:

use: {
  trace: 'on-first-retry', // Capture trace when retrying failed tests
}

// View trace after test run
// npx playwright show-trace test-results/trace.zip

CI/CD Integration

GitHub Actions Artifacts

- name: Run Playwright tests
  run: npx playwright test
  continue-on-error: true

- name: Upload test artifacts
  uses: actions/upload-artifact@v4
  if: always()
  with:
    name: test-evidence
    path: |
      test-results/
      playwright-report/
    retention-days: 30

Jenkins Artifacts

pipeline {
  stages {
    stage('Test') {
      steps {
        sh 'npx playwright test'
      }
      post {
        always {
          archiveArtifacts artifacts: 'test-results/**/*', allowEmptyArchive: true
          publishHTML([reportDir: 'playwright-report', reportFiles: 'index.html'])
        }
      }
    }
  }
}

Step-Level Evidence

For audit trails and debugging, capture evidence at each significant step:

test('complete purchase flow', async ({ page }) => {
  await page.goto('/products');
  await page.screenshot({ path: 'evidence/01-products-page.png' });

  await page.click('[data-testid="add-to-cart"]');
  await page.screenshot({ path: 'evidence/02-added-to-cart.png' });

  await page.goto('/cart');
  await page.screenshot({ path: 'evidence/03-cart-page.png' });

  await page.click('#checkout');
  await page.fill('#card-number', '4242424242424242');
  await page.screenshot({ path: 'evidence/04-payment-form.png' });

  await page.click('#submit-payment');
  await expect(page.locator('.confirmation')).toBeVisible();
  await page.screenshot({ path: 'evidence/05-confirmation.png' });
});

Integration with Allure Reports

@Attachment(value = "Page Screenshot", type = "image/png")
public byte[] captureScreen() {
    return ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
}

@Attachment(value = "API Response", type = "application/json")
public String attachApiResponse(Response response) {
    return response.body().asString();
}

Best Practices

  1. Always capture on failure — this is the minimum requirement
  2. Capture full page screenshots — partial screenshots miss context
  3. Name files descriptively — include test name, step, and timestamp
  4. Set retention policies — old screenshots consume storage; 30 days is typical
  5. Compress videos — balance quality with file size for CI storage
  6. Use Playwright traces — they provide far richer debugging data than video alone

Exercises

Exercise 1: Screenshot Strategy

Configure automatic screenshots on failure and at key steps. Run tests, review evidence, and verify it provides enough context to diagnose failures.

Exercise 2: Video Recording

Enable video recording in Playwright or Cypress. Introduce a deliberate failure and use the video to diagnose the issue without looking at code.

Exercise 3: CI/CD Evidence Pipeline

Configure CI pipeline to capture screenshots, videos, and traces. Upload as artifacts with 30-day retention. Create a process for reviewing evidence after nightly test runs.