Skip to content

Commit 27a6444

Browse files
committed
[add] load images with http headers
- extend ImageLoader.load params - Removed the old `image.decode` change as it's covered by the minimal browser versions supported here - add examples for Images with headers - Image - remove requestRef - no longer needed - rename `ImageLoader.abort` to `.release` The method is mostly used as cleanup (e.g. useEffect cleanup, or releasing resources when component unmounts) - Image - extract `useSource` hook Move the image loading effect here Changed the original logic slightly for less nesting Changed to cover cases where passing the same headers object was starting new loads, as it was treated as a different value due to referential equality - Image - add tests covering added/changed functionality around source - Image - handle cases where the source object only changes by reference When the source object changed by reference, but stays structurally the same, we should do nothing and not trigger the loading effect again - Image - extract ImageLoadingProps Update types to match RN and actual code - we don't call `onLoadStart` and `onLoadEnd` with any arguments - ImageLoader extract types.js - Image - resolve `onLoad` with `source` Use the same `nativeEvent` structure as in RN for the onLoad event - Rework Image loading and source management logic Since introducing the change to support headers changes to the original changes are needed: - support loading a default source with headers - handle source object changes - update uri resolving logic to handle blob URLs create by `URL.createObjectURL` - move the URI/source resolving logic to the `ImageLoader` BREAKING CHANGE `onLoad` was previously called with `nativeEvent` that was the browser Event object from the image.onload handler Since we can't spread or mutate the Event object to add `source` we have to either add it under a new key or remove it The browser Event does not expose very useful information, (no target, or size info), so it seems best to replace `nativeEvent` with the same structure used in `react-native`
1 parent fa47f80 commit 27a6444

File tree

7 files changed

+494
-165
lines changed

7 files changed

+494
-165
lines changed

