refactor: extract OverlayAvatar into component

I'm going to break up my MainMenu into smaller components, starting with
the OverlayAvatar. Testing it taught me how to handle stores in Vitest.

Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
This commit is contained in:
André Jaenisch 2024-07-08 12:17:31 +02:00
parent c779fad4b1
commit 179db76b5a
No known key found for this signature in database
GPG key ID: 5A668E771F1ED854
10 changed files with 278 additions and 2 deletions

View file

@ -0,0 +1,53 @@
<!--
Overlay avatar atom.
Copyright (C) 2024 André Jaenisch
SPDX-FileCopyrightText: 2024 André Jaenisch
SPDX-License-Identifier: AGPL-3.0-or-later
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<script>
import { getModalStore } from '@skeletonlabs/skeleton';
import { _ } from 'svelte-i18n';
import { Gear24, Info24, Moon24, Person24, SignOut24 } from 'svelte-octicons';
const modal = {
type: 'component',
component: 'settings'
};
const modalStore = getModalStore();
function openSettingsOnClick() {
modalStore.trigger(modal);
}
</script>
<div class="flex flex-col bg-white border-surface-300 border-rounded drop-shadow px-4 py-2">
<div class="bg-white flex gap-4">
<Person24 fill="currentColor" class="text-white bg-surface-400 rounded-full" />
<span class="text-surface-500 -mb-2">{$_('overlay.avatar.profile')}</span>
</div>
<div class="bg-white flex gap-4">
<Gear24 fill="currentColor" />
<button type="button" class="text-surface-500 -mb-2" on:click={openSettingsOnClick}
>{$_('overlay.avatar.settings')}</button
>
</div>
<div class="bg-white flex gap-4">
<Moon24 fill="currentColor" />
<span class="text-surface-500 -mb-2">{$_('overlay.avatar.dark_mode')}</span>
<span>Slider missing</span>
</div>
<div class="bg-white flex gap-4">
<Info24 fill="currentColor" />
<span class="text-surface-500 -mb-2">{$_('overlay.avatar.about_anvil')}</span>
</div>
<div class="bg-white flex gap-4">
<SignOut24 fill="currentColor" />
<span class="text-surface-500 -mb-2">{$_('overlay.avatar.sign_out')}</span>
</div>
</div>

View file

@ -1,4 +1,13 @@
{ {
"overlay": {
"avatar": {
"about_anvil": "",
"dark_mode": "",
"profile": "",
"settings": "",
"sign_out": ""
}
},
"page": { "page": {
"import_project": { "import_project": {
"form": { "form": {

View file

@ -1,4 +1,13 @@
{ {
"overlay": {
"avatar": {
"about_anvil": "",
"dark_mode": "",
"profile": "",
"settings": "",
"sign_out": ""
}
},
"page": { "page": {
"import_project": { "import_project": {
"form": { "form": {

View file

@ -1,4 +1,13 @@
{ {
"overlay": {
"avatar": {
"about_anvil": "About Anvil",
"dark_mode": "Dark mode",
"profile": "Profile",
"settings": "Settings",
"sign_out": "Sign out"
}
},
"page": { "page": {
"import_project": { "import_project": {
"form": { "form": {

View file

@ -1,4 +1,13 @@
{ {
"overlay": {
"avatar": {
"about_anvil": "",
"dark_mode": "",
"profile": "",
"settings": "",
"sign_out": ""
}
},
"page": { "page": {
"import_project": { "import_project": {
"form": { "form": {

View file

@ -1,4 +1,13 @@
{ {
"overlay": {
"avatar": {
"about_anvil": "",
"dark_mode": "",
"profile": "",
"settings": "",
"sign_out": ""
}
},
"page": { "page": {
"import_project": { "import_project": {
"form": { "form": {

View file

@ -0,0 +1,26 @@
/* Stories for OverlayAvatar atom.
* Copyright (C) 2024 André Jaenisch
* SPDX-FileCopyrightText: 2024 André Jaenisch
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import type { Meta, StoryObj } from '@storybook/svelte';
import OverlayAvatar from '$lib/components/atoms/OverlayAvatar.svelte';
const meta = {
title: 'Atoms/OverlayAvatar',
component: OverlayAvatar,
tags: ['autodocs']
} satisfies Meta<OverlayAvatar>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Plain: Story = {};

View file

@ -0,0 +1,137 @@
/* Component test for OverlayAvatar atom.
* Copyright (C) 2024 André Jaenisch
* SPDX-FileCopyrightText: 2024 André Jaenisch
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { initializeStores } from '@skeletonlabs/skeleton';
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/svelte';
import { readable } from 'svelte/store';
import { init, locale, register } from 'svelte-i18n';
import OverlayAvatar from '../../../src/lib/components/atoms/OverlayAvatar.svelte';
import enMessages from '../../../src/lib/i18n/locales/en.json';
describe('OverlayAvatar.svelte', () => {
beforeEach(() => {
register('en', () => import('../../../src/lib/i18n/locales/en.json'));
init({ fallbackLocale: 'en', initialLocale: 'en' });
locale.set('en');
});
it('should mount', () => {
// Arrange
// See https://testing-library.com/docs/svelte-testing-library/example#contexts
const modalStore = readable([]);
// Act
const { container } = render(OverlayAvatar, {
context: new Map([['modalStore', modalStore]])
});
// Assert
expect(container).toBeTruthy();
});
it('should have a Profile item', () => {
// Arrange
const modalStore = readable([]);
// Act
render(OverlayAvatar, {
context: new Map([['modalStore', modalStore]])
});
// Assert
expect(screen.getByText(enMessages.overlay.avatar.profile)).toBeInTheDocument();
});
it('should have a Settings item', () => {
// Arrange
const modalStore = readable([]);
// Act
render(OverlayAvatar, {
context: new Map([['modalStore', modalStore]])
});
// Assert
expect(screen.getByText(enMessages.overlay.avatar.settings)).toBeInTheDocument();
});
it('should have a Dark mode item', () => {
// Arrange
const modalStore = readable([]);
// Act
render(OverlayAvatar, {
context: new Map([['modalStore', modalStore]])
});
// Assert
expect(screen.getByText(enMessages.overlay.avatar.dark_mode)).toBeInTheDocument();
});
it('should have a About Anvil item', () => {
// Arrange
const modalStore = readable([]);
// Act
render(OverlayAvatar, {
context: new Map([['modalStore', modalStore]])
});
// Assert
expect(screen.getByText(enMessages.overlay.avatar.about_anvil)).toBeInTheDocument();
});
it('should have a Sign out item', () => {
// Arrange
const modalStore = readable([]);
// Act
render(OverlayAvatar, {
context: new Map([['modalStore', modalStore]])
});
// Assert
expect(screen.getByText(enMessages.overlay.avatar.sign_out)).toBeInTheDocument();
});
it('should open a modal on click on settings', () => {
// Arrange
const triggerSpy = vi.fn();
const { subscribe, set, update } = readable([]);
// Act
render(OverlayAvatar, {
context: new Map([
[
'modalStore',
{
subscribe,
set,
update,
trigger: triggerSpy,
close: vi.fn(),
clear: vi.fn()
}
]
])
});
const settings = screen.getByRole('button', {
name: enMessages.overlay.avatar.settings
});
settings.click();
// Assert
expect(triggerSpy).toHaveBeenCalledOnce();
});
});

13
tests/components/setup.ts Normal file
View file

@ -0,0 +1,13 @@
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(), // deprecated
removeListener: vi.fn(), // deprecated
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn()
}))
});

View file

@ -13,6 +13,7 @@
import { resolve } from 'node:path'; import { resolve } from 'node:path';
import { svelte } from '@sveltejs/vite-plugin-svelte'; import { svelte } from '@sveltejs/vite-plugin-svelte';
import { svelteTesting } from '@testing-library/svelte/vite';
import { defineConfig } from 'vitest/config'; import { defineConfig } from 'vitest/config';
export default defineConfig({ export default defineConfig({
@ -22,13 +23,14 @@ export default defineConfig({
$lib: resolve('./src/lib') $lib: resolve('./src/lib')
} }
}, },
plugins: [svelte({ hot: !process.env.VITEST })], plugins: [svelte({ hot: !process.env.VITEST }), svelteTesting()],
test: { test: {
coverage: { coverage: {
include: ['src'] include: ['src']
}, },
include: ['tests/**/*.test.ts'], include: ['tests/**/*.test.ts'],
environment: 'jsdom', environment: 'jsdom',
globals: true globals: true,
setupFiles: [resolve(__dirname, 'tests/components/setup.ts')]
} }
}); });