8 Commits

Author SHA1 Message Date
552379b2a6 stabilizing pipelines and setting screenshots retention
All checks were successful
Build Pull Request / Build Pull Request Archive (pull_request) Successful in 8s
Build Pull Request / Capture Pull Request Screenshots (pull_request) Successful in 56s
Build Pull Request / Publish Pull Request Screenshot Preview (pull_request) Successful in 2s
2026-07-10 23:02:51 -03:00
f9864072cf fixing version tagging 2026-07-10 22:58:55 -03:00
51341c33ca changing screenshots 2026-07-10 22:55:57 -03:00
8b5c52dd1e fixing pipeline
All checks were successful
Build Pull Request / Build Pull Request Archive (pull_request) Successful in 9s
Build Pull Request / Capture Pull Request Screenshots (pull_request) Successful in 55s
Build Pull Request / Publish Pull Request Screenshot Preview (pull_request) Successful in 1s
2026-07-10 22:53:05 -03:00
c3addb6d02 adding overwrite flag
Some checks failed
Build Pull Request / Build Pull Request Archive (pull_request) Successful in 9s
Build Pull Request / Capture Pull Request Screenshots (pull_request) Failing after 47s
Build Pull Request / Publish Pull Request Screenshot Preview (pull_request) Has been skipped
2026-07-10 22:46:53 -03:00
5597afc572 fixing job
Some checks failed
Build Pull Request / Build Pull Request Archive (pull_request) Successful in 8s
Build Pull Request / Capture Pull Request Screenshots (pull_request) Failing after 2s
Build Pull Request / Publish Pull Request Screenshot Preview (pull_request) Has been skipped
2026-07-10 22:44:57 -03:00
30372f800c adding pr support
Some checks failed
Build Pull Request / Build Pull Request Archive (pull_request) Successful in 42s
Build Pull Request / Capture Pull Request Screenshots (pull_request) Failing after 6s
Build Pull Request / Publish Pull Request Screenshot Preview (pull_request) Has been skipped
2026-07-10 22:41:00 -03:00
9e738cc0d5 Setting version from tag 2026-07-10 22:08:23 -03:00
12 changed files with 518 additions and 9 deletions

View File

@@ -20,11 +20,17 @@ jobs:
- name: Check out repository code - name: Check out repository code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- name: Setup required tools - name: Setup required tools
run: sudo apt-get install zip jq curl -y run: sudo apt-get install zip jq curl -y
- name: Install JS dependencies - name: Install JS dependencies
run: npm install run: npm ci
- name: Run build - name: Run build
run: | run: |

View File

