From e5d5c01850f2d33106f00a19017682f09e83e48b Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Tue, 4 Nov 2025 13:07:04 -0500 Subject: [PATCH] [web] Upgrade Chrome to 141 (for engine tests) (#177743) - Update Chrome to 141 for web engine tests. - Improve image codec tests so they exercise all frames. - Skip the frames of certain images that are known to cause problems in Chrome. Chrome bug for the problematic images: https://issues.chromium.org/456445108 Fixes https://github.com/flutter/flutter/issues/168686 --- .../ci/builders/linux_web_engine_test.json | 2 +- .../flutter/lib/web_ui/dev/package_lock.yaml | 6 +- .../web_ui/lib/src/engine/image_decoder.dart | 11 +- .../lib/src/engine/safe_browser_api.dart | 2 +- .../lib/web_ui/test/ui/codecs_test.dart | 122 ++++++++++-------- 5 files changed, 86 insertions(+), 57 deletions(-) diff --git a/engine/src/flutter/ci/builders/linux_web_engine_test.json b/engine/src/flutter/ci/builders/linux_web_engine_test.json index 62655908564..cc9bc812706 100644 --- a/engine/src/flutter/ci/builders/linux_web_engine_test.json +++ b/engine/src/flutter/ci/builders/linux_web_engine_test.json @@ -179,7 +179,7 @@ }, { "dependency": "chrome_and_driver", - "version": "125.0.6422.141" + "version": "141.0.7390.76" } ], "tasks": [ diff --git a/engine/src/flutter/lib/web_ui/dev/package_lock.yaml b/engine/src/flutter/lib/web_ui/dev/package_lock.yaml index 84a5584e471..648f4004ffa 100644 --- a/engine/src/flutter/lib/web_ui/dev/package_lock.yaml +++ b/engine/src/flutter/lib/web_ui/dev/package_lock.yaml @@ -1,9 +1,13 @@ # Please refer to the "Upgrade Browser Version" section in the README.md for # more details on how to update browser version numbers. chrome: - version: '133.0.6943.53' + # Latest version can be found here: + # https://googlechromelabs.github.io/chrome-for-testing/ + version: '141.0.7390.76' firefox: + # Latest version can be found here: + # https://www.firefox.com/en-US/releases/ version: '143.0' edge: diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/image_decoder.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/image_decoder.dart index f67065b0092..5ad987cf1be 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/image_decoder.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/image_decoder.dart @@ -134,7 +134,16 @@ abstract class BrowserImageDecoder implements ui.Codec { } final DecodeResult result = await webDecoder - .decode(DecodeOptions(frameIndex: _nextFrameIndex)) + // Using `completeFramesOnly: false` to get frames even from partially decoded images. + // Typically, this wouldn't work well in Flutter because Flutter doesn't support progressive + // image rendering. So this could result in frames being rendered at lower quality than + // expected. + // + // However, since we wait for the entire image to be decoded using [webDecoder.completed], + // this ends up being a non-issue in practice. + // + // For more details, see: https://issues.chromium.org/issues/456445108 + .decode(DecodeOptions(frameIndex: _nextFrameIndex, completeFramesOnly: false)) .toDart; final VideoFrame frame = result.image; _nextFrameIndex = (_nextFrameIndex + 1) % frameCount; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/safe_browser_api.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/safe_browser_api.dart index 5e049472eb3..0a1548c68a0 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/safe_browser_api.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/safe_browser_api.dart @@ -152,7 +152,7 @@ extension type DecodeResult(JSObject _) implements JSObject { /// /// * https://www.w3.org/TR/webcodecs/#dictdef-imagedecodeoptions extension type DecodeOptions._(JSObject _) implements JSObject { - external DecodeOptions({required int frameIndex}); + external DecodeOptions({required int frameIndex, required bool completeFramesOnly}); } /// The only frame in a static image, or one of the frames in an animated one. diff --git a/engine/src/flutter/lib/web_ui/test/ui/codecs_test.dart b/engine/src/flutter/lib/web_ui/test/ui/codecs_test.dart index 2ec58761c9c..f6a0869c908 100644 --- a/engine/src/flutter/lib/web_ui/test/ui/codecs_test.dart +++ b/engine/src/flutter/lib/web_ui/test/ui/codecs_test.dart @@ -18,35 +18,19 @@ void main() { } abstract class TestCodec { - TestCodec({required this.description}); - final String description; - - ui.Codec? _cachedCodec; - - Future getCodec() async => _cachedCodec ??= await createCodec(); - - Future createCodec(); - - void dispose() { - _cachedCodec?.dispose(); - _cachedCodec = null; - } -} - -abstract class TestFileCodec extends TestCodec { - TestFileCodec.fromTestFile(this.testFile, {required super.description}); + TestCodec.fromTestFile(this.testFile, {required this.description}); final String testFile; + final String description; Future createCodecFromTestFile(String testFile); - @override Future createCodec() { return createCodecFromTestFile(testFile); } } -class UrlTestCodec extends TestFileCodec { +class UrlTestCodec extends TestCodec { UrlTestCodec(super.testFile, this.codecFactory, String function) : super.fromTestFile(description: 'created with $function("$testFile")'); @@ -58,7 +42,7 @@ class UrlTestCodec extends TestFileCodec { } } -class FetchTestCodec extends TestFileCodec { +class FetchTestCodec extends TestCodec { FetchTestCodec(super.testFile, this.codecFactory, String function) : super.fromTestFile( description: @@ -70,7 +54,7 @@ class FetchTestCodec extends TestFileCodec { @override Future createCodecFromTestFile(String testFile) async { - final HttpFetchResponse response = await httpFetch(testFile); + final HttpFetchResponse response = await httpFetch('/test_images/$testFile'); if (!response.hasPayload) { throw Exception('Unable to fetch() image test file "$testFile"'); @@ -81,7 +65,7 @@ class FetchTestCodec extends TestFileCodec { } } -class BitmapTestCodec extends TestFileCodec { +class BitmapTestCodec extends TestCodec { BitmapTestCodec(super.testFile, this.codecFactory, String function) : super.fromTestFile( description: @@ -94,7 +78,7 @@ class BitmapTestCodec extends TestFileCodec { @override Future createCodecFromTestFile(String testFile) async { final DomHTMLImageElement imageElement = createDomHTMLImageElement(); - imageElement.src = testFile; + imageElement.src = '/test_images/$testFile'; imageElement.decoding = 'async'; await imageElement.decode(); @@ -168,26 +152,25 @@ Future testMain() async { ); testCodecs.add( FetchTestCodec( - '/test_images/$testFile', + testFile, (Uint8List bytes) => renderer.instantiateImageCodec(bytes), 'renderer.instantiateImageCodec', ), ); testCodecs.add( FetchTestCodec( - '/test_images/$testFile', + testFile, (Uint8List bytes) => renderer.instantiateImageCodec( bytes, targetWidth: testTargetWidth, targetHeight: testTargetHeight, ), - 'renderer.instantiateImageCodec ' - '($testTargetWidth x $testTargetHeight)', + 'renderer.instantiateImageCodec ($testTargetWidth x $testTargetHeight)', ), ); testCodecs.add( BitmapTestCodec( - 'test_images/$testFile', + testFile, (DomImageBitmap bitmap) async => renderer.createImageFromImageBitmap(bitmap), 'renderer.createImageFromImageBitmap', ), @@ -204,33 +187,47 @@ Future testMain() async { mockHttpFetchResponseFactory = null; }); - group('Codecs', () { - final List testCodecs = createTestCodecs(); - for (final TestCodec testCodec in testCodecs) { - test('${testCodec.description} can create an image', () async { - try { - final ui.Codec codec = await testCodec.getCodec(); - final ui.FrameInfo frameInfo = await codec.getNextFrame(); - final ui.Image image = frameInfo.image; - expect(image, isNotNull); - expect(image.width, isNonZero); - expect(image.height, isNonZero); - expect(image.colorSpace, isNotNull); - } catch (e) { - throw TestFailure('Failed to get image for ${testCodec.description}: $e'); - } - }); + void runCodecTest(TestCodec testCodec) { + const problematicChromeImages = >{ + // Frame 2 cause Chrome to crash. + // https://issues.chromium.org/456445108 + 'crbug445556737.png': {2}, + // Frames 2 and 3 cause Chrome to crash. + // https://issues.chromium.org/456445108 + 'interlaced-multiframe-with-blending.png': {2, 3}, + }; - test('${testCodec.description} can be decoded with toByteData', () async { - ui.Image image; + test('${testCodec.description} can create an image and convert it to byte array', () async { + final ui.Codec codec = await testCodec.createCodec(); + + final Set problematicFrames; + if (isChromium && problematicChromeImages.containsKey(testCodec.testFile)) { + // Encountered an image with known problematic frames on Chromium. + problematicFrames = problematicChromeImages[testCodec.testFile]!; + } else { + problematicFrames = {}; + } + + for (int i = 0; i < codec.frameCount; i++) { + if (problematicFrames.contains(i)) { + printWarning( + 'Skipping frame $i of ${testCodec.description} due to known Chromium crash bug.', + ); + continue; + } + + final ui.Image image; try { - final ui.Codec codec = await testCodec.getCodec(); final ui.FrameInfo frameInfo = await codec.getNextFrame(); image = frameInfo.image; } catch (e) { - throw TestFailure('Failed to get image for ${testCodec.description}: $e'); + codec.dispose(); + throw TestFailure('Failed to get image at frame $i for ${testCodec.description}: $e'); } + expect(image.width, isNonZero); + expect(image.height, isNonZero); + final ByteData? byteData = await image.toByteData(); expect( byteData, @@ -249,12 +246,31 @@ Future testMain() async { '${testCodec.description} toByteData() should ' 'contain nonzero value', ); - }); - } - for (final testCodec in testCodecs) { - testCodec.dispose(); - } + } + + // After all frames are decoded and tested, dispose the codec. + codec.dispose(); + }); + } + + group('Codecs (default browserSupportsImageDecoder)', () { + createTestCodecs().forEach(runCodecTest); }); + + if (browserSupportsImageDecoder) { + // For the sake of completeness, test codec fallback logic on browsers that support + // `ImageDecoder`. + group('Codecs (browserSupportsImageDecoder=false)', () { + setUpAll(() { + browserSupportsImageDecoder = false; + }); + tearDownAll(() { + debugResetBrowserSupportsImageDecoder(); + }); + + createTestCodecs().forEach(runCodecTest); + }); + } }); test('crossOrigin requests cause an error', () async {