Skip to content

Commit 808116c

Browse files
authored
[codex] Manage visual regression baselines from CI (#1294)
* test: manage visual regression baselines from CI Move screenshot tests and tracked baselines under a dedicated visual test tree, split visual test scripts from regular browser tests, and add CI-owned baseline update workflows. Guard local --update runs so screenshot baselines are only refreshed by the manual GitHub Actions workflow. * test: split visual behavior coverage Move non-screenshot assertions out of the CI-owned visual test tree so regular Vitest browser runs cover them locally. Keep the visual test tree focused on screenshot assertions while preserving the same behavioral coverage in root test modules.
1 parent 673a5cf commit 808116c

145 files changed

Lines changed: 659 additions & 436 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/pull-request.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,31 @@ jobs:
5454
run: yarn build:res
5555
- name: Vitest
5656
run: yarn ci:test
57+
Visual_Regression:
58+
runs-on: ubuntu-latest
59+
container:
60+
# Keep this image tag aligned with package.json's playwright version.
61+
image: mcr.microsoft.com/playwright:v1.59.1-noble
62+
steps:
63+
- name: Checkout
64+
uses: actions/checkout@v6.0.2
65+
- name: Setup Node.js environment
66+
uses: actions/setup-node@v6.3.0
67+
with:
68+
node-version-file: ".node-version"
69+
cache: yarn
70+
- name: Enable Corepack
71+
run: corepack enable
72+
- name: Install dependencies
73+
run: yarn
74+
- name: Build ReScript
75+
run: yarn build:res
76+
- name: Visual regression tests
77+
run: yarn ci:test:visual
78+
- name: Upload visual regression artifacts
79+
if: failure()
80+
uses: actions/upload-artifact@v4
81+
with:
82+
name: visual-regression-artifacts
83+
path: |
84+
apps/docs/.vitest-attachments
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
name: Update Visual Regression Screenshots
2+
on:
3+
workflow_dispatch:
4+
5+
permissions:
6+
contents: write
7+
8+
jobs:
9+
Update_Visual_Screenshots:
10+
runs-on: ubuntu-latest
11+
container:
12+
# Keep this image tag aligned with package.json's playwright version.
13+
image: mcr.microsoft.com/playwright:v1.59.1-noble
14+
steps:
15+
- name: Refuse default branch
16+
if: github.ref_name == github.event.repository.default_branch
17+
run: |
18+
echo "Visual screenshot baselines must be updated from a feature branch."
19+
exit 1
20+
- name: Checkout
21+
uses: actions/checkout@v6.0.2
22+
with:
23+
ref: ${{ github.ref }}
24+
- name: Setup Node.js environment
25+
uses: actions/setup-node@v6.3.0
26+
with:
27+
node-version-file: ".node-version"
28+
cache: yarn
29+
- name: Enable Corepack
30+
run: corepack enable
31+
- name: Install dependencies
32+
run: yarn
33+
- name: Build ReScript
34+
run: yarn build:res
35+
- name: Update visual screenshot baselines
36+
run: yarn workspace @rescript-lang/docs ci:test:visual:update
37+
- name: Commit changed baselines
38+
id: commit-baselines
39+
run: |
40+
git add apps/docs/__tests__/visual/__screenshots__
41+
42+
if git diff --cached --quiet; then
43+
echo "changed=false" >> "$GITHUB_OUTPUT"
44+
echo "No visual screenshot baseline changes were produced." >> "$GITHUB_STEP_SUMMARY"
45+
exit 0
46+
fi
47+
48+
git config user.name "github-actions[bot]"
49+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
50+
51+
git commit \
52+
-m "test: update visual regression screenshots" \
53+
-m "Updated from the GitHub Actions visual baseline workflow."
54+
55+
echo "changed=true" >> "$GITHUB_OUTPUT"
56+
- name: Push changed baselines
57+
if: steps.commit-baselines.outputs.changed == 'true'
58+
run: git push origin HEAD:${{ github.ref_name }}
59+
- name: Summarize changed baselines
60+
if: steps.commit-baselines.outputs.changed == 'true'
61+
run: |
62+
echo "## Visual screenshot baseline changes" >> "$GITHUB_STEP_SUMMARY"
63+
echo "" >> "$GITHUB_STEP_SUMMARY"
64+
git diff --name-only HEAD~1 HEAD -- apps/docs/__tests__/visual/__screenshots__ \
65+
| sed 's/^/- /' >> "$GITHUB_STEP_SUMMARY"

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ apps/docs/.env.local
105105
.worktrees
106106

107107
# Vitest screenshots
108-
!apps/docs/__tests__/__screenshots__/**/*
108+
!apps/docs/__tests__/**/__screenshots__/**/*
109109
.vitest-attachments
110110
apps/docs/.vitest-attachments
111111
apps/guide/.vitest-attachments
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
open ReactRouter
2+
open Vitest
3+
4+
@get external textContent: WebAPI.DOMAPI.element => string = "textContent"
5+
6+
let mockAuthor: BlogFrontmatter.author = {
7+
username: "test-author",
8+
fullname: "Test Author",
9+
role: "Developer",
10+
imgUrl: "https://rescript-lang.org/brand/rescript-brandmark.svg",
11+
social: X("testauthor"),
12+
}
13+
14+
let mockFrontmatter: BlogFrontmatter.t = {
15+
author: mockAuthor,
16+
co_authors: [],
17+
date: DateStr.fromString("2025-01-15"),
18+
previewImg: Nullable.null,
19+
articleImg: Nullable.null,
20+
title: "Test Blog Post Title",
21+
badge: Nullable.null,
22+
description: Nullable.Value("A short description of the blog post for testing."),
23+
}
24+
25+
test("blog article marks body content for DocSearch crawling", async () => {
26+
await viewport(1440, 900)
27+
28+
let _screen = await render(
29+
<BrowserRouter>
30+
<BlogArticle frontmatter=mockFrontmatter isArchived=false path="/blog/test-article">
31+
<p> {React.string("This is the blog post body content for testing.")} </p>
32+
</BlogArticle>
33+
</BrowserRouter>,
34+
)
35+
36+
switch document->WebAPI.Document.querySelector("article.DocSearch-content") {
37+
| Value(_) => ()
38+
| Null => failwith("expected blog article body to be marked as DocSearch content")
39+
}
40+
41+
let lvl0 = switch document->WebAPI.Document.querySelector("article .DocSearch-lvl0") {
42+
| Value(element) => element
43+
| Null => failwith("expected blog article to render a DocSearch lvl0 marker")
44+
}
45+
46+
expect(lvl0->textContent)->toBe("Blog")
47+
})
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
open ReactRouter
2+
open Vitest
3+
4+
@get external textContent: WebAPI.DOMAPI.element => string = "textContent"
5+
6+
test("breadcrumbs do not repeat the current page crumb when it is already included", async () => {
7+
await viewport(1440, 900)
8+
9+
let apiBreadcrumbs: list<Url.breadcrumb> = list{
10+
{Url.name: "Docs", href: "/docs/manual/api"},
11+
{Url.name: "API", href: "/docs/manual/api"},
12+
{Url.name: "Stdlib", href: "/docs/manual/api/stdlib"},
13+
}
14+
15+
let screen = await render(
16+
<MemoryRouter initialEntries=["/docs/manual/api/stdlib"]>
17+
<div dataTestId="breadcrumbs-wrapper">
18+
<Breadcrumbs crumbs=apiBreadcrumbs />
19+
</div>
20+
</MemoryRouter>,
21+
)
22+
23+
let _breadcrumbs = await screen->getByTestId("breadcrumbs")
24+
let breadcrumbs = switch document->WebAPI.Document.querySelector("[data-testid='breadcrumbs']") {
25+
| Value(breadcrumbs) => breadcrumbs
26+
| Null => failwith("expected breadcrumbs")
27+
}
28+
29+
expect(breadcrumbs->textContent)->toBe("Docs / API / Stdlib")
30+
})
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
open Vitest
2+
3+
test("calls onClick when clicked", async () => {
4+
await viewport(1440, 500)
5+
6+
let handleClick = fn()
7+
8+
let screen = await render(
9+
<div dataTestId="button-wrapper">
10+
<Button onClick=handleClick> {React.string("Clickable")} </Button>
11+
</div>,
12+
)
13+
14+
let btn = await screen->getByText("Clickable")
15+
await btn->click
16+
17+
expect(handleClick)->toHaveBeenCalled
18+
})
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
open ReactRouter
2+
open Vitest
3+
4+
@get external textContent: WebAPI.DOMAPI.element => string = "textContent"
5+
6+
let mockCategories: array<SidebarNav.Category.t> = [
7+
{
8+
name: "Overview",
9+
items: [
10+
{name: "Introduction", href: "/docs/manual/introduction"},
11+
{name: "Installation", href: "/docs/manual/installation"},
12+
],
13+
},
14+
{
15+
name: "Language Features",
16+
items: [
17+
{name: "Primitive Types", href: "/docs/manual/primitive-types"},
18+
{name: "Record", href: "/docs/manual/record"},
19+
{name: "Object", href: "/docs/manual/object"},
20+
],
21+
},
22+
]
23+
24+
let mockToc: TableOfContents.t = {
25+
title: "Introduction",
26+
entries: [
27+
{header: "What is ReScript", href: "#what-is-rescript"},
28+
{header: "Prerequisites", href: "#prerequisites"},
29+
{header: "Getting Started", href: "#getting-started"},
30+
],
31+
}
32+
33+
test("docs layout marks the textual content for DocSearch crawling", async () => {
34+
await viewport(1440, 900)
35+
36+
let screen = await render(
37+
<MemoryRouter initialEntries=["/docs/manual/introduction"]>
38+
<DocsLayout categories=mockCategories activeToc=mockToc docSearchLvl0="Manual">
39+
<div> {React.string("This is the documentation content.")} </div>
40+
</DocsLayout>
41+
</MemoryRouter>,
42+
)
43+
44+
let _mainContent = await screen->getByTestId("side-layout-children")
45+
46+
let mainContent = switch document->WebAPI.Document.querySelector(
47+
"[data-testid='side-layout-children']",
48+
) {
49+
| Value(element) => element
50+
| Null => failwith("expected docs layout main content")
51+
}
52+
53+
let className = switch mainContent->WebAPI.Element.getAttribute("class") {
54+
| Value(value) => value
55+
| Null => ""
56+
}
57+
58+
expect(className->String.includes("DocSearch-content"))->toBe(true)
59+
60+
let lvl0 = switch document->WebAPI.Document.querySelector(".DocSearch-lvl0") {
61+
| Value(element) => element
62+
| Null => failwith("expected docs layout to render a DocSearch lvl0 marker")
63+
}
64+
65+
expect(lvl0->textContent)->toBe("Manual")
66+
})
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
open ReactRouter
2+
open Vitest
3+
4+
test("docs overview uses unversioned docs links", async () => {
5+
await viewport(1440, 900)
6+
7+
let _screen = await render(
8+
<MemoryRouter initialEntries=["/docs"]>
9+
<div dataTestId="docs-overview-wrapper">
10+
<DocsOverview.default />
11+
</div>
12+
</MemoryRouter>,
13+
)
14+
15+
let overviewLink = switch document->WebAPI.Document.querySelector(
16+
"a[href='/docs/manual/introduction']",
17+
) {
18+
| Value(link) => link
19+
| Null => failwith("expected docs overview to link to the unversioned manual introduction")
20+
}
21+
await element(overviewLink)->toBeVisible
22+
23+
let genTypeLink = switch document->WebAPI.Document.querySelector(
24+
"a[href='/docs/manual/typescript-integration']",
25+
) {
26+
| Value(link) => link
27+
| Null => failwith("expected docs overview to link to the unversioned GenType docs page")
28+
}
29+
await element(genTypeLink)->toBeVisible
30+
31+
switch document->WebAPI.Document.querySelector("a[href*='/docs/manual/v']") {
32+
| Value(_) => failwith("expected docs overview to avoid versioned manual links")
33+
| Null => ()
34+
}
35+
})

0 commit comments

Comments
 (0)