Skip to content
Merged
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
99 changes: 99 additions & 0 deletions .github/workflows/handbook-build-check.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: Handbook Build Check

# PR-only build verification for Dockerfile.handbook + the Goldens-assembled
# screenshots. Does NOT push to Docker Hub and does NOT deploy — that
# remains the job of handbook-deploy.yaml (develop push → DEV → PRD).
#
# Path filter covers everything that goes into the handbook image:
# - docs/handbook/** handbook HTML, README, en/de subtrees
# - scripts/assemble-handbook-screenshots.sh handbook→Golden mapping
# - test/goldens/** Golden baselines (source of every
# screenshot the handbook serves)
# - Dockerfile.handbook multi-stage build
# - handbook.nginx.conf nginx config
# - handbook.htpasswd access gate

on:
pull_request:
# Include ready_for_review so PRs flipped from Draft to Ready also
# trigger this check (default trigger list misses that event).
types: [opened, synchronize, reopened, ready_for_review]
paths:
- "docs/handbook/**"
- "scripts/assemble-handbook-screenshots.sh"
- "test/goldens/**"
- "Dockerfile.handbook"
- "handbook.nginx.conf"
- "handbook.htpasswd"
- ".github/workflows/handbook-build-check.yaml"

permissions:
contents: read

concurrency:
group: handbook-build-check-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
build:
name: Build handbook image + container smoke
# Skip on draft PRs (matches the rest of pull-request.yaml).
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

