feat: bringing it all together

This is now showing the MainMenu on the profile page which in turn
pulls in the Settings modal.

I wasn't able to register the modal on Storybook yet, which means
that there won't be any on the Profile template or page story.

The implementation in Skeleton is causing some accessibility
warnings. However, since there next version is taking a radical
different approach I'm inclined to let it be for now. We should
document it somewhere, I guess.

Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
This commit is contained in:
André Jaenisch 2024-07-27 14:12:08 +02:00
parent 581ab76ca7
commit f20587cf1f
No known key found for this signature in database
GPG key ID: 5A668E771F1ED854
12 changed files with 639 additions and 259 deletions

View file

@ -12,33 +12,19 @@ You should have received a copy of the GNU Affero General Public License along w
--> -->
<script> <script>
import { import { AppRail, AppRailAnchor, AppRailTile } from '@skeletonlabs/skeleton';
AppRail, import { createEventDispatcher } from 'svelte';
AppRailAnchor,
AppRailTile,
Drawer,
getDrawerStore
} from '@skeletonlabs/skeleton';
import { _, locale, locales } from 'svelte-i18n'; import { _, locale, locales } from 'svelte-i18n';
import { Bell24, Gear24, Key24, Paintbrush24, Person24 } from 'svelte-octicons'; import { Bell24, Gear24, Key24, Paintbrush24, Person24 } from 'svelte-octicons';
let currentTile = 0; let currentTile = 0;
const drawerStore = getDrawerStore(); const dispatcher = createEventDispatcher();
function onTileChanged(ev) { function onTileChanged(ev) {
const settings = { id: ev.target.value }; dispatcher('switch-settings', ev.target.value);
drawerStore.open(settings);
} }
</script> </script>
<Drawer>
{#if $drawerStore.id === 'profile'}
<b>Profile settings go here</b>
{:else}
{$drawerStore.id}
{/if}
</Drawer>
<AppRail aspectRatio="auto" gap="gap-8" height="h-auto" width="w-[230px]" spacing="my-2"> <AppRail aspectRatio="auto" gap="gap-8" height="h-auto" width="w-[230px]" spacing="my-2">
<svelte:fragment slot="lead"> <svelte:fragment slot="lead">
<AppRailTile <AppRailTile

View file

@ -42,7 +42,8 @@ You should have received a copy of the GNU Affero General Public License along w
<div class="flex flex-col bg-white border-surface-300 rounded-border px-4"> <div class="flex flex-col bg-white border-surface-300 rounded-border px-4">
<div class="flex gap-4 bg-white"> <div class="flex gap-4 bg-white">
<Code24 fill="currentColor" /> <Code24 fill="currentColor" />
<span class="text-surface-500 -mb-2 flex-1">{$_('page.profile.menu.details.repository')}</span> <span class="text-surface-500 -mb-2 flex-1">{$_('page.profile.menu.details.repository')}</span
>
<ChevronDown24 fill="currentColor" /> <ChevronDown24 fill="currentColor" />
</div> </div>
<div class="flex flex-col ps-4"> <div class="flex flex-col ps-4">
@ -52,7 +53,8 @@ You should have received a copy of the GNU Affero General Public License along w
</div> </div>
<div class="flex gap-4 bg-white"> <div class="flex gap-4 bg-white">
<GitBranch24 fill="currentColor" /> <GitBranch24 fill="currentColor" />
<span class="text-surface-500 -mb-2 flex-1">{$_('page.profile.menu.details.branches')}</span> <span class="text-surface-500 -mb-2 flex-1">{$_('page.profile.menu.details.branches')}</span
>
</div> </div>
<div class="flex gap-4 bg-white"> <div class="flex gap-4 bg-white">
<GitCommit24 fill="currentColor" /> <GitCommit24 fill="currentColor" />
@ -70,9 +72,10 @@ You should have received a copy of the GNU Affero General Public License along w
</div> </div>
<div class="flex gap-4 bg-white"> <div class="flex gap-4 bg-white">
<GitPullRequest24 fill="currentColor" /> <GitPullRequest24 fill="currentColor" />
<span class="text-surface-500 -mb-2 flex-1">{$_('page.profile.menu.details.merge_requests')}</span> <span class="text-surface-500 -mb-2 flex-1"
<span >{$_('page.profile.menu.details.merge_requests')}</span
class="text-surface-500 text-center bg-secondary-200 rounded-full min-w-6 self-baseline" >
<span class="text-surface-500 text-center bg-secondary-200 rounded-full min-w-6 self-baseline"
>2</span >2</span
> >
</div> </div>
@ -88,7 +91,9 @@ You should have received a copy of the GNU Affero General Public License along w
</div> </div>
<div class="flex gap-4 bg-white"> <div class="flex gap-4 bg-white">
<CommentDiscussion24 fill="currentColor" /> <CommentDiscussion24 fill="currentColor" />
<span class="text-surface-500 -mb-2 flex-1">{$_('page.profile.menu.details.moderation')}</span> <span class="text-surface-500 -mb-2 flex-1"
>{$_('page.profile.menu.details.moderation')}</span
>
</div> </div>
</div> </div>
<div class="flex gap-4 bg-white"> <div class="flex gap-4 bg-white">
@ -98,4 +103,3 @@ You should have received a copy of the GNU Affero General Public License along w
</div> </div>
</div> </div>
</div> </div>

View file

@ -14,9 +14,18 @@ You should have received a copy of the GNU Affero General Public License along w
<script> <script>
import SettingsSidebar from '../atoms/SettingsSidebar.svelte'; import SettingsSidebar from '../atoms/SettingsSidebar.svelte';
import SettingsProfile from './SettingsProfile.svelte'; import SettingsProfile from './SettingsProfile.svelte';
let activeSetting = 'profile';
function onTileChanged(ev) {
activeSetting = ev.detail;
}
</script> </script>
<div class="flex bg-surface-50"> <div class="flex bg-surface-50">
<SettingsSidebar /> <SettingsSidebar on:switch-settings={onTileChanged} />
<SettingsProfile />
{#if activeSetting === 'profile'}
<SettingsProfile />
{/if}
</div> </div>

View file

@ -20,6 +20,7 @@ You should have received a copy of the GNU Affero General Public License along w
import Created from '../atoms/Created.svelte'; import Created from '../atoms/Created.svelte';
import DisplayName from '../atoms/DisplayName.svelte'; import DisplayName from '../atoms/DisplayName.svelte';
import History from '../molecules/History.svelte'; import History from '../molecules/History.svelte';
import MainMenu from '../molecules/MainMenu.svelte';
import Project from '../molecules/Project.svelte'; import Project from '../molecules/Project.svelte';
/** /**
@ -71,129 +72,140 @@ You should have received a copy of the GNU Affero General Public License along w
}; };
</script> </script>
<section class="w-full mx-auto flex px-8 pt-8"> <div class="flex h-full w-full">
<!-- Profile header --> <aside class="w-[400px]">
<div> <MainMenu />
<Avatar avatar={data.user.avatar} displayName={data.user.display_name} /> </aside>
</div> <main>
<!-- Board --> <section class="w-full mx-auto flex px-8 pt-8">
<div class="flex flex-1 flex-col ps-8"> <!-- Profile header -->
<div class="self-end"> <div>
<Created created_at={data.user.created_at} locale={data.locale} /> <Avatar avatar={data.user.avatar} displayName={data.user.display_name} />
<BlockOrReport />
</div>
<!-- Top Row -->
<div class="flex mt-2">
<!-- Board -->
<div class="flex flex-col flex-1 gap-2">
<div class="flex items-end">
<DisplayName displayName={data.user.display_name} {i18n} pronoun={data.user.pronoun} />
</div>
<span class="leading-tight">
{data.user.username}@{data.user.instance}
</span>
</div> </div>
<!-- Interaction Links --> <!-- Board -->
<button type="button" class="btn-icon border rounded-none self-start"> <div class="flex flex-1 flex-col ps-8">
<Star16 fill="currentColor" /> <div class="self-end">
</button> <Created created_at={data.user.created_at} locale={data.locale} />
</div> <BlockOrReport />
</div> </div>
</section> <!-- Top Row -->
<section class="w-full h-full mx-auto flex flex-wrap mt-8 px-8"> <div class="flex mt-2">
<!-- Board --> <!-- Board -->
<div class="flex flex-1 flex-col px-4"> <div class="flex flex-col flex-1 gap-2">
<div class="space-y-6"> <div class="flex items-end">
<h2 class="h2 font-semibold leading-tight text-base px-4"> <DisplayName
{$_('page.profile.projects.heading')} displayName={data.user.display_name}
</h2> {i18n}
{#if data?.user?.followingsMap} pronoun={data.user.pronoun}
<ul> />
{#each data.user.followingsMap as following} </div>
{#if following.type === 'Project'} <span class="leading-tight">
<li> {data.user.username}@{data.user.instance}
<Project </span>
collaborators={mapCollaboratorsToMember( </div>
[data.user.person], <!-- Interaction Links -->
data.user.collaboratorsMap, <button type="button" class="btn-icon border rounded-none self-start">
following.id <Star16 fill="currentColor" />
)} </button>
{i18n} </div>
project={following} </div>
/> </section>
</li> <section class="w-full h-full mx-auto flex flex-wrap mt-8 px-8">
{/if} <!-- Board -->
{/each} <div class="flex flex-1 flex-col px-4">
</ul> <div class="space-y-6">
<h2 class="h2 font-semibold leading-tight text-base px-4">
{$_('page.profile.projects.heading')}
</h2>
{#if data?.user?.followingsMap}
<ul>
{#each data.user.followingsMap as following}
{#if following.type === 'Project'}
<li>
<Project
collaborators={mapCollaboratorsToMember(
[data.user.person],
data.user.collaboratorsMap,
following.id
)}
{i18n}
project={following}
/>
</li>
{/if}
{/each}
</ul>
<h2 class="h2 font-semibold leading-tight text-base px-4"> <h2 class="h2 font-semibold leading-tight text-base px-4">
{$_('page.profile.repositories.heading')} {$_('page.profile.repositories.heading')}
</h2> </h2>
<section class="flex flex-col gap-4"> <section class="flex flex-col gap-4">
{#each data.user.followingsMap as following} {#each data.user.followingsMap as following}
{#if following.type === 'Repository'} {#if following.type === 'Repository'}
<div class="border border-surface-100"> <div class="border border-surface-100">
<Project collaborators={[]} {i18n} project={following} /> <Project collaborators={[]} {i18n} project={following} />
</div> </div>
{/if} {/if}
{/each} {/each}
</section> </section>
<div> <div>
<p>The following is DEBUG information and will be removed in the near future.</p> <p>The following is DEBUG information and will be removed in the near future.</p>
{#if data?.user} {#if data?.user}
<details> <details>
<summary>Person</summary> <summary>Person</summary>
<pre>{JSON.stringify(data.user.person, null, 2)}</pre> <pre>{JSON.stringify(data.user.person, null, 2)}</pre>
</details> </details>
<details> <details>
<summary>Followings</summary> <summary>Followings</summary>
<pre>{JSON.stringify(data.user.followings, null, 2)}</pre> <pre>{JSON.stringify(data.user.followings, null, 2)}</pre>
</details> </details>
<details> <details>
<summary>Followings mapped</summary> <summary>Followings mapped</summary>
<p>These are the contents of the items in the previous dump.</p> <p>These are the contents of the items in the previous dump.</p>
<pre>{JSON.stringify(data.user.followingsMap, null, 2)}</pre> <pre>{JSON.stringify(data.user.followingsMap, null, 2)}</pre>
</details> </details>
<details> <details>
<summary>Collaborators mapped</summary> <summary>Collaborators mapped</summary>
<p>These are the contents of the items in the previous dump.</p> <p>These are the contents of the items in the previous dump.</p>
<pre>{JSON.stringify(data.user.collaboratorsMap, null, 2)}</pre> <pre>{JSON.stringify(data.user.collaboratorsMap, null, 2)}</pre>
</details> </details>
<details> <details>
<summary>Commits</summary> <summary>Commits</summary>
<p>These are the contents of the items in the previous dump.</p> <p>These are the contents of the items in the previous dump.</p>
<pre>{JSON.stringify(data.user.commits, null, 2)}</pre> <pre>{JSON.stringify(data.user.commits, null, 2)}</pre>
</details> </details>
<details> <details>
<summary>Commits mapped</summary> <summary>Commits mapped</summary>
<p>These are the contents of the items in the previous dump.</p> <p>These are the contents of the items in the previous dump.</p>
<pre>{JSON.stringify(data.user.commitsMap, null, 2)}</pre> <pre>{JSON.stringify(data.user.commitsMap, null, 2)}</pre>
</details> </details>
{/if}
</div>
{:else}
<p class="px-4">
{$_('page.profile.projects.empty')}
{@html $_('page.profile.projects.add_or_import', {
values: {
addElementOpen: '<a class="anchor" href="#">',
addElementClose: '</a>',
importElementOpen: `<a class="anchor" href="/projects/import/" data-sb-kind=${sb}>`,
importElementClose: '</a>'
}
})}
</p>
{/if} {/if}
</div> </div>
{:else} </div>
<p class="px-4"> <!-- History -->
{$_('page.profile.projects.empty')} <div class="flex flex-1 flex-col px-4">
{@html $_('page.profile.projects.add_or_import', { <div class="space-y-6">
values: { <History
addElementOpen: '<a class="anchor" href="#">', commitsMap={data?.user?.commitsMap || []}
addElementClose: '</a>', {data}
importElementOpen: `<a class="anchor" href="/projects/import/" data-sb-kind=${sb}>`, followingsMap={data?.user?.followingsMap || []}
importElementClose: '</a>' />
} </div>
})} </div>
</p> </section>
{/if} </main>
</div> </div>
</div>
<!-- History -->
<div class="flex flex-1 flex-col px-4">
<div class="space-y-6">
<History
commitsMap={data?.user?.commitsMap || []}
{data}
followingsMap={data?.user?.followingsMap || []}
/>
</div>
</div>
</section>

View file

@ -7,8 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<script> <script>
import { locale } from 'svelte-i18n'; import { locale } from 'svelte-i18n';
import { initializeStores } from '@skeletonlabs/skeleton'; import { computePosition, autoUpdate, offset, shift, flip, arrow } from '@floating-ui/dom';
import { initializeStores, storePopup, Modal } from '@skeletonlabs/skeleton';
import Settings from '$lib/components/molecules/Settings.svelte';
import { mapLocaleToDir } from '$lib/i18n'; import { mapLocaleToDir } from '$lib/i18n';
// The ordering of these imports is critical to your app working properly // The ordering of these imports is critical to your app working properly
@ -25,8 +27,14 @@ SPDX-License-Identifier: AGPL-3.0-or-later
} }
}); });
const modalRegistry = {
settings: { ref: Settings }
};
initializeStores(); initializeStores();
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
</script> </script>
<Modal components={modalRegistry} />
<!-- Page Route Content --> <!-- Page Route Content -->
<slot /> <slot />

View file

@ -0,0 +1,26 @@
/* Stories for Settings molecule.
* 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 Settings from '$lib/components/molecules/Settings.svelte';
const meta = {
title: 'Molecules/Settings',
component: Settings,
tags: ['autodocs']
} satisfies Meta<Settings>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Plain: Story = {};

View file

@ -14,7 +14,7 @@ import { computePosition, autoUpdate, offset, shift, flip, arrow } from '@floati
import { storePopup } from '@skeletonlabs/skeleton'; import { storePopup } from '@skeletonlabs/skeleton';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/svelte'; import { render, screen } from '@testing-library/svelte';
import { get, readable } from 'svelte/store'; import { readable } from 'svelte/store';
import { init, locale, register } from 'svelte-i18n'; import { init, locale, register } from 'svelte-i18n';
import MainMenuSummary from '../../../src/lib/components/atoms/MainMenuSummary.svelte'; import MainMenuSummary from '../../../src/lib/components/atoms/MainMenuSummary.svelte';

View file

@ -27,13 +27,10 @@ describe('SettingsSidebar.svelte', () => {
it('should mount', () => { it('should mount', () => {
// Arrange // Arrange
// See https://testing-library.com/docs/svelte-testing-library/example#contexts // Nothing to prepare
const drawerStore = readable([]);
// Act // Act
const { container } = render(SettingsSidebar, { const { container } = render(SettingsSidebar);
context: new Map([['drawerStore', drawerStore]])
});
// Assert // Assert
expect(container).toBeTruthy(); expect(container).toBeTruthy();
@ -41,13 +38,10 @@ describe('SettingsSidebar.svelte', () => {
it('should have a headline', () => { it('should have a headline', () => {
// Arrange // Arrange
// See https://testing-library.com/docs/svelte-testing-library/example#contexts // Nothing to prepare
const drawerStore = readable([]);
// Act // Act
render(SettingsSidebar, { render(SettingsSidebar);
context: new Map([['drawerStore', drawerStore]])
});
// Assert // Assert
expect(screen.getByText(enMessages.settings.headline)).toBeInTheDocument(); expect(screen.getByText(enMessages.settings.headline)).toBeInTheDocument();
@ -55,13 +49,10 @@ describe('SettingsSidebar.svelte', () => {
it('should have a profile tile', () => { it('should have a profile tile', () => {
// Arrange // Arrange
// See https://testing-library.com/docs/svelte-testing-library/example#contexts // Nothing to prepare
const drawerStore = readable([]);
// Act // Act
render(SettingsSidebar, { render(SettingsSidebar);
context: new Map([['drawerStore', drawerStore]])
});
// Assert // Assert
expect(screen.getByText(enMessages.settings.profile.label)).toBeInTheDocument(); expect(screen.getByText(enMessages.settings.profile.label)).toBeInTheDocument();
@ -69,13 +60,10 @@ describe('SettingsSidebar.svelte', () => {
it('should have an account tile', () => { it('should have an account tile', () => {
// Arrange // Arrange
// See https://testing-library.com/docs/svelte-testing-library/example#contexts // Nothing to prepare
const drawerStore = readable([]);
// Act // Act
render(SettingsSidebar, { render(SettingsSidebar);
context: new Map([['drawerStore', drawerStore]])
});
// Assert // Assert
expect(screen.getByText(enMessages.settings.account.label)).toBeInTheDocument(); expect(screen.getByText(enMessages.settings.account.label)).toBeInTheDocument();
@ -83,13 +71,10 @@ describe('SettingsSidebar.svelte', () => {
it('should have a SSH/GPG keys tile', () => { it('should have a SSH/GPG keys tile', () => {
// Arrange // Arrange
// See https://testing-library.com/docs/svelte-testing-library/example#contexts // Nothing to prepare
const drawerStore = readable([]);
// Act // Act
render(SettingsSidebar, { render(SettingsSidebar);
context: new Map([['drawerStore', drawerStore]])
});
// Assert // Assert
expect(screen.getByText(enMessages.settings.ssh_gpg_keys.label)).toBeInTheDocument(); expect(screen.getByText(enMessages.settings.ssh_gpg_keys.label)).toBeInTheDocument();
@ -97,13 +82,10 @@ describe('SettingsSidebar.svelte', () => {
it('should have an appearance tile', () => { it('should have an appearance tile', () => {
// Arrange // Arrange
// See https://testing-library.com/docs/svelte-testing-library/example#contexts // Nothing to prepare
const drawerStore = readable([]);
// Act // Act
render(SettingsSidebar, { render(SettingsSidebar);
context: new Map([['drawerStore', drawerStore]])
});
// Assert // Assert
expect(screen.getByText(enMessages.settings.appearance.label)).toBeInTheDocument(); expect(screen.getByText(enMessages.settings.appearance.label)).toBeInTheDocument();
@ -111,15 +93,48 @@ describe('SettingsSidebar.svelte', () => {
it('should have a notifications tile', () => { it('should have a notifications tile', () => {
// Arrange // Arrange
// See https://testing-library.com/docs/svelte-testing-library/example#contexts // Nothing to prepare
const drawerStore = readable([]);
// Act // Act
render(SettingsSidebar, { render(SettingsSidebar);
context: new Map([['drawerStore', drawerStore]])
});
// Assert // Assert
expect(screen.getByText(enMessages.settings.notifications.label)).toBeInTheDocument(); expect(screen.getByText(enMessages.settings.notifications.label)).toBeInTheDocument();
}); });
describe('when clicking a tile', () => {
it('should dispatch a switch-settings event when clicking a tile', () => {
// Arrange
const listener = vi.fn();
// Act
const { component } = render(SettingsSidebar);
component.$on('switch-settings', listener);
const profileTile = screen.getByText(enMessages.settings.profile.label);
expect(profileTile).toBeInTheDocument();
profileTile.click();
// Assert
expect(listener).toHaveBeenCalledOnce();
});
it('should pass the clicked tile in the event', () => {
// Arrange
const listener = vi.fn();
// Act
const { component } = render(SettingsSidebar);
component.$on('switch-settings', listener);
const profileTile = screen.getByText(enMessages.settings.profile.label);
expect(profileTile).toBeInTheDocument();
profileTile.click();
// Assert
expect(listener).toHaveBeenCalledWith(
new CustomEvent('switch-settings', {
detail: 'profile'
})
);
});
});
}); });

View file

@ -109,7 +109,9 @@ describe('MainMenuDetails.svelte', () => {
render(MainMenuDetails); render(MainMenuDetails);
// Assert // Assert
expect(screen.getByText(enMessages.page.profile.menu.details.merge_requests)).toBeInTheDocument(); expect(
screen.getByText(enMessages.page.profile.menu.details.merge_requests)
).toBeInTheDocument();
}); });
it('should have a people item', () => { it('should have a people item', () => {

View file

@ -0,0 +1,72 @@
/* Component test for Settings molecule.
* 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 '@testing-library/jest-dom';
import { render, screen } from '@testing-library/svelte';
import { tick } from 'svelte';
import { init, locale, register } from 'svelte-i18n';
import Settings from '../../../src/lib/components/molecules/Settings.svelte';
import enMessages from '../../../src/lib/i18n/locales/en.json';
describe('Settings.svelte', () => {
beforeEach(() => {
register('en', () => import('../../../src/lib/i18n/locales/en.json'));
init({ fallbackLocale: 'en', initialLocale: 'en' });
locale.set('en');
});
it('should mount', () => {
// Arrange
// Nothing to prepare
// Act
const { container } = render(Settings);
// Assert
expect(container).toBeTruthy();
});
it('should have an profile section', () => {
// Arrange
// Nothing to prepare
// Act
render(Settings);
// Assert
// There are multiple nodes with that name and I haven't determined the appropriate heading level
expect(screen.getAllByText(enMessages.settings.profile.headline)).not.toHaveLength(0);
});
describe('when switching the active setting', () => {
it('should not have an profile section', async () => {
// Arrange
// Nothing to prepare
// Act
render(Settings);
const profileTileAndSection = screen.getAllByText(
enMessages.settings.profile.headline
).length;
const notificationTile = screen.getByText(enMessages.settings.notifications.label);
notificationTile.click();
// Wait for reactivity
await tick();
// Assert
expect(screen.queryAllByText(enMessages.settings.profile.headline).length).toBeLessThan(
profileTileAndSection
);
});
});
});

View file

@ -10,9 +10,12 @@
* 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/>. * 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 { computePosition, autoUpdate, offset, shift, flip, arrow } from '@floating-ui/dom';
import { storePopup } from '@skeletonlabs/skeleton';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/svelte'; import { render, screen } from '@testing-library/svelte';
import { init, locale, register } from 'svelte-i18n'; import { init, locale, register } from 'svelte-i18n';
import { readable } from 'svelte/store';
import Profile from '../../../src/lib/components/pages/Profile.svelte'; import Profile from '../../../src/lib/components/pages/Profile.svelte';
import enMessages from '../../../src/lib/i18n/locales/en.json'; import enMessages from '../../../src/lib/i18n/locales/en.json';
@ -26,12 +29,24 @@ describe('Profile.svelte', () => {
it('should mount', () => { it('should mount', () => {
// Arrange // Arrange
const data = { // See https://testing-library.com/docs/svelte-testing-library/example#contexts
user: {} const modalStore = readable([]);
// See https://www.skeleton.dev/utilities/popups
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
const props = {
data: {
user: {}
}
}; };
const context = new Map([
['modalStore', modalStore],
['storePopup', storePopup]
]);
// Act // Act
const { container } = render(Profile, { data }); const { container } = render(Profile, { context, props });
// Assert // Assert
expect(container).toBeTruthy(); expect(container).toBeTruthy();
@ -39,12 +54,24 @@ describe('Profile.svelte', () => {
it('should have a h1', () => { it('should have a h1', () => {
// Arrange // Arrange
const data = { // See https://testing-library.com/docs/svelte-testing-library/example#contexts
user: {} const modalStore = readable([]);
// See https://www.skeleton.dev/utilities/popups
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
const props = {
data: {
user: {}
}
}; };
const context = new Map([
['modalStore', modalStore],
['storePopup', storePopup]
]);
// Act // Act
render(Profile, { data }); render(Profile, { context, props });
const h1 = screen.getByRole('heading', { level: 1 }); const h1 = screen.getByRole('heading', { level: 1 });
// Assert // Assert
@ -54,12 +81,24 @@ describe('Profile.svelte', () => {
it('should have a block button', () => { it('should have a block button', () => {
// Arrange // Arrange
const data = { // See https://testing-library.com/docs/svelte-testing-library/example#contexts
user: {} const modalStore = readable([]);
// See https://www.skeleton.dev/utilities/popups
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
const props = {
data: {
user: {}
}
}; };
const context = new Map([
['modalStore', modalStore],
['storePopup', storePopup]
]);
// Act // Act
render(Profile, { data }); render(Profile, { context, props });
const button = screen.getByRole('button', { name: 'block' }); const button = screen.getByRole('button', { name: 'block' });
// Assert // Assert
@ -68,12 +107,24 @@ describe('Profile.svelte', () => {
it('should have a report button', () => { it('should have a report button', () => {
// Arrange // Arrange
const data = { // See https://testing-library.com/docs/svelte-testing-library/example#contexts
user: {} const modalStore = readable([]);
// See https://www.skeleton.dev/utilities/popups
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
const props = {
data: {
user: {}
}
}; };
const context = new Map([
['modalStore', modalStore],
['storePopup', storePopup]
]);
// Act // Act
render(Profile, { data }); render(Profile, { context, props });
const button = screen.getByRole('button', { name: 'report' }); const button = screen.getByRole('button', { name: 'report' });
// Assert // Assert
@ -83,12 +134,24 @@ describe('Profile.svelte', () => {
// FIXME: Reenable once emoji was replaced with svelte-octicon // FIXME: Reenable once emoji was replaced with svelte-octicon
it.skip('should have a like button', () => { it.skip('should have a like button', () => {
// Arrange // Arrange
const data = { // See https://testing-library.com/docs/svelte-testing-library/example#contexts
user: {} const modalStore = readable([]);
// See https://www.skeleton.dev/utilities/popups
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
const props = {
data: {
user: {}
}
}; };
const context = new Map([
['modalStore', modalStore],
['storePopup', storePopup]
]);
// Act // Act
render(Profile, { data }); render(Profile, { context, props });
const button = screen.getByRole('button', { name: enMessages.page.profile.activities.like }); const button = screen.getByRole('button', { name: enMessages.page.profile.activities.like });
// Assert // Assert
@ -98,12 +161,24 @@ describe('Profile.svelte', () => {
describe('projects', () => { describe('projects', () => {
it('should have a h2', () => { it('should have a h2', () => {
// Arrange // Arrange
const data = { // See https://testing-library.com/docs/svelte-testing-library/example#contexts
user: {} const modalStore = readable([]);
// See https://www.skeleton.dev/utilities/popups
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
const props = {
data: {
user: {}
}
}; };
const context = new Map([
['modalStore', modalStore],
['storePopup', storePopup]
]);
// Act // Act
render(Profile, { data }); render(Profile, { context, props });
const h2 = screen.getByRole('heading', { const h2 = screen.getByRole('heading', {
level: 2, level: 2,
name: enMessages.page.profile.projects.heading name: enMessages.page.profile.projects.heading
@ -115,12 +190,24 @@ describe('Profile.svelte', () => {
it('should add a project', () => { it('should add a project', () => {
// Arrange // Arrange
const data = { // See https://testing-library.com/docs/svelte-testing-library/example#contexts
user: {} const modalStore = readable([]);
// See https://www.skeleton.dev/utilities/popups
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
const props = {
data: {
user: {}
}
}; };
const context = new Map([
['modalStore', modalStore],
['storePopup', storePopup]
]);
// Act // Act
render(Profile, { data }); render(Profile, { context, props });
const a = screen.getByRole('link', { name: 'Add a project' }); const a = screen.getByRole('link', { name: 'Add a project' });
// Assert // Assert
@ -129,12 +216,24 @@ describe('Profile.svelte', () => {
it('should import a project', () => { it('should import a project', () => {
// Arrange // Arrange
const data = { // See https://testing-library.com/docs/svelte-testing-library/example#contexts
user: {} const modalStore = readable([]);
// See https://www.skeleton.dev/utilities/popups
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
const props = {
data: {
user: {}
}
}; };
const context = new Map([
['modalStore', modalStore],
['storePopup', storePopup]
]);
// Act // Act
render(Profile, { data }); render(Profile, { context, props });
const a = screen.getByRole('link', { name: 'import a project' }); const a = screen.getByRole('link', { name: 'import a project' });
// Assert // Assert
@ -146,12 +245,24 @@ describe('Profile.svelte', () => {
describe('history', () => { describe('history', () => {
it('should have a h2', () => { it('should have a h2', () => {
// Arrange // Arrange
const data = { // See https://testing-library.com/docs/svelte-testing-library/example#contexts
user: {} const modalStore = readable([]);
// See https://www.skeleton.dev/utilities/popups
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
const props = {
data: {
user: {}
}
}; };
const context = new Map([
['modalStore', modalStore],
['storePopup', storePopup]
]);
// Act // Act
render(Profile, { data }); render(Profile, { context, props });
const h2 = screen.getByRole('heading', { const h2 = screen.getByRole('heading', {
level: 2, level: 2,
name: enMessages.page.profile.history.heading name: enMessages.page.profile.history.heading
@ -163,16 +274,28 @@ describe('Profile.svelte', () => {
it('should have an entry for created account', () => { it('should have an entry for created account', () => {
// Arrange // Arrange
const data = { // See https://testing-library.com/docs/svelte-testing-library/example#contexts
user: { const modalStore = readable([]);
created_with: 'Anvil', // See https://www.skeleton.dev/utilities/popups
instance: 'domain.example', storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
username: 'jane_doe'
const props = {
data: {
user: {
created_with: 'Anvil',
instance: 'domain.example',
username: 'jane_doe'
}
} }
}; };
const context = new Map([
['modalStore', modalStore],
['storePopup', storePopup]
]);
// Act // Act
render(Profile, { data }); render(Profile, { context, props });
const li = screen.getByRole('listitem'); const li = screen.getByRole('listitem');
// Assert // Assert
@ -183,9 +306,9 @@ describe('Profile.svelte', () => {
expect(li).toHaveTextContent( expect(li).toHaveTextContent(
new RegExp( new RegExp(
enMessages.page.profile.history.activities.setup.description enMessages.page.profile.history.activities.setup.description
.replace('{username}', data.user.username) .replace('{username}', props.data.user.username)
.replace('{instance}', data.user.instance) .replace('{instance}', props.data.user.instance)
.replace('{created_with}', data.user.created_with) .replace('{created_with}', props.data.user.created_with)
) )
); );
}); });

View file

@ -10,9 +10,12 @@
* 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/>. * 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 { computePosition, autoUpdate, offset, shift, flip, arrow } from '@floating-ui/dom';
import { storePopup } from '@skeletonlabs/skeleton';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/svelte'; import { render, screen } from '@testing-library/svelte';
import { init, locale, register } from 'svelte-i18n'; import { init, locale, register } from 'svelte-i18n';
import { readable } from 'svelte/store';
import Profile from '../../../src/lib/components/templates/Profile.svelte'; import Profile from '../../../src/lib/components/templates/Profile.svelte';
import enMessages from '../../../src/lib/i18n/locales/en.json'; import enMessages from '../../../src/lib/i18n/locales/en.json';
@ -26,12 +29,24 @@ describe('Profile.svelte', () => {
it('should mount', () => { it('should mount', () => {
// Arrange // Arrange
const data = { // See https://testing-library.com/docs/svelte-testing-library/example#contexts
user: {} const modalStore = readable([]);
// See https://www.skeleton.dev/utilities/popups
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
const props = {
data: {
user: {}
}
}; };
const context = new Map([
['modalStore', modalStore],
['storePopup', storePopup]
]);
// Act // Act
const { container } = render(Profile, { data }); const { container } = render(Profile, { context, props });
// Assert // Assert
expect(container).toBeTruthy(); expect(container).toBeTruthy();
@ -39,12 +54,24 @@ describe('Profile.svelte', () => {
it('should have a h1', () => { it('should have a h1', () => {
// Arrange // Arrange
const data = { // See https://testing-library.com/docs/svelte-testing-library/example#contexts
user: {} const modalStore = readable([]);
// See https://www.skeleton.dev/utilities/popups
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
const props = {
data: {
user: {}
}
}; };
const context = new Map([
['modalStore', modalStore],
['storePopup', storePopup]
]);
// Act // Act
render(Profile, { data }); render(Profile, { context, props });
const h1 = screen.getByRole('heading', { level: 1 }); const h1 = screen.getByRole('heading', { level: 1 });
// Assert // Assert
@ -54,12 +81,24 @@ describe('Profile.svelte', () => {
it('should have a block button', () => { it('should have a block button', () => {
// Arrange // Arrange
const data = { // See https://testing-library.com/docs/svelte-testing-library/example#contexts
user: {} const modalStore = readable([]);
// See https://www.skeleton.dev/utilities/popups
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
const props = {
data: {
user: {}
}
}; };
const context = new Map([
['modalStore', modalStore],
['storePopup', storePopup]
]);
// Act // Act
render(Profile, { data }); render(Profile, { context, props });
const button = screen.getByRole('button', { name: 'block' }); const button = screen.getByRole('button', { name: 'block' });
// Assert // Assert
@ -68,12 +107,24 @@ describe('Profile.svelte', () => {
it('should have a report button', () => { it('should have a report button', () => {
// Arrange // Arrange
const data = { // See https://testing-library.com/docs/svelte-testing-library/example#contexts
user: {} const modalStore = readable([]);
// See https://www.skeleton.dev/utilities/popups
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
const props = {
data: {
user: {}
}
}; };
const context = new Map([
['modalStore', modalStore],
['storePopup', storePopup]
]);
// Act // Act
render(Profile, { data }); render(Profile, { context, props });
const button = screen.getByRole('button', { name: 'report' }); const button = screen.getByRole('button', { name: 'report' });
// Assert // Assert
@ -83,12 +134,24 @@ describe('Profile.svelte', () => {
// FIXME: Reenable once emoji was replaced with svelte-octicon // FIXME: Reenable once emoji was replaced with svelte-octicon
it.skip('should have a like button', () => { it.skip('should have a like button', () => {
// Arrange // Arrange
const data = { // See https://testing-library.com/docs/svelte-testing-library/example#contexts
user: {} const modalStore = readable([]);
// See https://www.skeleton.dev/utilities/popups
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
const props = {
data: {
user: {}
}
}; };
const context = new Map([
['modalStore', modalStore],
['storePopup', storePopup]
]);
// Act // Act
render(Profile, { data }); render(Profile, { context, props });
const button = screen.getByRole('button', { name: enMessages.page.profile.activities.like }); const button = screen.getByRole('button', { name: enMessages.page.profile.activities.like });
// Assert // Assert
@ -98,12 +161,24 @@ describe('Profile.svelte', () => {
describe('projects', () => { describe('projects', () => {
it('should have a h2', () => { it('should have a h2', () => {
// Arrange // Arrange
const data = { // See https://testing-library.com/docs/svelte-testing-library/example#contexts
user: {} const modalStore = readable([]);
// See https://www.skeleton.dev/utilities/popups
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
const props = {
data: {
user: {}
}
}; };
const context = new Map([
['modalStore', modalStore],
['storePopup', storePopup]
]);
// Act // Act
render(Profile, { data }); render(Profile, { context, props });
const h2 = screen.getByRole('heading', { const h2 = screen.getByRole('heading', {
level: 2, level: 2,
name: enMessages.page.profile.projects.heading name: enMessages.page.profile.projects.heading
@ -115,12 +190,24 @@ describe('Profile.svelte', () => {
it('should add a project', () => { it('should add a project', () => {
// Arrange // Arrange
const data = { // See https://testing-library.com/docs/svelte-testing-library/example#contexts
user: {} const modalStore = readable([]);
// See https://www.skeleton.dev/utilities/popups
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
const props = {
data: {
user: {}
}
}; };
const context = new Map([
['modalStore', modalStore],
['storePopup', storePopup]
]);
// Act // Act
render(Profile, { data }); render(Profile, { context, props });
const a = screen.getByRole('link', { name: 'Add a project' }); const a = screen.getByRole('link', { name: 'Add a project' });
// Assert // Assert
@ -129,12 +216,24 @@ describe('Profile.svelte', () => {
it('should import a project', () => { it('should import a project', () => {
// Arrange // Arrange
const data = { // See https://testing-library.com/docs/svelte-testing-library/example#contexts
user: {} const modalStore = readable([]);
// See https://www.skeleton.dev/utilities/popups
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
const props = {
data: {
user: {}
}
}; };
const context = new Map([
['modalStore', modalStore],
['storePopup', storePopup]
]);
// Act // Act
render(Profile, { data }); render(Profile, { context, props });
const a = screen.getByRole('link', { name: 'import a project' }); const a = screen.getByRole('link', { name: 'import a project' });
// Assert // Assert
@ -146,12 +245,24 @@ describe('Profile.svelte', () => {
describe('history', () => { describe('history', () => {
it('should have a h2', () => { it('should have a h2', () => {
// Arrange // Arrange
const data = { // See https://testing-library.com/docs/svelte-testing-library/example#contexts
user: {} const modalStore = readable([]);
// See https://www.skeleton.dev/utilities/popups
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
const props = {
data: {
user: {}
}
}; };
const context = new Map([
['modalStore', modalStore],
['storePopup', storePopup]
]);
// Act // Act
render(Profile, { data }); render(Profile, { context, props });
const h2 = screen.getByRole('heading', { const h2 = screen.getByRole('heading', {
level: 2, level: 2,
name: enMessages.page.profile.history.heading name: enMessages.page.profile.history.heading
@ -163,16 +274,28 @@ describe('Profile.svelte', () => {
it('should have an entry for created account', () => { it('should have an entry for created account', () => {
// Arrange // Arrange
const data = { // See https://testing-library.com/docs/svelte-testing-library/example#contexts
user: { const modalStore = readable([]);
created_with: 'Anvil', // See https://www.skeleton.dev/utilities/popups
instance: 'domain.example', storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
username: 'jane_doe'
const props = {
data: {
user: {
created_with: 'Anvil',
instance: 'domain.example',
username: 'jane_doe'
}
} }
}; };
const context = new Map([
['modalStore', modalStore],
['storePopup', storePopup]
]);
// Act // Act
render(Profile, { data }); render(Profile, { context, props });
const li = screen.getByRole('listitem'); const li = screen.getByRole('listitem');
// Assert // Assert
@ -183,9 +306,9 @@ describe('Profile.svelte', () => {
expect(li).toHaveTextContent( expect(li).toHaveTextContent(
new RegExp( new RegExp(
enMessages.page.profile.history.activities.setup.description enMessages.page.profile.history.activities.setup.description
.replace('{username}', data.user.username) .replace('{username}', props.data.user.username)
.replace('{instance}', data.user.instance) .replace('{instance}', props.data.user.instance)
.replace('{created_with}', data.user.created_with) .replace('{created_with}', props.data.user.created_with)
) )
); );
}); });