packages/react-native-web-examples/pages/image/index.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@ const dataBase64Svg =
1515
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nMjAwJyBoZWlnaHQ9JzIwMCcgZmlsbD0iIzAwMDAwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDEwMCAxMDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxnPjxwYXRoIGQ9Ik0yNS44NjcsNDguODUzQzMyLjgwNiw1MC4xNzYsNDYuNDYsNTIuNSw2MS4yMTUsNTIuNWgwLjAwNWM5LjcxLDAsMTguNDAxLTEuMDU3LDI1LjkzOC0yLjkxMyAgIGMwLjE1OS0wLjA0NiwwLjM1LTAuMTM1LDAuNTY1LTAuMTg3YzAuMjgyLTAuMDcyLDAuNTY1LTAuMTY0LDAuODQ0LTAuMjM4YzMuMTg0LTAuOTY0LDIuNTc3LTMuMDUxLDIuMTk5LTMuODUyICAgYy00LjE2Ni03LjcxOS0xNS4wODYtMjMuNDE1LTM1LjAyOC0yMy40MTVjLTIyLjE2OSwwLTMwLjI2MiwxMC42MzUtMzMuMTQsMTkuNTg5QzIyLjU0NSw0Mi4zMzMsMjIuNDA3LDQ3LjEzNSwyNS44NjcsNDguODUzeiAgICBNMjguNjc2LDM4LjAzMmMwLjAxMy0wLjAzNiwwLjYxNC0xLjYyNiwxLjkyMy0xLjAwOGMxLjEzMywwLjUzNSwwLjk2MSwxLjU2MywwLjg4NywxLjg1Yy0wLjAwNywwLjAyNC0wLjAxNCwwLjA0OC0wLjAyMSwwLjA3MyAgIGMwLDAuMDAxLTAuMDAxLDAuMDA0LTAuMDAxLDAuMDA0bDAsMGMtMC4yNDksMC45MjktMC40MDQsMi4wODYtMC4wMTcsMi44NmMwLjE2LDAuMzE5LDAuNDkyLDAuNzY4LDEuNTQyLDAuOTg3bDAuMzY2LDAuMDc3ICAgYzIwLjgxNiw0LjM2LDM2LDIuOTMzLDQ1LjY3OCwwLjYyNmwtMC4wMDQsMC4wMDJjMCwwLDAuMDA1LTAuMDAyLDAuMDA3LTAuMDAzYzAuMjEyLTAuMDUsMC40MjEtMC4xMDEsMC42MjgtMC4xNTIgICBjMC41MDktMC4wNSwxLjE3MywwLjA3OCwxLjM5OSwxYzAuMzUxLDEuNDI0LTAuOTczLDEuODk1LTEuMjE3LDEuOTY5Yy01LjMyNSwxLjI3OS0xMi4yNjYsMi4zMDYtMjAuODM1LDIuMzA3ICAgYy03LjUwNSwwLTE2LjI1NS0wLjc4Ny0yNi4yNTctMi44ODJsLTAuMzY0LTAuMDc3Yy0yLjEyLTAuNDQyLTMuMTExLTEuNjMzLTMuNTY5LTIuNTU1QzI3Ljk4NSw0MS40MjEsMjguMjgxLDM5LjQxNiwyOC42NzYsMzguMDMyICAgeiI+PC9wYXRoPjxjaXJjbGUgY3g9IjEwLjQ5MyIgY3k9IjIzLjQ1NSIgcj0iMC42MTkiPjwvY2lyY2xlPjxwYXRoIGQ9Ik0yLjA4LDI4LjMwOGMwLjY3Ni0wLjE3OCwwLjk4My0wLjM1MiwxLjE3NC0wLjVDNC42OSwyNi42OSw2LjUsMjcuNDgzLDcuNSwyOC4zNTd2MC4wMDJjMCwwLDEuNzExLDEuMjM1LDAuNzM3LDIuMjAyICAgYy0wLjk3NCwwLjk2NS0yLjMxOSwwLjAwNi0yLjMxOSwwLjAwNmwwLjAzNSwwLjAxNmMtMC4zMjctMC4yMDMtMC42LTAuNTYxLTAuNzgtMC41ODRjLTAuMzcsMC4yNi0wLjg3NiwwLjUtMS40NzYsMC41SDMuNyAgIGMwLDAtMS4zNDUsMC43MDksMC4xNzgsMS42NTJjMC4wMDEsMC4wMDEsMC4wMDIsMC4wNzIsMC4wMDQsMC4wNzNjMy45MzksMi4zNDIsOC4yNzEsNS43MDEsOC4yNzEsOC44OCAgIGMwLDAuNjkxLDAuMiwxNy4wNDIsMTcuNjI2LDI0LjczOWwwLjk2NywwLjQ0MmwtMC4xLDEuMDU5Yy0wLjQyMSw0LjM5LDEuMTQ1LDEwLjE5MSwxMC45OTMsMTIuODg4bDAuMTEzLDAuMDM4ICAgYzAuMDY3LDAuMDIzLDYuNzMyLDIuNDI5LDEwLjkwNywyLjQyOWMxLjU4NCwwLDIuMTU1LTAuMzUyLDIuMjQzLTAuNTYxYzAuMDg1LTAuMjAyLDAuNjEyLTIuMTY0LTYuMzMyLTkuMzg3bDAuMDAyLTAuMTgzICAgYzAsMC0yLjQ3Ny0zLjA3LDEuNTMzLTMuMDdjMC4wMSwwLDAuMDE5LDAsMC4wMjksMGMxLjI4NSwwLDIuNjA4LDAuMjE1LDMuOTgsMC4xODRjNC43NzEtMC4xMTcsOS4zMTYtMC40MjUsMTMuNTA2LTEuMDk2ICAgbDAuNDc0LTAuMDI4bDAuNjY4LDAuMTU4YzkuNjUxLDQuOTQ4LDE2LjczOCw3LjcxNiwxOS43MzgsNy43MTZ2MC4wMDZjMCwwLDAuMTY0LDAuMDExLDAuMjMsMC4wMDQgICBjLTAuMTg5LTAuNzIzLTIuMjMtMi44LTcuMjMtOS4wNzl2MC4wMjFjMCwwLTEuNTEyLTEuNjU4LDAuNzk3LTIuNjUzYzAuMDYzLTAuMDI2LDAuMDA4LDAuMDIzLDAuMDYtMC4wMDEgICBjOC42MzktMy41MDksMTMuNTAxLTguMjA0LDE1LjQxMS0xMS43NzVjMS4xNDUtMi4xMjksMC4yMDYtMi43ODQtMC42NTktMi45NzZjLTAuMzE3LTAuMDM4LTAuNjM0LTAuMDYyLTAuOTEyLTAuMDYyICAgYy0wLjIwNSwwLTAuMzc5LDAuMDEtMC41MjgsMC4wMjdsLTMuMTQzLDEuMjE0QzgzLjczMiw1My45MjYsNzMuMjE4LDU1LjUsNjEuMjIsNTUuNWMtMC4wMDIsMC0wLjAwNSwwLTAuMDA1LDAgICBjLTE1LjEyOCwwLTI5LjEwMS0yLjQzMi0zNi4wODMtMy43NzFsLTAuMTczLTAuMTExbC0wLjE2LTAuMTI2Yy01Ljg1OC0yLjY4MS01LjEzNy0xMC4yMDItNS4xMDMtMTAuNTE5bDAuMDYtMC4zICAgYzAuODk1LTIuODM4LDIuNDY3LTYuMzUyLDUuMjEzLTkuNzE5Yy0xLjgwOC0xLjM2OS00LjU5LTQuMTg4LTQuNDMtOC40OTRjMC4wNDYtMS4yNDQtMC40ODYtMi41MDgtMS40OTgtMy41NTkgICBjLTEuNDk4LTEuNTU1LTMuNzg1LTIuNDQ2LTYuMjc0LTIuNDQ2Yy0xLjc3LDAtMy41NTMsMC40NDItNS4yOTMsMS4zMTRjLTQuMDYxLDIuMDM1LTQuODU1LDQuNzM2LTUuNjkyLDcuNTk2ICAgYy0wLjEzNiwwLjQ2OC0wLjI4NCwwLjkzOS0wLjQzOCwxLjQxYy0wLjAwNiwwLjAxOS0wLjAyMiwwLjAzNS0wLjAyOCwwLjA1NkMwLjgzMywyOC40MjMsMS42OTEsMjguMzksMi4wOCwyOC4zMDh6IE0xMC40OTMsMTkuOTA4ICAgYzEuOTU2LDAsMy41NDgsMS41OTEsMy41NDgsMy41NDdjMCwxLjk1Ny0xLjU5MiwzLjU0OC0zLjU0OCwzLjU0OGMtMS45NTcsMC0zLjU0OC0xLjU5Mi0zLjU0OC0zLjU0OCAgIEM2Ljk0NCwyMS40OTksOC41MzYsMTkuOTA4LDEwLjQ5MywxOS45MDh6Ij48L3BhdGg+PC9nPjwvc3ZnPg==';
1616
const dataSvg =
1717
'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>';
18+
const sourceWithHeaders = {
19+
uri: placeholder,
20+
headers: {
21+
'x-token': '0012345'
22+
}
23+
};
24+
const sourceWithHeadersAndRedirect = {
25+
uri: source,
26+
headers: {
27+
'x-token': '0012345'
28+
}
29+
};
1830

1931
function Divider() {
2032
return <View style={styles.divider} />;
@@ -31,8 +43,8 @@ export default function ImagePage() {
3143
onError={() => {
3244
console.log('error');
3345
}}
34-
onLoad={() => {
35-
console.log('load');
46+
onLoad={(result) => {
47+
console.log('load', result);
3648
}}
3749
onLoadEnd={() => {
3850
console.log('load-end');
@@ -114,6 +126,17 @@ export default function ImagePage() {
114126
/>
115127
</View>
116128
</View>
129+
<Divider />
130+
<View style={styles.row}>
131+
<View style={styles.column}>
132+
<Text style={[styles.text]}>With Headers</Text>
133+
<Image source={sourceWithHeaders} style={styles.image} />
134+
</View>
135+
<View style={styles.column}>
136+
<Text style={[styles.text]}>Headers & Redirect</Text>
137+
<Image source={sourceWithHeadersAndRedirect} style={styles.image} />
138+
</View>
139+
</View>
117140
</Example>
118141
);
119142
}

packages/react-native-web/src/exports/Image/__tests__/__snapshots__/index-test.js.snap

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -329,14 +329,14 @@ exports[`components/Image prop "style" removes other unsupported View styles 1`]
329329
>
330330
<div
331331
class="css-view-175oi2r r-backgroundColor-1niwhzg r-backgroundPosition-vvn4in r-backgroundRepeat-u6sd8q r-bottom-1p0dtai r-height-1pi2tsx r-left-1d2f490 r-position-u8s1d r-right-zchlnj r-top-ipm5af r-width-13qz1uu r-zIndex-1wyyakw r-backgroundSize-4gszlv"
332-
style="filter: url(#tint-57);"
332+
style="filter: url(#tint-96);"
333333
/>
334334
<svg
335335
style="position: absolute; height: 0px; visibility: hidden; width: 0px;"
336336
>
337337
<defs>
338338
<filter
339-
id="tint-57"
339+
id="tint-96"
340340
>
341341
<feflood
342342
flood-color="blue"
@@ -378,7 +378,7 @@ exports[`components/Image prop "style" supports "tintcolor" property (convert to
378378
>
379379
<div
380380
class="css-view-175oi2r r-backgroundColor-1niwhzg r-backgroundPosition-vvn4in r-backgroundRepeat-u6sd8q r-bottom-1p0dtai r-height-1pi2tsx r-left-1d2f490 r-position-u8s1d r-right-zchlnj r-top-ipm5af r-width-13qz1uu r-zIndex-1wyyakw r-backgroundSize-4gszlv"
381-
style="background-image: url(https://google.com/favicon.ico); filter: url(#tint-56);"
381+
style="background-image: url(https://google.com/favicon.ico); filter: url(#tint-94);"
382382
/>
383383
<img
384384
alt=""
@@ -391,7 +391,7 @@ exports[`components/Image prop "style" supports "tintcolor" property (convert to
391391
>
392392
<defs>
393393
<filter
394-
id="tint-56"
394+
id="tint-94"
395395
>
396396
<feflood
397397
flood-color="red"

packages/react-native-web/src/exports/Image/__tests__/index-test.js

Lines changed: 159 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ describe('components/Image', () => {
2121
beforeEach(() => {
2222
ImageUriCache._entries = {};
2323
window.Image = jest.fn(() => ({}));
24+
ImageLoader.load = jest
25+
.fn()
26+
.mockImplementation((source, onLoad, onError) => {
27+
onLoad({ source });
28+
});
2429
});
2530

2631
afterEach(() => {
@@ -107,9 +112,6 @@ describe('components/Image', () => {
107112
describe('prop "onLoad"', () => {
108113
test('is called after image is loaded from network', () => {
109114
jest.useFakeTimers();
110-
ImageLoader.load = jest.fn().mockImplementation((_, onLoad, onError) => {
111-
onLoad();
112-
});
113115
const onLoadStartStub = jest.fn();
114116
const onLoadStub = jest.fn();
115117
const onLoadEndStub = jest.fn();
@@ -127,9 +129,6 @@ describe('components/Image', () => {
127129

128130
test('is called after image is loaded from cache', () => {
129131
jest.useFakeTimers();
130-
ImageLoader.load = jest.fn().mockImplementation((_, onLoad, onError) => {
131-
onLoad();
132-
});
133132
const onLoadStartStub = jest.fn();
134133
const onLoadStub = jest.fn();
135134
const onLoadEndStub = jest.fn();
@@ -174,6 +173,38 @@ describe('components/Image', () => {
174173
expect(onLoadEndStub.mock.calls.length).toBe(2);
175174
});
176175

176+
test('is called on update if "headers" are modified', () => {
177+
const onLoadStartStub = jest.fn();
178+
const onLoadStub = jest.fn();
179+
const onLoadEndStub = jest.fn();
180+
const { rerender } = render(
181+
<Image
182+
onLoad={onLoadStub}
183+
onLoadEnd={onLoadEndStub}
184+
onLoadStart={onLoadStartStub}
185+
source={{
186+
uri: 'https://test.com/img.jpg',
187+
headers: { 'x-custom-header': 'abc123' }
188+
}}
189+
/>
190+
);
191+
act(() => {
192+
rerender(
193+
<Image
194+
onLoad={onLoadStub}
195+
onLoadEnd={onLoadEndStub}
196+
onLoadStart={onLoadStartStub}
197+
source={{
198+
uri: 'https://test.com/img.jpg',
199+
headers: { 'x-custom-header': '123abc' }
200+
}}
201+
/>
202+
);
203+
});
204+
expect(onLoadStub.mock.calls.length).toBe(2);
205+
expect(onLoadEndStub.mock.calls.length).toBe(2);
206+
});
207+
177208
test('is not called on update if "uri" is the same', () => {
178209
const onLoadStartStub = jest.fn();
179210
const onLoadStub = jest.fn();
@@ -225,6 +256,44 @@ describe('components/Image', () => {
225256
expect(onLoadStub.mock.calls.length).toBe(1);
226257
expect(onLoadEndStub.mock.calls.length).toBe(1);
227258
});
259+
260+
// This test verifies that wen source is declared in-line and the parent component
261+
// re-renders we aren't restarting the load process because the source is structurally equal
262+
test('is not called on update when "headers" and "uri" are not modified', () => {
263+
const onLoadStartStub = jest.fn();
264+
const onLoadStub = jest.fn();
265+
const onLoadEndStub = jest.fn();
266+
const { rerender } = render(
267+
<Image
268+
onLoad={onLoadStub}
269+
onLoadEnd={onLoadEndStub}
270+
onLoadStart={onLoadStartStub}
271+
source={{
272+
uri: 'https://test.com/img.jpg',
273+
width: 1,
274+
height: 1,
275+
headers: { 'x-custom-header': 'abc123' }
276+
}}
277+
/>
278+
);
279+
act(() => {
280+
rerender(
281+
<Image
282+
onLoad={onLoadStub}
283+
onLoadEnd={onLoadEndStub}
284+
onLoadStart={onLoadStartStub}
285+
source={{
286+
uri: 'https://test.com/img.jpg',
287+
width: 1,
288+
height: 1,
289+
headers: { 'x-custom-header': 'abc123' }
290+
}}
291+
/>
292+
);
293+
});
294+
expect(onLoadStub.mock.calls.length).toBe(1);
295+
expect(onLoadEndStub.mock.calls.length).toBe(1);
296+
});
228297
});
229298

230299
describe('prop "resizeMode"', () => {
@@ -244,8 +313,10 @@ describe('components/Image', () => {
244313
null,
245314
'',
246315
{},
316+
[],
247317
{ uri: '' },
248-
{ uri: 'https://google.com' }
318+
{ uri: 'https://google.com' },
319+
{ uri: 'https://google.com', headers: { 'x-custom-header': 'abc123' } }
249320
];
250321
sources.forEach((source) => {
251322
expect(() => render(<Image source={source} />)).not.toThrow();
@@ -261,11 +332,6 @@ describe('components/Image', () => {
261332

262333
test('is set immediately if the image was preloaded', () => {
263334
const uri = 'https://yahoo.com/favicon.ico';
264-
ImageLoader.load = jest
265-
.fn()
266-
.mockImplementationOnce((_, onLoad, onError) => {
267-
onLoad();
268-
});
269335
return Image.prefetch(uri).then(() => {
270336
const source = { uri };
271337
const { container } = render(<Image source={source} />, {
@@ -308,19 +374,32 @@ describe('components/Image', () => {
308374
test('is correctly updated only when loaded if defaultSource provided', () => {
309375
const defaultUri = 'https://testing.com/preview.jpg';
310376
const uri = 'https://testing.com/fullSize.jpg';
311-
let loadCallback;
312-
ImageLoader.load = jest
313-
.fn()
314-
.mockImplementationOnce((_, onLoad, onError) => {
315-
loadCallback = onLoad;
316-
});
377+
const calls = [];
378+
379+
// Capture calls and resolve them after render
380+
ImageLoader.load = jest.fn().mockImplementation((source, onLoad) => {
381+
calls.push({ source, onLoad });
382+
});
383+
317384
const { container } = render(
318385
<Image defaultSource={{ uri: defaultUri }} source={{ uri }} />
319386
);
387+
388+
// Both defaultSource and source are loaded at the same time
389+
// But we assume defaultSource is loaded quicker
390+
act(() => {
391+
const call = calls.find(({ source }) => source.uri === defaultUri);
392+
call.onLoad({ source: call.source });
393+
});
394+
320395
expect(container.firstChild).toMatchSnapshot();
396+
397+
// After a while the main source loads as well
321398
act(() => {
322-
loadCallback();
399+
const call = calls.find(({ source }) => source.uri === uri);
400+
call.onLoad({ source: call.source });
323401
});
402+
324403
expect(container.firstChild).toMatchSnapshot();
325404
});
326405

@@ -346,6 +425,67 @@ describe('components/Image', () => {
346425
'http://localhost/static/img@2x.png'
347426
);
348427
});
428+
429+
test('it correctly passes headers to ImageLoader', () => {
430+
const uri = 'https://google.com/favicon.ico';
431+
const headers = { 'x-custom-header': 'abc123' };
432+
const source = { uri, headers };
433+
render(<Image source={source} />);
434+
435+
expect(ImageLoader.load).toHaveBeenCalledWith(
436+
expect.objectContaining({ headers }),
437+
expect.any(Function),
438+
expect.any(Function)
439+
);
440+
});
441+
442+
test('it correctly passes uri to ImageLoader', () => {
443+
const uri = 'https://google.com/favicon.ico';
444+
const source = { uri };
445+
render(<Image source={source} />);
446+
447+
expect(ImageLoader.load).toHaveBeenCalledWith(
448+
expect.objectContaining({ uri }),
449+
expect.any(Function),
450+
expect.any(Function)
451+
);
452+
});
453+
454+
// A common case is `source` declared as an inline object, which cause is to be a
455+
// new object (with the same content) each time parent component renders
456+
test('it still loads the image if source object is changed', () => {
457+
ImageLoader.load.mockImplementation(() => {});
458+
459+
const releaseSpy = jest.spyOn(ImageLoader, 'release');
460+
461+
const uri = 'https://google.com/favicon.ico';
462+
const { rerender } = render(<Image source={{ uri }} />);
463+
rerender(<Image source={{ uri }} />);
464+
465+
// when the underlying source didn't change we expect the initial request is not cancelled due to re-render
466+
expect(releaseSpy).not.toHaveBeenCalled();
467+
});
468+
469+
test('falls back to default source when source or source.uri is removed', () => {
470+
const source = { uri: 'https://google.com/favicon.ico' };
471+
const defaultSource = { uri: 'http://localhost/static/img@2x.png' };
472+
473+
const { container, rerender } = render(
474+
<Image defaultSource={defaultSource} source={source} />
475+
);
476+
477+
rerender(<Image defaultSource={defaultSource} source={{ uri: '' }} />);
478+
expect(container.querySelector('img').src).toBe(defaultSource.uri);
479+
});
480+
481+
test('removes image if source or source.uri is removed and there is no default source', () => {
482+
const source = { uri: 'https://google.com/favicon.ico' };
483+
484+
const { container, rerender } = render(<Image source={source} />);
485+
486+
rerender(<Image source={{ uri: '' }} />);
487+
expect(container.querySelector('img')).toBe(null);
488+
});
349489
});
350490

351491
describe('prop "style"', () => {

0 commit comments

Comments
 (0)