Skip to content
Open
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
2 changes: 2 additions & 0 deletions .github/workflows/test-flutter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ jobs:
name: multi_packages_app_app
- dir: example/multi_packages_app/theme
name: multi_packages_app_theme
- dir: .
name: adaptive_test
defaults:
run:
working-directory: ${{ matrix.dir }}
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,23 @@ Future<void> testExecutable(FutureOr<void> Function() testMain) async {

> ℹ️ `loadFonts()` loads fonts from `pubspec.yaml` and from every separate package dependency as well.

A font bundled by a dependency is registered under both its manifest name
(`packages/my_theme/Roboto`) and its bare family name (`Roboto`), since that is
the name a `TextStyle` or a `ThemeData` asks for.

`loadFonts()` also registers the families the framework itself falls back to on
each platform — `Roboto` on Android, `CupertinoSystemText` on iOS, `Segoe UI` on
Windows, ... They are declared in no `pubspec.yaml`: they either ship with the
Flutter SDK or belong to the host OS. Without them, any widget that does not
specify a font — a `DatePickerDialog`, a `CupertinoButton` — renders as
placeholder blocks in your goldens.

Opt out to register nothing but what the manifest declares:

```dart
AdaptiveTestConfiguration.instance.setLoadPlatformFallbackFonts(false);
```

### Setting Up Test Devices

1. Define a set of device variants:
Expand Down
68 changes: 68 additions & 0 deletions example/multi_packages_app/app/test/src/fonts_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

/// Whether text laid out with [fontFamily] is rendered with a real font.
///
/// The placeholder font `flutter_test` uses for unknown families gives every
/// glyph the same advance, so a narrow and a wide string of the same length
/// measure the same. Any real proportional font measures them differently.
bool _rendersWithARealFont(String fontFamily) {
double widthOf(String text) {
final painter = TextPainter(
text: TextSpan(
text: text,
style: TextStyle(fontFamily: fontFamily, fontSize: 40),
),
textDirection: TextDirection.ltr,
)..layout();
final width = painter.width;
painter.dispose();

return width;
}

return widthOf('iiii') != widthOf('WWWW');
}

void main() {
group('loadFonts in an app whose fonts live in a dependency', () {
test('renders a font bundled by a dependency under its bare name', () {
// ThemeSans is bundled by the theme package this app depends on, so the
// manifest only exposes it as
// `packages/multi_packages_example_theme/ThemeSans` — but a widget or a
// theme defaulting to that family asks for the bare name.
expect(_rendersWithARealFont('ThemeSans'), isTrue);
});

test('keeps the family under its manifest name as well', () {
expect(
_rendersWithARealFont(
'packages/multi_packages_example_theme/ThemeSans',
),
isTrue,
);
});

test('renders the Material default typeface of the host platform', () {
final defaultFamily = Typography.material2021(
platform: TargetPlatform.iOS,
).black.bodyMedium?.fontFamily;

expect(defaultFamily, isNotNull);
// ignore: avoid-non-null-assertion, asserted right above
expect(_rendersWithARealFont(defaultFamily!), isTrue);
});

test('renders text with a family no font could ever provide', () {
// Apple's system typeface: it ships with iOS and cannot be bundled, so it
// is stood in for by the typeface the SDK ships.
const cupertinoTextTheme = CupertinoTextThemeData();
final cupertinoFamily = cupertinoTextTheme.textStyle.fontFamily;

expect(cupertinoFamily, isNotNull);
// ignore: avoid-non-null-assertion, asserted right above
expect(_rendersWithARealFont(cupertinoFamily!), isTrue);
});
});
}
7 changes: 6 additions & 1 deletion example/multi_packages_app/theme/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,9 @@ flutter:
- asset: fonts/Roboto-MediumItalic.ttf
- asset: fonts/Roboto-Regular.ttf
- asset: fonts/Roboto-Thin.ttf
- asset: fonts/Roboto-ThinItalic.ttf
- asset: fonts/Roboto-ThinItalic.ttf
# Declared from an existing asset, to cover a font family bundled by a
# dependency that does not collide with a platform default one.
- family: ThemeSans
fonts:
- asset: fonts/Roboto-Regular.ttf
13 changes: 7 additions & 6 deletions lib/adaptive_test.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
export 'src/adaptive/adaptive_test.dart';
export 'src/adaptive/devices_data.dart';
export 'src/adaptive/widgets/adaptive_wrapper.dart';
export 'src/adaptive/window_configuration_tester.dart';
export 'src/adaptive/window_config.dart';
export 'src/adaptive/window_config_data/dynamic_island_data.dart';
export 'src/adaptive/window_config_data/punch_hole_data.dart';
export 'src/adaptive/window_config_data/system_nav_bar_data.dart'
show SystemNavBarData;
export 'src/adaptive/window_config_data/window_config_data.dart';
export 'src/adaptive/window_configuration_tester.dart';
export 'src/configuration.dart';
export 'src/helpers/await_images.dart';
export 'src/helpers/font_registration.dart';
export 'src/helpers/fonts_loader.dart';
export 'src/helpers/goldens_difference.dart';
export 'src/helpers/skip_test_extension.dart';
export 'src/adaptive/window_config_data/window_config_data.dart';
export 'src/adaptive/window_config_data/system_nav_bar_data.dart'
show SystemNavBarData;
export 'src/adaptive/window_config_data/punch_hole_data.dart';
export 'src/adaptive/window_config_data/dynamic_island_data.dart';
2 changes: 2 additions & 0 deletions lib/src/adaptive/devices_data.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// ignore_for_file: constant_identifier_names

import 'package:adaptive_test/src/adaptive/window_config_data/dynamic_island_data.dart';
import 'package:adaptive_test/src/adaptive/window_config_data/punch_hole_data.dart';
import 'package:adaptive_test/src/adaptive/window_config_data/system_nav_bar_data.dart';
Expand Down
2 changes: 1 addition & 1 deletion lib/src/adaptive/widgets/adaptive_wrapper.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import 'package:adaptive_test/src/adaptive/widgets/layers/hardware_layer.dart';
import 'package:adaptive_test/src/adaptive/widgets/layers/keyboard_layer.dart';
import 'package:adaptive_test/src/adaptive/widgets/layers/system_nav_bar_layer.dart';
import 'package:adaptive_test/src/adaptive/window_config_data/window_config_data.dart';
import 'package:adaptive_test/src/adaptive/window_config.dart';
import 'package:adaptive_test/src/adaptive/window_config_data/window_config_data.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

Expand Down
2 changes: 1 addition & 1 deletion lib/src/adaptive/widgets/layers/keyboard_layer.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:adaptive_test/src/adaptive/window_configuration_tester.dart';
import 'package:adaptive_test/src/adaptive/window_config.dart';
import 'package:adaptive_test/src/adaptive/window_configuration_tester.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class ThreeButtonSystemNavBarLayer extends StatelessWidget {
children: const [
Icons.arrow_back_ios_rounded,
Icons.circle,
Icons.square_rounded
Icons.square_rounded,
]
.map(
(iconData) => Icon(
Expand Down
3 changes: 2 additions & 1 deletion lib/src/adaptive/window_config_data/window_config_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ class WindowConfigData extends Equatable {
/// This is null when the device has no notch.
final Size? notchSize;

/// Describe the size of the device physical screen top dynamic island in `dp`.
/// Describe the size of the device physical screen top dynamic island in
/// `dp`.
///
/// This is null when the device has no dynamic island.
final DynamicIslandData? dynamicIsland;
Expand Down
6 changes: 4 additions & 2 deletions lib/src/adaptive/window_configuration_tester.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ extension WidgetTesterWithConfigurableWindow on WidgetTester {
addTearDown(view.resetViewInsets);
}

/// Configure the tester window to represent an opened keyboard on the given device variant.
/// Configure the tester window to represent an opened keyboard on the given
/// device variant.
void configureOpenedKeyboardWindow(WindowConfigData windowConfig) {
view.viewInsets = windowConfig.viewInsets;
view.padding = windowConfig.padding.copyWith(bottom: 0);
}

/// Configure the tester window to represent a closed keyboard on the given device variant.
/// Configure the tester window to represent a closed keyboard on the given
/// device variant.
void configureClosedKeyboardWindow(WindowConfigData windowConfig) {
view.resetViewInsets();
view.padding = windowConfig.padding;
Expand Down
20 changes: 19 additions & 1 deletion lib/src/configuration.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// ignore_for_file: use_setters_to_change_properties

import 'package:adaptive_test/src/adaptive/window_config.dart';
import 'package:adaptive_test/src/adaptive/window_config_data/window_config_data.dart';
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -46,6 +48,21 @@ class AdaptiveTestConfiguration {
_failTestOnWrongPlatform = failTestOnWrongPlatform;
}

bool _loadPlatformFallbackFonts = true;

bool get loadPlatformFallbackFonts => _loadPlatformFallbackFonts;

/// Whether [loadFonts] also registers the font families the framework falls
/// back to on each platform, e.g. `Roboto` or `CupertinoSystemText`. They are
/// not declared in any `pubspec.yaml`, so without this the text using them
/// renders as placeholder blocks in goldens.
///
/// Defaults to true. Set it to false to keep the previous behavior, for
/// instance if you would rather register those families yourself.
void setLoadPlatformFallbackFonts(bool loadPlatformFallbackFonts) {
_loadPlatformFallbackFonts = loadPlatformFallbackFonts;
}

WindowVariant? _deviceVariant;

WindowVariant get deviceVariant {
Expand All @@ -65,7 +82,8 @@ See: https://api.flutter.dev/flutter/flutter_test/flutter_test-library.html

/// Set the devices variant on which you want your test to run.
///
/// Eg [iPhone8], [iPhone13], [iPhone16],[iPhone16Dark], [iPadPro], [desktop], [pixel5], [pixel9].
/// Eg [iPhone8], [iPhone13], [iPhone16],[iPhone16Dark], [iPadPro],
/// [desktop], [pixel5], [pixel9].
void setDeviceVariants(Set<WindowConfigData> deviceConfigs) {
_deviceVariant = WindowVariant(deviceConfigs);
}
Expand Down
47 changes: 47 additions & 0 deletions lib/src/helpers/font_loading_policy.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import 'package:adaptive_test/src/helpers/platform_fonts.dart';
import 'package:meta/meta.dart';

/// Which font families [loadFonts] provides beyond the ones the font manifest
/// declares, and under which names.
///
/// [platformFamilies] are the families the framework falls back to when a
/// widget does not specify a font, [sdkFamilies] the ones the Flutter SDK ships
/// font files for.
@immutable
@internal
class FontLoadingPolicy {
const FontLoadingPolicy({
required this.platformFamilies,
required this.sdkFamilies,
});

/// Provides the platform default families, gathered from the SDK.
factory FontLoadingPolicy.platformAware() => FontLoadingPolicy(
platformFamilies: platformDefaultFontFamilies(),
sdkFamilies: sdkFontFamilies(),
);

/// Registers nothing but what the font manifest declares.
const FontLoadingPolicy.manifestOnly()
: platformFamilies = const {},
sdkFamilies = const {};

final Set<String> platformFamilies;
final Set<String> sdkFamilies;

/// Whether a font bundled by a dependency, exposed by the manifest as
/// `packages/my_theme/Roboto`, is also registered under its bare [family]
/// name — the name a `TextStyle` or a `ThemeData` asks for.
///
/// It is, unless it would shadow a platform default the SDK ships a font for.
/// A dependency usually bundles a single weight of such a family while the
/// SDK ships all of them, and on a device that bundled font would not shadow
/// the platform one either: it is only reachable through its `packages/`
/// name.
bool registersBareFamilyName(String family) =>
!platformFamilies.contains(family) || !sdkFamilies.contains(family);

/// The families that must be provided on top of the manifest, so that text
/// laid out with a platform default does not render as placeholder blocks.
Set<String> get familiesToProvide => platformFamilies;
}
14 changes: 14 additions & 0 deletions lib/src/helpers/font_registration.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:flutter/services.dart';

/// Registers [assets] under [family] and returns that family name, so that
/// callers can collect what they loaded without going through shared state.
Future<String> registerFontFamily(
String family,
Iterable<Future<ByteData>> assets,
) async {
final loader = FontLoader(family);
assets.forEach(loader.addFont);
await loader.load();

return family;
}
Loading
Loading