mirror of
https://github.com/immich-app/immich.git
synced 2025-12-22 09:15:34 +03:00
refactor: more elements (#22095)
This commit is contained in:
108
web/src/lib/elements/__test__/format-message.spec.ts
Normal file
108
web/src/lib/elements/__test__/format-message.spec.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import FormatTagB from '$lib/elements/__test__/format-tag-b.svelte';
|
||||
import FormatMessage from '$lib/elements/FormatMessage.svelte';
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
import { init, locale, register, waitLocale, type Translations } from 'svelte-i18n';
|
||||
import { describe } from 'vitest';
|
||||
|
||||
const getSanitizedHTML = (container: HTMLElement) => container.innerHTML.replaceAll('<!---->', '');
|
||||
|
||||
describe('FormatMessage component', () => {
|
||||
beforeAll(async () => {
|
||||
register('en', () =>
|
||||
Promise.resolve({
|
||||
hello: 'Hello {name}',
|
||||
html: 'Hello <b>{name}</b>',
|
||||
plural: 'You have <b>{count, plural, one {# item} other {# items}}</b>',
|
||||
xss: '<image/src/onerror=prompt(8)>',
|
||||
plural_with_html: 'You have {count, plural, other {<b>#</b> items}}',
|
||||
select_with_html: 'Item is {status, select, other {<b>disabled</b>}}',
|
||||
ordinal_with_html: '{count, selectordinal, other {<b>#th</b>}} item',
|
||||
}),
|
||||
);
|
||||
|
||||
await init({ fallbackLocale: 'en' });
|
||||
await waitLocale('en');
|
||||
});
|
||||
|
||||
it('formats a plain text message', () => {
|
||||
render(FormatMessage, {
|
||||
key: 'hello' as Translations,
|
||||
values: { name: 'test' },
|
||||
});
|
||||
expect(screen.getByText('Hello test')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('throws an error when locale is empty', async () => {
|
||||
await locale.set(undefined);
|
||||
expect(() => render(FormatMessage, { key: '' as Translations })).toThrowError();
|
||||
await locale.set('en');
|
||||
});
|
||||
|
||||
it('shows raw message when value is empty', () => {
|
||||
render(FormatMessage, {
|
||||
key: 'hello' as Translations,
|
||||
});
|
||||
expect(screen.getByText('Hello {name}')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows message when slot is empty', () => {
|
||||
render(FormatMessage, {
|
||||
key: 'html' as Translations,
|
||||
values: { name: 'test' },
|
||||
});
|
||||
expect(screen.getByText('Hello test')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a message with html', () => {
|
||||
const { container } = render(FormatTagB, {
|
||||
key: 'html' as Translations,
|
||||
values: { name: 'test' },
|
||||
});
|
||||
expect(getSanitizedHTML(container)).toBe('Hello <strong>test</strong>');
|
||||
});
|
||||
|
||||
it('renders a message with html and plural', () => {
|
||||
const { container } = render(FormatTagB, {
|
||||
key: 'plural' as Translations,
|
||||
values: { count: 1 },
|
||||
});
|
||||
expect(getSanitizedHTML(container)).toBe('You have <strong>1 item</strong>');
|
||||
});
|
||||
|
||||
it('protects against XSS injection', () => {
|
||||
render(FormatMessage, {
|
||||
key: 'xss' as Translations,
|
||||
});
|
||||
expect(screen.getByText('<image/src/onerror=prompt(8)>')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays the message key when not found', () => {
|
||||
render(FormatMessage, { key: 'invalid.key' as Translations });
|
||||
expect(screen.getByText('invalid.key')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('supports html tags inside plurals', () => {
|
||||
const { container } = render(FormatTagB, {
|
||||
key: 'plural_with_html' as Translations,
|
||||
values: { count: 10 },
|
||||
});
|
||||
expect(getSanitizedHTML(container)).toBe('You have <strong>10</strong> items');
|
||||
});
|
||||
|
||||
it('supports html tags inside select', () => {
|
||||
const { container } = render(FormatTagB, {
|
||||
key: 'select_with_html' as Translations,
|
||||
values: { status: true },
|
||||
});
|
||||
expect(getSanitizedHTML(container)).toBe('Item is <strong>disabled</strong>');
|
||||
});
|
||||
|
||||
it('supports html tags inside selectordinal', () => {
|
||||
const { container } = render(FormatTagB, {
|
||||
key: 'ordinal_with_html' as Translations,
|
||||
values: { count: 4 },
|
||||
});
|
||||
expect(getSanitizedHTML(container)).toBe('<strong>4th</strong> item');
|
||||
});
|
||||
});
|
||||
20
web/src/lib/elements/__test__/format-tag-b.svelte
Normal file
20
web/src/lib/elements/__test__/format-tag-b.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import FormatMessage from '$lib/elements/FormatMessage.svelte';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import type { Translations } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
key: Translations;
|
||||
values: ComponentProps<typeof FormatMessage>['values'];
|
||||
}
|
||||
|
||||
let { key, values }: Props = $props();
|
||||
</script>
|
||||
|
||||
<FormatMessage {key} {values}>
|
||||
{#snippet children({ tag, message })}
|
||||
{#if tag === 'b'}
|
||||
<strong>{message}</strong>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</FormatMessage>
|
||||
78
web/src/lib/elements/__test__/star-rating.spec.ts
Normal file
78
web/src/lib/elements/__test__/star-rating.spec.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import StarRating from '$lib/elements/StarRating.svelte';
|
||||
import { render } from '@testing-library/svelte';
|
||||
|
||||
describe('StarRating component', () => {
|
||||
it('renders correctly', () => {
|
||||
const component = render(StarRating, {
|
||||
count: 3,
|
||||
rating: 2,
|
||||
readOnly: false,
|
||||
onRating: vi.fn(),
|
||||
});
|
||||
const container = component.getByTestId('star-container') as HTMLImageElement;
|
||||
expect(container.className).toBe('flex flex-row');
|
||||
|
||||
const radioButtons = component.getAllByRole('radio') as HTMLInputElement[];
|
||||
expect(radioButtons.length).toBe(3);
|
||||
const labels = component.getAllByTestId('star') as HTMLLabelElement[];
|
||||
expect(labels.length).toBe(3);
|
||||
const labelText = component.getAllByText('rating_count') as HTMLSpanElement[];
|
||||
expect(labelText.length).toBe(3);
|
||||
const clearButton = component.getByRole('button') as HTMLButtonElement;
|
||||
expect(clearButton).toBeInTheDocument();
|
||||
|
||||
// Check the clear button content
|
||||
expect(clearButton.textContent).toBe('rating_clear');
|
||||
|
||||
// Check the initial state
|
||||
expect(radioButtons[0].checked).toBe(false);
|
||||
expect(radioButtons[1].checked).toBe(true);
|
||||
expect(radioButtons[2].checked).toBe(false);
|
||||
|
||||
// Check the radio button attributes
|
||||
for (const [index, radioButton] of radioButtons.entries()) {
|
||||
expect(radioButton.id).toBe(labels[index].htmlFor);
|
||||
expect(radioButton.name).toBe('stars');
|
||||
expect(radioButton.value).toBe((index + 1).toString());
|
||||
expect(radioButton.disabled).toBe(false);
|
||||
expect(radioButton.className).toBe('sr-only');
|
||||
}
|
||||
|
||||
// Check the label attributes
|
||||
for (const label of labels) {
|
||||
expect(label.className).toBe('cursor-pointer');
|
||||
expect(label.tabIndex).toBe(-1);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders correctly with readOnly', () => {
|
||||
const component = render(StarRating, {
|
||||
count: 3,
|
||||
rating: 2,
|
||||
readOnly: true,
|
||||
onRating: vi.fn(),
|
||||
});
|
||||
const radioButtons = component.getAllByRole('radio') as HTMLInputElement[];
|
||||
expect(radioButtons.length).toBe(3);
|
||||
const labels = component.getAllByTestId('star') as HTMLLabelElement[];
|
||||
expect(labels.length).toBe(3);
|
||||
const clearButton = component.queryByRole('button');
|
||||
expect(clearButton).toBeNull();
|
||||
|
||||
// Check the initial state
|
||||
expect(radioButtons[0].checked).toBe(false);
|
||||
expect(radioButtons[1].checked).toBe(true);
|
||||
expect(radioButtons[2].checked).toBe(false);
|
||||
|
||||
// Check the radio button attributes
|
||||
for (const [index, radioButton] of radioButtons.entries()) {
|
||||
expect(radioButton.id).toBe(labels[index].htmlFor);
|
||||
expect(radioButton.disabled).toBe(true);
|
||||
}
|
||||
|
||||
// Check the label attributes
|
||||
for (const label of labels) {
|
||||
expect(label.className).toBe('');
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user