Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/frontend-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ on:
- 'frontend/**'
- '.github/workflows/frontend-tests.yml'
workflow_dispatch:
inputs:
update-snapshots:
description: 'Regenerate Playwright visual snapshots and commit them to the branch'
type: boolean
default: false

# Least privilege: these jobs only need to read the repo to check it out.
# Artifact upload uses the separate ACTIONS_RUNTIME_TOKEN, not GITHUB_TOKEN,
Expand Down Expand Up @@ -46,6 +51,9 @@ jobs:

e2e:
name: E2E tests (Playwright)
# Skipped when a dispatch asks for snapshot regeneration instead — running
# both would fail this job against the stale snapshots being replaced.
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.update-snapshots) }}
timeout-minutes: 60
runs-on: ubuntu-latest
# Pinned to match the installed @playwright/test version (1.60.0); bump both
Expand All @@ -70,3 +78,42 @@ jobs:
name: playwright-report
path: frontend/playwright-report/
retention-days: 30

# Visual snapshots must be rendered in the exact CI environment (container
# image, fonts, GPU rasterization) to be pixel-stable, so regeneration runs
# here rather than on a developer machine. Trigger manually:
# gh workflow run frontend-tests.yml --ref <branch> -f update-snapshots=true
# The job commits the refreshed PNGs back to the triggering branch.
update-snapshots:
name: Regenerate visual snapshots
if: ${{ github.event_name == 'workflow_dispatch' && inputs.update-snapshots }}
timeout-minutes: 60
runs-on: ubuntu-latest
permissions:
contents: write
container:
image: mcr.microsoft.com/playwright:v1.60.0-noble
options: --user 1001
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v6
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Build Frontend
run: npm run build
- name: Regenerate snapshots
run: npx playwright test --update-snapshots
- name: Commit refreshed snapshots
working-directory: .
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add frontend/tests/*-snapshots
if git diff --cached --quiet; then
echo "Snapshots unchanged; nothing to commit."
else
git commit -m "Regenerate Playwright visual snapshots"
git push origin HEAD:"$GITHUB_REF_NAME"
fi
35 changes: 14 additions & 21 deletions VISUAL_REGRESSION.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

This project uses Playwright to run visual regression tests that capture screenshots and compare them against baseline images. Currently, we have tests set up only for the demo page of the application.

## Running the tests
The tests run as part of the **Frontend Tests** workflow (`.github/workflows/frontend-tests.yml`), in the `e2e` job. Only the Chromium project is enabled in `frontend/playwright.config.ts`, so there is a single baseline:

1. Navigate to the **Actions** tab in GitHub
2. Select **Playwright Visual Regression Tests** workflow
3. Click **Run workflow** button and choose the branch you wish to run tests on
4. The workflow will run tests against all browsers (Chromium, Firefox, WebKit)
```
frontend/tests/demo-page-visual.spec.ts-snapshots/
└── demo-page-chromium-linux.png
```

## Understanding test results

Expand All @@ -28,23 +28,16 @@ When tests fail:

## Updating baseline images

If the UI changes are **intentional** and you want to update the baselines:
Baselines are only pixel-stable when rendered in CI's pinned Playwright container, so don't regenerate them on your own machine. If the UI changes are **intentional**, use the regeneration job:

1. From the Playwright report, download the actual images for each browser
2. Replace the existing baseline images in `frontend/tests/demo-page-visual.spec.ts-snapshots/`
3. Rename downloaded images to match existing baseline names:
- `demo-page-chromium-linux.png`
- `demo-page-firefox-linux.png`
- `demo-page-webkit-linux.png`
4. Commit and push the updated baseline images
5. Re-run the visual regression test to verify it passes
1. Navigate to the **Actions** tab in GitHub and select the **Frontend Tests** workflow
2. Click **Run workflow**, choose your branch, and check **Regenerate Playwright visual snapshots**
3. The `update-snapshots` job re-renders the baselines in the same container the tests use and commits them back to your branch

## Baseline image locations
Or from the CLI:

Current baseline images are stored in:
```
frontend/tests/demo-page-visual.spec.ts-snapshots/
├── demo-page-chromium-linux.png
├── demo-page-firefox-linux.png
└── demo-page-webkit-linux.png
```sh
gh workflow run frontend-tests.yml --ref <branch> -f update-snapshots=true
```

Alternatively, you can download the actual image from a failed run's playwright-report artifact, rename it to `demo-page-chromium-linux.png`, and commit it in place of the baseline.
48 changes: 48 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
"dependencies": {
"@ai-sdk/openai": "^2.0.0",
"@auth0/auth0-react": "^2.2.4",
"@fontsource-variable/dm-sans": "^5.2.8",
"@fontsource-variable/source-serif-4": "^5.2.9",
"@fontsource/dm-mono": "^5.2.7",
"@lexical/react": "^0.16.1",
"@posthog/react": "^1.9.0",
"@react-hook/window-size": "^3.1.1",
Expand Down
82 changes: 54 additions & 28 deletions frontend/src/components/navbar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,73 @@
import {
PageName,
pageNameAtom,
} from '@/contexts/pageContext';
AiOutlineAudit,
AiOutlineEdit,
AiOutlineMessage,
} from 'react-icons/ai';
import type { IconType } from 'react-icons';

import { PageName, pageNameAtom } from '@/contexts/pageContext';

import { useAtom } from 'jotai';
import classes from './styles.module.css';

/**
* An array of objects representing the names and titles of pages.
* Each object contains the following properties:
*
* @property {PageName} name - The name identifier of the page.
* @property {string} title - The display title of the page.
*/

type Page = {
name: PageName;
title: string;
hint: string;
icon: IconType;
};

const pageNames: Page[] = [
{ name: PageName.Draft, title: 'Draft', hint: 'Generate suggestions' },
{ name: PageName.Revise, title: 'Revise', hint: 'Improve your text' },
{ name: PageName.Chat, title: 'Chat', hint: 'Ask about your doc' },
{
name: PageName.Draft,
title: 'Draft',
hint: 'Generate suggestions',
icon: AiOutlineEdit,
},
{
name: PageName.Revise,
title: 'Revise',
hint: 'Improve your text',
icon: AiOutlineAudit,
},
{
name: PageName.Chat,
title: 'Chat',
hint: 'Ask about your doc',
icon: AiOutlineMessage,
},
];

export default function Navbar() {
const [page, changePage] = useAtom(pageNameAtom);

return (
<div className={classes.tabs}>
{pageNames.map(({ name: pageName, title: pageTitle, hint }) => (
<button
key={pageName}
type="button"
onClick={() => changePage(pageName)}
className={`${classes.tabBtn} ${page === pageName ? classes.active : ''}`}
>
{pageTitle}
<span className={classes.tabHint}>{hint}</span>
</button>
))}
</div>
);
<header className={classes.header}>
<div className={classes.brandRow}>
<span className={classes.brandMark} aria-hidden="true">
</span>
<span className={classes.wordmark}>Thoughtful</span>
<span className={classes.brandTagline}>
AI that helps you think
</span>
</div>

<nav className={classes.tabs} aria-label="Tool pages">
{pageNames.map(({ name, title, hint, icon: Icon }) => (
<button
key={name}
type="button"
title={hint}
onClick={() => changePage(name)}
className={`${classes.tabBtn} ${page === name ? classes.active : ''}`}
aria-current={page === name ? 'page' : undefined}
>
<Icon className={classes.tabIcon} aria-hidden="true" />
{title}
</button>
))}
</nav>
</header>
);
}
Loading
Loading