Files
privacy.sexy/tests/unit/presentation/components/Shared/Icon/UseSvgLoader.spec.ts
undergroundwires 48730bca05 Implement new UI component for icons #230
- Introduce `AppIcon.vue`, offering improved performance over the
  previous `fort-awesome` dependency. This implementation reduces bundle
  size by 67.31KB (tested for web using `npm run build -- --mode prod`).
- Migrate Font Awesome 5 icons to Font Awesome 6.

This commit facilitates migration to Vue 3.0 (#230) and ensures no Vue
component remains tightly bound to a specific Vue version, enhancing
code portability.

Font Awesome license is not included because Font Awesome revokes its
right:

> "Attribution is no longer required as of Font Awesome 3.0"
>
> Sources:
>
> - https://fontawesome.com/v4/license/ (archived: https://web.archive.org/web/20231003213441/https://fontawesome.com/v4/license/, https://archive.ph/Yy9j5)
> - https://github.com/FortAwesome/Font-Awesome/wiki (archived: https://web.archive.org/web/20231003214646/https://github.com/FortAwesome/Font-Awesome/wiki, https://archive.ph/C6sXv)

This commit removes following third-party production dependencies:

- `@fortawesome/vue-fontawesome`
- `@fortawesome/free-solid-svg-icons`
- `@fortawesome/free-regular-svg-icons`
- `@fortawesome/free-brands-svg-icons`
- `@fortawesome/fontawesome-svg-core`
2023-10-11 18:38:19 +02:00

161 lines
5.8 KiB
TypeScript

import {
describe, it, expect, beforeEach,
} from 'vitest';
import { ref } from 'vue';
import { IconName } from '@/presentation/components/Shared/Icon/IconName';
import { FileLoaders, clearIconCache, useSvgLoader } from '@/presentation/components/Shared/Icon/UseSvgLoader';
import { waitForValueChange } from '@tests/shared/WaitForValueChange';
describe('useSvgLoader', () => {
beforeEach(() => {
clearIconCache();
});
describe('SVG loading', () => {
it('renders initial SVG content based on icon name', async () => {
// arrange
const expectedIconName: IconName = 'magnifying-glass';
const expectedIconContent = '<svg id="expected-content"/>';
const { loaders, addIcon } = useSvgMock();
addIcon(expectedIconName, expectedIconContent);
// act
const { svgContent } = useSvgLoader(() => expectedIconName, loaders);
await waitForValueChange(svgContent);
// assert
expect(svgContent.value).to.equal(expectedIconContent);
});
it('updates SVG content when icon name changes', async () => {
// arrange
const initialIconName: IconName = 'magnifying-glass';
const iconName = ref<IconName>(initialIconName);
const initialIconContent = '<svg id="initial"/>';
const updatedIconName: IconName = 'copy';
const updatedIconContent = '<svg id="updated"/>';
const { addIcon, loaders } = useSvgMock();
addIcon(initialIconName, initialIconContent);
addIcon(updatedIconName, updatedIconContent);
// act
const { svgContent } = useSvgLoader(() => iconName.value, loaders);
await waitForValueChange(svgContent);
iconName.value = updatedIconName;
await waitForValueChange(svgContent);
// assert
expect(svgContent.value).to.equal(updatedIconContent);
});
it('lazy loads SVG icons and does not preload', async () => {
// arrange
const expectedIconName: IconName = 'magnifying-glass';
const unexpectedIconName: IconName = 'copy';
const { addIcon, getSvgFetchCount, loaders } = useSvgMock();
addIcon(expectedIconName);
addIcon(unexpectedIconName);
// act
const { svgContent } = useSvgLoader(() => expectedIconName, loaders);
await waitForValueChange(svgContent);
// assert
expect(getSvgFetchCount(expectedIconName)).to.equal(1);
expect(getSvgFetchCount(unexpectedIconName)).to.equal(0);
});
it('avoids loading same SVG content multiple times for concurrent calls', async () => {
// arrange
const expectedIconName: IconName = 'magnifying-glass';
const { addIcon, getSvgFetchCount, loaders } = useSvgMock();
addIcon(expectedIconName);
// act
const { svgContent: svgContent1 } = useSvgLoader(() => expectedIconName, loaders);
const { svgContent: svgContent2 } = useSvgLoader(() => expectedIconName, loaders);
await Promise.all([
waitForValueChange(svgContent1),
waitForValueChange(svgContent2),
]);
// assert
expect(getSvgFetchCount(expectedIconName)).to.equal(1);
});
});
describe('SVG content manipulation', () => {
it('sets path fill color to currentColor', async () => {
// arrange
const expectedIconName: IconName = 'magnifying-glass';
const { addIcon, loaders } = useSvgMock();
addIcon(expectedIconName, '<svg id="svg-with-paths"><path /><path /></svg>');
// act
const { svgContent } = useSvgLoader(() => expectedIconName, loaders);
await waitForValueChange(svgContent);
// assert
const svgElement = new DOMParser().parseFromString(svgContent.value, 'image/svg+xml');
const pathElements = Array.from(svgElement.querySelectorAll('path'));
expect(pathElements).to.have.lengthOf(2, svgContent.value);
const fillAttributeValues = pathElements.map((el: Element) => el.getAttribute('fill'));
expect(fillAttributeValues).to.have.members(['currentColor', 'currentColor']);
});
it('removes comments from loaded SVG', async () => {
// arrange
const commentLine = '<!-- This is a comment -->';
const expectedIconName: IconName = 'magnifying-glass';
const { addIcon, loaders } = useSvgMock();
addIcon(expectedIconName, `<svg>${commentLine}<path></path></svg>`);
// act
const { svgContent } = useSvgLoader(() => expectedIconName, loaders);
await waitForValueChange(svgContent);
// assert
expect(svgContent.value).not.to.include(commentLine);
});
});
describe('icon cache management', () => {
it('reloads SVG content after clearing cache', async () => {
// arrange
const expectedIconName: IconName = 'magnifying-glass';
const { addIcon, getSvgFetchCount, loaders } = useSvgMock();
addIcon(expectedIconName);
// act
const { svgContent } = useSvgLoader(() => expectedIconName, loaders);
await waitForValueChange(svgContent);
expect(getSvgFetchCount(expectedIconName)).to.equal(1);
clearIconCache();
const { svgContent: newSvgContent } = useSvgLoader(() => expectedIconName, loaders);
await waitForValueChange(newSvgContent);
// assert
expect(getSvgFetchCount(expectedIconName)).to.equal(2);
});
});
});
function useSvgMock() {
const ICON_PATH_PREFIX = '/assets/icons/';
function getPath(iconName: IconName) {
return `${ICON_PATH_PREFIX}${iconName}.svg`;
}
const svgFetchCount = {} as Record<IconName, number>;
const loaders = {} as FileLoaders;
function addIcon(iconName: IconName, svgContent = '<svg id="stub" />') {
const path = getPath(iconName);
svgFetchCount[iconName] = 0;
loaders[path] = () => {
svgFetchCount[iconName] += 1;
return Promise.resolve(svgContent);
};
}
function getSvgFetchCount(iconName: IconName): number {
return svgFetchCount[iconName];
}
return {
loaders,
getSvgFetchCount,
getPath,
addIcon,
};
}