@@ -0,0 +1,178 @@
name: Build Pull Request
on:
pull_request:
types:
- opened
- reopened
- synchronize
env:
ARCHIVE_NAME: vision-start-pr-${{ gitea.event.pull_request.number }}.zip
jobs:
build:
name: Build Pull Request Archive
runs-on: ubuntu-amd64
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- name: Set up required tools
run: sudo apt-get install zip wget -y
- name: Install JS dependencies
run: npm ci
- name: Run build
run: |
bash scripts/prepare_release.sh
npm run build
- name: Prepare archive
run: |
mkdir vision-start
mv dist vision-start/
mv extension vision-start/
mv manifest.json vision-start/
- name: Create zip archive
run: |
cd vision-start
zip -r ../${{ env.ARCHIVE_NAME }} *
- name: Upload build artifact
uses: actions/upload-artifact@v3
with:
name: pr-release-zip
path: ${{ env.ARCHIVE_NAME }}
capture_screenshots:
name: Capture Pull Request Screenshots
runs-on: ubuntu-amd64
needs: build
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- name: Set up required tools
run: sudo apt-get install unzip -y
- name: Install JS dependencies
run: npm ci
- name: Download build artifact
uses: actions/download-artifact@v3
with:
name: pr-release-zip
- name: Unpack build artifact
run: |
unzip -o -q ${{ env.ARCHIVE_NAME }}
if [ ! -d dist ]; then
echo "The build artifact does not contain dist/."
unzip -l ${{ env.ARCHIVE_NAME }}
exit 1
fi
- name: Install Playwright Chromium
run: npx playwright install --with-deps chromium
- name: Capture screenshots
run: npm run capture:screenshots
- name: Upload screenshot artifact
uses: actions/upload-artifact@v3
with:
name: pr-screenshots
retention-days: 30
path: |
screenshots/home.png
screenshots/editing.png
screenshots/configuration.png
publish_screenshot_preview:
name: Publish Pull Request Screenshot Preview
runs-on: ubuntu-amd64
needs: capture_screenshots
if: ${{ gitea.event.pull_request.head.repo.full_name == gitea.repository }}
permissions:
actions: read
issues: write
steps:
- name: Set up required tools
run: sudo apt-get install curl jq -y
- name: Download screenshot artifact
uses: actions/download-artifact@v3
with:
name: pr-screenshots
path: screenshots
- name: Update pull request screenshot preview
continue-on-error: true
env:
GITEA_API_URL: ${{ gitea.server_url }}/api/v1
GITEA_TOKEN: ${{ gitea.token }}
REPOSITORY: ${{ gitea.repository }}
PR_NUMBER: ${{ gitea.event.pull_request.number }}
run: |
set -euo pipefail
OWNER="${REPOSITORY%%/*}"
REPO="${REPOSITORY#*/}"
MARKER='<!-- vision-start-pr-screenshots -->'
COMMENTS_URL="$GITEA_API_URL/repos/$OWNER/$REPO/issues/$PR_NUMBER/comments"
COMMENTS=$(curl -fsS -H "Authorization: token $GITEA_TOKEN" "$COMMENTS_URL")
COMMENT_ID=$(printf '%s' "$COMMENTS" | jq -r --arg marker "$MARKER" '.[] | select(.body | contains($marker)) | .id' | tail -n 1)
if [ -z "$COMMENT_ID" ] || [ "$COMMENT_ID" = "null" ]; then
PAYLOAD=$(jq -n --arg body "$MARKER" '{body: $body}')
COMMENT_ID=$(curl -fsS -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H 'Content-Type: application/json' \
--data "$PAYLOAD" \
"$COMMENTS_URL" | jq -r '.id')
fi
ASSETS_URL="$GITEA_API_URL/repos/$OWNER/$REPO/issues/comments/$COMMENT_ID/assets"
for ASSET_ID in $(curl -fsS -H "Authorization: token $GITEA_TOKEN" "$ASSETS_URL" | jq -r '.[].id'); do
curl -fsS -X DELETE -H "Authorization: token $GITEA_TOKEN" "$ASSETS_URL/$ASSET_ID"
done
upload_asset() {
curl -fsS -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-F "attachment=@$1;type=image/png" \
"$ASSETS_URL" | jq -r '.browser_download_url'
}
HOME_URL=$(upload_asset screenshots/home.png)
EDITING_URL=$(upload_asset screenshots/editing.png)
CONFIGURATION_URL=$(upload_asset screenshots/configuration.png)
BODY=$(printf '%s\n\n%s\n\n%s\n%s\n\n%s\n%s\n\n%s\n%s' \
"$MARKER" \
'## Visual preview' \
'### Home' \
"![Home]($HOME_URL)" \
'### Editing' \
"![Editing]($EDITING_URL)" \
'### Configuration' \
"![Configuration]($CONFIGURATION_URL)")
PAYLOAD=$(jq -n --arg body "$BODY" '{body: $body}')
curl -fsS -X PATCH \
-H "Authorization: token $GITEA_TOKEN" \
-H 'Content-Type: application/json' \
--data "$PAYLOAD" \
"$GITEA_API_URL/repos/$OWNER/$REPO/issues/comments/$COMMENT_ID"

View File

@@ -20,11 +20,28 @@ jobs:
- name: Check out repository code - name: Check out repository code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set version from tag
env:
RELEASE_TAG: ${{ gitea.ref_name }}
run: |
if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Expected a vX.Y.Z tag, got: $RELEASE_TAG"
exit 1
fi
VERSION="${RELEASE_TAG#v}"
sed -i -e "s/\"version\": \"0\.0\.0\"/\"version\": \"$VERSION\"/" manifest.json
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- name: Setup required tools - name: Setup required tools
run: sudo apt-get install zip jq curl -y run: sudo apt-get install zip jq curl -y
- name: Install JS dependencies - name: Install JS dependencies
run: npm install run: npm ci
- name: Run build - name: Run build
run: | run: |
@@ -83,7 +100,7 @@ jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build, virus-total-check] needs: [build, virus-total-check, capture_screenshots]
steps: steps:
- name: Check out repository code - name: Check out repository code
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -91,6 +108,11 @@ jobs:
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
with: with:
name: release-zip name: release-zip
- name: Download screenshot artifacts
uses: actions/download-artifact@v3
with:
name: release-screenshots
path: release-screenshots
- name: Release zip - name: Release zip
uses: akkuman/gitea-release-action@v1 uses: akkuman/gitea-release-action@v1
with: with:
@@ -101,7 +123,11 @@ jobs:
**Virus Total Detection Ratio:** ${{ needs.virus-total-check.outputs.detection-ratio }} **Virus Total Detection Ratio:** ${{ needs.virus-total-check.outputs.detection-ratio }}
name: ${{ gitea.ref_name }} name: ${{ gitea.ref_name }}
tag_name: ${{ gitea.ref_name }} tag_name: ${{ gitea.ref_name }}
files: vision-start-${{ gitea.ref_name }}.zip files: |
vision-start-${{ gitea.ref_name }}.zip
release-screenshots/home.png
release-screenshots/editing.png
release-screenshots/configuration.png
build_vision_start: build_vision_start:
name: Build Vision Start Image name: Build Vision Start Image
@@ -111,6 +137,17 @@ jobs:
- name: Check out repository - name: Check out repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set version from tag
env:
RELEASE_TAG: ${{ gitea.ref_name }}
run: |
if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Expected a vX.Y.Z tag, got: $RELEASE_TAG"
exit 1
fi
VERSION="${RELEASE_TAG#v}"
sed -i -e "s/\"version\": \"0\.0\.0\"/\"version\": \"$VERSION\"/" manifest.json
- name: Log in to Container Registry - name: Log in to Container Registry
run: | run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" \ echo "${{ secrets.REGISTRY_PASSWORD }}" \
@@ -146,3 +183,38 @@ jobs:
cd ${{ secrets.PROD_DIR }} cd ${{ secrets.PROD_DIR }}
docker compose pull docker compose pull
docker compose up -d --force-recreate docker compose up -d --force-recreate
capture_screenshots:
name: Capture Vision Start Screenshots
runs-on: ubuntu-amd64
needs: deploy_vision_start
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- name: Install JS dependencies
run: npm ci
- name: Install Playwright Chromium
run: npx playwright install --with-deps chromium
- name: Capture release screenshots
env:
SCREENSHOT_BASE_URL: http://vision-start.ivanch.me
run: npm run capture:screenshots
- name: Upload release screenshots
uses: actions/upload-artifact@v3
with:
name: release-screenshots
retention-days: 30
path: |
screenshots/home.png
screenshots/editing.png
screenshots/configuration.png

View File

@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Vision Startpage", "name": "Vision Startpage",
"version": "1.0.0", "version": "0.0.0",
"description": "A light, modern and customizable startpage.", "description": "A light, modern and customizable startpage.",
"chrome_url_overrides": { "chrome_url_overrides": {
"newtab": "index.html" "newtab": "index.html"

48
package-lock.json generated
View File

@@ -19,6 +19,7 @@
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.5", "@types/react-dom": "^19.1.5",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"playwright": "1.61.1",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.11",
"typescript": "~5.7.2", "typescript": "~5.7.2",
@@ -2387,6 +2388,53 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/playwright": {
"version": "1.61.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.61.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.61.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.6", "version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",

View File

@@ -6,7 +6,8 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview",
"capture:screenshots": "node scripts/capture_screenshots.mjs"
}, },
"dependencies": { "dependencies": {
"@hello-pangea/dnd": "^18.0.1", "@hello-pangea/dnd": "^18.0.1",
@@ -20,6 +21,7 @@
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.5", "@types/react-dom": "^19.1.5",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"playwright": "1.61.1",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.11",
"typescript": "~5.7.2", "typescript": "~5.7.2",

View File

@@ -40,6 +40,7 @@ Live instances / artifacts:
| Container | Node 22 Alpine build stage → nginx Alpine serving `dist/` | | Container | Node 22 Alpine build stage → nginx Alpine serving `dist/` |
| Extension packaging | Manifest V3 (`manifest.json`) consuming `dist/` + `manifest.json` zipped as `vision-start-<tag>.zip` | | Extension packaging | Manifest V3 (`manifest.json`) consuming `dist/` + `manifest.json` zipped as `vision-start-<tag>.zip` |
| CI/CD | Gitea Actions workflows (`.gitea/workflows/`) | | CI/CD | Gitea Actions workflows (`.gitea/workflows/`) |
| Release screenshots | Playwright 1.61.1 + Chromium capture the deployed production page at a fixed 1280×800 viewport |
Entry points: `index.html``index.tsx``App.tsx`. Entry points: `index.html``index.tsx``App.tsx`.
@@ -125,13 +126,16 @@ vision-start/
│ ├── favicon.ico │ ├── favicon.ico
│ └── icon-metadata.json # Dashboard Icons metadata (gitignored; fetched at release build) │ └── icon-metadata.json # Dashboard Icons metadata (gitignored; fetched at release build)
├── screenshots/ # README screenshots (dark page, editing, configuration) ├── screenshots/ # README/release screenshots (home, editing, configuration; regenerated at 1280×800)
├── scripts/ ├── scripts/
│ ├── capture_screenshots.mjs # Loads demoData, captures home/edit/configuration with Playwright, and can target a deployed URL
│ ├── demoData.json # Base64-encoded localStorage fixture used for release screenshots
│ ├── prepare_release.sh # Downloads icon-metadata.json from homarr-labs/dashboard-icons │ ├── prepare_release.sh # Downloads icon-metadata.json from homarr-labs/dashboard-icons
│ └── check_virustotal.sh # Uploads the release zip to VirusTotal, waits for & reports the verdict │ └── check_virustotal.sh # Uploads the release zip to VirusTotal, waits for & reports the verdict
├── .gitea/workflows/ ├── .gitea/workflows/
│ ├── main.yaml # On push to main: build, push staging Docker image, SSH-deploy to staging │ ├── main.yaml # On push to main: build, push staging Docker image, SSH-deploy to staging
│ ├── pull-request.yaml # On PR updates: build/zip, capture screenshots, and publish an inline visual preview
│ └── release.yaml # On v* tag: build, zip, VirusTotal check, Gitea release, push latest image, SSH-deploy to prod │ └── release.yaml # On v* tag: build, zip, VirusTotal check, Gitea release, push latest image, SSH-deploy to prod
├── Dockerfile # Node 22 build → nginx serving dist/ (with gzip via nginx.conf) ├── Dockerfile # Node 22 build → nginx serving dist/ (with gzip via nginx.conf)
@@ -208,6 +212,9 @@ Build, then combine `dist/` + `manifest.json` into a folder and "Load unpacked"
`Dockerfile` builds in Node 22 Alpine (`npm ci` → runs `scripts/prepare_release.sh``npm run build`) and serves `/app/dist` + `manifest.json` via nginx:alpine on port 80. `Dockerfile` builds in Node 22 Alpine (`npm ci` → runs `scripts/prepare_release.sh``npm run build`) and serves `/app/dist` + `manifest.json` via nginx:alpine on port 80.
### CI/CD (Gitea Actions) ### CI/CD (Gitea Actions)
- `release.yaml` validates each `vX.Y.Z` tag and stamps the version into `manifest.json` (`"version": "0.0.0"` → the tag) in each build checkout before producing the extension archive and production image.
- **`pull-request.yaml`** — Triggers on pull request open, reopen, and synchronization. It builds and uploads a PR extension archive containing `dist/`, unpacks that archive in a separate Playwright job to generate the three demo screenshots, and uploads both artifacts (screenshots are retained for 30 days). For same-repository PRs, it maintains one Gitea PR comment with inline image attachments; fork PRs retain artifacts but skip the comment because their workflow token is read-only.
- After `deploy_vision_start` succeeds, `release.yaml` uses Playwright Chromium against the deployed production page and seeds each browser context from `scripts/demoData.json`. It regenerates `home.png`, `editing.png`, and `configuration.png` at exactly 1280×800, uploads them as artifacts (retained for 30 days), and attaches them as individual Gitea release assets.
- **`main.yaml`** — Triggers on push to `main` (and `workflow_dispatch`). Builds, pushes a `staging` multi-arch (amd64/arm64) image to `git.ivanch.me/ivanch/vision-start:staging`, then SSH-deploys on the staging host via `docker compose up -d --force-recreate`. - **`main.yaml`** — Triggers on push to `main` (and `workflow_dispatch`). Builds, pushes a `staging` multi-arch (amd64/arm64) image to `git.ivanch.me/ivanch/vision-start:staging`, then SSH-deploys on the staging host via `docker compose up -d --force-recreate`.
- **`release.yaml`** — Triggers on `v*` tags. Builds, zips `dist/` + `manifest.json` as `vision-start-<tag>.zip`, runs `scripts/check_virustotal.sh` against it (publishes analysis URL + detection ratio on the release body), creates a Gitea release, pushes a `latest` multi-arch image, and SSH-deploys to production. - **`release.yaml`** — Triggers on `v*` tags. Builds, zips `dist/` + `manifest.json` as `vision-start-<tag>.zip`, runs `scripts/check_virustotal.sh` against it (publishes analysis URL + detection ratio on the release body), creates a Gitea release, pushes a `latest` multi-arch image, and SSH-deploys to production.
@@ -247,9 +254,9 @@ External assets fetched at build time by `scripts/prepare_release.sh`:
| Icon fetch / picker / metadata | `components/utils/iconService.ts`, `components/WebsiteEditModal.tsx`, `public/icon-metadata.json` | | Icon fetch / picker / metadata | `components/utils/iconService.ts`, `components/WebsiteEditModal.tsx`, `public/icon-metadata.json` |
| chrome.storage.local access | `components/utils/StorageLocalManager.ts` | | chrome.storage.local access | `components/utils/StorageLocalManager.ts` |
| Export/import config | `components/services/ConfigurationService.ts` (`exportConfig`, `importConfig`) | | Export/import config | `components/services/ConfigurationService.ts` (`exportConfig`, `importConfig`) |
| Release packaging | `scripts/prepare_release.sh`, `scripts/check_virustotal.sh`, `.gitea/workflows/release.yaml` | | Build/release/PR pipelines | `scripts/prepare_release.sh`, `scripts/capture_screenshots.mjs`, `scripts/check_virustotal.sh`, `.gitea/workflows/pull-request.yaml`, `.gitea/workflows/release.yaml` |
| Docker build | `Dockerfile` | | Docker build | `Dockerfile` |
--- ---
_Last updated: 2026-07-08. Generated as a general project overview; not a coding-style guide._ _Last updated: 2026-07-10. Generated as a general project overview; not a coding-style guide._

Binary file not shown.

Before

Width:  |  Height:  |  Size: 609 KiB

After

Width:  |  Height:  |  Size: 479 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -0,0 +1,190 @@
import { spawn } from 'node:child_process';
import { mkdir, readFile } from 'node:fs/promises';
import path from 'node:path';
import { chromium } from 'playwright';
const viewport = { width: 1280, height: 800 };
const host = '127.0.0.1';
const port = 4173;
const configuredBaseUrl = process.env.SCREENSHOT_BASE_URL?.replace(/\/+$/, '');
const baseUrl = configuredBaseUrl || `http://${host}:${port}`;
const outputDirectory = process.env.SCREENSHOT_OUTPUT_DIR || 'screenshots';
const demoDataPath = process.env.SCREENSHOT_DEMO_DATA || 'scripts/demoData.json';
const viteCli = path.resolve('node_modules/vite/bin/vite.js');
const imgurImageCache = new Map();
const delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
const waitForServer = async (preview) => {
const deadline = Date.now() + 30_000;
let lastError;
while (Date.now() < deadline) {
if (previewError) throw previewError;
if (preview && preview.exitCode !== null) {
throw new Error(`Vite preview exited with code ${preview.exitCode}.`);
}
try {
const response = await fetch(baseUrl);
if (response.ok) return;
lastError = new Error(`Vite preview returned ${response.status}.`);
} catch (error) {
lastError = error;
}
await delay(250);
}
throw new Error(`Screenshot target did not respond at ${baseUrl}. ${lastError?.message || ''}`.trim());
};
const loadDemoData = async () => {
const encodedData = JSON.parse(await readFile(demoDataPath, 'utf8'));
if (!encodedData || typeof encodedData !== 'object' || Array.isArray(encodedData)) {
throw new Error(`${demoDataPath} must contain an object of base64-encoded localStorage values.`);
}
return Object.entries(encodedData).map(([key, value]) => {
if (typeof value !== 'string') {
throw new Error(`${demoDataPath} contains a non-string value for ${key}.`);
}
return { key, value: Buffer.from(value, 'base64').toString('utf8') };
});
};
const fulfillImgurRequest = async (route) => {
const url = route.request().url();
let image = imgurImageCache.get(url);
if (!image) {
try {
const response = await fetch(url);
if (!response.ok) {
await route.continue();
return;
}
image = {
body: Buffer.from(await response.arrayBuffer()),
contentType: response.headers.get('content-type') || 'image/jpeg',
};
imgurImageCache.set(url, image);
} catch {
await route.continue();
return;
}
}
await route.fulfill({
status: 200,
headers: { 'content-type': image.contentType },
body: image.body,
});
};
const stopPreview = async (preview) => {
if (preview.exitCode !== null) return;
const exited = new Promise((resolve) => preview.once('exit', resolve));
preview.kill('SIGTERM');
await Promise.race([exited, delay(5_000)]);
if (preview.exitCode === null) preview.kill('SIGKILL');
};
const assertPngDimensions = (image, filename) => {
const width = image.readUInt32BE(16);
const height = image.readUInt32BE(20);
if (width !== viewport.width || height !== viewport.height) {
throw new Error(`${filename} was generated at ${width}x${height}, expected ${viewport.width}x${viewport.height}.`);
}
};
const capture = async (page, filename) => {
const image = await page.screenshot({
path: path.join(outputDirectory, filename),
type: 'png',
fullPage: false,
scale: 'css',
animations: 'disabled',
});
assertPngDimensions(image, filename);
};
const loadPage = async (page) => {
await page.goto(baseUrl, { waitUntil: 'domcontentloaded' });
await page.locator('main').waitFor({ state: 'visible' });
await page.evaluate(() => document.fonts.ready);
await delay(1_500);
};
let previewError;
const preview = configuredBaseUrl
? undefined
: spawn(
process.execPath,
[viteCli, 'preview', '--host', host, '--port', String(port), '--strictPort'],
{ stdio: 'inherit' },
);
if (preview) {
preview.once('error', (error) => {
previewError = error;
});
}
try {
await waitForServer(preview);
await mkdir(outputDirectory, { recursive: true });
const demoData = await loadDemoData();
const browser = await chromium.launch();
const context = await browser.newContext({
viewport,
screen: viewport,
deviceScaleFactor: 1,
locale: 'en-US',
timezoneId: 'America/Sao_Paulo',
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
});
await context.route('https://i.imgur.com/**', fulfillImgurRequest);
await context.addInitScript((storageItems) => {
storageItems.forEach(({ key, value }) => localStorage.setItem(key, value));
}, demoData);
const page = await context.newPage();
try {
await loadPage(page);
await capture(page, 'home.png');
await page.getByRole('button', { name: 'Edit page' }).click();
await capture(page, 'editing.png');
await loadPage(page);
await page.getByRole('button', { name: 'Open configuration' }).click();
await page.getByRole('dialog').waitFor({ state: 'visible' });
await page.waitForFunction(
() => {
const drawer = document.querySelector('.liquid-drawer');
if (!drawer) return false;
const { left, right } = drawer.getBoundingClientRect();
return left < window.innerWidth && right <= window.innerWidth;
},
undefined,
{ timeout: 5_000 },
);
await capture(page, 'configuration.png');
} finally {
await context.close();
await browser.close();
}
} finally {
if (preview) await stopPreview(preview);
}

6
scripts/demoData.json Normal file
View File

@@ -0,0 +1,6 @@
{
"categories": "W3siaWQiOiIxIiwibmFtZSI6IlNlYXJjaCIsIndlYnNpdGVzIjpbeyJpZCI6IjEiLCJuYW1lIjoiR29vZ2xlIiwidXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlLmNvbSIsImljb24iOiJodHRwczovL3d3dy5nb29nbGUuY29tL3MyL2Zhdmljb25zP2RvbWFpbj1nb29nbGUuY29tJnN6PTEyOCIsImNhdGVnb3J5SWQiOiIxIn0seyJpZCI6IjE3NTMwNDQxODAxMDgiLCJuYW1lIjoiWW91VHViZSIsInVybCI6Imh0dHBzOi8vd3d3LnlvdXR1YmUuY29tLyIsImljb24iOiJodHRwczovL2Nkbi5qc2RlbGl2ci5uZXQvZ2gvaG9tYXJyLWxhYnMvZGFzaGJvYXJkLWljb25zL3N2Zy95b3V0dWJlLnN2ZyIsImNhdGVnb3J5SWQiOiIxIn0seyJpZCI6IjE3NTMwNDQyMjI2OTQiLCJuYW1lIjoiRHJpdmUiLCJ1cmwiOiJodHRwczovL2RyaXZlLmdvb2dsZS5jb20vZHJpdmUvdS8wL215LWRyaXZlIiwiaWNvbiI6Imh0dHBzOi8vY2RuLmpzZGVsaXZyLm5ldC9naC9ob21hcnItbGFicy9kYXNoYm9hcmQtaWNvbnMvc3ZnL2dvb2dsZS1kcml2ZS5zdmciLCJjYXRlZ29yeUlkIjoiMSJ9LHsiaWQiOiIxNzUzMDQ0NDE0ODIwIiwibmFtZSI6IkdtYWlsIiwidXJsIjoiaHR0cHM6Ly9tYWlsLmdvb2dsZS5jb20vbWFpbC91LzAvP3RhYj13bSNpbmJveCIsImljb24iOiJodHRwczovL2Nkbi5qc2RlbGl2ci5uZXQvZ2gvaG9tYXJyLWxhYnMvZGFzaGJvYXJkLWljb25zL3N2Zy9nbWFpbC5zdmciLCJjYXRlZ29yeUlkIjoiMSJ9LHsiaWQiOiIxNzUzMDQ0NDI3NTYwIiwibmFtZSI6Ikxhc3QuZm0iLCJ1cmwiOiJodHRwczovL3d3dy5sYXN0LmZtL2hvbWUiLCJpY29uIjoiaHR0cHM6Ly93d3cuZ29vZ2xlLmNvbS9zMi9mYXZpY29ucz9kb21haW49d3d3Lmxhc3QuZm0mc3o9MTI4IiwiY2F0ZWdvcnlJZCI6IjEifV19LHsiaWQiOiIxNzUzMDQ0NDQ2NzU1IiwibmFtZSI6IkNvZGUiLCJ3ZWJzaXRlcyI6W3siaWQiOiIxNzUzMDQ0NDU5MjcyIiwibmFtZSI6IkdpdGh1YiIsInVybCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9mZWVkIiwiaWNvbiI6Imh0dHBzOi8vY2RuLmpzZGVsaXZyLm5ldC9naC9ob21hcnItbGFicy9kYXNoYm9hcmQtaWNvbnMvc3ZnL2dpdGh1Yi1saWdodC5zdmciLCJjYXRlZ29yeUlkIjoiMTc1MzA0NDQ0Njc1NSJ9LHsiaWQiOiIxNzgzNzI4ODcwNjgzIiwibmFtZSI6IkFXUyIsInVybCI6IiIsImljb24iOiJodHRwczovL2Nkbi5qc2RlbGl2ci5uZXQvZ2gvaG9tYXJyLWxhYnMvZGFzaGJvYXJkLWljb25zL3N2Zy9hd3MtbGlnaHQuc3ZnIiwiY2F0ZWdvcnlJZCI6IjE3NTMwNDQ0NDY3NTUifSx7ImlkIjoiMTc4MzcyODkxNTg1MyIsIm5hbWUiOiJEb2NrZXIgSHViIiwidXJsIjoiIiwiaWNvbiI6Imh0dHBzOi8vY2RuLmpzZGVsaXZyLm5ldC9naC9ob21hcnItbGFicy9kYXNoYm9hcmQtaWNvbnMvc3ZnL2RvY2tlci5zdmciLCJjYXRlZ29yeUlkIjoiMTc1MzA0NDQ0Njc1NSJ9LHsiaWQiOiIxNzgzNzI4OTkyMzkwIiwibmFtZSI6IkZpZ21hIiwidXJsIjoiIiwiaWNvbiI6Imh0dHBzOi8vY2RuLmpzZGVsaXZyLm5ldC9naC9ob21hcnItbGFicy9kYXNoYm9hcmQtaWNvbnMvc3ZnL2ZpZ21hLnN2ZyIsImNhdGVnb3J5SWQiOiIxNzUzMDQ0NDQ2NzU1In1dfSx7ImlkIjoiMTc1MzA0NDY2ODAxMiIsIm5hbWUiOiJNZWRpYSIsIndlYnNpdGVzIjpbeyJpZCI6IjE3NTMwNDQ3MDYwMjQiLCJuYW1lIjoiTmV0ZmxpeCIsInVybCI6Imh0dHA6Ly90di5oYXZlbi8iLCJpY29uIjoiaHR0cHM6Ly9jZG4uanNkZWxpdnIubmV0L2doL2hvbWFyci1sYWJzL2Rhc2hib2FyZC1pY29ucy9zdmcvbmV0ZmxpeC5zdmciLCJjYXRlZ29yeUlkIjoiMTc1MzA0NDY2ODAxMiJ9LHsiaWQiOiIxNzUzMDQ0NzQwOTg2IiwibmFtZSI6IlByaW1lIiwidXJsIjoiaHR0cDovL29tdi5oYXZlbiIsImljb24iOiJodHRwczovL2Nkbi5qc2RlbGl2ci5uZXQvZ2gvaG9tYXJyLWxhYnMvZGFzaGJvYXJkLWljb25zL3N2Zy9wcmltZS12aWRlby1hbHQtZGFyay5zdmciLCJjYXRlZ29yeUlkIjoiMTc1MzA0NDY2ODAxMiJ9LHsiaWQiOiIxNzgzNzI4Nzg0MjU2IiwibmFtZSI6IkhCTyIsInVybCI6IiIsImljb24iOiJodHRwczovL2Nkbi5qc2RlbGl2ci5uZXQvZ2gvaG9tYXJyLWxhYnMvZGFzaGJvYXJkLWljb25zL3N2Zy9oYm8tbGlnaHQuc3ZnIiwiY2F0ZWdvcnlJZCI6IjE3NTMwNDQ2NjgwMTIifSx7ImlkIjoiMTc1MzA0NDgyODg0MCIsIm5hbWUiOiJUcmFrdCIsInVybCI6Imh0dHBzOi8vYXBwLnRyYWt0LnR2LyIsImljb24iOiJodHRwczovL3d3dy5nb29nbGUuY29tL3MyL2Zhdmljb25zP2RvbWFpbj1hcHAudHJha3QudHYmc3o9MTI4IiwiY2F0ZWdvcnlJZCI6IjE3NTMwNDQ2NjgwMTIifSx7ImlkIjoiMTc4MzcyODcyOTQyNCIsIm5hbWUiOiJMZXR0ZXJib3hkIiwidXJsIjoiaHR0cHM6Ly9sZXR0ZXJib3hkLmNvbS8iLCJpY29uIjoiaHR0cHM6Ly93d3cuZ29vZ2xlLmNvbS9zMi9mYXZpY29ucz9kb21haW49bGV0dGVyYm94ZC5jb20mc3o9MTI4IiwiY2F0ZWdvcnlJZCI6IjE3NTMwNDQ2NjgwMTIifV19XQ==",
"config": "eyJ0aXRsZSI6IiIsImN1cnJlbnRXYWxscGFwZXJzIjpbIkFic3RyYWN0IFJlZCJdLCJ3YWxscGFwZXJGcmVxdWVuY3kiOiIxZCIsIndhbGxwYXBlckJsdXIiOjAsIndhbGxwYXBlckJyaWdodG5lc3MiOjEwOCwid2FsbHBhcGVyT3BhY2l0eSI6MTAwLCJ0aXRsZVNpemUiOiJtZWRpdW0iLCJhbGlnbm1lbnQiOiJtaWRkbGUiLCJob3Jpem9udGFsQWxpZ25tZW50IjoibWlkZGxlIiwidGlsZVNpemUiOiJzbWFsbCIsImNsb2NrIjp7ImVuYWJsZWQiOnRydWUsInNpemUiOiJ0aW55IiwiZm9udCI6Im1vbm9zcGFjZSIsImZvcm1hdCI6Img6bW0gQSJ9LCJzZXJ2ZXJXaWRnZXQiOnsiZW5hYmxlZCI6dHJ1ZSwicGluZ0ZyZXF1ZW5jeSI6MTUsInNlcnZlcnMiOlt7ImlkIjoiMTc4MzcyOTE4MzU1MyIsIm5hbWUiOiJBZEd1YXJkIiwiYWRkcmVzcyI6Imh0dHBzOi8vZ29vZ2xlLmNvbSJ9LHsiaWQiOiIxNzgzNzI5MjI3NDQyIiwibmFtZSI6IlByb3htb3giLCJhZGRyZXNzIjoiaHR0cHM6Ly9nb29nbGUuY29tIn1dfX0=",
"userWallpapers": "W3sibmFtZSI6ImRhcmstYWJzdHJhY3QtMjU2MHgxNDQwLWNvbnRlbXBvcmFyeS1kZXNpZ24tc2xlZWstbGluZXMtMjY0MjYuanBnIn1d",
"wallpaperState": "eyJsYXN0V2FsbHBhcGVyQ2hhbmdlIjoiMjA0Ni0wNy0xMVQwMDoyMzozMS4zODdaIiwiY3VycmVudEluZGV4IjowfQ=="
}