Skip to content

Automatic Screenshots

The documentation supports automatically generated screenshots that are updated via a CI pipeline on every pull request. Screenshots are captured via a Selenium browser (Chrome) directly from the running application.

Screenshot Directive

To request a screenshot in a Markdown file, an HTML comment with the screenshot parameters is inserted:

<!-- screenshot: admin-inventarliste route=/index-test.php?r=inventory/admin selector=#inventory-grid role=admin -->
![Inventory List](../../assets/screens/admin-inventarliste.png)

Parameters

Parameter Required Default Description
route Yes* URL path relative to BASE_URL
selector No body CSS selector to wait for
role No admin Login role: admin or user
fullpage No false Full page or viewport only
width No 1440 Viewport width in pixels
height No 900 Viewport height in pixels
delay No 500 Wait time in ms after selector is found

*route can be defined in the manifest file.

ID Conventions

  • Only letters, digits, hyphens, and underscores: a-z, A-Z, 0-9, -, _
  • Recommended scheme: <area>-<page>[-<detail>]
  • Examples: admin-inventarliste, user-dashboard, admin-einstellungen-regeln

Manifest File

For reusable or centrally managed screenshots, the file docs/screenshots.manifest.yml is available:

screenshots:
  admin-inventarliste:
    route: "/index-test.php?r=inventory/admin"
    selector: "#inventory-grid"
    role: admin

Values in the manifest serve as defaults. Inline directives in Markdown override the manifest entries.

Generating Screenshots Locally

Prerequisites

  • Docker (for Selenium Chrome)
  • PHP 7.4+ with Composer
  • Access to the running application (local or staging)

Step 1: Start Selenium

docker run -d -p 4444:4444 --shm-size=2g selenium/standalone-chrome:latest

Step 2: Set Environment Variables

export BEHAT_BASE_URL=http://localhost:8080
export SELENIUM_URL=http://localhost:4444/wd/hub
export BEHAT_USER=admin@calhelp.de
export BEHAT_PASS=admin
# Optional for "user" role:
# export BEHAT_FRONTEND_USER=user@example.com
# export BEHAT_FRONTEND_PASS=userpass

Credentials

Never check credentials into version control. Use environment variables exclusively.

Step 3: Generate Screenshots

cd httpdocs/protected
vendor/bin/behat --suite=docs --no-interaction

Output Directory

Generated screenshots are placed under docs/assets/screens/:

docs/assets/screens/
├── admin-inventarliste.png        # Current screenshot
├── admin-inventarliste.prev.png   # Backup of the previous screenshot
└── .gitkeep

When overwriting an existing screenshot, the old one is automatically backed up as <id>.prev.png.

CI Pipeline

The GitHub Actions workflow file .github/workflows/docs-screenshots.yml is automatically triggered on pull requests when Markdown files under docs/ or the manifest file are changed.

Workflow

  1. PR with Markdown changes is created
  2. Workflow starts Selenium Chrome as a service container
  3. Behat suite docs is executed
  4. Generated/updated screenshots are committed to the PR branch
  5. The commit contains [skip ci] to prevent infinite loops

Infinite Loop Protection

Three protection mechanisms:

  1. github.actor != 'github-actions[bot]' — Job is skipped for bot commits
  2. [skip ci] in the commit message — GitHub Actions does not start a new run
  3. paths: filter — only .md files and the manifest trigger the workflow

Required GitHub Secrets

Secret Description Status
BEHAT_USER Admin username Existing
BEHAT_PASS Admin password Existing
DOCS_SCREENSHOT_BASE_URL URL of the staging instance Create new
BEHAT_FRONTEND_USER Frontend username Create new
BEHAT_FRONTEND_PASS Frontend password Create new

Security

The screenshot runner contains several security mechanisms:

  • Route blacklist: URLs containing /logout, /delete, /remove, /destroy, etc. are blocked
  • Relative URLs only: External URLs are rejected
  • Destructive elements disabled: After loading, every link/button with dangerous text (Delete, Remove, Logout, etc.) is disabled via JavaScript
  • No clicks on unknown links: Navigation occurs exclusively via direct URL calls