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;