-
Notifications
You must be signed in to change notification settings - Fork 79
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(ui): improve icon fetching. WF-141
- Loading branch information
Showing
12 changed files
with
844 additions
and
55 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
37 changes: 37 additions & 0 deletions
37
src/ui/src/components/shared/SharedImgWithFallback.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { describe, it, expect, vi, beforeEach, Mock } from "vitest"; | ||
import SharedImgWithFallback from "./SharedImgWithFallback.vue"; | ||
import { flushPromises, shallowMount } from "@vue/test-utils"; | ||
|
||
describe("SharedImgWithFallback", () => { | ||
let fetch: Mock; | ||
|
||
beforeEach(() => { | ||
fetch = vi.fn().mockResolvedValue({ | ||
ok: true, | ||
headers: new Map([["Content-Type", "image/png"]]), | ||
}); | ||
global.fetch = fetch; | ||
}); | ||
|
||
it("should use the last image because the first two are not valid", async () => { | ||
fetch | ||
.mockRejectedValueOnce(new Error()) | ||
.mockResolvedValueOnce({ | ||
ok: true, | ||
headers: new Map([["Content-Type", "text/html"]]), | ||
}) | ||
.mockResolvedValue({ | ||
ok: true, | ||
headers: new Map([["Content-Type", "image/png"]]), | ||
}); | ||
|
||
const wrapper = shallowMount(SharedImgWithFallback, { | ||
props: { urls: ["/img1.svg", "/img2.svg", "/img3.svg"] }, | ||
}); | ||
expect(wrapper.get("img").attributes().src).toBe(""); | ||
|
||
await flushPromises(); | ||
|
||
expect(wrapper.get("img").attributes().src).toBe("/img3.svg"); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<template> | ||
<img :src="src" /> | ||
</template> | ||
|
||
<script lang="ts" setup> | ||
import { useAssetContentType } from "@/composables/useAssetContentType"; | ||
import { PropType, ref, toRef, watch } from "vue"; | ||
const props = defineProps({ | ||
urls: { type: Array as PropType<string[]>, required: true }, | ||
}); | ||
const src = ref(""); | ||
const { fetchAssetContentType } = useAssetContentType(); | ||
watch( | ||
toRef(props, "urls"), | ||
async (urls) => { | ||
src.value = ""; | ||
for (const url of urls) { | ||
const contentType = await fetchAssetContentType(url); | ||
// ensure that the content type is valid and not HTML (the server can responds with a default HTML page) | ||
if (!contentType || contentType === "text/html") continue; | ||
return (src.value = url); | ||
} | ||
}, | ||
{ immediate: true }, | ||
); | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { useAssetContentType } from "./useAssetContentType"; | ||
import { beforeEach, describe, it, expect, Mock, vi } from "vitest"; | ||
|
||
describe(useAssetContentType.name, () => { | ||
let fetch: Mock; | ||
|
||
beforeEach(() => { | ||
fetch = vi.fn().mockResolvedValue({ | ||
ok: true, | ||
headers: new Map([["Content-Type", "image/png"]]), | ||
}); | ||
global.fetch = fetch; | ||
|
||
useAssetContentType().clearCache(); | ||
}); | ||
|
||
it("should handle error ", async () => { | ||
fetch.mockRejectedValue(new Error()); | ||
const { fetchAssetContentType } = useAssetContentType(); | ||
|
||
expect(await fetchAssetContentType("https://test.com")).toBeUndefined(); | ||
expect(fetch).toHaveBeenCalledOnce(); | ||
}); | ||
|
||
it("should cache fetch call in sequential calls", async () => { | ||
const { fetchAssetContentType } = useAssetContentType(); | ||
|
||
expect(await fetchAssetContentType("https://test.com")).toBe( | ||
"image/png", | ||
); | ||
expect(await fetchAssetContentType("https://test.com")).toBe( | ||
"image/png", | ||
); | ||
expect(fetch).toHaveBeenCalledOnce(); | ||
}); | ||
|
||
it("should cache fetch call in parrallel call", async () => { | ||
vi.useFakeTimers(); | ||
|
||
fetch.mockResolvedValue( | ||
new Promise((res) => | ||
setTimeout( | ||
() => | ||
res({ | ||
ok: true, | ||
headers: new Map([["Content-Type", "image/png"]]), | ||
}), | ||
3_000, | ||
), | ||
), | ||
); | ||
|
||
const { fetchAssetContentType } = useAssetContentType(); | ||
|
||
const res1 = fetchAssetContentType("https://test.com"); | ||
const res2 = fetchAssetContentType("https://test.com"); | ||
|
||
vi.advanceTimersByTime(3_000); | ||
|
||
expect(await res1).toBe("image/png"); | ||
expect(await res2).toBe("image/png"); | ||
|
||
expect(fetch).toHaveBeenCalledOnce(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
const cacheUrlContentType = new Map<string, Promise<undefined | string>>(); | ||
|
||
/** | ||
* Do an HTTP `HEAD` call to get the `Content-Type` of an URL. Handle parrallel calls and use a cache mechanism. | ||
*/ | ||
export function useAssetContentType() { | ||
function fetchAssetContentType(url: string) { | ||
const cachedValue = cacheUrlContentType.get(url); | ||
if (cachedValue !== undefined) return cachedValue; | ||
|
||
// we store the promise instead of the result to handle concurent calls | ||
const promise = fetch(url, { method: "HEAD" }) | ||
.then((r) => { | ||
if (!r.ok) return undefined; | ||
return r.headers.get("Content-Type") || undefined; | ||
}) | ||
.catch(() => undefined); | ||
|
||
cacheUrlContentType.set(url, promise); | ||
|
||
return promise; | ||
} | ||
|
||
function clearCache() { | ||
cacheUrlContentType.clear(); | ||
} | ||
|
||
return { fetchAssetContentType, clearCache }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { describe, expect, it } from "vitest"; | ||
import { convertAbsolutePathtoFullURL } from "./url"; | ||
|
||
describe(convertAbsolutePathtoFullURL.name, () => { | ||
it("should convert the URL", () => { | ||
expect( | ||
convertAbsolutePathtoFullURL( | ||
"/assets/image.png", | ||
"http://localhost:3000/", | ||
), | ||
).toBe("http://localhost:3000/assets/image.png"); | ||
}); | ||
|
||
it("should convert the URL with a current path", () => { | ||
expect( | ||
convertAbsolutePathtoFullURL( | ||
"/assets/image.png", | ||
"http://localhost:3000/hello/?foo=bar", | ||
), | ||
).toBe("http://localhost:3000/hello/assets/image.png"); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
/** | ||
* Convert absoule URL to full URLs in case the application is hosted on a subpath. | ||
* | ||
* ```js | ||
* convertAbsolutePathtoFullURL("/assets/image.png", "http://localhost:3000/hello/?foo=bar") | ||
* // => 'http://localhost:3000/hello/assets/image.png' | ||
* ``` | ||
*/ | ||
export function convertAbsolutePathtoFullURL( | ||
path: string, | ||
base = window.location.toString(), | ||
) { | ||
return new URL(`.${path}`, base).toString(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters