Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Binary file added assets/fonts/NunitoSans-Bold.ttf
Binary file not shown.
Binary file added assets/fonts/NunitoSans-Medium.ttf
Binary file not shown.
Binary file added assets/fonts/NunitoSans-Regular.ttf
Binary file not shown.
Binary file added assets/fonts/NunitoSans-SemiBold.ttf
Binary file not shown.
Binary file added assets/fonts/PlayfairDisplay-Bold.ttf
Binary file not shown.
Binary file added assets/fonts/PlayfairDisplay-SemiBold.ttf
Binary file not shown.
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ How the codebase works -- implementation guides, architecture, and technical ref
- **[widget-standards.md](code/widget-standards.md)** - Widget patterns and standards
- **[ui-components.md](code/ui-components.md)** - Shared UI components (OverflowMenu)
- **[network.md](code/network.md)** - Network security configuration and allowlist
- **[fonts.md](code/fonts.md)** - Bundled typefaces, why they aren't fetched at runtime, and how to add a weight
- **[api/](code/api/)** - API documentation
- [README.md](code/api/README.md) - API overview
- [data-api-reference.md](code/api/data-api-reference.md) - Complete API reference
Expand Down
79 changes: 79 additions & 0 deletions docs/code/fonts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Fonts

The app pairs **Playfair Display** (headings, app bar title) with **Nunito Sans**
(body, labels). Both are declared in `lib/app_theme.dart` via `buildAppTextTheme`
and resolved through the `google_fonts` package.

## The font files are bundled, not fetched

`assets/fonts/` ships the exact `.ttf` files `google_fonts` would otherwise
download from `fonts.gstatic.com` at runtime. `google_fonts` checks the asset
bundle before it checks the network, so bundling means the typefaces resolve
locally on every platform.

Two things this buys us:

- **First paint has no network dependency.** Previously the app rendered in a
fallback face until the font download finished — over festival wifi, or on a
phone with no signal, that could be the whole session.
- **Golden tests are deterministic.** `test/flutter_test_config.dart` sets
`GoogleFonts.config.allowRuntimeFetching = false` for the whole test suite,
so goldens render the real faces identically on any machine, online or not.