- name: Verify assemble-handbook-screenshots.sh runs locally
# Independent sanity-check of the Bash script before the Docker
# multi-stage build runs the same logic. Catches obvious
# mapping errors (missing Golden) without needing the Docker
# daemon to surface them.
run: |
set -euo pipefail
bash scripts/assemble-handbook-screenshots.sh /tmp/handbook-shots
count=$(ls -1 /tmp/handbook-shots/*.png | wc -l | tr -d ' ')
if [ "$count" != "26" ]; then
echo "expected 26 screenshots, got $count" >&2
exit 1
fi

- name: Build handbook image (no push)
run: docker build -f Dockerfile.handbook -t realunit-handbook:pr-check .

- name: Smoke-test container starts and serves /healthz
run: |
set -euo pipefail
docker run -d --name handbook -p 8080:8080 realunit-handbook:pr-check
# Give nginx a moment to start.
for i in $(seq 1 10); do
if curl --fail --silent http://127.0.0.1:8080/healthz; then
break
fi
sleep 1
done
# /healthz must return 200 (unauthenticated by config).
curl --fail --silent http://127.0.0.1:8080/healthz | grep -q OK

# Gated path must return 401 without credentials (auth wall active).
code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/de/)
if [ "$code" != "401" ]; then
echo "expected 401 from /de/ without auth, got $code" >&2
docker logs handbook
exit 1
fi

# Screenshots dir must contain all 26 PNGs assembled from Goldens.
# Hit one of them through the auth gate to verify wiring end-to-end.
for name in 01-welcome 11-dashboard 26-terms; do
code=$(curl -s -o /dev/null -w '%{http_code}' -u "${HANDBOOK_USER:-x}:${HANDBOOK_PASS:-x}" "http://127.0.0.1:8080/screenshots/${name}.png")
# 200 (auth happens to match) or 401 (auth fails but file exists)
# both prove the file is on disk. 404 means it was not assembled.
if [ "$code" = "404" ]; then
echo "screenshot ${name}.png missing from /usr/share/nginx/html/screenshots/" >&2
docker logs handbook
exit 1
fi
done

docker stop handbook
3 changes: 3 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ See the README's "Release versioning" section for the full table and the typical
```bash
rg "^//\s*@no-integration-test:" lib/
```
- Visual-regression Goldens under `test/goldens/screens/` are also the source of the 26 screenshots served at `handbook.realunit.app`. When you add a handbook page, you MUST add a matching Golden test AND a row in the mapping table at `scripts/assemble-handbook-screenshots.sh` — the handbook will not pick up a Maestro-captured PNG anymore. The `Handbook Build Check` workflow on every PR runs the assembly script and fails loudly if a mapped Golden is missing.
- Why: single source of truth — a UI regression that breaks a Golden also breaks the handbook image before either ships; eliminates the previous "two pipelines, two truths" problem.
- See: [`docs/visual-regression-tests.md`](docs/visual-regression-tests.md) section "Handbook screenshots are sourced from Goldens".

[^integration-test]: Activates once an `integration_test/` directory exists in the repo; until then, treat option 1 as N/A and the `// @no-integration-test:` annotation as the documenting form.

Expand Down
22 changes: 20 additions & 2 deletions Dockerfile.handbook
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,35 @@
# and dev-handbook.realunit.app (DEV) via Cloudflare Tunnel.
#
# Built independently of the Flutter app: no Flutter toolchain, no app code.
# Build context is the repo root; only docs/handbook/ is copied in.
# Build context is the repo root; only docs/handbook/, scripts/, and
# test/goldens/ are copied in.
#
# The 26 screenshots (`screenshots/NN-name.png`) are assembled from the
# visual-regression Golden baselines under `test/goldens/screens/` via
# scripts/assemble-handbook-screenshots.sh — one Golden per handbook
# flow, see the mapping in that script. The legacy Maestro-captured
# `docs/handbook/screenshots/` directory is still copied as part of the
# `docs/handbook/` COPY below (so e.g. README.md and any future de/en
# split survive); the assembled-from-goldens screenshots in the second
# COPY replace it whole.
#
# Note: docs/handbook/mails/ is git-ignored and populated at CI build time
# by the "Generate RealUnit mail previews from api repo" step in
# .github/workflows/handbook.yaml — DFXswiss/api is the single source of
# truth for those previews. The COPY below picks them up automatically as
# truth for those previews. The first COPY picks them up automatically as
# part of the docs/handbook/ tree.

FROM alpine:3.20 AS screenshots-builder
WORKDIR /work
RUN apk add --no-cache bash coreutils
COPY scripts/assemble-handbook-screenshots.sh ./scripts/
COPY test/goldens/screens/ ./test/goldens/screens/
RUN bash ./scripts/assemble-handbook-screenshots.sh /out

FROM nginx:1.27.5-alpine

COPY docs/handbook/ /usr/share/nginx/html/
COPY --from=screenshots-builder /out/ /usr/share/nginx/html/screenshots/
COPY handbook.nginx.conf /etc/nginx/conf.d/default.conf
COPY handbook.htpasswd /etc/nginx/handbook.htpasswd

Expand Down
58 changes: 58 additions & 0 deletions docs/visual-regression-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,61 @@ and Hardware-Determinismus (identical Skia/CoreText/HW across runs —
no GitHub image bump can drift the baselines). The cost argument does
**not** apply — `DFXswiss/realunit-app` is public, GitHub Actions on
public repos are free even for macOS minutes.

## Handbook screenshots are sourced from Goldens

The 26 PNGs the handbook serves at `handbook.realunit.app/screenshots/`
are assembled from the Golden baselines at docker-build time. One
Golden → one handbook page, via the explicit mapping in
`scripts/assemble-handbook-screenshots.sh`. The handbook does **not**
have its own screenshot set anymore.

### Why

- **Single source of truth.** A UI regression that flips a Golden also
breaks the handbook image before either ships. The pixel-checked
baseline IS the documentation.
- **Determinism.** dfx01's headless Skia/Open Sans render is byte-stable
across CI runs. The previous Maestro-driven iOS-Simulator capture
drifted on Apple Silicon + iOS 26 driver hangs
(mobile-dev-inc/maestro#3137).
- **Cycle time.** The handbook image rebuilds when Goldens change in
seconds. No 30-minute Maestro suite to refresh a page.

### Where each handbook page comes from

Authoritative mapping table lives in
`scripts/assemble-handbook-screenshots.sh` — keep it in sync with
`.maestro/handbook/*.yaml` (one entry per flow). The script copies the
Golden into the output directory with the handbook's expected
`NN-name.png` filename; the Dockerfile multi-stage build then layers
that directory into `/usr/share/nginx/html/screenshots/`.

### When you add a new handbook page

1. Add the `.maestro/handbook/<NN>-<name>.yaml` flow (still useful as
integration smoke even if no longer the screenshot source — see
Maestro section below for current PR-gate vs nightly status).
2. Add a Golden test under `test/goldens/screens/<screen>/` that
renders the same UI state as the handbook flow's terminal screen.
3. Add a row to the `MAPPING` array in
`scripts/assemble-handbook-screenshots.sh` pointing at the new
Golden file.
4. Open the PR. The `Handbook Build Check` workflow runs
`docker build` and a container smoke (`/healthz` + auth gate +
probe `/screenshots/<NN>-*.png`). A missing Golden surfaces here
as a missing-source error from the assembly script before docker
even spins up.

### When you change an existing handbook page

Touch the Golden test (or the underlying widget/copy), let CI regenerate
the baseline on dfx01, and commit the new PNG. The handbook picks up
the change automatically on the next docker build — no separate
handbook-screenshot recapture step needed.

### Reviewing a handbook visual change

Pull the artifact or diff the PNG in `test/goldens/screens/**/` like
any other Golden review. There is no second set of handbook PNGs to
also check.
17 changes: 15 additions & 2 deletions lib/screens/legal/subpages/legal_document_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,16 @@ class LegalDocumentParams {
class LegalDocumentPage extends StatefulWidget {
final LegalDocumentParams params;

const LegalDocumentPage({super.key, required this.params});
/// Pre-loaded markdown content for golden tests. When provided, skips the
/// rootBundle asset load so the page renders the loaded state synchronously.
@visibleForTesting
final String? initialMarkdownContent;

const LegalDocumentPage({
super.key,
required this.params,
this.initialMarkdownContent,
});

@override
State<LegalDocumentPage> createState() => _LegalDocumentPageState();
Expand All @@ -36,7 +45,11 @@ class _LegalDocumentPageState extends State<LegalDocumentPage> {
@override
void initState() {
super.initState();
_loadMarkdown();
if (widget.initialMarkdownContent != null) {
_markdownContent = widget.initialMarkdownContent;
} else {
_loadMarkdown();
}
}

Future<void> _loadMarkdown() async {
Expand Down
9 changes: 7 additions & 2 deletions lib/screens/welcome/welcome_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,19 @@ import 'package:realunit_wallet/styles/colors.dart';
import 'package:realunit_wallet/styles/icons.dart';

class WelcomePage extends StatefulWidget {
const WelcomePage({super.key});
/// Pre-sets the second-step state for golden testing the create-vs-restore
/// choice screen without driving a tap through the first-step Card.
@visibleForTesting
final bool initialShowSecondStep;

const WelcomePage({super.key, this.initialShowSecondStep = false});

@override
State<WelcomePage> createState() => _WelcomePageState();
}

class _WelcomePageState extends State<WelcomePage> {
bool showSecondStep = false;
late bool showSecondStep = widget.initialShowSecondStep;

@override
Widget build(BuildContext context) => Scaffold(
Expand Down
10 changes: 8 additions & 2 deletions lib/styles/themes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ ThemeData get realUnitTheme => ThemeData(
.symmetric(vertical: 14.0, horizontal: 20.0),
),
textStyle: WidgetStatePropertyAll(
RealUnitTextStyle.body.base.copyWith(fontWeight: .w600),
RealUnitTextStyle.body.base.copyWith(
fontFamily: RealUnitTextStyle.fontFamily,
fontWeight: .w600,
),
),
),
),
Expand All @@ -79,7 +82,10 @@ ThemeData get realUnitTheme => ThemeData(
return RealUnitColors.basic.white;
}),
textStyle: WidgetStatePropertyAll(
RealUnitTextStyle.body.base.copyWith(fontWeight: .w600),
RealUnitTextStyle.body.base.copyWith(
fontFamily: RealUnitTextStyle.fontFamily,
fontWeight: .w600,
),
),
),
),
Expand Down
88 changes: 88 additions & 0 deletions scripts/assemble-handbook-screenshots.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/env bash
#
# Assemble the 26 handbook screenshots from the visual-regression Golden
# baselines. Output layout matches the legacy `docs/handbook/screenshots/`
# directory so handbook HTML links (`<img src="../screenshots/NN-name.png">`)
# keep working unchanged.
#
# Usage:
# scripts/assemble-handbook-screenshots.sh <output-dir>
#
# Used both by:
# - the Dockerfile.handbook build (multi-stage, copies the output dir
# into /usr/share/nginx/html/screenshots/)
# - local previews (`scripts/assemble-handbook-screenshots.sh /tmp/h && ls /tmp/h`)
#
# Source of truth for every handbook page is one Golden PNG under
# `test/goldens/screens/<screen>/goldens/macos/<file>.png`. The mapping
# table below was established in the gap-audit on PR #568. When a new
# handbook page is added, append a row here AND add the corresponding
# Golden test — never source a screenshot from anywhere else.

set -euo pipefail

if [ "$#" -ne 1 ]; then
echo "usage: $0 <output-dir>" >&2
exit 2
fi

OUT="$1"
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
GOLDENS_ROOT="$REPO_ROOT/test/goldens/screens"

mkdir -p "$OUT"

# handbook-name → relative golden path (under test/goldens/screens/)
# Keep this list in sync with .maestro/handbook/*.yaml (one entry per flow).
MAPPING=(
"01-welcome=home/goldens/macos/home_page_default.png"
"02-create-vs-restore=welcome/goldens/macos/welcome_page_ios.png"
"03-software-wallet-terms=welcome/goldens/macos/welcome_page_second_step.png"
"04-seed-hidden=create_wallet/goldens/macos/create_wallet_page_default.png"
"05-seed-revealed=create_wallet/goldens/macos/create_wallet_page_revealed.png"
"06-verify-seed=verify_seed/goldens/macos/verify_seed_page_default.png"
"07-onboarding-completed=onboarding/goldens/macos/onboarding_completed_page_default.png"
"08-pin-setup=pin/goldens/macos/setup_pin_page_default.png"
"09-pin-confirm=pin/goldens/macos/setup_pin_page_confirming.png"
"10-biometric-prompt=pin/goldens/macos/biometric_prompt_sheet_default.png"
"11-dashboard=home/goldens/macos/home_page_loaded.png"
"12-settings=settings/goldens/macos/settings_page_default.png"
"13-settings-languages=settings_languages/goldens/macos/settings_languages_page_default.png"
"14-settings-currency=settings_currencies/goldens/macos/settings_currencies_page_default.png"
"15-settings-network=settings_network/goldens/macos/settings_network_page_default.png"
"16-settings-wallet-address=settings_wallet_address/goldens/macos/settings_wallet_address_page_default.png"
"17-settings-backup-pin=pin/goldens/macos/verify_pin_page_seed_backup.png"
"18-settings-seed-hidden=settings_seed/goldens/macos/settings_seed_page_default.png"
"19-settings-seed-revealed=settings_seed/goldens/macos/settings_seed_page_revealed.png"
"20-settings-legal-documents=settings_legal_documents/goldens/macos/settings_legal_documents_page_default.png"
"21-settings-aktionariat-documents=settings_legal_documents/goldens/macos/settings_aktionariat_documents_page_default.png"
"22-settings-dfx-documents=settings_legal_documents/goldens/macos/settings_dfx_documents_page_default.png"
"23-settings-contact=settings_contact/goldens/macos/settings_contact_page_default.png"
"24-settings-delete-wallet=settings/goldens/macos/settings_confirm_logout_wallet_sheet_default.png"
"25-restore-wallet=restore_wallet/goldens/macos/restore_wallet_page_default.png"
"26-terms=legal/goldens/macos/legal_document_page_terms_loaded.png"
)

missing=()
for entry in "${MAPPING[@]}"; do
name="${entry%%=*}"
src_rel="${entry#*=}"
src="$GOLDENS_ROOT/$src_rel"
if [ ! -f "$src" ]; then
missing+=("$name → $src_rel")
continue
fi
cp "$src" "$OUT/$name.png"
done

if [ "${#missing[@]}" -gt 0 ]; then
echo "error: handbook-screenshot sources missing from Goldens:" >&2
printf ' %s\n' "${missing[@]}" >&2
echo >&2
echo "Run the visual-regression suite + commit baselines, or update the" >&2
echo "mapping in $0 to point at an existing Golden." >&2
exit 1
fi

count=$(ls -1 "$OUT"/*.png | wc -l | tr -d ' ')
echo "assembled $count handbook screenshots into $OUT"
Binary file modified test/goldens/screens/buy/goldens/macos/buy_initial.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/goldens/screens/buy/goldens/macos/buy_payment_info_loaded.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading