diff --git a/assets/fonts/NunitoSans-Bold.ttf b/assets/fonts/NunitoSans-Bold.ttf new file mode 100644 index 00000000..fbab37eb Binary files /dev/null and b/assets/fonts/NunitoSans-Bold.ttf differ diff --git a/assets/fonts/NunitoSans-Medium.ttf b/assets/fonts/NunitoSans-Medium.ttf new file mode 100644 index 00000000..cf06382f Binary files /dev/null and b/assets/fonts/NunitoSans-Medium.ttf differ diff --git a/assets/fonts/NunitoSans-Regular.ttf b/assets/fonts/NunitoSans-Regular.ttf new file mode 100644 index 00000000..ede44dcd Binary files /dev/null and b/assets/fonts/NunitoSans-Regular.ttf differ diff --git a/assets/fonts/NunitoSans-SemiBold.ttf b/assets/fonts/NunitoSans-SemiBold.ttf new file mode 100644 index 00000000..835a5e77 Binary files /dev/null and b/assets/fonts/NunitoSans-SemiBold.ttf differ diff --git a/assets/fonts/OFL-NunitoSans.txt b/assets/fonts/OFL-NunitoSans.txt new file mode 100644 index 00000000..2ae17359 --- /dev/null +++ b/assets/fonts/OFL-NunitoSans.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Sans Project Authors (https://github.com/Fonthausen/NunitoSans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/OFL-PlayfairDisplay.txt b/assets/fonts/OFL-PlayfairDisplay.txt new file mode 100644 index 00000000..13153890 --- /dev/null +++ b/assets/fonts/OFL-PlayfairDisplay.txt @@ -0,0 +1,93 @@ +Copyright 2017 The Playfair Display Project Authors (https://github.com/clauseggers/Playfair-Display), with Reserved Font Name "Playfair Display" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/PlayfairDisplay-Bold.ttf b/assets/fonts/PlayfairDisplay-Bold.ttf new file mode 100644 index 00000000..d1a6c30e Binary files /dev/null and b/assets/fonts/PlayfairDisplay-Bold.ttf differ diff --git a/assets/fonts/PlayfairDisplay-SemiBold.ttf b/assets/fonts/PlayfairDisplay-SemiBold.ttf new file mode 100644 index 00000000..92822a01 Binary files /dev/null and b/assets/fonts/PlayfairDisplay-SemiBold.ttf differ diff --git a/docs/README.md b/docs/README.md index 77b8a90b..532f9ab2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 diff --git a/docs/code/fonts.md b/docs/code/fonts.md new file mode 100644 index 00000000..c26646d4 --- /dev/null +++ b/docs/code/fonts.md @@ -0,0 +1,94 @@ +# 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. + +## Licensing + +Bundling means the app **redistributes** the font binaries rather than linking to +Google's CDN, and both families are licensed under the [SIL Open Font License +1.1](https://openfontlicense.org/), which requires the licence to travel with the +files. `assets/fonts/` therefore also holds `OFL-NunitoSans.txt` and +`OFL-PlayfairDisplay.txt`, and `registerFontLicenses()` in `lib/app_theme.dart` +adds them to Flutter's `LicenseRegistry` so they show up in the app's standard +"View licences" page. It is called from `main()` before `runApp`. + +**Bundling a third family means registering its licence too** — add the entry to +`fontLicenseAssets` in `lib/app_theme.dart`. `test/app_theme_test.dart` loads +every declared licence asset for real, so a wrong path fails the suite instead of +silently registering nothing. + +## 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_.g.dart` in the pub cache); + each entry is `GoogleFontsFile('', )`. +2. Download it and verify both the hash and the length match: + + ```bash + curl -sSL -o assets/fonts/-.ttf \ + "https://fonts.gstatic.com/s/a/.ttf" + sha256sum assets/fonts/-.ttf # must equal + stat -c%s assets/fonts/-.ttf # must equal + ``` + +3. The filename must be `-.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 `. + +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. diff --git a/lib/app_theme.dart b/lib/app_theme.dart index f493fef5..03dea95d 100644 --- a/lib/app_theme.dart +++ b/lib/app_theme.dart @@ -1,8 +1,42 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:google_fonts/google_fonts.dart'; const Color appSeedColor = Color(0xFF2B3170); // CBF 2026: poster navy blue +/// The SIL Open Font License texts shipped alongside the bundled typefaces in +/// `assets/fonts/`, keyed by the licence entry name shown to users. +@visibleForTesting +const Map fontLicenseAssets = { + 'Nunito Sans': 'assets/fonts/OFL-NunitoSans.txt', + 'Playfair Display': 'assets/fonts/OFL-PlayfairDisplay.txt', +}; + +/// Reads each licence in [fontLicenseAssets] out of the asset bundle. +/// +/// Split out from [registerFontLicenses] so tests can consume the stream +/// directly — draining the global `LicenseRegistry.licenses` never completes +/// under `flutter_test`. +@visibleForTesting +Stream loadFontLicenses() async* { + for (final MapEntry entry in fontLicenseAssets.entries) { + final String license = await rootBundle.loadString(entry.value); + yield LicenseEntryWithLineBreaks([entry.key], license); + } +} + +/// Registers the OFL licences for the fonts bundled in `assets/fonts/` so they +/// appear in the app's "View licences" page. +/// +/// Bundling the font binaries (rather than letting `google_fonts` fetch them +/// from fonts.gstatic.com) means the app redistributes them, and the SIL Open +/// Font License requires the licence to travel with the files. Call this from +/// `main()` before `runApp`. +void registerFontLicenses() { + LicenseRegistry.addLicense(loadFontLicenses); +} + TextTheme buildAppTextTheme(ColorScheme colorScheme) { final base = GoogleFonts.nunitoSansTextTheme(); return base.copyWith( diff --git a/lib/main.dart b/lib/main.dart index 9c0a2b7d..f9535111 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,6 +26,10 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); + // The bundled typefaces in assets/fonts/ are redistributed under the SIL + // Open Font License, which requires the licence to ship with them. + registerFontLicenses(); + try { await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, diff --git a/pubspec.yaml b/pubspec.yaml index 5925e5ab..18a28459 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,3 +51,11 @@ 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. This entry covers the whole directory, so + # adding a weight means dropping the .ttf into assets/fonts/ — no edit here. + # The directory also holds the OFL licence texts. See docs/code/fonts.md. + - assets/fonts/ diff --git a/test/app_theme_test.dart b/test/app_theme_test.dart index d9d86637..d74516fc 100644 --- a/test/app_theme_test.dart +++ b/test/app_theme_test.dart @@ -1,6 +1,8 @@ import 'dart:math' as math; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:cambridge_beer_festival/app_theme.dart'; @@ -155,4 +157,84 @@ void main() { expect(appSeedColor, equals(const Color(0xFF2B3170))); }); }); + + group('registerFontLicenses', () { + // The font binaries in assets/fonts/ are redistributed under the SIL Open + // Font License, so the licence text has to ship with them. These tests load + // the real assets — a typo in a path would make them fail rather than + // silently register an empty licence. + // + // Asset reads go through `tester.runAsync`: inside testWidgets' FakeAsync + // zone a `rootBundle` load never completes, and the test hangs until the + // 10-minute timeout instead of failing. + testWidgets('every declared licence asset exists and is the OFL', ( + WidgetTester tester, + ) async { + expect(fontLicenseAssets, isNotEmpty); + + for (final MapEntry entry in fontLicenseAssets.entries) { + final String? text = await tester.runAsync( + () => rootBundle.loadString(entry.value), + ); + expect( + text, + contains('SIL Open Font License'), + reason: '${entry.key} licence asset should be the OFL', + ); + } + }); + + test('covers every bundled font family', () { + // If a third family is ever bundled, its licence must be registered too. + expect( + fontLicenseAssets.keys, + containsAll(['Nunito Sans', 'Playfair Display']), + ); + }); + + testWidgets('feeds the entries into the LicenseRegistry', ( + WidgetTester tester, + ) async { + // Drop Flutter's own collectors so the registry yields only ours. + LicenseRegistry.reset(); + addTearDown(LicenseRegistry.reset); + + registerFontLicenses(); + + // runAsync escapes testWidgets' fake-async zone. The collector awaits a + // real asset read, which never completes on the fake clock — draining it + // directly hangs until the 10-minute test timeout. + final List? entries = await tester.runAsync( + () => LicenseRegistry.licenses.toList(), + ); + + expect( + entries!.expand((LicenseEntry entry) => entry.packages), + containsAll(fontLicenseAssets.keys), + ); + }); + + testWidgets('yields one licence entry per family, carrying its text', ( + WidgetTester tester, + ) async { + // Drains the collector directly, bypassing the global registry. + final List? entries = await tester.runAsync( + () => loadFontLicenses().toList(), + ); + + expect(entries, isNotNull); + expect(entries!, hasLength(fontLicenseAssets.length)); + expect( + entries.expand((LicenseEntry entry) => entry.packages), + containsAll(fontLicenseAssets.keys), + ); + for (final LicenseEntry entry in entries) { + expect( + entry.paragraphs.first.text, + contains('Copyright'), + reason: 'licence entry for ${entry.packages} should carry its text', + ); + } + }); + }); } diff --git a/test/brewery_screen_screenshot_test.dart b/test/brewery_screen_screenshot_test.dart index 99b31828..701f0326 100644 --- a/test/brewery_screen_screenshot_test.dart +++ b/test/brewery_screen_screenshot_test.dart @@ -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'; @@ -110,13 +111,10 @@ void main() { return ChangeNotifierProvider.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', diff --git a/test/drink_detail_screen_screenshot_test.dart b/test/drink_detail_screen_screenshot_test.dart index 82d0ebd2..8594b553 100644 --- a/test/drink_detail_screen_screenshot_test.dart +++ b/test/drink_detail_screen_screenshot_test.dart @@ -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'; @@ -70,13 +71,10 @@ void main() { return ChangeNotifierProvider.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), ), ); diff --git a/test/flutter_test_config.dart b/test/flutter_test_config.dart new file mode 100644 index 00000000..33c39466 --- /dev/null +++ b/test/flutter_test_config.dart @@ -0,0 +1,26 @@ +import 'dart:async'; + +import 'package:google_fonts/google_fonts.dart'; + +/// Wraps the `main()` of every test file under `test/`, discovered +/// automatically by `flutter test` and run once per file. +/// +/// 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 testExecutable(FutureOr Function() testMain) async { + GoogleFonts.config.allowRuntimeFetching = false; + await testMain(); +} diff --git a/test/goldens/brewery_screen_dark.png b/test/goldens/brewery_screen_dark.png index 6e426175..a50636db 100644 Binary files a/test/goldens/brewery_screen_dark.png and b/test/goldens/brewery_screen_dark.png differ diff --git a/test/goldens/brewery_screen_light.png b/test/goldens/brewery_screen_light.png index 473f7c29..5aad2837 100644 Binary files a/test/goldens/brewery_screen_light.png and b/test/goldens/brewery_screen_light.png differ diff --git a/test/goldens/drink_detail_screen_long_name_light.png b/test/goldens/drink_detail_screen_long_name_light.png index 1be363d7..8989f5c1 100644 Binary files a/test/goldens/drink_detail_screen_long_name_light.png and b/test/goldens/drink_detail_screen_long_name_light.png differ diff --git a/test/goldens/drink_detail_screen_medium_name_dark.png b/test/goldens/drink_detail_screen_medium_name_dark.png index 401cc4d9..29a6db8e 100644 Binary files a/test/goldens/drink_detail_screen_medium_name_dark.png and b/test/goldens/drink_detail_screen_medium_name_dark.png differ diff --git a/test/goldens/drink_detail_screen_medium_name_light.png b/test/goldens/drink_detail_screen_medium_name_light.png index 86fb87f4..48f6fcdb 100644 Binary files a/test/goldens/drink_detail_screen_medium_name_light.png and b/test/goldens/drink_detail_screen_medium_name_light.png differ diff --git a/test/goldens/drink_detail_screen_with_similar_light.png b/test/goldens/drink_detail_screen_with_similar_light.png index 11336285..36ed936b 100644 Binary files a/test/goldens/drink_detail_screen_with_similar_light.png and b/test/goldens/drink_detail_screen_with_similar_light.png differ diff --git a/test/goldens/style_screen_with_description_dark.png b/test/goldens/style_screen_with_description_dark.png index 12a961c3..22eb246a 100644 Binary files a/test/goldens/style_screen_with_description_dark.png and b/test/goldens/style_screen_with_description_dark.png differ diff --git a/test/goldens/style_screen_with_description_light.png b/test/goldens/style_screen_with_description_light.png index 1c8ac6db..1b17cbfc 100644 Binary files a/test/goldens/style_screen_with_description_light.png and b/test/goldens/style_screen_with_description_light.png differ diff --git a/test/screens/goldens/my_festival_screen_dark.png b/test/screens/goldens/my_festival_screen_dark.png index 69fb383f..98dd2a67 100644 Binary files a/test/screens/goldens/my_festival_screen_dark.png and b/test/screens/goldens/my_festival_screen_dark.png differ diff --git a/test/screens/goldens/my_festival_screen_light.png b/test/screens/goldens/my_festival_screen_light.png index 0cfcc21b..11a618d1 100644 Binary files a/test/screens/goldens/my_festival_screen_light.png and b/test/screens/goldens/my_festival_screen_light.png differ diff --git a/test/screens/my_festival_screen_test.dart b/test/screens/my_festival_screen_test.dart index 5cd5965e..b46dedd6 100644 --- a/test/screens/my_festival_screen_test.dart +++ b/test/screens/my_festival_screen_test.dart @@ -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; @@ -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(); @@ -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(); diff --git a/test/style_screen_screenshot_test.dart b/test/style_screen_screenshot_test.dart index 9990fa58..e28ac8ad 100644 --- a/test/style_screen_screenshot_test.dart +++ b/test/style_screen_screenshot_test.dart @@ -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'; @@ -111,13 +112,10 @@ void main() { return ChangeNotifierProvider.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), ), ); @@ -162,13 +160,7 @@ void main() { ChangeNotifierProvider.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'), ), ), diff --git a/test/widgets/drink_card_test.dart b/test/widgets/drink_card_test.dart index cdc5b5d3..9b651c7f 100644 --- a/test/widgets/drink_card_test.dart +++ b/test/widgets/drink_card_test.dart @@ -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'; @@ -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, diff --git a/test/widgets/goldens/drink_card_search_excerpt_dark.png b/test/widgets/goldens/drink_card_search_excerpt_dark.png index efbc11bd..4c9e340b 100644 Binary files a/test/widgets/goldens/drink_card_search_excerpt_dark.png and b/test/widgets/goldens/drink_card_search_excerpt_dark.png differ diff --git a/test/widgets/goldens/drink_card_search_excerpt_light.png b/test/widgets/goldens/drink_card_search_excerpt_light.png index 3ccfe681..d4545855 100644 Binary files a/test/widgets/goldens/drink_card_search_excerpt_light.png and b/test/widgets/goldens/drink_card_search_excerpt_light.png differ diff --git a/test/widgets/goldens/drink_card_tasted_multiple_dark.png b/test/widgets/goldens/drink_card_tasted_multiple_dark.png index 6eb31d16..314bce3a 100644 Binary files a/test/widgets/goldens/drink_card_tasted_multiple_dark.png and b/test/widgets/goldens/drink_card_tasted_multiple_dark.png differ diff --git a/test/widgets/goldens/drink_card_tasted_multiple_light.png b/test/widgets/goldens/drink_card_tasted_multiple_light.png index c2766779..44b5d6be 100644 Binary files a/test/widgets/goldens/drink_card_tasted_multiple_light.png and b/test/widgets/goldens/drink_card_tasted_multiple_light.png differ diff --git a/test/widgets/goldens/drink_card_want_to_try_light.png b/test/widgets/goldens/drink_card_want_to_try_light.png index 732aa99f..e5b3e395 100644 Binary files a/test/widgets/goldens/drink_card_want_to_try_light.png and b/test/widgets/goldens/drink_card_want_to_try_light.png differ diff --git a/web/_headers b/web/_headers index bb10526a..b1ba1526 100644 --- a/web/_headers +++ b/web/_headers @@ -8,7 +8,10 @@ # - script-src keeps Flutter bootstrap working via inline serviceWorkerVersion script in web/index.html # - script-src blob: required for Flutter 3.44 skwasm threading: a blob URL worker does dynamic import() # - script-src/connect-src www.gstatic.com: Flutter loads canvaskit.js/canvaskit.wasm from Google CDN - # - connect-src/font-src allow Google Fonts (google_fonts pkg fetches via XHR at runtime) + # - connect-src/font-src allow Google Fonts. The typefaces are bundled in + # assets/fonts/ and google_fonts prefers assets over the network, so these + # origins are now only a fallback for a weight that isn't bundled. They can + # be dropped once we're confident no fetch ever happens. See docs/code/fonts.md. # - connect-src allows app API plus Firebase Analytics/Crashlytics endpoints Content-Security-Policy: default-src 'self'; base-uri 'self'; frame-ancestors 'none'; object-src 'none'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' blob: https://www.gstatic.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://data.cambeerfestival.app https://www.google-analytics.com https://region1.google-analytics.com https://firebaseinstallations.googleapis.com https://firebaselogging-pa.googleapis.com https://fonts.googleapis.com https://fonts.gstatic.com https://www.gstatic.com; worker-src 'self' blob:; X-Content-Type-Options: nosniff