Before this, golden tests rendered every glyph as the blocky `FlutterTest`
placeholder box, which meant no golden could catch a typography or text-layout
regression (issue #520).

## Which variants are bundled

Only the weights `buildAppTextTheme` actually requests:

| File | Used by |
|---|---|
| `NunitoSans-Regular.ttf` (w400) | `bodyLarge`, `bodyMedium`, `bodySmall`, and the `nunitoSansTextTheme()` base |
| `NunitoSans-Medium.ttf` (w500) | `labelSmall`, base theme label styles |
| `NunitoSans-SemiBold.ttf` (w600) | `titleSmall`, `labelLarge`, `labelMedium` |
| `NunitoSans-Bold.ttf` (w700) | `titleMedium` |
| `PlayfairDisplay-SemiBold.ttf` (w600) | `displaySmall`, `headlineLarge/Medium/Small`, `titleLarge` |
| `PlayfairDisplay-Bold.ttf` (w700) | `displayLarge`, `displayMedium`, `appBarTheme.titleTextStyle` |

Roughly 674 KB in total.

## Adding a weight

If you add a style to `buildAppTextTheme` that needs a weight not in the table
above, **the test suite will fail loudly** with:

```
GoogleFonts.config.allowRuntimeFetching is false but font X was not found in
the application assets.
```

That failure is the feature — it stops a new weight from silently depending on a
network fetch that only works on a connected machine. To fix it, add the variant:

1. Find the file hash for the family/weight in the `google_fonts` package
manifest (`lib/src/google_fonts_parts/part_<letter>.g.dart` in the pub cache);
each entry is `GoogleFontsFile('<sha256>', <length>)`.
2. Download it and verify both the hash and the length match:

```bash
curl -sSL -o assets/fonts/<Family>-<Variant>.ttf \
"https://fonts.gstatic.com/s/a/<sha256>.ttf"
sha256sum assets/fonts/<Family>-<Variant>.ttf # must equal <sha256>
stat -c%s assets/fonts/<Family>-<Variant>.ttf # must equal <length>
```

3. The filename must be `<FamilyWithoutSpaces>-<Variant>.ttf` — that is exactly
the name quoted in the failure message. `assets/fonts/` is already listed in
`pubspec.yaml`, so no manifest edit is needed.
4. Regenerate the affected goldens and review them by eye:
`./bin/mise run goldens:update <test_file>`.

Do **not** fix this by re-enabling runtime fetching in tests.

## Known gap

Material icon glyphs still render as hollow boxes in goldens — `MaterialIcons`
is not loaded by `flutter_test`. That is unrelated to this setup and unchanged by
it; goldens cover icon *position and size*, not the glyph itself.
7 changes: 7 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,10 @@ flutter:
uses-material-design: true
assets:
- assets/
# Bundled so `google_fonts` resolves the app's typefaces from assets rather
# than fetching them from fonts.gstatic.com at runtime. This makes golden
# tests deterministic (see test/flutter_test_config.dart) and removes a
# network round-trip from first paint. Only the variants buildAppTextTheme
# actually asks for are shipped; adding a new weight to app_theme.dart means
# adding the matching file here. See docs/code/fonts.md.
Comment thread
richardthe3rd marked this conversation as resolved.
Outdated
- assets/fonts/
12 changes: 5 additions & 7 deletions test/brewery_screen_screenshot_test.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:cambridge_beer_festival/app_theme.dart';
import 'package:cambridge_beer_festival/screens/screens.dart';
import 'package:cambridge_beer_festival/models/models.dart';
import 'package:cambridge_beer_festival/providers/providers.dart';
Expand Down Expand Up @@ -110,13 +111,10 @@ void main() {
return ChangeNotifierProvider<BeerProvider>.value(
value: provider,
child: MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF2B3170),
brightness: brightness,
),
useMaterial3: true,
),
// Use the real app theme so these goldens cover `appBarTheme`,
// `navigationBarTheme` and the text theme, not just the colour
// scheme (#520).
theme: buildAppTheme(brightness),
home: const BreweryScreen(
festivalId: 'cbf2025',
breweryId: 'brewery1',
Expand Down
12 changes: 5 additions & 7 deletions test/drink_detail_screen_screenshot_test.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:cambridge_beer_festival/app_theme.dart';
import 'package:cambridge_beer_festival/screens/screens.dart';
import 'package:cambridge_beer_festival/models/models.dart';
import 'package:cambridge_beer_festival/providers/providers.dart';
Expand Down Expand Up @@ -70,13 +71,10 @@ void main() {
return ChangeNotifierProvider<BeerProvider>.value(
value: provider,
child: MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF2B3170),
brightness: brightness,
),
useMaterial3: true,
),
// Use the real app theme so these goldens cover `appBarTheme`,
// `navigationBarTheme` and the text theme, not just the colour
// scheme (#520).
theme: buildAppTheme(brightness),
home: DrinkDetailScreen(festivalId: 'cbf2025', drinkId: drinkId),
),
);
Expand Down
26 changes: 26 additions & 0 deletions test/flutter_test_config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'dart:async';

import 'package:google_fonts/google_fonts.dart';

/// Runs once before every test under `test/`, discovered automatically by
/// `flutter test`.
Comment thread
richardthe3rd marked this conversation as resolved.
Outdated
///
/// Turning off runtime fetching forces `google_fonts` to resolve the app's
/// typefaces from the bundled assets in `assets/fonts/` instead of downloading
/// them from fonts.gstatic.com. That matters for two reasons:
///
/// * **Determinism.** Goldens render the real Playfair/Nunito faces on every
/// machine, online or not, instead of depending on whether the runner
/// happened to reach Google's CDN.
/// * **It fails loudly.** If `buildAppTextTheme` gains a weight that has no
/// matching file in `assets/fonts/`, `google_fonts` throws and the test
/// fails, rather than silently papering over it with a network fetch that
/// only works on a connected machine.
///
/// If a test starts failing with "allowRuntimeFetching is false but font
/// X was not found", the fix is to add that variant to `assets/fonts/` — not to
/// re-enable fetching. See docs/code/fonts.md.
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
GoogleFonts.config.allowRuntimeFetching = false;
await testMain();
}
Binary file modified test/goldens/brewery_screen_dark.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/brewery_screen_light.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/drink_detail_screen_long_name_light.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/drink_detail_screen_medium_name_dark.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/drink_detail_screen_medium_name_light.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/drink_detail_screen_with_similar_light.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/style_screen_with_description_dark.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/style_screen_with_description_light.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/screens/goldens/my_festival_screen_dark.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/screens/goldens/my_festival_screen_light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 2 additions & 14 deletions test/screens/my_festival_screen_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,6 @@ import 'package:shared_preferences/shared_preferences.dart';

