chore: add Playwright configuration for E2E tests#41
Conversation
✅ Deploy Preview for typst-docs-web ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
We need to exclude Playwright test files in our Vitest configuration. |
There was a problem hiding this comment.
I suggest documenting the workflow in README.md or tests/e2e/README.md. It did take me some time to figure it out.
- Execute
playwright install chromium. - Prepare a suitable
docs.jsonandmetadata.json. - Start
devand keep it running. - Run
test:e2e.
suitable: await page.goto("/docs/") requires the basePath to be /docs/.
|
Since this PR also sets up the CI workflow, I think a simple addition to the documentation should be sufficient for now. As for setting up the assets required for E2E tests, I suggest extracting the hardcoded parts from the current CI into a separate bash script. This way, we can reuse the same logic both in development and CI environments, which should make things much more convenient. What do you think? |
|
Since Playwright's VRT snapshots are unstable in the GitHub Actions environment, we've set up automatic commits of the results generated in CI. This makes it convenient for pull request reviewers to visually check for UI changes. Developers can also use VRT locally as needed. What do you think about this approach? |
There was a problem hiding this comment.
Pull request overview
This pull request introduces end-to-end testing infrastructure using Playwright for Visual Regression Testing (VRT) to manage UI layout specifications and prevent unintended changes. The implementation includes test configuration, a shell script for fetching documentation assets, and CI workflow updates.
Key changes:
- Adds Playwright E2E testing framework with visual snapshot testing for documentation pages
- Introduces
scripts/fetch-docs-assets.shto standardize asset fetching across CI and local development - Configures automated E2E testing in GitHub Actions CI workflow
Reviewed changes
Copilot reviewed 8 out of 204 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
playwright.config.ts |
Configures Playwright test runner with browser settings, timeouts, and snapshot comparison thresholds |
vitest.config.ts |
Excludes E2E tests from vitest runner to prevent conflicts |
tests/e2e/index.test.ts |
Implements VRT for documentation pages by iterating through docs.json and capturing full-page screenshots |
tests/e2e/index.test.ts-snapshots/*.png |
Binary snapshot files for baseline comparison (not reviewed in detail) |
scripts/fetch-docs-assets.sh |
Shell script to download docs.json, assets, and favicon from GitHub releases |
package.json |
Adds Playwright dependencies and test scripts |
bun.lock |
Lock file updates for new dependencies |
mise.toml |
Adds shellcheck and shfmt tools for shell script quality |
README.md |
Documents E2E testing setup and usage instructions |
.gitignore |
Ignores Playwright-generated files and reports |
.github/workflows/ci.yaml |
Refactors asset fetching to use new script and adds E2E test job with snapshot auto-commit |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| const docsJson = readFileSync("./public/docs.json", "utf-8"); | ||
| const docs = JSON.parse(docsJson) as unknown as Page[]; | ||
| const [flattenedPages, _] = flattenDocs(docs); |
There was a problem hiding this comment.
The unused variable _ on line 8 should be removed. When destructuring an array and intentionally ignoring a value, you can simply omit the variable or use array destructuring without assigning the unwanted value.
const [flattenedPages] = flattenDocs(docs);| const [flattenedPages, _] = flattenDocs(docs); | |
| const [flattenedPages] = flattenDocs(docs); |
| "/docs/reference/symbols/emoji/", | ||
| ]); | ||
|
|
||
| flattenedPages.forEach((page, _) => { |
There was a problem hiding this comment.
The forEach loop has an unused second parameter _ (index) that should be removed for cleaner code:
flattenedPages.forEach((page) => {| flattenedPages.forEach((page, _) => { | |
| flattenedPages.forEach((page) => { |
| - name: Install Playwright browsers | ||
| run: bun playwright install --with-deps | ||
| - name: Run Playwright E2E tests | ||
| run: bun run test:e2e:update |
There was a problem hiding this comment.
The CI workflow runs with --update-snapshots flag on line 117, which will automatically update snapshots even if they differ. This defeats the purpose of visual regression testing in CI.
For CI, you should:
- Run tests normally (without
--update-snapshots) to detect visual changes - Only update snapshots manually when changes are intentional
- The current setup will never fail on visual regressions since it always updates snapshots to match
Consider changing line 117 to:
run: bun run test:e2eAnd only use test:e2e:update locally or in a separate workflow for intentional updates.
| run: bun run test:e2e:update | |
| run: bun run test:e2e |
| ESCAPED_BASE=$(printf '%s' "$BASE" | sed 's|/|\\/|g') | ||
| sed -i "s/\/DOCS-BASE\//${ESCAPED_BASE}/g" "${DEST}/docs.json" |
There was a problem hiding this comment.
Security concern: The script escapes the BASE variable for sed but doesn't validate or sanitize it. A malicious BASE value could potentially:
- Break the sed command with special characters
- Execute unintended commands if BASE contains shell metacharacters
Consider adding validation:
# Validate BASE format (should start and end with /)
if [[ ! "$BASE" =~ ^/.*/$ ]]; then
echo "Error: BASE must start and end with '/' (e.g., '/docs/')" >&2
exit 2
fi| # Install Playwright browsers and dependencies | ||
| bun playwright install --with-deps | ||
|
|
There was a problem hiding this comment.
The documentation mentions using bun playwright install --with-deps but doesn't explain what --with-deps does or that it requires sudo privileges on Linux. Consider adding a note:
# Install Playwright browsers and dependencies (may require sudo on Linux)
bun playwright install --with-depsOr provide alternative instructions for users who can't use sudo:
# Install browsers only (no system dependencies)
bun playwright install| # Install Playwright browsers and dependencies | |
| bun playwright install --with-deps | |
| # Install Playwright browsers and dependencies (may require sudo on Linux) | |
| bun playwright install --with-deps | |
| # If you cannot use sudo or do not want to install system dependencies, | |
| # you can install only the Playwright browsers: | |
| bun playwright install |
| expect: { | ||
| timeout: 100_000, | ||
| /* Configure snapshot comparison */ | ||
| toHaveScreenshot: { maxDiffPixelRatio: 0.05 }, |
There was a problem hiding this comment.
[nitpick] The maxDiffPixelRatio of 0.05 (5% difference allowed) is quite permissive for visual regression testing. This means up to 5% of pixels can differ before a test fails, which could allow significant visual regressions to pass undetected. Consider:
- Starting with a stricter threshold (e.g., 0.01 or 1%)
- If certain pages have legitimate minor variations, configure per-test thresholds
- A 5% threshold may be too lenient to catch meaningful UI changes
| toHaveScreenshot: { maxDiffPixelRatio: 0.05 }, | |
| toHaveScreenshot: { maxDiffPixelRatio: 0.01 }, |
| git config user.name "github-actions[bot]" | ||
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | ||
|
|
||
| git add tests/e2e/ | ||
| if ! git diff --staged --quiet; then | ||
| git commit -m "test: update VRT snapshots" || echo "No changes to commit" | ||
| git push origin HEAD:${{ github.event.pull_request.head.ref }} | ||
| else | ||
| echo "No snapshot changes detected" |
There was a problem hiding this comment.
The snapshot commit logic (lines 118-129) will push directly to the PR branch, which can create issues:
- If the PR is from a fork, this will fail due to permission issues
- Auto-committing snapshots on every CI run can create commit spam
- The condition on line 126 references
github.event.pull_request.head.refwhich may not exist for workflow_dispatch or push events
Consider either:
- Remove auto-commit logic and require developers to update snapshots locally
- Only commit snapshots when explicitly triggered (e.g., via a label or comment)
- Add proper guards to check if this is a PR from the same repository
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add tests/e2e/ | |
| if ! git diff --staged --quiet; then | |
| git commit -m "test: update VRT snapshots" || echo "No changes to commit" | |
| git push origin HEAD:${{ github.event.pull_request.head.ref }} | |
| else | |
| echo "No snapshot changes detected" | |
| # Only attempt to commit and push if this is a PR from the same repository | |
| if [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ github.event.pull_request.head.repo.full_name }}" = "${{ github.repository }}" ]; then | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add tests/e2e/ | |
| if ! git diff --staged --quiet; then | |
| git commit -m "test: update VRT snapshots" || echo "No changes to commit" | |
| git push origin HEAD:${{ github.event.pull_request.head.ref }} | |
| else | |
| echo "No snapshot changes detected" | |
| fi | |
| else | |
| echo "Skipping snapshot commit: not a PR from the same repository." |
| else | ||
| # Use portable sed: escape slashes | ||
| ESCAPED_BASE=$(printf '%s' "$BASE" | sed 's|/|\\/|g') | ||
| sed -i "s/\/DOCS-BASE\//${ESCAPED_BASE}/g" "${DEST}/docs.json" |
There was a problem hiding this comment.
The shell script lacks proper error handling for the sed command on line 91. If sed -i is not available (e.g., on macOS which requires sed -i ''), the script will fail. The portable approach for in-place editing varies between GNU sed and BSD sed.
Consider using a more portable approach:
ESCAPED_BASE=$(printf '%s' "$BASE" | sed 's|/|\\/|g')
if sed --version >/dev/null 2>&1; then
# GNU sed
sed -i "s/\/DOCS-BASE\//${ESCAPED_BASE}/g" "${DEST}/docs.json"
else
# BSD sed (macOS)
sed -i '' "s/\/DOCS-BASE\//${ESCAPED_BASE}/g" "${DEST}/docs.json"
fiOr create a temporary file for true portability:
sed "s/\/DOCS-BASE\//${ESCAPED_BASE}/g" "${DEST}/docs.json" > "${DEST}/docs.json.tmp"
mv "${DEST}/docs.json.tmp" "${DEST}/docs.json"| sed -i "s/\/DOCS-BASE\//${ESCAPED_BASE}/g" "${DEST}/docs.json" | |
| sed "s/\/DOCS-BASE\//${ESCAPED_BASE}/g" "${DEST}/docs.json" > "${DEST}/docs.json.tmp" && mv "${DEST}/docs.json.tmp" "${DEST}/docs.json" |
| cat >"${DEST}/metadata.json" <<EOF | ||
| { | ||
| "\$schema": "../metadata.schema.json", | ||
| "language": "en-US", | ||
| "version": "${VERSION}", | ||
| "typstOfficialUrl": "https://typst.app", | ||
| "typstOfficialDocsUrl": "https://typst.app/docs/", | ||
| "githubOrganizationUrl": "https://github.com/${ORG}", | ||
| "socialLinks": [ | ||
| { "url": "https://github.com/${ORG}/typst-docs-web" }, | ||
| { | ||
| "title": "Discord (Typst)", | ||
| "url": "https://discord.gg/2uDybryKPe" | ||
| } | ||
| ], | ||
| "originUrl": "https://example.com/", | ||
| "basePath": "${BASE}", | ||
| "displayTranslationStatus": false | ||
| } | ||
| EOF |
There was a problem hiding this comment.
The JSON in metadata.json is not properly escaped for shell heredoc. If VERSION, ORG, or BASE contain special characters (like quotes or backslashes), the generated JSON will be invalid.
Consider using jq to generate the JSON safely:
jq -n \
--arg version "$VERSION" \
--arg org "$ORG" \
--arg base "$BASE" \
'{
"$schema": "../metadata.schema.json",
"language": "en-US",
"version": $version,
"typstOfficialUrl": "https://typst.app",
"typstOfficialDocsUrl": "https://typst.app/docs/",
"githubOrganizationUrl": ("https://github.com/" + $org),
"socialLinks": [
{ "url": ("https://github.com/" + $org + "/typst-docs-web") },
{
"title": "Discord (Typst)",
"url": "https://discord.gg/2uDybryKPe"
}
],
"originUrl": "https://example.com/",
"basePath": $base,
"displayTranslationStatus": false
}' > "${DEST}/metadata.json"Alternatively, escape the variables properly if staying with heredoc.
| cat >"${DEST}/metadata.json" <<EOF | |
| { | |
| "\$schema": "../metadata.schema.json", | |
| "language": "en-US", | |
| "version": "${VERSION}", | |
| "typstOfficialUrl": "https://typst.app", | |
| "typstOfficialDocsUrl": "https://typst.app/docs/", | |
| "githubOrganizationUrl": "https://github.com/${ORG}", | |
| "socialLinks": [ | |
| { "url": "https://github.com/${ORG}/typst-docs-web" }, | |
| { | |
| "title": "Discord (Typst)", | |
| "url": "https://discord.gg/2uDybryKPe" | |
| } | |
| ], | |
| "originUrl": "https://example.com/", | |
| "basePath": "${BASE}", | |
| "displayTranslationStatus": false | |
| } | |
| EOF | |
| jq -n \ | |
| --arg version "$VERSION" \ | |
| --arg org "$ORG" \ | |
| --arg base "$BASE" \ | |
| '{ | |
| "$schema": "../metadata.schema.json", | |
| "language": "en-US", | |
| "version": $version, | |
| "typstOfficialUrl": "https://typst.app", | |
| "typstOfficialDocsUrl": "https://typst.app/docs/", | |
| "githubOrganizationUrl": ("https://github.com/" + $org), | |
| "socialLinks": [ | |
| { "url": ("https://github.com/" + $org + "/typst-docs-web") }, | |
| { | |
| "title": "Discord (Typst)", | |
| "url": "https://discord.gg/2uDybryKPe" | |
| } | |
| ], | |
| "originUrl": "https://example.com/", | |
| "basePath": $base, | |
| "displayTranslationStatus": false | |
| }' > "${DEST}/metadata.json" |
|
Testing every single page would probably be overkill. I think it's sufficient to pick a few representative pages from each category and check those. I'll review Copilot's suggestions later. |



close #40
This pull request introduces end-to-end testing using Playwright to implement Visual Regression Testing (VRT). This will explicitly manage UI layout specifications and prevent unintended changes.
Changes