Named screenshots: from a test step to a manual image#

AI-SAI-Generated Summary H-RHuman Reviewed H-EHuman Edited

I wanted the images captured during testing to be useful when documenting the application. Giving each image a feature and step name made it easier to find the right screen, connect it to a user story, and reuse it in a manual draft.

This walkthrough follows takeNamedScreenshot from its implementation to a complete test example and a Markdown manual section. It also shows takeDocScreenshot, which adds a documentation folder mapping. Reconstructed tests and manual sections are labeled as illustrations.

How the capture gets its filename#

The screenshot root supports the two working directories used by the suite: the project root and its src directory.

function getScreenshotsRoot(): string {
    // Check common locations relative to cwd
    const cwd = process.cwd();
    if (cwd.endsWith('src')) {
        // Running from src/ directory
        return path.resolve(cwd, '..', '_output', 'screenshots');
    }
    // Running from project root
    return path.resolve(cwd, '_output', 'screenshots');
}

The complete named-capture function joins that root to a feature prefix and normalized step name:

export async function takeNamedScreenshot(
  page: Page,
  functionName: string,
  stepName: string,
  waitBeforeCapture: number = 500
): Promise<void> {
  await page.waitForTimeout(waitBeforeCapture);
  const safeStepName = stepName.replace(/\s+/g, '_').toLowerCase();
  const screenshotPath = path.join(getScreenshotsRoot(), `${functionName}_${safeStepName}.png`);
  await page.screenshot({ path: screenshotPath });
  console.log(`Screenshot captured: ${screenshotPath}`);
}

For example, this illustrative call assumes the active page already displays the detail screen:

import { takeNamedScreenshot } from '../../HelperFiles/components/helperFunctions/utilities';

await takeNamedScreenshot(page, 'proof_of_address', 'Detail View');
functionName:  proof_of_address
stepName:      Detail View
normalized:    detail_view
output:       <suite>/_output/screenshots/proof_of_address_detail_view.png

Only stepName is normalized. The caller keeps the prefix suitable for a filename. Reusing the same prefix and step replaces the same target file, which suits refreshing a manual image but does not provide a history of captures. Concurrent tests need distinct names if their artifacts must be retained separately.

The fourth argument overrides the default 500-millisecond delay. That wait is a historical implementation detail; it does not establish that the intended page state has loaded. A capture belongs after the relevant action and state checks.

A complete workflow that produces meaningful image names#

This illustrative spec adapts the existing Proof of Address workflow into one test, keeping its imports, setup, verification calls, captures, and cleanup visible together. The complete Proof of Address walkthrough shows the original six serial tests.

import { test, type BrowserContext, type Page } from '@playwright/test';
import { columnNames } from '../../HelperFiles/cit-app-config.js';
import { setupTestPage } from '../../HelperFiles/components/settings-helpers.js';
import { clickViewActionInTable } from '../../HelperFiles/components/common.js';
import { safeClick } from '../../HelperFiles/components/buttons.js';
import {
    checkColumnHeaders,
    verifyFieldsPresent
} from '../../HelperFiles/components/verification_checks.js';
import {
    takeNamedScreenshot,
    takeDocScreenshot
} from '../../HelperFiles/components/helperFunctions/utilities.js';

test.describe('Proof of Address captures', () => {
    let context: BrowserContext;
    let page: Page;

    test.beforeAll(async ({ browser }) => {
        ({ context, page } = await setupTestPage(browser, {
            menuItem: 'Account',
            subMenuItem: 'Proof of Address'
        }));
    });

    test.afterAll(async () => {
        await context?.close();
    });

    test('Capture the list, detail fields, and return to the list', async () => {
        await checkColumnHeaders(page, columnNames.ProofOfAddress.HomePage);
        await takeNamedScreenshot(page, 'proof_of_address', 'list_page');

        await clickViewActionInTable(page);
        await verifyFieldsPresent(page, columnNames.ProofOfAddress.DetailFields);
        await takeNamedScreenshot(page, 'proof_of_address', 'detail_view');
        await takeDocScreenshot(page, 'Investors', 'proof-of-address-view-details');

        await safeClick(page, 'Back');
        await takeNamedScreenshot(page, 'proof_of_address', 'back_to_list');
    });
});

The context belongs to this suite, and closing it closes its page. verifyFieldsPresent asserts field visibility before the detail capture. The older checkColumnHeaders helper logs missing columns without failing the test; the full walkthrough explains that behavior.

The requested named outputs form a small vocabulary of screens:

_output/screenshots/
├── proof_of_address_list_page.png
├── proof_of_address_detail_view.png
└── proof_of_address_back_to_list.png

Or keep screenshot labels in JSON#

An illustrative screenshots.json at the suite root can supply the names:

{
  "feature": "proof_of_address",
  "detailView": "detail_view",
  "waitBeforeCapture": 500
}

This alternative call runs after the detail view is ready. The file path is relative to the test process’s working directory, so this example assumes execution from the suite root:

import { readFileSync } from 'node:fs';
import { takeNamedScreenshot } from '../../HelperFiles/components/helperFunctions/utilities';

type ScreenshotConfig = {
    feature: string;
    detailView: string;
    waitBeforeCapture: number;
};

const config: ScreenshotConfig = JSON.parse(
    readFileSync('screenshots.json', 'utf8')
);

await takeNamedScreenshot(
    page,
    config.feature,
    config.detailView,
    config.waitBeforeCapture
);

The values map directly to the existing parameters. As with the address configuration, the TypeScript annotation describes the expected file shape without validating JSON at runtime.

Route a capture to a documentation section#

The related takeDocScreenshot helper handles the destination as well as the name. The mapping includes these source entries:

const DOC_SECTION_FOLDERS: Record<string, string> = {
  'Communication': 'Communication',
  'Investors': 'Account/Investors',
  'Users': 'User/Users',
  'User-Detail': 'User/Users/User-Detail',
  'SponsorOrg': 'Account/SponsorshipOrganization',
  'CITCorp': 'Account/CITCorporations',

That excerpt is the opening of a larger mapping object. Its consumer is the complete function below:

export async function takeDocScreenshot(
  page: Page,
  section: string,
  filename: string,
  waitBeforeCapture: number = 500
): Promise<void> {
  await page.waitForTimeout(waitBeforeCapture);

  // Get the folder path for this section
  const folderPath = DOC_SECTION_FOLDERS[section] || section;

  // Check for environment variable override (direct output to docs _static folder)
  const docsDirOverride = process.env.DOC_SCREENSHOT_DIR;
  const fs = await import('fs');

  let docsRoot: string;
  let ext = '.png';

  if (docsDirOverride) {
    // Output directly to documentation _static folder
    docsRoot = path.join(docsDirOverride, folderPath);
    // Use .jpg for documentation (matches existing files)
    ext = '.jpg';
  } else {
    // Default: screenshots/docs subfolder
    docsRoot = path.join(getScreenshotsRoot(), 'docs', folderPath);
  }

  if (!fs.existsSync(docsRoot)) {
    fs.mkdirSync(docsRoot, { recursive: true });
  }

  const screenshotPath = path.join(docsRoot, `${filename}${ext}`);
  await page.screenshot({ path: screenshotPath });
  console.log(`[Doc Screenshot] ${screenshotPath}`);
}

The actual Proof of Address spec uses these two calls together:

    test('View proof of address details', async () => {
        await clickViewActionInTable(page);
        await takeNamedScreenshot(page, 'proof_of_address', 'detail_view');
        await takeDocScreenshot(page, 'Investors', 'proof-of-address-view-details');
    });

Capture

Destination from the suite root

Named capture

_output/screenshots/proof_of_address_detail_view.png

Documentation capture, default

_output/screenshots/docs/Account/Investors/proof-of-address-view-details.png

Documentation capture, with DOC_SCREENSHOT_DIR

<DOC_SCREENSHOT_DIR>/Account/Investors/proof-of-address-view-details.jpg

Investors selects an asset folder, even though the screen is Proof of Address. It does not identify the login role. The environment override also changes the output extension to .jpg; a manual reference must use the destination and extension actually produced.

Put the reviewed image into a manual#

The documented generator groups screenshots by feature prefix and uses names such as *_detail* and *_view* while organizing draft pages. It also examines tests to determine page structure.

For the named-image route, the flow is concrete:

  1. Run the relevant workflow and inspect the result and captured image.

  2. Collect proof_of_address_* images for the feature’s draft pages.

  3. Place the reviewed image in the manual’s static asset folder.

  4. Reference that asset from the procedure and write an appropriate caption.

This illustrative manual section assumes proof_of_address_detail_view.png has been copied into the manual’s static asset folder. The entire example is shown as Markdown source:

# View Proof of Address details

1. Open **Account → Proof of Address**.
2. Locate the record you want to inspect.
3. Select the record's **View** action.

```{figure} /_static/citcorp_app/ProofOfAddress/proof_of_address_detail_view.png
:alt: Proof of Address record with its detail fields and history

The detail screen for the selected Proof of Address record.
```

Review the record's displayed fields and history, then select **Back**
to return to the list.

takeNamedScreenshot supplies the named image. It does not copy that image to the manual or write the surrounding explanation. The manual-draft chapter follows the stories, test steps, configuration, generator output, and author review through that remaining work.

Continue to selector diagnostics, or return to Functions explained.