import '../provider_test.mocks.dart';

/// A theme for golden tests that mirrors the app's seed colour but avoids
/// `google_fonts` (which fetches over the network and fails under test) — the
/// same approach the other screen screenshot tests use.
ThemeData _goldenTheme(Brightness brightness) => ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF2B3170),
brightness: brightness,
),
useMaterial3: true,
brightness: brightness,
);

void main() {
group('MyFestivalScreen', () {
late MockDrinkRepository mockDrinkRepository;
Expand Down Expand Up @@ -680,7 +668,7 @@ void main() {
await tester.binding.setSurfaceSize(const Size(400, 800));
addTearDown(() => tester.binding.setSurfaceSize(null));
await tester.pumpWidget(
createTestWidget(theme: _goldenTheme(Brightness.light)),
createTestWidget(theme: buildAppTheme(Brightness.light)),
);
await tester.pumpAndSettle();

Expand Down Expand Up @@ -715,7 +703,7 @@ void main() {
await tester.binding.setSurfaceSize(const Size(400, 800));
addTearDown(() => tester.binding.setSurfaceSize(null));
await tester.pumpWidget(
createTestWidget(theme: _goldenTheme(Brightness.dark)),
createTestWidget(theme: buildAppTheme(Brightness.dark)),
);
await tester.pumpAndSettle();

Expand Down
20 changes: 6 additions & 14 deletions test/style_screen_screenshot_test.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:cambridge_beer_festival/app_theme.dart';
import 'package:cambridge_beer_festival/screens/screens.dart';
import 'package:cambridge_beer_festival/models/models.dart';
import 'package:cambridge_beer_festival/providers/providers.dart';
Expand Down Expand Up @@ -111,13 +112,10 @@ void main() {
return ChangeNotifierProvider<BeerProvider>.value(
value: provider,
child: MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF2B3170),
brightness: Brightness.light,
),
useMaterial3: true,
),
// Use the real app theme so these goldens cover `appBarTheme`,
// `navigationBarTheme` and the text theme, not just the colour
// scheme (#520).
theme: buildAppTheme(Brightness.light),
home: StyleScreen(festivalId: 'cbf2025', style: style),
),
);
Expand Down Expand Up @@ -162,13 +160,7 @@ void main() {
ChangeNotifierProvider<BeerProvider>.value(
value: provider,
child: MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF2B3170),
brightness: Brightness.dark,
),
useMaterial3: true,
),
theme: buildAppTheme(Brightness.dark),
home: const StyleScreen(festivalId: 'cbf2025', style: 'IPA'),
),
),
Expand Down
12 changes: 4 additions & 8 deletions test/widgets/drink_card_test.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:cambridge_beer_festival/app_theme.dart';
import 'package:cambridge_beer_festival/widgets/drink_card.dart';
import 'package:cambridge_beer_festival/models/models.dart';

Expand Down Expand Up @@ -41,14 +42,9 @@ void main() {
String searchQuery = '',
}) {
return MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF2B3170),
brightness: brightness,
),
useMaterial3: true,
brightness: brightness,
),
// Use the real app theme so these goldens cover the text theme and the
// colours it bakes in, not just the colour scheme (#520).
theme: buildAppTheme(brightness),
home: Scaffold(
body: DrinkCard(
drink: drink,
Expand Down
Binary file modified test/widgets/goldens/drink_card_search_excerpt_dark.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/widgets/goldens/drink_card_search_excerpt_light.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/widgets/goldens/drink_card_tasted_multiple_dark.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/widgets/goldens/drink_card_tasted_multiple_light.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/widgets/goldens/drink_card_want_to_try_light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading