[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
This commit is contained in:
Mouad Debbar 2025-11-04 13:07:04 -05:00 committed by GitHub
parent a913d39131
commit e5d5c01850
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 86 additions and 57 deletions

View File

@ -179,7 +179,7 @@
},
{
"dependency": "chrome_and_driver",
"version": "125.0.6422.141"
"version": "141.0.7390.76"
}
],
"tasks": [

View File

@ -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:

View File

@ -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;

View File

@ -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.

View File

@ -18,35 +18,19 @@ void main() {
}
abstract class TestCodec {
TestCodec({required this.description});
final String description;
ui.Codec? _cachedCodec;
Future<ui.Codec> getCodec() async => _cachedCodec ??= await createCodec();
Future<ui.Codec> 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<ui.Codec> createCodecFromTestFile(String testFile);
@override
Future<ui.Codec> 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<ui.Codec> 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<ui.Codec> 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<void> 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<void> testMain() async {
mockHttpFetchResponseFactory = null;
});
group('Codecs', () {
final List<TestCodec> 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 = <String, Set<int>>{
// 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<int> problematicFrames;
if (isChromium && problematicChromeImages.containsKey(testCodec.testFile)) {
// Encountered an image with known problematic frames on Chromium.
problematicFrames = problematicChromeImages[testCodec.testFile]!;
} else {
problematicFrames = <int>{};
}
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<void> 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 {