diff --git a/.github/workflow-scripts/maestro-android.js b/.github/workflow-scripts/maestro-android.js
index 62ee99a2279d..74507ff6bcab 100644
--- a/.github/workflow-scripts/maestro-android.js
+++ b/.github/workflow-scripts/maestro-android.js
@@ -60,9 +60,10 @@ async function executeFlowInFolder(flowFolder) {
const filePath = `${flowFolder}/${file}`;
if (fs.lstatSync(filePath).isDirectory()) {
await executeFlowInFolder(filePath);
- } else {
+ } else if (file.endsWith('.yml') || file.endsWith('.yaml')) {
await executeFlowWithRetries(filePath, 0);
}
+ // Skip non-flow files (e.g. screenshot baselines under screenshots/).
}
}
diff --git a/packages/rn-tester/.maestro/button.yml b/packages/rn-tester/.maestro/button.yml
index 7c0caa0b846e..865b2c345118 100644
--- a/packages/rn-tester/.maestro/button.yml
+++ b/packages/rn-tester/.maestro/button.yml
@@ -1,7 +1,11 @@
appId: ${APP_ID} # iOS: com.meta.RNTester.localDevelopment | Android: com.facebook.react.uiapp
---
- launchApp
-- assertVisible: 'Components'
+# Wait for the JS bundle to render the landing screen instead of asserting
+# immediately, which races app cold start on CI.
+- extendedWaitUntil:
+ visible: 'Components'
+ timeout: 30000
- scrollUntilVisible:
element:
id: 'Button'
diff --git a/packages/rn-tester/.maestro/flatlist.yml b/packages/rn-tester/.maestro/flatlist.yml
index ec75f6ccfb10..0915de4f6c00 100644
--- a/packages/rn-tester/.maestro/flatlist.yml
+++ b/packages/rn-tester/.maestro/flatlist.yml
@@ -1,7 +1,11 @@
appId: ${APP_ID} # iOS: com.meta.RNTester.localDevelopment | Android: com.facebook.react.uiapp
---
- launchApp
-- assertVisible: 'Components'
+# Wait for the JS bundle to render the landing screen instead of asserting
+# immediately, which races app cold start on CI.
+- extendedWaitUntil:
+ visible: 'Components'
+ timeout: 30000
- scrollUntilVisible:
element:
id: 'Flatlist'
diff --git a/packages/rn-tester/.maestro/helpers/launch-app-and-search.yml b/packages/rn-tester/.maestro/helpers/launch-app-and-search.yml
index b6fca0b4990b..d9463c0908ab 100644
--- a/packages/rn-tester/.maestro/helpers/launch-app-and-search.yml
+++ b/packages/rn-tester/.maestro/helpers/launch-app-and-search.yml
@@ -1,5 +1,9 @@
appId: ${APP_ID} # iOS: com.meta.RNTester.localDevelopment | Android: com.facebook.react.uiapp
---
- launchApp
-- assertVisible: 'Components'
+# Wait for the JS bundle to render the landing screen instead of asserting
+# immediately, which races app cold start on CI.
+- extendedWaitUntil:
+ visible: 'Components'
+ timeout: 30000
- runFlow: ./search.yml
diff --git a/packages/rn-tester/.maestro/image-blur-prefetch.yml b/packages/rn-tester/.maestro/image-blur-prefetch.yml
new file mode 100644
index 000000000000..42841c7c27ab
--- /dev/null
+++ b/packages/rn-tester/.maestro/image-blur-prefetch.yml
@@ -0,0 +1,35 @@
+appId: ${APP_ID} # iOS: com.meta.RNTester.localDevelopment | Android: com.facebook.react.uiapp
+---
+# Prefetch an image, then render the same URI with blurRadius so the blur
+# postprocessor applies to the already-decoded bitmap. Cross-platform.
+- runFlow: ./helpers/launch-app-and-search.yml
+- inputText:
+ text: 'Image'
+- assertVisible:
+ id: 'Image'
+- tapOn:
+ id: 'Image'
+- assertVisible:
+ id: 'example_search'
+- tapOn:
+ id: 'example_search'
+- inputText: 'Blur Radius with Prefetch'
+- hideKeyboard
+- scrollUntilVisible:
+ element:
+ id: 'image-blur-prefetch'
+ direction: DOWN
+ speed: 40
+ timeout: 10000
+- extendedWaitUntil:
+ visible: 'prefetch: ok'
+ timeout: 20000
+- extendedWaitUntil:
+ visible: 'blurred image: loaded'
+ timeout: 20000
+# Change this to takeScreenshot when you want to update the screenshot
+- assertScreenshot:
+ path: screenshots/image-blur-prefetch-${maestro.platform}
+ cropOn:
+ id: 'image-blur-prefetch'
+ thresholdPercentage: 95
diff --git a/packages/rn-tester/.maestro/image-progressive-jpeg.yml b/packages/rn-tester/.maestro/image-progressive-jpeg.yml
new file mode 100644
index 000000000000..3879585cf9b7
--- /dev/null
+++ b/packages/rn-tester/.maestro/image-progressive-jpeg.yml
@@ -0,0 +1,40 @@
+appId: ${APP_ID} # iOS: com.meta.RNTester.localDevelopment | Android: com.facebook.react.uiapp
+tags:
+ - android-only
+---
+# Android-only: loads a JPEG with progressiveRenderingEnabled and logs load
+# events. iOS skips this flow (empty=pass).
+- runFlow:
+ when:
+ platform: Android
+ commands:
+ - runFlow: ./helpers/launch-app-and-search.yml
+ - inputText:
+ text: 'Image'
+ - assertVisible:
+ id: 'Image'
+ - tapOn:
+ id: 'Image'
+ - assertVisible:
+ id: 'example_search'
+ - tapOn:
+ id: 'example_search'
+ - inputText: 'Progressive JPEG'
+ - hideKeyboard
+ - scrollUntilVisible:
+ element:
+ id: 'image-progressive-jpeg'
+ direction: DOWN
+ speed: 40
+ timeout: 10000
+ # loadStart fires on mount -> proves the progressive load path engaged.
+ # Assert by id since each event is its own element (progress % is dynamic).
+ - extendedWaitUntil:
+ visible:
+ id: 'progressive-jpeg-loadstart'
+ timeout: 15000
+ # Final load event (network image; relies on the same host as image.yml).
+ - extendedWaitUntil:
+ visible:
+ id: 'progressive-jpeg-load'
+ timeout: 20000
diff --git a/packages/rn-tester/.maestro/image-wide-gamut.yml b/packages/rn-tester/.maestro/image-wide-gamut.yml
new file mode 100644
index 000000000000..6bd5d7f9f8ee
--- /dev/null
+++ b/packages/rn-tester/.maestro/image-wide-gamut.yml
@@ -0,0 +1,43 @@
+appId: ${APP_ID} # iOS: com.meta.RNTester.localDevelopment | Android: com.facebook.react.uiapp
+---
+# Alpha transparency + sRGB vs Display-P3 wide-gamut. Color fidelity needs a
+# screenshot, so we assert load status only. Cross-platform.
+- runFlow: ./helpers/launch-app-and-search.yml
+- inputText:
+ text: 'Image'
+- assertVisible:
+ id: 'Image'
+- tapOn:
+ id: 'Image'
+- assertVisible:
+ id: 'example_search'
+- tapOn:
+ id: 'example_search'
+- inputText: 'Wide Gamut'
+- hideKeyboard
+- scrollUntilVisible:
+ element:
+ id: 'image-wide-gamut'
+ direction: DOWN
+ speed: 40
+ timeout: 10000
+- extendedWaitUntil:
+ visible: 'alpha: loaded'
+ timeout: 15000
+- scrollUntilVisible:
+ element: 'sRGB: loaded'
+ direction: DOWN
+ speed: 40
+ timeout: 20000
+# External URL; accept loaded or error.
+- scrollUntilVisible:
+ element: 'P3: (loaded|error)'
+ direction: DOWN
+ speed: 40
+ timeout: 20000
+# Change this to takeScreenshot when you want to update the screenshot
+- assertScreenshot:
+ path: screenshots/image-wide-gamut-${maestro.platform}
+ cropOn:
+ id: 'image-wide-gamut'
+ thresholdPercentage: 95
diff --git a/packages/rn-tester/.maestro/legacy-native-module.yml b/packages/rn-tester/.maestro/legacy-native-module.yml
index 1d81fd734c83..e68fbaa30edd 100644
--- a/packages/rn-tester/.maestro/legacy-native-module.yml
+++ b/packages/rn-tester/.maestro/legacy-native-module.yml
@@ -1,8 +1,12 @@
appId: ${APP_ID} # iOS: com.meta.RNTester.localDevelopment | Android: com.facebook.react.uiapp
---
- launchApp
-- assertVisible:
- text: 'APIs'
+# Wait for the JS bundle to render the landing screen instead of asserting
+# immediately, which races app cold start on CI.
+- extendedWaitUntil:
+ visible:
+ text: 'APIs'
+ timeout: 30000
- tapOn:
id: 'apis-tab'
- runFlow: ./helpers/search.yml
diff --git a/packages/rn-tester/.maestro/modal.yml b/packages/rn-tester/.maestro/modal.yml
index e0e2de4ef3c2..e3b7bef53886 100644
--- a/packages/rn-tester/.maestro/modal.yml
+++ b/packages/rn-tester/.maestro/modal.yml
@@ -1,7 +1,11 @@
appId: ${APP_ID} # iOS: com.meta.RNTester.localDevelopment | Android: com.facebook.react.uiapp
---
- launchApp
-- assertVisible: 'Components'
+# Wait for the JS bundle to render the landing screen instead of asserting
+# immediately, which races app cold start on CI.
+- extendedWaitUntil:
+ visible: 'Components'
+ timeout: 30000
- scrollUntilVisible:
element:
id: 'Modal'
diff --git a/packages/rn-tester/.maestro/screenshots/image-blur-prefetch-android.png b/packages/rn-tester/.maestro/screenshots/image-blur-prefetch-android.png
new file mode 100644
index 000000000000..a7cc26fe09c6
Binary files /dev/null and b/packages/rn-tester/.maestro/screenshots/image-blur-prefetch-android.png differ
diff --git a/packages/rn-tester/.maestro/screenshots/image-blur-prefetch-ios.png b/packages/rn-tester/.maestro/screenshots/image-blur-prefetch-ios.png
new file mode 100644
index 000000000000..f2ba74d26cee
Binary files /dev/null and b/packages/rn-tester/.maestro/screenshots/image-blur-prefetch-ios.png differ
diff --git a/packages/rn-tester/.maestro/screenshots/image-wide-gamut-android.png b/packages/rn-tester/.maestro/screenshots/image-wide-gamut-android.png
new file mode 100644
index 000000000000..0bf568815889
Binary files /dev/null and b/packages/rn-tester/.maestro/screenshots/image-wide-gamut-android.png differ
diff --git a/packages/rn-tester/.maestro/screenshots/image-wide-gamut-ios.png b/packages/rn-tester/.maestro/screenshots/image-wide-gamut-ios.png
new file mode 100644
index 000000000000..c1006ed7ae12
Binary files /dev/null and b/packages/rn-tester/.maestro/screenshots/image-wide-gamut-ios.png differ
diff --git a/packages/rn-tester/js/examples/Image/ImageExample.js b/packages/rn-tester/js/examples/Image/ImageExample.js
index 44301cbe3f1f..55d4a8e7da26 100644
--- a/packages/rn-tester/js/examples/Image/ImageExample.js
+++ b/packages/rn-tester/js/examples/Image/ImageExample.js
@@ -29,15 +29,23 @@ import {
View,
} from 'react-native';
+const ALPHA_PNG_ASSET = require('../../assets/alpha-hotdog.png');
+
const IMAGE1 =
'https://www.facebook.com/assets/fb_lite_messaging/E2EE-settings@3x.png';
const IMAGE2 =
'https://www.facebook.com/ar_effect/external_textures/648609739826677.png';
-
const base64Icon =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAABLCAQAAACSR7JhAAADtUlEQVR4Ac3YA2Bj6QLH0XPT1Fzbtm29tW3btm3bfLZtv7e2ObZnms7d8Uw098tuetPzrxv8wiISrtVudrG2JXQZ4VOv+qUfmqCGGl1mqLhoA52oZlb0mrjsnhKpgeUNEs91Z0pd1kvihA3ULGVHiQO2narKSHKkEMulm9VgUyE60s1aWoMQUbpZOWE+kaqs4eLEjdIlZTcFZB0ndc1+lhB1lZrIuk5P2aib1NBpZaL+JaOGIt0ls47SKzLC7CqrlGF6RZ09HGoNy1lYl2aRSWL5GuzqWU1KafRdoRp0iOQEiDzgZPnG6DbldcomadViflnl/cL93tOoVbsOLVM2jylvdWjXolWX1hmfZbGR/wjypDjFLSZIRov09BgYmtUqPQPlQrPapecLgTIy0jMgPKtTeob2zWtrGH3xvjUkPCtNg/tm1rjwrMa+mdUkPd3hWbH0jArPGiU9ufCsNNWFZ40wpwn+62/66R2RUtoso1OB34tnLOcy7YB1fUdc9e0q3yru8PGM773vXsuZ5YIZX+5xmHwHGVvlrGPN6ZSiP1smOsMMde40wKv2VmwPPVXNut4sVpUreZiLBHi0qln/VQeI/LTMYXpsJtFiclUN+5HVZazim+Ky+7sAvxWnvjXrJFneVtLWLyPJu9K3cXLWeOlbMTlrIelbMDlrLenrjEQOtIF+fuI9xRp9ZBFp6+b6WT8RrxEpdK64BuvHgDk+vUy+b5hYk6zfyfs051gRoNO1usU12WWRWL73/MMEy9pMi9qIrR4ZpV16Rrvduxazmy1FSvuFXRkqTnE7m2kdb5U8xGjLw/spRr1uTov4uOgQE+0N/DvFrG/Jt7i/FzwxbA9kDanhf2w+t4V97G8lrT7wc08aA2QNUkuTfW/KimT01wdlfK4yEw030VfT0RtZbzjeMprNq8m8tnSTASrTLti64oBNdpmMQm0eEwvfPwRbUBywG5TzjPCsdwk3IeAXjQblLCoXnDVeoAz6SfJNk5TTzytCNZk/POtTSV40NwOFWzw86wNJRpubpXsn60NJFlHeqlYRbslqZm2jnEZ3qcSKgm0kTli3zZVS7y/iivZTweYXJ26Y+RTbV1zh3hYkgyFGSTKPfRVbRqWWVReaxYeSLarYv1Qqsmh1s95S7G+eEWK0f3jYKTbV6bOwepjfhtafsvUsqrQvrGC8YhmnO9cSCk3yuY984F1vesdHYhWJ5FvASlacshUsajFt2mUM9pqzvKGcyNJW0arTKN1GGGzQlH0tXwLDgQTurS8eIQAAAABJRU5ErkJggg==';
const IMAGE_PREFETCH_URL = `${IMAGE1}?r=1&t=${Date.now()}`;
const prefetchTask = Image.prefetch(IMAGE_PREFETCH_URL);
+// Remote JPEG (RN OSS test fixture) used by the progressive example. Trusted by
+// the API 24 Android CI emulator and reachable on both platforms.
+const LARGE_JPEG =
+ 'https://www.facebook.com/assets/react_native_oss_tests/large-image@1x.jpg';
+// Display-P3 wide-gamut sample (WebKit color-gamut test image).
+const WIDE_GAMUT_P3_URL =
+ 'https://webkit.org/blog-files/color-gamut/Webkit-logo-P3.png';
type ImageSource = Readonly<{
uri: string,
@@ -845,6 +853,142 @@ function ImageGetSizePlatformTest(props: PlatformTestComponentBaseProps) {
);
}
+function ProgressiveJpegExample(): React.Node {
+ const [loadStarted, setLoadStarted] = useState(false);
+ const [progress, setProgress] = useState(null);
+ const [loaded, setLoaded] = useState(false);
+ const [uri] = useState(() => `${LARGE_JPEG}?progressive=${Date.now()}`);
+ return (
+
+ setLoadStarted(true)}
+ onProgress={event => {
+ const {loaded: bytesLoaded, total} = event.nativeEvent;
+ if (total > 0) {
+ setProgress(Math.round((bytesLoaded / total) * 100));
+ }
+ }}
+ onLoad={() => setLoaded(true)}
+ />
+ {loadStarted ? (
+
+ loadStart
+
+ ) : null}
+ {progress != null ? (
+
+ progress {progress}%
+
+ ) : null}
+ {loaded ? (
+ load
+ ) : null}
+
+ );
+}
+
+function BlurRadiusPrefetchExample(): React.Node {
+ const [uri] = useState(() => `${IMAGE2}?blurPrefetch=${Date.now()}`);
+ const [prefetchStatus, setPrefetchStatus] = useState('pending');
+ const [loadStatus, setLoadStatus] = useState('pending');
+
+ useEffect(() => {
+ let cancelled = false;
+ void Image.prefetch(uri).then(
+ () => {
+ if (!cancelled) {
+ setPrefetchStatus('ok');
+ }
+ },
+ () => {
+ if (!cancelled) {
+ setPrefetchStatus('failed');
+ }
+ },
+ );
+ return () => {
+ cancelled = true;
+ };
+ }, [uri]);
+
+ return (
+
+
+ prefetch: {prefetchStatus}
+
+ {prefetchStatus === 'ok' ? (
+ setLoadStatus('loaded')}
+ onError={() => setLoadStatus('error')}
+ />
+ ) : null}
+
+ blurred image: {loadStatus}
+
+
+ );
+}
+
+function WideGamutTransparencyExample(): React.Node {
+ const [alphaStatus, setAlphaStatus] = useState('loading');
+ const [srgbStatus, setSrgbStatus] = useState('loading');
+ const [p3Status, setP3Status] = useState('loading');
+ return (
+
+
+ Alpha / transparency
+
+
+ setAlphaStatus('loaded')}
+ onError={() => setAlphaStatus('error')}
+ />
+
+
+ alpha: {alphaStatus}
+
+ sRGB vs Display-P3
+
+
+ sRGB
+ setSrgbStatus('loaded')}
+ onError={() => setSrgbStatus('error')}
+ />
+
+
+ Display-P3
+ setP3Status('loaded')}
+ onError={() => setP3Status('error')}
+ />
+
+
+
+ sRGB: {srgbStatus}
+
+ P3: {p3Status}
+
+ );
+}
+
const styles = StyleSheet.create({
base: {
width: 64,
@@ -1095,6 +1239,11 @@ const styles = StyleSheet.create({
alignItems: 'center',
marginTop: 10,
},
+ checkerBackground: {
+ backgroundColor: '#cccccc',
+ padding: 4,
+ alignSelf: 'flex-start',
+ },
});
exports.displayName = undefined as ?string;
@@ -1957,4 +2106,32 @@ exports.examples = [
},
platform: 'android',
},
+ {
+ title: 'Progressive JPEG',
+ name: 'progressive-jpeg',
+ description:
+ 'Loads a JPEG with progressiveRenderingEnabled and logs progress/load events.',
+ render: function (): React.Node {
+ return ;
+ },
+ platform: 'android',
+ },
+ {
+ title: 'Blur Radius with Prefetch',
+ name: 'blur-radius-prefetch',
+ description:
+ 'Prefetches then renders the same URI with blurRadius to ensure the blur postprocessor is applied on prefetched images.',
+ render: function (): React.Node {
+ return ;
+ },
+ },
+ {
+ title: 'Wide Gamut and Transparency',
+ name: 'wide-gamut',
+ description:
+ 'Alpha transparency and sRGB vs Display-P3 comparison targets for screenshot tests.',
+ render: function (): React.Node {
+ return ;
+ },
+ },
] as Array;