astro-better-declarative-screenshots 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/check-screenshots.yml +68 -0
- package/CONFIGURATION.md +251 -0
- package/Highlight.astro +31 -0
- package/README.md +141 -0
- package/Screenshot.astro +131 -0
- package/example/groups.mdx +33 -0
- package/example/screenshot.config.mjs +72 -0
- package/index.mjs +56 -0
- package/package.json +36 -0
- package/scripts/check-screenshots.mjs +188 -0
- package/scripts/take-screenshots.mjs +162 -0
- package/src/capture.mjs +108 -0
- package/src/chrome.mjs +153 -0
- package/src/config.mjs +122 -0
- package/src/diff.mjs +80 -0
- package/src/discover.mjs +128 -0
- package/src/docker.mjs +123 -0
- package/src/highlight.mjs +133 -0
- package/src/naming.mjs +78 -0
- package/src/placeholder.mjs +53 -0
- package/tests/chrome.test.mjs +61 -0
- package/tests/config.test.mjs +96 -0
- package/tests/diff.test.mjs +85 -0
- package/tests/discover.test.mjs +118 -0
- package/tests/naming.test.mjs +72 -0
- package/tests/placeholder.test.mjs +35 -0
- package/vitest.config.mjs +12 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
name: Check screenshots
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
# every Monday at 9am UTC -- catches visual drift from app updates
|
|
6
|
+
- cron: '0 9 * * 1'
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
inputs:
|
|
9
|
+
filter:
|
|
10
|
+
description: 'Filter: only check screenshots matching this string'
|
|
11
|
+
required: false
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
check:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- uses: actions/setup-node@v4
|
|
21
|
+
with:
|
|
22
|
+
node-version: '20'
|
|
23
|
+
cache: npm
|
|
24
|
+
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: npm ci
|
|
27
|
+
|
|
28
|
+
- name: Install Playwright browsers
|
|
29
|
+
run: npx playwright install --with-deps webkit
|
|
30
|
+
|
|
31
|
+
- name: Check screenshots
|
|
32
|
+
run: |
|
|
33
|
+
FILTER="${{ github.event.inputs.filter }}"
|
|
34
|
+
if [ -n "$FILTER" ]; then
|
|
35
|
+
npx check-screenshots --filter="$FILTER" --diff-dir=.screenshot-diffs
|
|
36
|
+
else
|
|
37
|
+
npx check-screenshots --diff-dir=.screenshot-diffs
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
- name: Upload diff artifacts
|
|
41
|
+
if: failure()
|
|
42
|
+
uses: actions/upload-artifact@v4
|
|
43
|
+
with:
|
|
44
|
+
name: screenshot-diffs
|
|
45
|
+
path: .screenshot-diffs/
|
|
46
|
+
retention-days: 14
|
|
47
|
+
|
|
48
|
+
- name: Comment on PR with diff summary
|
|
49
|
+
if: failure() && github.event_name == 'pull_request'
|
|
50
|
+
uses: actions/github-script@v7
|
|
51
|
+
with:
|
|
52
|
+
script: |
|
|
53
|
+
const fs = require('fs');
|
|
54
|
+
const diffs = fs.readdirSync('.screenshot-diffs').filter(f => f.endsWith('.diff.png'));
|
|
55
|
+
const body = [
|
|
56
|
+
'## Screenshot changes detected',
|
|
57
|
+
'',
|
|
58
|
+
`${diffs.length} screenshot(s) changed:`,
|
|
59
|
+
...diffs.map(f => `- \`${f.replace('.diff.png', '')}\``),
|
|
60
|
+
'',
|
|
61
|
+
'Download the `screenshot-diffs` artifact to review the changes.',
|
|
62
|
+
'If they are intentional, run `take-screenshots` locally to update the reference files.',
|
|
63
|
+
].join('\n');
|
|
64
|
+
github.rest.issues.createComment({
|
|
65
|
+
...context.repo,
|
|
66
|
+
issue_number: context.issue.number,
|
|
67
|
+
body,
|
|
68
|
+
});
|
package/CONFIGURATION.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Configuration reference
|
|
2
|
+
|
|
3
|
+
All configuration lives in `screenshot.config.mjs` at your Astro project root.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
export default {
|
|
7
|
+
// ...
|
|
8
|
+
};
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Top-level options
|
|
14
|
+
|
|
15
|
+
### `baseUrl` (required)
|
|
16
|
+
|
|
17
|
+
The base URL of the running app. Screenshot URLs are resolved relative to this.
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
baseUrl: 'http://localhost:9011'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### `outputDir`
|
|
24
|
+
|
|
25
|
+
Where to write screenshot PNGs, relative to the project root.
|
|
26
|
+
|
|
27
|
+
Default: `'./src/assets/screenshots'`
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
outputDir: 'public/screenshots'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Note: if you store screenshots in `public/`, they are served as static files without Astro's asset pipeline. If you store them in `src/assets/`, Astro can optimize them. Pick based on whether you want image optimization.
|
|
34
|
+
|
|
35
|
+
### `browser`
|
|
36
|
+
|
|
37
|
+
Playwright browser engine.
|
|
38
|
+
|
|
39
|
+
Default: `'webkit'`
|
|
40
|
+
|
|
41
|
+
Options: `'webkit'` | `'chromium'` | `'firefox'`
|
|
42
|
+
|
|
43
|
+
WebKit gives the most accurate rendering for macOS Safari-style chrome. Use Chromium for CI environments where WebKit may not be available.
|
|
44
|
+
|
|
45
|
+
### `colorScheme`
|
|
46
|
+
|
|
47
|
+
Default color scheme for captured pages.
|
|
48
|
+
|
|
49
|
+
Default: `'light'`
|
|
50
|
+
|
|
51
|
+
Options: `'light'` | `'dark'`
|
|
52
|
+
|
|
53
|
+
### `strict`
|
|
54
|
+
|
|
55
|
+
When `true`, `<Screenshot>` throws at build time if the PNG file is missing. Defaults to the value of the `SCREENSHOTS_STRICT` environment variable.
|
|
56
|
+
|
|
57
|
+
Default: `false`
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## `window`
|
|
62
|
+
|
|
63
|
+
Default viewport dimensions.
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
window: { width: 1280, height: 800 }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Per-screenshot overrides are available via the `<Screenshot width height />` props.
|
|
70
|
+
|
|
71
|
+
### `minWindow` / `maxWindow`
|
|
72
|
+
|
|
73
|
+
Viewport size guardrails. Any per-screenshot `width`/`height` values are clamped to these bounds.
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
minWindow: { width: 640, height: 400 },
|
|
77
|
+
maxWindow: { width: 2560, height: 1600 },
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### `maxFullPage`
|
|
81
|
+
|
|
82
|
+
Maximum dimensions for full-page captures.
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
maxFullPage: { width: 2560, height: 8000 }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## `chrome`
|
|
91
|
+
|
|
92
|
+
Window chrome composited over each screenshot.
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
chrome: {
|
|
96
|
+
style: 'safari-macos', // or 'none'
|
|
97
|
+
showUrl: true,
|
|
98
|
+
dark: false,
|
|
99
|
+
shadowBlur: 40,
|
|
100
|
+
shadowPadding: 48,
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### `chrome.style`
|
|
105
|
+
|
|
106
|
+
`'safari-macos'` renders a macOS Safari-style title bar with traffic lights and a URL bar.
|
|
107
|
+
`'none'` skips chrome entirely.
|
|
108
|
+
|
|
109
|
+
### `chrome.showUrl`
|
|
110
|
+
|
|
111
|
+
Whether to render the URL bar inside the chrome.
|
|
112
|
+
|
|
113
|
+
### `chrome.dark`
|
|
114
|
+
|
|
115
|
+
Use dark chrome regardless of page color scheme.
|
|
116
|
+
|
|
117
|
+
### `chrome.shadowBlur`
|
|
118
|
+
|
|
119
|
+
Drop shadow blur radius in pixels. Set to `0` to disable the shadow.
|
|
120
|
+
|
|
121
|
+
### `chrome.shadowPadding`
|
|
122
|
+
|
|
123
|
+
Extra padding added around the window to accommodate the shadow. Increase this if the shadow is clipped.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## `docker`
|
|
128
|
+
|
|
129
|
+
Docker service configuration. The CLI boots this service before capturing screenshots and shuts it down when done.
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
docker: {
|
|
133
|
+
compose: 'docker-compose.yml',
|
|
134
|
+
service: 'app',
|
|
135
|
+
kickstart: 'kickstart/bootstrap.json',
|
|
136
|
+
healthcheck: {
|
|
137
|
+
url: 'http://localhost:9011/api/status',
|
|
138
|
+
timeout: 60000,
|
|
139
|
+
interval: 2000,
|
|
140
|
+
},
|
|
141
|
+
env: {
|
|
142
|
+
DATABASE_PASSWORD: 'change-in-production',
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### `docker.compose`
|
|
148
|
+
|
|
149
|
+
Path to a `docker-compose.yml` file, relative to the project root. The CLI runs `docker compose -f <compose> up -d [service]`.
|
|
150
|
+
|
|
151
|
+
### `docker.service`
|
|
152
|
+
|
|
153
|
+
The specific Docker Compose service to start. If omitted, all services in the file are started.
|
|
154
|
+
|
|
155
|
+
### `docker.bootstrap`
|
|
156
|
+
|
|
157
|
+
Path to a seed/bootstrap file to pass into the container, relative to the project root. Two environment variables become available inside `docker-compose.yml`:
|
|
158
|
+
|
|
159
|
+
- `SCREENSHOT_BOOTSTRAP_PATH` -- absolute path to the file on the host
|
|
160
|
+
- `SCREENSHOT_BOOTSTRAP_CONTENT` -- the file's full text content
|
|
161
|
+
|
|
162
|
+
Wire these into your container however your app needs them. Examples:
|
|
163
|
+
|
|
164
|
+
**FusionAuth** (kickstart):
|
|
165
|
+
```yaml
|
|
166
|
+
environment:
|
|
167
|
+
FUSIONAUTH_APP_KICKSTART_FILE: ${SCREENSHOT_BOOTSTRAP_PATH}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Node.js app** (seed script via bind mount):
|
|
171
|
+
```yaml
|
|
172
|
+
volumes:
|
|
173
|
+
- ${SCREENSHOT_BOOTSTRAP_PATH}:/app/seed.json:ro
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Generic** (pass as env var for a custom entrypoint):
|
|
177
|
+
```yaml
|
|
178
|
+
environment:
|
|
179
|
+
APP_SEED_DATA: ${SCREENSHOT_BOOTSTRAP_CONTENT}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`kickstart` is accepted as a legacy alias for `bootstrap`.
|
|
183
|
+
|
|
184
|
+
### `docker.postStart`
|
|
185
|
+
|
|
186
|
+
Optional shell command to run after the health check passes and before `beforeAll`. Runs in the project root with the project's environment. Use for database migrations, extra seeding, or any setup that can't be bundled into the container's startup.
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
postStart: 'node scripts/seed-extra.mjs'
|
|
190
|
+
// or: 'docker exec myapp-1 npm run db:migrate'
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### `docker.healthcheck.url`
|
|
194
|
+
|
|
195
|
+
URL polled to determine when the app is ready. The CLI waits for an HTTP 200 response before proceeding.
|
|
196
|
+
|
|
197
|
+
### `docker.healthcheck.timeout`
|
|
198
|
+
|
|
199
|
+
Maximum time in milliseconds to wait for the health check. Default: `60000` (1 minute).
|
|
200
|
+
|
|
201
|
+
### `docker.healthcheck.interval`
|
|
202
|
+
|
|
203
|
+
Polling interval in milliseconds. Default: `2000`.
|
|
204
|
+
|
|
205
|
+
### `docker.env`
|
|
206
|
+
|
|
207
|
+
Extra environment variables merged into the container environment when running `docker compose up`.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Hooks
|
|
212
|
+
|
|
213
|
+
Hooks are async functions that let you run code at key points in the capture lifecycle.
|
|
214
|
+
|
|
215
|
+
### `beforeAll`
|
|
216
|
+
|
|
217
|
+
Called once before any screenshots are taken. Receives a Playwright `BrowserContext`. Use this for login flows or global app state setup.
|
|
218
|
+
|
|
219
|
+
```js
|
|
220
|
+
beforeAll: async (context) => {
|
|
221
|
+
const page = await context.newPage();
|
|
222
|
+
await page.goto('http://localhost:9011/admin/login');
|
|
223
|
+
await page.fill('#loginId', 'admin@example.com');
|
|
224
|
+
await page.fill('#password', 'supersecret');
|
|
225
|
+
await page.click('[type=submit]');
|
|
226
|
+
await page.waitForURL('**/admin/**');
|
|
227
|
+
await page.close();
|
|
228
|
+
},
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### `beforeScreenshot`
|
|
232
|
+
|
|
233
|
+
Called before each individual screenshot. Receives the Playwright `Page` (already navigated to the target URL) and a spec object `{ url, name, highlights }`. Use this to dismiss notifications, close modals, or set up per-page state.
|
|
234
|
+
|
|
235
|
+
```js
|
|
236
|
+
beforeScreenshot: async (page, { url, name }) => {
|
|
237
|
+
await page.evaluate(() => {
|
|
238
|
+
document.querySelectorAll('.notification').forEach(el => el.remove());
|
|
239
|
+
});
|
|
240
|
+
},
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### `afterAll`
|
|
244
|
+
|
|
245
|
+
Called once after all screenshots are taken. Use for cleanup.
|
|
246
|
+
|
|
247
|
+
```js
|
|
248
|
+
afterAll: async () => {
|
|
249
|
+
console.log('done');
|
|
250
|
+
},
|
|
251
|
+
```
|
package/Highlight.astro
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
selector: string;
|
|
4
|
+
style?: 'border' | 'arrow' | 'both';
|
|
5
|
+
color?: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
borderWidth?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
selector,
|
|
12
|
+
style = 'border',
|
|
13
|
+
color = '#f60',
|
|
14
|
+
label,
|
|
15
|
+
borderWidth = 3,
|
|
16
|
+
} = Astro.props;
|
|
17
|
+
---
|
|
18
|
+
{/*
|
|
19
|
+
Renders a hidden sentinel element that Screenshot.astro parses at build time
|
|
20
|
+
to discover which elements to highlight and how.
|
|
21
|
+
The custom element is stripped from the final HTML by the screenshot pipeline.
|
|
22
|
+
*/}
|
|
23
|
+
<screenshot-highlight
|
|
24
|
+
data-selector={selector}
|
|
25
|
+
data-style={style}
|
|
26
|
+
data-color={color}
|
|
27
|
+
data-label={label ?? ''}
|
|
28
|
+
data-border-width={String(borderWidth)}
|
|
29
|
+
aria-hidden="true"
|
|
30
|
+
style="display:none"
|
|
31
|
+
></screenshot-highlight>
|
package/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# astro-better-declarative-screenshots
|
|
2
|
+
|
|
3
|
+
Declarative, Docker-backed screenshots for Astro documentation sites.
|
|
4
|
+
|
|
5
|
+
Define which pages to screenshot and which elements to highlight directly in your MDX. The package handles starting Docker, capturing pages with Playwright WebKit, compositing a macOS Safari-style window chrome, and writing PNGs. A CI workflow detects visual regressions by diffing new captures against committed references.
|
|
6
|
+
|
|
7
|
+
## How it works
|
|
8
|
+
|
|
9
|
+
1. You declare screenshots in MDX using `<Screenshot>` and `<Highlight>` components.
|
|
10
|
+
2. Running `take-screenshots` scans your source files, boots Docker, opens each URL in Playwright WebKit, injects highlights, composites chrome, and writes PNGs to your output directory.
|
|
11
|
+
3. You commit the PNG files. The `<Screenshot>` component renders them as `<img>` tags at build time.
|
|
12
|
+
4. In CI, `check-screenshots` recaptures every page and diffs against the committed files. It exits non-zero if anything changed beyond the configured pixel threshold, and uploads diff images as build artifacts.
|
|
13
|
+
|
|
14
|
+
## Quick start
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm install astro-better-declarative-screenshots
|
|
18
|
+
npx playwright install --with-deps webkit
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Add the integration to `astro.config.mjs`:
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
import { defineConfig } from 'astro/config';
|
|
25
|
+
import screenshots from 'astro-better-declarative-screenshots';
|
|
26
|
+
|
|
27
|
+
export default defineConfig({
|
|
28
|
+
integrations: [screenshots()],
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Create `screenshot.config.mjs` at your project root (see [CONFIGURATION.md](./CONFIGURATION.md) for all options):
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
export default {
|
|
36
|
+
baseUrl: 'http://localhost:9011',
|
|
37
|
+
outputDir: 'public/screenshots',
|
|
38
|
+
docker: {
|
|
39
|
+
compose: 'docker-compose.yml',
|
|
40
|
+
service: 'app',
|
|
41
|
+
kickstart: 'kickstart/bootstrap.json',
|
|
42
|
+
healthcheck: {
|
|
43
|
+
url: 'http://localhost:9011/api/status',
|
|
44
|
+
timeout: 60000,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Use `<Screenshot>` in your MDX files:
|
|
51
|
+
|
|
52
|
+
```mdx
|
|
53
|
+
import Screenshot from 'astro-better-declarative-screenshots/Screenshot.astro';
|
|
54
|
+
import Highlight from 'astro-better-declarative-screenshots/Highlight.astro';
|
|
55
|
+
|
|
56
|
+
<Screenshot url="/admin/group/add">
|
|
57
|
+
<Highlight selector="#name" style="border" color="#f60" label="Group name" />
|
|
58
|
+
</Screenshot>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Take screenshots:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
npx take-screenshots
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Components
|
|
68
|
+
|
|
69
|
+
### `<Screenshot>`
|
|
70
|
+
|
|
71
|
+
| Prop | Type | Default | Description |
|
|
72
|
+
|------|------|---------|-------------|
|
|
73
|
+
| `url` | `string` | required | URL path (relative) or full URL to capture |
|
|
74
|
+
| `id` | `string` | auto-derived | Explicit output filename (without extension) |
|
|
75
|
+
| `alt` | `string` | derived from name | Alt text for the rendered image |
|
|
76
|
+
| `width` | `number` | from config | Viewport width for this screenshot |
|
|
77
|
+
| `height` | `number` | from config | Viewport height for this screenshot |
|
|
78
|
+
| `fullPage` | `boolean` | `false` | Capture full scrollable page height |
|
|
79
|
+
|
|
80
|
+
### `<Highlight>`
|
|
81
|
+
|
|
82
|
+
Must be a direct child of `<Screenshot>`.
|
|
83
|
+
|
|
84
|
+
| Prop | Type | Default | Description |
|
|
85
|
+
|------|------|---------|-------------|
|
|
86
|
+
| `selector` | `string` | required | CSS selector for the element to highlight |
|
|
87
|
+
| `style` | `'border' \| 'arrow' \| 'both'` | `'border'` | How to draw the highlight |
|
|
88
|
+
| `color` | `string` | `'#f60'` | Highlight color (any CSS color) |
|
|
89
|
+
| `label` | `string` | `''` | Text label drawn near the highlighted element |
|
|
90
|
+
| `borderWidth` | `number` | `3` | Border thickness in pixels |
|
|
91
|
+
|
|
92
|
+
## Filename derivation
|
|
93
|
+
|
|
94
|
+
Screenshot filenames are derived from the URL path and highlight selectors:
|
|
95
|
+
|
|
96
|
+
- `/admin/group/add` with no highlights -> `admin-group-add.png`
|
|
97
|
+
- `/admin/group` with `#name` highlighted -> `admin-group-name.png`
|
|
98
|
+
- Same URL, same selectors, second occurrence -> `admin-group-1.png`
|
|
99
|
+
|
|
100
|
+
To avoid collision issues, set an explicit `id` prop when the same URL appears multiple times with the same highlights.
|
|
101
|
+
|
|
102
|
+
## CLI reference
|
|
103
|
+
|
|
104
|
+
### `take-screenshots`
|
|
105
|
+
|
|
106
|
+
Captures all screenshots and writes them to `outputDir`.
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
npx take-screenshots [--filter <string>] [--strict]
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Options:
|
|
113
|
+
- `--filter <string>` -- only capture screenshots whose name or URL contains this string
|
|
114
|
+
- `--strict` -- exit non-zero if any screenshot fails to capture
|
|
115
|
+
|
|
116
|
+
### `check-screenshots`
|
|
117
|
+
|
|
118
|
+
Captures all screenshots, diffs against committed references, and exits non-zero if anything changed.
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
npx check-screenshots [--filter <string>] [--diff-dir <path>] [--threshold <ratio>] [--fail-on-missing]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Options:
|
|
125
|
+
- `--filter <string>` -- only check screenshots matching this string
|
|
126
|
+
- `--diff-dir <path>` -- where to write diff images (default: `.screenshot-diffs`)
|
|
127
|
+
- `--threshold <ratio>` -- fraction of pixels that may differ before failure (default: `0.001` = 0.1%)
|
|
128
|
+
- `--fail-on-missing` -- exit non-zero if any reference file is missing
|
|
129
|
+
|
|
130
|
+
## Environment variables
|
|
131
|
+
|
|
132
|
+
- `SCREENSHOTS_STRICT=true` -- strict mode: the `<Screenshot>` component throws at build time if the PNG is missing
|
|
133
|
+
- `SCREENSHOTS_DIR` -- override the output directory (useful for CI artifact staging)
|
|
134
|
+
|
|
135
|
+
## CI setup
|
|
136
|
+
|
|
137
|
+
See [`.github/workflows/check-screenshots.yml`](./.github/workflows/check-screenshots.yml) for an example weekly workflow that runs `check-screenshots` and uploads diff images as artifacts.
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT
|
package/Screenshot.astro
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { deriveName } from './src/naming.mjs';
|
|
3
|
+
import { loadConfig } from './src/config.mjs';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
interface HighlightData {
|
|
8
|
+
selector: string;
|
|
9
|
+
style: string;
|
|
10
|
+
color: string;
|
|
11
|
+
label: string;
|
|
12
|
+
borderWidth: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
url: string;
|
|
17
|
+
/** explicit filename (without extension) -- overrides auto-derived name */
|
|
18
|
+
id?: string;
|
|
19
|
+
/** alt text for the rendered image */
|
|
20
|
+
alt?: string;
|
|
21
|
+
/** override window width for this screenshot */
|
|
22
|
+
width?: number;
|
|
23
|
+
/** override window height for this screenshot */
|
|
24
|
+
height?: number;
|
|
25
|
+
/** capture the full scrollable page height */
|
|
26
|
+
fullPage?: boolean;
|
|
27
|
+
/** outputDir relative to project root; falls back to env SCREENSHOTS_DIR or 'src/assets/screenshots' */
|
|
28
|
+
outputDir?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
url,
|
|
33
|
+
id,
|
|
34
|
+
alt,
|
|
35
|
+
width,
|
|
36
|
+
height,
|
|
37
|
+
fullPage = false,
|
|
38
|
+
outputDir,
|
|
39
|
+
} = Astro.props;
|
|
40
|
+
|
|
41
|
+
// parse Highlight children from the rendered slot HTML
|
|
42
|
+
function parseHighlights(html: string): HighlightData[] {
|
|
43
|
+
const found: HighlightData[] = [];
|
|
44
|
+
// match <screenshot-highlight data-selector="..." ...></screenshot-highlight>
|
|
45
|
+
const tagRe = /<screenshot-highlight([^>]*)>/gi;
|
|
46
|
+
const attrRe = /data-([\w-]+)="([^"]*)"/g;
|
|
47
|
+
let tagMatch: RegExpExecArray | null;
|
|
48
|
+
while ((tagMatch = tagRe.exec(html)) !== null) {
|
|
49
|
+
const attrs: Record<string, string> = {};
|
|
50
|
+
let attrMatch: RegExpExecArray | null;
|
|
51
|
+
while ((attrMatch = attrRe.exec(tagMatch[1])) !== null) {
|
|
52
|
+
attrs[attrMatch[1]] = attrMatch[2];
|
|
53
|
+
}
|
|
54
|
+
if (attrs['selector']) {
|
|
55
|
+
found.push({
|
|
56
|
+
selector: attrs['selector'] ?? '',
|
|
57
|
+
style: attrs['style'] ?? 'border',
|
|
58
|
+
color: attrs['color'] ?? '#f60',
|
|
59
|
+
label: attrs['label'] ?? '',
|
|
60
|
+
borderWidth: parseInt(attrs['border-width'] ?? '3', 10),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return found;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const slotHtml = Astro.slots.has('default') ? await Astro.slots.render('default') : '';
|
|
68
|
+
const highlights = parseHighlights(slotHtml);
|
|
69
|
+
|
|
70
|
+
const name = id ?? deriveName(url, highlights);
|
|
71
|
+
|
|
72
|
+
// resolve output directory: prop > env > screenshot.config.mjs > default
|
|
73
|
+
let resolvedOutputDir = outputDir ?? process.env.SCREENSHOTS_DIR;
|
|
74
|
+
if (!resolvedOutputDir) {
|
|
75
|
+
try {
|
|
76
|
+
const cfg = await loadConfig(process.cwd());
|
|
77
|
+
resolvedOutputDir = cfg.outputDir;
|
|
78
|
+
} catch {
|
|
79
|
+
resolvedOutputDir = 'src/assets/screenshots';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// resolve the public path the browser/img uses.
|
|
84
|
+
// files in public/ are served from the root, so strip 'public/' prefix.
|
|
85
|
+
// files in src/assets/ go through Astro's pipeline -- reference via root-relative path.
|
|
86
|
+
const servePath = resolvedOutputDir
|
|
87
|
+
.replace(/^\.\//, '')
|
|
88
|
+
.replace(/^public\//, '');
|
|
89
|
+
const imgPath = `/${servePath}/${name}.png`;
|
|
90
|
+
|
|
91
|
+
// check whether the file exists on disk (uses full resolvedOutputDir, not stripped)
|
|
92
|
+
const absolutePath = path.resolve(process.cwd(), resolvedOutputDir, `${name}.png`);
|
|
93
|
+
const fileExists = existsSync(absolutePath);
|
|
94
|
+
|
|
95
|
+
const strict = process.env.SCREENSHOTS_STRICT === 'true';
|
|
96
|
+
if (!fileExists && strict) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`[astro-better-declarative-screenshots] Missing screenshot: ${name}.png\n` +
|
|
99
|
+
`Run \`take-screenshots\` to generate it, or set SCREENSHOTS_STRICT=false to use placeholders.`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// data attributes encode the screenshot spec for the CLI scanner
|
|
104
|
+
// so `discover.mjs` can also pick these up from the built HTML if needed
|
|
105
|
+
const specData = JSON.stringify({ url, highlights, width, height, fullPage });
|
|
106
|
+
---
|
|
107
|
+
<figure
|
|
108
|
+
class="screenshot-figure"
|
|
109
|
+
data-screenshot-name={name}
|
|
110
|
+
data-screenshot-spec={specData}
|
|
111
|
+
>
|
|
112
|
+
{fileExists ? (
|
|
113
|
+
<img
|
|
114
|
+
src={imgPath}
|
|
115
|
+
alt={alt ?? name.replace(/-/g, ' ')}
|
|
116
|
+
loading="lazy"
|
|
117
|
+
decoding="async"
|
|
118
|
+
class="screenshot-img"
|
|
119
|
+
/>
|
|
120
|
+
) : (
|
|
121
|
+
<div
|
|
122
|
+
class="screenshot-placeholder"
|
|
123
|
+
role="img"
|
|
124
|
+
aria-label={alt ?? `Placeholder for ${name}`}
|
|
125
|
+
style={`width:${width ?? 1280}px;max-width:100%;aspect-ratio:${(width ?? 1280) / (height ?? 800)};background:#f0f0f0;display:flex;align-items:center;justify-content:center;font-family:sans-serif;color:#999;font-size:14px;border:1px dashed #ccc;`}
|
|
126
|
+
>
|
|
127
|
+
Screenshot not yet generated: {name}.png<br />
|
|
128
|
+
Run <code>take-screenshots</code> to generate it.
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
</figure>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Groups
|
|
3
|
+
description: Learn how FusionAuth groups work.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
import Screenshot from 'astro-better-declarative-screenshots/Screenshot.astro';
|
|
7
|
+
import Highlight from 'astro-better-declarative-screenshots/Highlight.astro';
|
|
8
|
+
|
|
9
|
+
## Creating a group
|
|
10
|
+
|
|
11
|
+
Navigate to **Groups** in the FusionAuth Admin UI to manage your groups.
|
|
12
|
+
|
|
13
|
+
{/* Basic screenshot -- no highlights */}
|
|
14
|
+
<Screenshot url="/admin/group/" />
|
|
15
|
+
|
|
16
|
+
{/* Screenshot with a highlighted field */}
|
|
17
|
+
<Screenshot url="/admin/group/add">
|
|
18
|
+
<Highlight selector="#name" style="border" color="#f60" label="Group name field" />
|
|
19
|
+
</Screenshot>
|
|
20
|
+
|
|
21
|
+
{/* Multiple highlights on one page */}
|
|
22
|
+
<Screenshot url="/admin/group/add">
|
|
23
|
+
<Highlight selector="#name" style="border" color="#f60" label="Name" />
|
|
24
|
+
<Highlight selector="#description" style="arrow" color="#07c" label="Description" />
|
|
25
|
+
</Screenshot>
|
|
26
|
+
|
|
27
|
+
{/* Full-page capture with explicit window size */}
|
|
28
|
+
<Screenshot
|
|
29
|
+
url="/admin/group/members"
|
|
30
|
+
width={1440}
|
|
31
|
+
height={900}
|
|
32
|
+
fullPage={true}
|
|
33
|
+
/>
|