diff --git a/lib/web_ui/dev/goldens_lock.yaml b/lib/web_ui/dev/goldens_lock.yaml index 1b80d23509540..8bb0d377fa78e 100644 --- a/lib/web_ui/dev/goldens_lock.yaml +++ b/lib/web_ui/dev/goldens_lock.yaml @@ -1,2 +1,2 @@ repository: https://github.com/flutter/goldens.git -revision: b85f9093e6bc6d4e7cbb7f97491667c143c4a360 +revision: 44f00682eee2afd7042c02ce802199c1c4ff223e diff --git a/lib/web_ui/dev/test_runner.dart b/lib/web_ui/dev/test_runner.dart index 842a5318b26b6..a914de394ed97 100644 --- a/lib/web_ui/dev/test_runner.dart +++ b/lib/web_ui/dev/test_runner.dart @@ -88,6 +88,14 @@ class TestCommand extends Command with ArgUtils { '.dart_tool/goldens. Use this option to bulk-update all screenshots, ' 'for example, when a new browser version affects pixels.', ) + ..addFlag( + 'fetch-goldens-repo', + defaultsTo: true, + negatable: true, + help: 'Whether to fetch the goldens repo. Set this to false to iterate ' + 'on golden tests without fearing that the fetcher will overwrite ' + 'your local changes.', + ) ..addOption( 'browser', defaultsTo: 'chrome', @@ -165,39 +173,41 @@ class TestCommand extends Command with ArgUtils { final FilePath dir = FilePath.fromWebUi(''); print(''); print('Initial test run is done!'); - print('Watching ${dir.relativeToCwd}/lib and ${dir.relativeToCwd}/test to re-run tests'); + print( + 'Watching ${dir.relativeToCwd}/lib and ${dir.relativeToCwd}/test to re-run tests'); print(''); PipelineWatcher( - dir: dir.absolute, - pipeline: testPipeline, - ignore: (event) { - // Ignore font files that are copied whenever tests run. - if (event.path.endsWith('.ttf')) { - return true; - } + dir: dir.absolute, + pipeline: testPipeline, + ignore: (event) { + // Ignore font files that are copied whenever tests run. + if (event.path.endsWith('.ttf')) { + return true; + } - // Ignore auto-generated JS files. - // The reason we are using `.contains()` instead of `.endsWith()` is - // because the auto-generated files could end with any of the - // following: - // - // - browser_test.dart.js - // - browser_test.dart.js.map - // - browser_test.dart.js.deps - if (event.path.contains('browser_test.dart.js')) { - return true; - } + // Ignore auto-generated JS files. + // The reason we are using `.contains()` instead of `.endsWith()` is + // because the auto-generated files could end with any of the + // following: + // + // - browser_test.dart.js + // - browser_test.dart.js.map + // - browser_test.dart.js.deps + if (event.path.contains('browser_test.dart.js')) { + return true; + } - // React to changes in lib/ and test/ folders. - final String relativePath = path.relative(event.path, from: dir.absolute); - if (relativePath.startsWith('lib/') || relativePath.startsWith('test/')) { - return false; - } + // React to changes in lib/ and test/ folders. + final String relativePath = + path.relative(event.path, from: dir.absolute); + if (relativePath.startsWith('lib/') || + relativePath.startsWith('test/')) { + return false; + } - // Ignore anything else. - return true; - } - ).start(); + // Ignore anything else. + return true; + }).start(); // Return a never-ending future. return Completer().future; } else { @@ -217,7 +227,8 @@ class TestCommand extends Command with ArgUtils { bool unitTestResult = await runUnitTests(); bool integrationTestResult = await runIntegrationTests(); if (integrationTestResult != unitTestResult) { - print('Tests run. Integration tests passed: $integrationTestResult ' + print( + 'Tests run. Integration tests passed: $integrationTestResult ' 'unit tests passed: $unitTestResult'); } return integrationTestResult && unitTestResult; @@ -225,7 +236,8 @@ class TestCommand extends Command with ArgUtils { return await runUnitTests(); } } - throw UnimplementedError('Unknown test type requested: $testTypesRequested'); + throw UnimplementedError( + 'Unknown test type requested: $testTypesRequested'); } on TestFailureException { return true; } @@ -774,6 +786,7 @@ const List _kTestFonts = [ 'ahem.ttf', 'Roboto-Regular.ttf', 'NotoNaskhArabic-Regular.ttf', + 'NotoColorEmoji.ttf', ]; void _copyTestFontsIntoWebUi() { diff --git a/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart b/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart index 5c6ad00aab42b..0aa256fccaad1 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart @@ -211,15 +211,19 @@ Future _registerSymbolsAndEmoji() async { String? symbolsFontUrl = extractUrlFromCss(symbolsCss); String? emojiFontUrl = extractUrlFromCss(emojiCss); - if (symbolsFontUrl == null || emojiFontUrl == null) { - html.window.console - .warn('Error parsing CSS for Noto Emoji and Symbols font.'); + if (symbolsFontUrl != null) { + notoDownloadQueue.add(_ResolvedNotoSubset( + symbolsFontUrl, 'Noto Sans Symbols', const [])); + } else { + html.window.console.warn('Error parsing CSS for Noto Symbols font.'); } - notoDownloadQueue.add(_ResolvedNotoSubset( - symbolsFontUrl!, 'Noto Sans Symbols', const [])); - notoDownloadQueue.add(_ResolvedNotoSubset( - emojiFontUrl!, 'Noto Color Emoji Compat', const [])); + if (emojiFontUrl != null) { + notoDownloadQueue.add(_ResolvedNotoSubset( + emojiFontUrl, 'Noto Color Emoji Compat', const [])); + } else { + html.window.console.warn('Error parsing CSS for Noto Emoji font.'); + } } /// Finds the minimum set of fonts which covers all of the [codeunits]. diff --git a/lib/web_ui/lib/src/engine/canvaskit/text.dart b/lib/web_ui/lib/src/engine/canvaskit/text.dart index 3b7735e284086..fe6f91331fcb4 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/text.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/text.dart @@ -668,14 +668,14 @@ class CkParagraphBuilder implements ui.ParagraphBuilder { typefaces.addAll(typefacesForFamily); } } - List codeUnitsSupported = List.filled(text.length, false); + List codeUnits = text.runes.toList(); + List codeUnitsSupported = List.filled(codeUnits.length, false); for (SkTypeface typeface in typefaces) { SkFont font = SkFont(typeface); Uint8List glyphs = font.getGlyphIDs(text); assert(glyphs.length == codeUnitsSupported.length); for (int i = 0; i < glyphs.length; i++) { - codeUnitsSupported[i] |= - glyphs[i] != 0 || _isControlCode(text.codeUnitAt(i)); + codeUnitsSupported[i] |= glyphs[i] != 0 || _isControlCode(codeUnits[i]); } } @@ -683,7 +683,7 @@ class CkParagraphBuilder implements ui.ParagraphBuilder { List missingCodeUnits = []; for (int i = 0; i < codeUnitsSupported.length; i++) { if (!codeUnitsSupported[i]) { - missingCodeUnits.add(text.codeUnitAt(i)); + missingCodeUnits.add(codeUnits[i]); } } _findFontsForMissingCodeunits(missingCodeUnits); diff --git a/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart b/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart index 955ea4c54e48a..cc2fd0d1cff55 100644 --- a/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart +++ b/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart @@ -19,7 +19,7 @@ void main() { internalBootstrapBrowserTest(() => testMain); } -const ui.Rect kDefaultRegion = const ui.Rect.fromLTRB(0, 0, 500, 250); +const ui.Rect kDefaultRegion = const ui.Rect.fromLTRB(0, 0, 100, 50); Future matchPictureGolden(String goldenFile, CkPicture picture, {ui.Rect region = kDefaultRegion, bool write = false}) async { @@ -105,15 +105,15 @@ void testMain() { final CkCanvas canvas = recorder.beginRecording(kDefaultRegion); pb = CkParagraphBuilder( - CkParagraphStyle( - fontSize: 32, - ), + CkParagraphStyle(), ); + pb.pushStyle(ui.TextStyle(fontSize: 32)); pb.addText('مرحبا'); + pb.pop(); final CkParagraph paragraph = pb.build(); paragraph.layout(ui.ParagraphConstraints(width: 1000)); - canvas.drawParagraph(paragraph, ui.Offset(200, 120)); + canvas.drawParagraph(paragraph, ui.Offset(0, 0)); await matchPictureGolden( 'canvaskit_font_fallback_arabic.png', recorder.endRecording()); @@ -121,6 +121,70 @@ void testMain() { // TODO: https://github.com/flutter/flutter/issues/71520 }, skip: isIosSafari || isFirefox); + test('will download Noto Emojis and Noto Symbols if no matching Noto Font', + () async { + final Completer fontChangeCompleter = Completer(); + // Intercept the system font change message. + ui.window.onPlatformMessage = (String name, ByteData? data, + ui.PlatformMessageResponseCallback? callback) { + if (name == 'flutter/system') { + const JSONMessageCodec codec = JSONMessageCodec(); + final dynamic message = codec.decodeMessage(data); + if (message is Map) { + if (message['type'] == 'fontsChange') { + fontChangeCompleter.complete(); + } + } + } + if (savedCallback != null) { + savedCallback!(name, data, callback); + } + }; + + TestDownloader.mockDownloads[ + 'https://fonts.googleapis.com/css2?family=Noto+Color+Emoji+Compat'] = + ''' +/* arabic */ +@font-face { + font-family: 'Noto Color Emoji'; + src: url(packages/ui/assets/NotoColorEmoji.ttf) format('ttf'); +} +'''; + + expect(skiaFontCollection.globalFontFallbacks, ['Roboto']); + + // Creating this paragraph should cause us to start to download the + // fallback font. + CkParagraphBuilder pb = CkParagraphBuilder( + CkParagraphStyle(), + ); + pb.addText('Hello 😊'); + + await fontChangeCompleter.future; + + expect(skiaFontCollection.globalFontFallbacks, + contains('Noto Color Emoji Compat 0')); + + final CkPictureRecorder recorder = CkPictureRecorder(); + final CkCanvas canvas = recorder.beginRecording(kDefaultRegion); + + pb = CkParagraphBuilder( + CkParagraphStyle(), + ); + pb.pushStyle(ui.TextStyle(fontSize: 26)); + pb.addText('Hello 😊'); + pb.pop(); + final CkParagraph paragraph = pb.build(); + paragraph.layout(ui.ParagraphConstraints(width: 1000)); + + canvas.drawParagraph(paragraph, ui.Offset(0, 0)); + + await matchPictureGolden( + 'canvaskit_font_fallback_emoji.png', recorder.endRecording()); + // TODO: https://github.com/flutter/flutter/issues/60040 + // TODO: https://github.com/flutter/flutter/issues/71520 + }, skip: isIosSafari || isFirefox); + test('will gracefully fail if we cannot parse the Google Fonts CSS', () async { TestDownloader.mockDownloads[