Files
privacy.sexy/tests/unit/presentation/components/Shared/Icon/AppIcon.spec.ts
undergroundwires 8ccaec7af6 Fix unresponsive copy button on instructions modal
This commit fixes the bug where the "Copy" button does not copy when
clicked on download instructions modal (on Linux and macOS).

This commit also introduces several improvements to the UI components
related to copy action and their interaction with the clipboard feature.

It adds more tests to avoid regression of the bugs and improves
maintainability, testability and adherence to Vue's reactive principles.

Changes include:

- Fix non-responsive copy button in the download instructions modal by
  triggering a `click` event in `AppIcon.vue`.
- Improve `TheCodeButtons.vue`:
  - Remove redundant `getCurrentCode` function.
  - Separate components for each button for better separation of
    concerns and higher testability.
  - Use the `gap` property in the flexbox layout, replacing the less
    explicit sibling combinator approach.
- Add `useClipboard` compositional hook for more idiomatic Vue approach
  to interacting with the clipboard.
- Add `useCurrentCode` compositional hook to handle current code state
  more effectively with unified logic.
- Abstract clipboard operations to an interface to isolate
  responsibilities.
- Switch clipboard implementation to the `navigator.clipboard` API,
  moving away from the deprecated `document.execCommand`.
- Move clipboard logic to the presentation layer to conform to
  separation of concerns and domain-driven design principles.
- Improve `IconButton.vue` component to increase reusability with
  consistent sizing.
2023-11-06 21:55:43 +01:00

115 lines
3.7 KiB
TypeScript

import {
describe, it, expect,
} from 'vitest';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
import { IconName } from '@/presentation/components/Shared/Icon/IconName';
import { UseSvgLoaderStub } from '@tests/unit/shared/Stubs/UseSvgLoaderStub';
describe('AppIcon.vue', () => {
it('renders the correct SVG content based on the icon prop', async () => {
// arrange
const expectedIconName: IconName = 'magnifying-glass';
const expectedIconContent = '<svg id="expected-svg" />';
const svgLoaderStub = new UseSvgLoaderStub();
svgLoaderStub.withSvgIcon(expectedIconName, expectedIconContent);
// act
const wrapper = mountComponent({
iconPropValue: expectedIconName,
loader: svgLoaderStub,
});
await nextTick();
// assert
const actualSvg = extractAndNormalizeSvg(wrapper.html());
const expectedSvg = extractAndNormalizeSvg(expectedIconContent);
expect(actualSvg).to.equal(
expectedSvg,
`Expected:\n\n${expectedSvg}\n\nActual:\n\n${actualSvg}`,
);
});
it('updates the SVG content when the icon prop changes', async () => {
// arrange
const initialIconName: IconName = 'magnifying-glass';
const updatedIconName: IconName = 'copy';
const updatedIconContent = '<svg id="updated-svg" />';
const svgLoaderStub = new UseSvgLoaderStub();
svgLoaderStub.withSvgIcon(initialIconName, '<svg id="initial-svg" />');
svgLoaderStub.withSvgIcon(updatedIconName, updatedIconContent);
// act
const wrapper = mountComponent({
iconPropValue: initialIconName,
loader: svgLoaderStub,
});
await wrapper.setProps({ icon: updatedIconName });
await nextTick();
// assert
const actualSvg = extractAndNormalizeSvg(wrapper.html());
const expectedSvg = extractAndNormalizeSvg(updatedIconContent);
expect(actualSvg).to.equal(
expectedSvg,
`Expected:\n\n${expectedSvg}\n\nActual:\n\n${actualSvg}`,
);
});
it('emits `click` event when clicked', async () => {
// arrange
const wrapper = mountComponent();
// act
await wrapper.trigger('click');
// assert
expect(wrapper.emitted().click).to.have.lengthOf(1);
});
});
function mountComponent(options?: {
readonly iconPropValue?: IconName,
readonly loader?: UseSvgLoaderStub,
}) {
const iconName = options?.iconPropValue ?? 'globe';
const loaderStub = options?.loader ?? new UseSvgLoaderStub().withSvgIcon(iconName, '<svg />');
return shallowMount(AppIcon, {
props: {
icon: iconName,
},
global: {
provide: {
useSvgLoaderHook: loaderStub.get(),
},
},
});
}
function extractAndNormalizeSvg(svgString: string): string {
const svg = extractSvg(svgString);
return normalizeSvg(svg);
}
function extractSvg(svgString: string): string {
const svgMatches = svgString.match(/<svg[\s\S]*?(<\/svg>|\/>)/g);
if (!svgMatches || svgMatches.length === 0) {
throw new Error(`No SVG found in: ${svgString}`);
}
if (svgMatches.length > 1) {
throw new Error(`Multiple SVGs found in: ${svgString}`);
}
const svgContent = svgMatches[0];
return svgContent;
}
function normalizeSvg(svgString: string): string {
return svgString
.replace(/\n/g, '') // Remove newlines
.replace(/\s+/g, ' ') // Replace all whitespace sequences with a single space
.replace(/> </g, '><') // Remove spaces between tags
.replace(/ <\//g, '</') // Remove spaces before closing tags
.replace(/\s+\/>/g, '/>') // Remove spaces before self-closing tag end
.replace(/<(\w+)([^>]*)><\/\1>/g, '<$1$2/>') // Convert to self-closing SVG tags
.trim(); // Remove leading and trailing spaces
}