summaryrefslogtreecommitdiff
path: root/ui
diff options
context:
space:
mode:
authorOwen Jacobson <owen@grimoire.ca>2025-02-15 15:17:03 -0500
committerOwen Jacobson <owen@grimoire.ca>2025-02-21 17:49:38 -0500
commitfc0f1654a56d2247728a766f43e72ff169704888 (patch)
tree945f44c9a90bf51de20c61a5a8c5ed82c2c05009 /ui
parent36cadfe00cacc6a6523f9862d3f7a08a9d0ce611 (diff)
Hoist global state access out of individual components.
Access to "global" (maybe "external?") state is now handled at the top level of the component hierarchy, in `+page.svelte`, `+layout.svelte`, and their associated scripts. State is otherwise passed down through props, and changes are passed up through callbacks. This is - hopefully - groundwork for refactoring state management a bit. I wanted to move access to state out to a smaller number of places, so that I have fewer places to update to implement reconnect logic. My broader goal is to make it easier to refactor these kinds of external side effects, as well, though no such changes are in this branch. This change also makes testing a mile easier, since tests can interact with props and callbacks instead of emulating the whole HTTP request stack and the Pilcrow API. This change removes do-very-little tests.
Diffstat (limited to 'ui')
-rw-r--r--ui/lib/components/ActiveChannel.svelte6
-rw-r--r--ui/lib/components/ChangePassword.svelte24
-rw-r--r--ui/lib/components/CreateChannelForm.svelte15
-rw-r--r--ui/lib/components/Invites.svelte8
-rw-r--r--ui/lib/components/LogIn.svelte27
-rw-r--r--ui/lib/components/LogOut.svelte10
-rw-r--r--ui/lib/components/Message.svelte7
-rw-r--r--ui/lib/components/MessageInput.svelte25
-rw-r--r--ui/lib/components/MessageRun.svelte10
-rw-r--r--ui/routes/(app)/+layout.svelte16
-rw-r--r--ui/routes/(app)/ch/[channel]/+page.svelte28
-rw-r--r--ui/routes/(app)/me/+page.svelte31
-rw-r--r--ui/routes/(login)/invite/[invite]/+page.svelte18
-rw-r--r--ui/routes/(login)/login/+page.svelte18
-rw-r--r--ui/routes/(login)/setup/+page.svelte18
-rw-r--r--ui/tests/lib/components/ActiveChannel.svelte.test.js22
-rw-r--r--ui/tests/lib/components/ChangePassword.svelte.test.js90
-rw-r--r--ui/tests/lib/components/Channel.svelte.test.js22
-rw-r--r--ui/tests/lib/components/ChannelList.svelte.test.js22
-rw-r--r--ui/tests/lib/components/CreateChannelForm.svelte.test.js65
-rw-r--r--ui/tests/lib/components/LogIn.svelte.test.js38
-rw-r--r--ui/tests/lib/components/MessageInput.svelte.test.js61
-rw-r--r--ui/tests/lib/components/MessageRun.svelte.test.js22
23 files changed, 264 insertions, 339 deletions
diff --git a/ui/lib/components/ActiveChannel.svelte b/ui/lib/components/ActiveChannel.svelte
index 9c181e4..f7837aa 100644
--- a/ui/lib/components/ActiveChannel.svelte
+++ b/ui/lib/components/ActiveChannel.svelte
@@ -1,9 +1,9 @@
<script>
import MessageRun from './MessageRun.svelte';
- let { messageRuns } = $props();
+ let { messageRuns, deleteMessage = async (id) => {} } = $props();
</script>
-{#each messageRuns as { sender, messages }}
- <MessageRun {sender} {messages} />
+{#each messageRuns as { sender, ownMessage, messages }}
+ <MessageRun {sender} {ownMessage} {messages} {deleteMessage} />
{/each}
diff --git a/ui/lib/components/ChangePassword.svelte b/ui/lib/components/ChangePassword.svelte
index bf94ea7..742d4f1 100644
--- a/ui/lib/components/ChangePassword.svelte
+++ b/ui/lib/components/ChangePassword.svelte
@@ -1,28 +1,26 @@
<script>
- import { changePassword } from '$lib/apiServer.js';
+ let { changePassword = async (currentPassword, newPassword) => {} } = $props();
- let currentPassword = $state(''),
- newPassword = $state(''),
- confirmPassword = $state(''),
- pending = $state(false),
- form;
+ let currentPassword = $state('');
+ let newPassword = $state('');
+ let confirmPassword = $state('');
+ let pending = $state(false);
let valid = $derived(newPassword === confirmPassword && newPassword !== currentPassword);
let disabled = $derived(pending || !valid);
async function onsubmit(event) {
event.preventDefault();
pending = true;
- let response = await changePassword(currentPassword, newPassword);
- switch (response.status) {
- case 200:
- form.reset();
- break;
+ try {
+ await changePassword(currentPassword, newPassword);
+ event.target.reset();
+ } finally {
+ pending = false;
}
- pending = false;
}
</script>
-<form class="form" {onsubmit} bind:this={form}>
+<form class="form" {onsubmit}>
<label
>current password
<input
diff --git a/ui/lib/components/CreateChannelForm.svelte b/ui/lib/components/CreateChannelForm.svelte
index 85c85bb..471c2b7 100644
--- a/ui/lib/components/CreateChannelForm.svelte
+++ b/ui/lib/components/CreateChannelForm.svelte
@@ -1,21 +1,22 @@
<script>
- import { createChannel } from '$lib/apiServer';
+ let { createChannel = async (name) => {} } = $props();
let name = $state('');
let disabled = $state(false);
- async function handleSubmit(event) {
+ async function onsubmit(event) {
event.preventDefault();
disabled = true;
- const response = await createChannel(name);
- if (200 <= response.status && response.status < 300) {
- name = '';
+ try {
+ await createChannel(name);
+ event.target.reset();
+ } finally {
+ disabled = false;
}
- disabled = false;
}
</script>
-<form onsubmit={handleSubmit}>
+<form {onsubmit}>
<input type="text" placeholder="create channel" bind:value={name} {disabled} />
<button type="submit">&#x2795;</button>
</form>
diff --git a/ui/lib/components/Invites.svelte b/ui/lib/components/Invites.svelte
index 27d3754..226ccce 100644
--- a/ui/lib/components/Invites.svelte
+++ b/ui/lib/components/Invites.svelte
@@ -1,15 +1,11 @@
<script>
- import { createInvite } from '$lib/apiServer';
import Invite from '$lib/components/Invite.svelte';
- let invites = $state([]);
+ let { invites, createInvite = async () => {} } = $props();
async function onsubmit(event) {
event.preventDefault();
- let response = await createInvite();
- if (response.status == 200) {
- invites.push(response.data);
- }
+ await createInvite();
}
</script>
diff --git a/ui/lib/components/LogIn.svelte b/ui/lib/components/LogIn.svelte
index 5bfdae2..c49ea3b 100644
--- a/ui/lib/components/LogIn.svelte
+++ b/ui/lib/components/LogIn.svelte
@@ -1,20 +1,29 @@
<script>
- let {
- username = $bindable(),
- password = $bindable(),
- legend = 'sign in',
- disabled,
- onsubmit
- } = $props();
+ let { legend = 'sign in', logIn = async (username, password) => {} } = $props();
+
+ let username = $state();
+ let password = $state();
+ let disabled = $state(false);
+
+ async function onsubmit(event) {
+ event.preventDefault();
+ disabled = true;
+ try {
+ await logIn(username, password);
+ event.target.reset();
+ } finally {
+ disabled = false;
+ }
+ }
</script>
<div>
<form class="form" {onsubmit}>
- <label for="username">
+ <label>
username
<input name="username" type="text" placeholder="username" bind:value={username} {disabled} />
</label>
- <label for="password">
+ <label>
password
<input
name="password"
diff --git a/ui/lib/components/LogOut.svelte b/ui/lib/components/LogOut.svelte
index 1cb8fb5..bb24681 100644
--- a/ui/lib/components/LogOut.svelte
+++ b/ui/lib/components/LogOut.svelte
@@ -1,15 +1,9 @@
<script>
- import { goto } from '$app/navigation';
- import { logOut } from '$lib/apiServer.js';
- import { currentUser } from '$lib/store';
+ let { logOut = async () => {} } = $props();
async function onsubmit(event) {
event.preventDefault();
- const response = await logOut();
- if (200 <= response.status && response.status < 300) {
- currentUser.set(null);
- await goto('/login');
- }
+ await logOut();
}
</script>
diff --git a/ui/lib/components/Message.svelte b/ui/lib/components/Message.svelte
index 1b1598b..dacd900 100644
--- a/ui/lib/components/Message.svelte
+++ b/ui/lib/components/Message.svelte
@@ -1,16 +1,15 @@
<script>
import { DateTime } from 'luxon';
- import { deleteMessage } from '$lib/apiServer';
function scroll(message) {
message.scrollIntoView();
}
- let { id, at, body, renderedBody, editable = false } = $props();
+ let { id, at, body, renderedBody, editable = false, deleteMessage = async (id) => {} } = $props();
let deleteArmed = $state(false);
let atFormatted = $derived(at.toLocaleString(DateTime.DATETIME_SHORT));
- function onDelete(event) {
+ function ondelete(event) {
event.preventDefault();
if (deleteArmed) {
deleteArmed = false;
@@ -29,7 +28,7 @@
<div class="handle">
{atFormatted}
{#if editable}
- <button onclick={onDelete}>&#x1F5D1;&#xFE0F;</button>
+ <button onclick={ondelete}>&#x1F5D1;&#xFE0F;</button>
{/if}
</div>
<section use:scroll class="message-body">
diff --git a/ui/lib/components/MessageInput.svelte b/ui/lib/components/MessageInput.svelte
index 1eb1d7b..69a8298 100644
--- a/ui/lib/components/MessageInput.svelte
+++ b/ui/lib/components/MessageInput.svelte
@@ -1,29 +1,30 @@
<script>
- import { postToChannel } from '$lib/apiServer';
+ let { sendMessage = async (message) => {} } = $props();
- let { channel } = $props();
-
- let form;
let value = $state('');
let disabled = $state(false);
- async function onSubmit(event) {
+ async function onsubmit(event) {
event.preventDefault();
disabled = true;
- await postToChannel(channel, value);
- form.reset();
- disabled = false;
+ try {
+ await sendMessage(value);
+ event.target.reset();
+ } finally {
+ disabled = false;
+ }
}
- function onKeyDown(event) {
+ function onkeydown(event) {
let modifier = event.shiftKey || event.altKey || event.ctrlKey || event.metaKey;
if (!modifier && event.key === 'Enter') {
- onSubmit(event);
+ event.preventDefault();
+ event.target.form.requestSubmit();
}
}
</script>
-<form bind:this={form} onsubmit={onSubmit}>
- <textarea onkeydown={onKeyDown} bind:value {disabled} placeholder="Say something..."></textarea>
+<form {onsubmit}>
+ <textarea {onkeydown} bind:value {disabled} placeholder="Say something..."></textarea>
<button type="submit">&raquo;</button>
</form>
diff --git a/ui/lib/components/MessageRun.svelte b/ui/lib/components/MessageRun.svelte
index bee64e8..f1facd3 100644
--- a/ui/lib/components/MessageRun.svelte
+++ b/ui/lib/components/MessageRun.svelte
@@ -1,18 +1,14 @@
<script>
- import { logins, currentUser } from '$lib/store';
import Message from '$lib/components/Message.svelte';
- let { sender, messages } = $props();
-
- let name = $derived($logins.get(sender));
- let ownMessage = $derived($currentUser !== null && $currentUser.id == sender);
+ let { sender, messages, ownMessage, deleteMessage = async (id) => {} } = $props();
</script>
<div class="message-run" class:own-message={ownMessage} class:other-message={!ownMessage}>
<span class="username">
- @{name}:
+ @{sender}:
</span>
{#each messages as message}
- <Message {...message} editable={ownMessage} />
+ <Message {...message} editable={ownMessage} {deleteMessage} />
{/each}
</div>
diff --git a/ui/routes/(app)/+layout.svelte b/ui/routes/(app)/+layout.svelte
index 6339abd..02f7d19 100644
--- a/ui/routes/(app)/+layout.svelte
+++ b/ui/routes/(app)/+layout.svelte
@@ -5,7 +5,7 @@
import { getContext, onDestroy, onMount } from 'svelte';
import TinyGesture from 'tinygesture';
- import { boot, subscribeToEvents } from '$lib/apiServer';
+ import * as api from '$lib/apiServer.js';
import { channelsList, currentUser, logins, messages, onEvent } from '$lib/store';
import ChannelList from '$lib/components/ChannelList.svelte';
@@ -58,20 +58,20 @@
return;
}
gesture = new TinyGesture(window);
- gesture.on('swiperight', (event) => {
+ gesture.on('swiperight', () => {
pageContext.showMenu = true;
});
- gesture.on('swipeleft', (event) => {
+ gesture.on('swipeleft', () => {
pageContext.showMenu = false;
});
}
onMount(async () => {
- let response = await boot();
+ let response = await api.boot();
switch (response.status) {
case 200:
onBooted(response.data);
- events = subscribeToEvents(response.data.resume_point);
+ events = api.subscribeToEvents(response.data.resume_point);
events.onmessage = onEvent.fromMessage;
break;
case 401:
@@ -109,6 +109,10 @@
event.returnValue = '';
return '';
}
+
+ async function createChannel(name) {
+ await api.createChannel(name);
+ }
</script>
<svelte:window {onbeforeunload} />
@@ -125,7 +129,7 @@
<nav id="sidebar" data-expanded={pageContext.showMenu}>
<ChannelList active={channel} channels={enrichedChannels} />
<div class="create-channel">
- <CreateChannelForm />
+ <CreateChannelForm {createChannel} />
</div>
</nav>
<main>
diff --git a/ui/routes/(app)/ch/[channel]/+page.svelte b/ui/routes/(app)/ch/[channel]/+page.svelte
index 84cb0ae..8de9859 100644
--- a/ui/routes/(app)/ch/[channel]/+page.svelte
+++ b/ui/routes/(app)/ch/[channel]/+page.svelte
@@ -3,10 +3,22 @@
import { page } from '$app/state';
import ActiveChannel from '$lib/components/ActiveChannel.svelte';
import MessageInput from '$lib/components/MessageInput.svelte';
- import { channelsList, messages } from '$lib/store';
+ import { channelsList, currentUser, logins, messages } from '$lib/store';
+ import * as api from '$lib/apiServer';
let channel = $derived(page.params.channel);
- let messageRuns = $derived($messages.inChannel(channel));
+ let messageRuns = $derived(
+ $messages.inChannel(channel).map(({ sender, messages }) => {
+ let senderName = $derived($logins.get(sender));
+ let ownMessage = $derived($currentUser !== null && $currentUser.id === sender);
+
+ return {
+ sender: senderName,
+ ownMessage,
+ messages
+ };
+ })
+ );
let activeChannel;
function inView(parentElement, element) {
@@ -63,13 +75,21 @@
clearTimeout(lastReadCallback); // Fine if lastReadCallback is null still.
lastReadCallback = setTimeout(setLastRead, 2 * 1000);
}
+
+ async function sendMessage(message) {
+ await api.postToChannel(channel, message);
+ }
+
+ async function deleteMessage(id) {
+ await api.deleteMessage(id);
+ }
</script>
<svelte:window onkeydown={handleKeydown} />
<div class="active-channel" {onscroll} bind:this={activeChannel}>
- <ActiveChannel {messageRuns} />
+ <ActiveChannel {messageRuns} {deleteMessage} />
</div>
<div class="create-message">
- <MessageInput {channel} />
+ <MessageInput {sendMessage} />
</div>
diff --git a/ui/routes/(app)/me/+page.svelte b/ui/routes/(app)/me/+page.svelte
index 14a9db8..ab214e9 100644
--- a/ui/routes/(app)/me/+page.svelte
+++ b/ui/routes/(app)/me/+page.svelte
@@ -2,10 +2,35 @@
import LogOut from '$lib/components/LogOut.svelte';
import Invites from '$lib/components/Invites.svelte';
import ChangePassword from '$lib/components/ChangePassword.svelte';
+
+ import { goto } from '$app/navigation';
+ import * as api from '$lib/apiServer.js';
+ import { currentUser } from '$lib/store';
+
+ let invites = $state([]);
+
+ async function logOut() {
+ const response = await api.logOut();
+ if (200 <= response.status && response.status < 300) {
+ currentUser.set(null);
+ await goto('/login');
+ }
+ }
+
+ async function changePassword(currentPassword, newPassword) {
+ await api.changePassword(currentPassword, newPassword);
+ }
+
+ async function createInvite() {
+ let response = await api.createInvite();
+ if (response.status === 200) {
+ invites.push(response.data);
+ }
+ }
</script>
-<ChangePassword />
+<ChangePassword {changePassword} />
<hr />
-<Invites />
+<Invites {invites} {createInvite} />
<hr />
-<LogOut />
+<LogOut {logOut} />
diff --git a/ui/routes/(login)/invite/[invite]/+page.svelte b/ui/routes/(login)/invite/[invite]/+page.svelte
index 0c01286..04341e5 100644
--- a/ui/routes/(login)/invite/[invite]/+page.svelte
+++ b/ui/routes/(login)/invite/[invite]/+page.svelte
@@ -1,26 +1,16 @@
<script>
import { goto } from '$app/navigation';
- import { acceptInvite } from '$lib/apiServer';
+ import * as api from '$lib/apiServer';
import LogIn from '$lib/components/LogIn.svelte';
let { data } = $props();
- let username = $state(''),
- password = $state('');
- let pending = false;
- let disabled = $derived(pending);
-
- async function onSubmit(event, inviteId) {
- event.preventDefault();
- pending = true;
- const response = await acceptInvite(inviteId, username, password);
+ async function acceptInvite(inviteId, username, password) {
+ const response = await api.acceptInvite(inviteId, username, password);
if (200 <= response.status && response.status < 300) {
- username = '';
- password = '';
await goto('/');
}
- pending = false;
}
</script>
@@ -32,5 +22,5 @@
<div class="invite-text">
<p>Hi there! {invite.issuer} invites you to the conversation.</p>
</div>
- <LogIn {disabled} bind:username bind:password onsubmit={(event) => onSubmit(event, invite.id)} />
+ <LogIn logIn={async (username, password) => acceptInvite(invite.id, username, password)} />
{/await}
diff --git a/ui/routes/(login)/login/+page.svelte b/ui/routes/(login)/login/+page.svelte
index 9157cef..b1f7cf2 100644
--- a/ui/routes/(login)/login/+page.svelte
+++ b/ui/routes/(login)/login/+page.svelte
@@ -1,25 +1,15 @@
<script>
import { goto } from '$app/navigation';
- import { logIn } from '$lib/apiServer';
+ import * as api from '$lib/apiServer';
import LogIn from '$lib/components/LogIn.svelte';
- let username = '',
- password = '';
- let pending = false;
- $: disabled = pending;
-
- async function onSubmit(event) {
- event.preventDefault();
- pending = true;
- const response = await logIn(username, password);
+ async function logIn(username, password) {
+ const response = await api.logIn(username, password);
if (200 <= response.status && response.status < 300) {
- username = '';
- password = '';
await goto('/');
}
- pending = false;
}
</script>
-<LogIn {disabled} bind:username bind:password onsubmit={onSubmit} />
+<LogIn {logIn} />
diff --git a/ui/routes/(login)/setup/+page.svelte b/ui/routes/(login)/setup/+page.svelte
index c63f198..0b5a824 100644
--- a/ui/routes/(login)/setup/+page.svelte
+++ b/ui/routes/(login)/setup/+page.svelte
@@ -1,25 +1,15 @@
<script>
import { goto } from '$app/navigation';
- import { setup } from '$lib/apiServer';
+ import * as api from '$lib/apiServer';
import LogIn from '$lib/components/LogIn.svelte';
- let username = $state(''),
- password = $state('');
- let pending = false;
- let disabled = $derived(pending);
-
- async function onSubmit(event) {
- event.preventDefault();
- pending = true;
- const response = await setup(username, password);
+ async function logIn(username, password) {
+ const response = await api.setup(username, password);
if (200 <= response.status && response.status < 300) {
- username = '';
- password = '';
await goto('/');
}
- pending = false;
}
</script>
-<LogIn {disabled} bind:username bind:password legend="set up" onsubmit={onSubmit} />
+<LogIn legend="set up" {logIn} />
diff --git a/ui/tests/lib/components/ActiveChannel.svelte.test.js b/ui/tests/lib/components/ActiveChannel.svelte.test.js
deleted file mode 100644
index 183c823..0000000
--- a/ui/tests/lib/components/ActiveChannel.svelte.test.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { flushSync, mount, unmount } from 'svelte';
-import { afterEach, beforeEach, describe, expect, test } from 'vitest';
-import ActiveChannel from '$lib/components/ActiveChannel.svelte';
-
-let component;
-
-describe('ActiveChannel', () => {
- beforeEach(() => {
- component = mount(ActiveChannel, {
- target: document.body // `document` exists because of jsdom
- });
- flushSync();
- });
-
- afterEach(() => {
- unmount(component);
- });
-
- test('mounts', async () => {
- expect(component).toBeTruthy();
- });
-});
diff --git a/ui/tests/lib/components/ChangePassword.svelte.test.js b/ui/tests/lib/components/ChangePassword.svelte.test.js
index 9eb40f5..9db6974 100644
--- a/ui/tests/lib/components/ChangePassword.svelte.test.js
+++ b/ui/tests/lib/components/ChangePassword.svelte.test.js
@@ -1,52 +1,66 @@
-import { flushSync, mount, unmount } from 'svelte';
-import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
+import { render, screen } from '@testing-library/svelte';
+import userEvent from '@testing-library/user-event';
+import { beforeEach, expect, test, describe, it, vi } from 'vitest';
import ChangePassword from '$lib/components/ChangePassword.svelte';
-import axios from 'axios';
-let component;
+const user = userEvent.setup();
-let mocks = vi.hoisted(() => ({
- post: vi.fn()
+const mocks = vi.hoisted(() => ({
+ changePassword: vi.fn()
}));
-describe('ChangePassword', () => {
- beforeEach(() => {
- vi.mock('axios', async (importActual) => {
- const actual = await importActual();
-
- const mockAxios = {
- default: {
- ...actual.default,
- create: vi.fn(() => ({
- ...actual.default.create(),
- post: mocks.post
- }))
- }
- };
-
- return mockAxios;
+describe('ChangePassword', async () => {
+ beforeEach(async () => {
+ render(ChangePassword, {
+ changePassword: mocks.changePassword
});
- mocks.post.mockResolvedValue({ status: 200 });
+ });
- component = mount(ChangePassword, {
- target: document.body // `document` exists because of jsdom
- });
- flushSync();
+ it('submits valid password changes', async () => {
+ const oldPassword = screen.getByLabelText('current password');
+ await user.type(oldPassword, 'old password');
+
+ const newPassword = screen.getByLabelText('new password');
+ await user.type(newPassword, 'new password');
+
+ const confirmPassword = screen.getByLabelText('confirm new password');
+ await user.type(confirmPassword, 'new password');
+
+ const create = screen.getByRole('button');
+ await user.click(create);
+
+ expect(mocks.changePassword).toHaveBeenCalledExactlyOnceWith('old password', 'new password');
});
- afterEach(() => {
- unmount(component);
+ it('is disabled when old password matches new password', async () => {
+ const oldPassword = screen.getByLabelText('new password');
+ await user.type(oldPassword, 'old password');
+
+ const newPassword = screen.getByLabelText('new password');
+ await user.type(newPassword, 'new password');
+
+ const confirmPassword = screen.getByLabelText('confirm new password');
+ await user.type(confirmPassword, 'new password');
+
+ const create = screen.getByRole('button');
+ await user.click(create);
+
+ expect(mocks.changePassword).not.toHaveBeenCalled();
});
- test('onsubmit happy path', async () => {
- // Set value in all three inputs at once:
- const inputs = document.body.querySelectorAll('input[type=password]');
- inputs.value = 'pass';
- // Click the button, then flush the changes so you can synchronously write expectations
- document.body.querySelector('button[type=submit]').click();
- flushSync();
+ it('is disabled when new passwords differ', async () => {
+ const oldPassword = screen.getByLabelText('new password');
+ await user.type(oldPassword, 'old password');
+
+ const newPassword = screen.getByLabelText('new password');
+ await user.type(newPassword, 'new password A');
+
+ const confirmPassword = screen.getByLabelText('confirm new password');
+ await user.type(confirmPassword, 'new password B');
+
+ const create = screen.getByRole('button');
+ await user.click(create);
- // expect(axios.post).toHaveBeenCalledWith('/password', { password: 'pass', to: 'pass' });
- expect(Array.from(inputs.values()).map((i) => i.value)).toEqual(['', '', '']);
+ expect(mocks.changePassword).not.toHaveBeenCalled();
});
});
diff --git a/ui/tests/lib/components/Channel.svelte.test.js b/ui/tests/lib/components/Channel.svelte.test.js
deleted file mode 100644
index a6fdab9..0000000
--- a/ui/tests/lib/components/Channel.svelte.test.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { flushSync, mount, unmount } from 'svelte';
-import { afterEach, beforeEach, describe, expect, test } from 'vitest';
-import Channel from '$lib/components/Channel.svelte';
-
-let component;
-
-describe('Channel', () => {
- beforeEach(() => {
- component = mount(Channel, {
- target: document.body // `document` exists because of jsdom
- });
- flushSync();
- });
-
- afterEach(() => {
- unmount(component);
- });
-
- test('mounts', async () => {
- expect(component).toBeTruthy();
- });
-});
diff --git a/ui/tests/lib/components/ChannelList.svelte.test.js b/ui/tests/lib/components/ChannelList.svelte.test.js
deleted file mode 100644
index 7b5ae4a..0000000
--- a/ui/tests/lib/components/ChannelList.svelte.test.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { flushSync, mount, unmount } from 'svelte';
-import { afterEach, beforeEach, describe, expect, test } from 'vitest';
-import ChannelList from '$lib/components/ChannelList.svelte';
-
-let component;
-
-describe('ChannelList', () => {
- beforeEach(() => {
- component = mount(ChannelList, {
- target: document.body // `document` exists because of jsdom
- });
- flushSync();
- });
-
- afterEach(() => {
- unmount(component);
- });
-
- test('mounts', async () => {
- expect(component).toBeTruthy();
- });
-});
diff --git a/ui/tests/lib/components/CreateChannelForm.svelte.test.js b/ui/tests/lib/components/CreateChannelForm.svelte.test.js
index a4a4695..15d3cfd 100644
--- a/ui/tests/lib/components/CreateChannelForm.svelte.test.js
+++ b/ui/tests/lib/components/CreateChannelForm.svelte.test.js
@@ -1,52 +1,37 @@
-import { flushSync, mount, unmount } from 'svelte';
-import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
+import { render, screen } from '@testing-library/svelte';
+import userEvent from '@testing-library/user-event';
+import { beforeEach, expect, test, describe, it, vi } from 'vitest';
import CreateChannelForm from '$lib/components/CreateChannelForm.svelte';
-import axios from 'axios';
-let component;
+const user = userEvent.setup();
-let mocks = vi.hoisted(() => ({
- post: vi.fn()
+const mocks = vi.hoisted(() => ({
+ createChannel: vi.fn()
}));
-describe('CreateChannelForm', () => {
- beforeEach(() => {
- vi.mock('axios', async (importActual) => {
- const actual = await importActual();
-
- const mockAxios = {
- default: {
- ...actual.default,
- create: vi.fn(() => ({
- ...actual.default.create(),
- post: mocks.post
- }))
- }
- };
-
- return mockAxios;
+describe('CreateChannelForm', async () => {
+ beforeEach(async () => {
+ render(CreateChannelForm, {
+ createChannel: mocks.createChannel
});
- mocks.post.mockResolvedValue({ status: 200 });
-
- component = mount(CreateChannelForm, {
- target: document.body // `document` exists because of jsdom
- });
- flushSync();
});
- afterEach(() => {
- unmount(component);
- });
+ describe('creates channels', async () => {
+ it('with a non-empty name', async () => {
+ const input = screen.getByRole('textbox');
+ await user.type(input, 'channel name');
- test('onsubmit happy path', async () => {
- // Set value on the one input this should match:
- const inputs = document.body.querySelectorAll('input[type=text]');
- inputs.value = 'channel name';
- // Click the button, then flush the changes so you can synchronously write expectations
- document.body.querySelector('button[type=submit]').click();
- flushSync();
+ const create = screen.getByRole('button');
+ await user.click(create);
- expect(mocks.post).toHaveBeenCalled();
- expect(Array.from(inputs.values()).map((i) => i.value)).toEqual(['']);
+ expect(mocks.createChannel).toHaveBeenCalledExactlyOnceWith('channel name');
+ });
+
+ it('with an empty name', async () => {
+ const create = screen.getByRole('button');
+ await user.click(create);
+
+ expect(mocks.createChannel).toHaveBeenCalledExactlyOnceWith('');
+ });
});
});
diff --git a/ui/tests/lib/components/LogIn.svelte.test.js b/ui/tests/lib/components/LogIn.svelte.test.js
index b64d846..ab77c11 100644
--- a/ui/tests/lib/components/LogIn.svelte.test.js
+++ b/ui/tests/lib/components/LogIn.svelte.test.js
@@ -1,22 +1,34 @@
-import { flushSync, mount, unmount } from 'svelte';
-import { afterEach, beforeEach, describe, expect, test } from 'vitest';
+import { render, screen } from '@testing-library/svelte';
+import userEvent from '@testing-library/user-event';
+import { beforeEach, expect, test, describe, it, vi } from 'vitest';
import LogIn from '$lib/components/LogIn.svelte';
-let component;
+const user = userEvent.setup();
-describe('LogIn', () => {
- beforeEach(() => {
- component = mount(LogIn, {
- target: document.body // `document` exists because of jsdom
+const mocks = vi.hoisted(() => ({
+ logIn: vi.fn()
+}));
+
+describe('LogIn', async () => {
+ beforeEach(async () => {
+ render(LogIn, {
+ logIn: mocks.logIn
});
- flushSync();
});
- afterEach(() => {
- unmount(component);
- });
+ it('sends a login request', async () => {
+ const username = screen.getByLabelText('username');
+ await user.type(username, 'my username');
+
+ const password = screen.getByLabelText('password');
+ await user.type(password, 'my very creative and long password');
+
+ const signIn = screen.getByRole('button');
+ await user.click(signIn);
- test('mounts', async () => {
- expect(component).toBeTruthy();
+ expect(mocks.logIn).toHaveBeenCalledExactlyOnceWith(
+ 'my username',
+ 'my very creative and long password'
+ );
});
});
diff --git a/ui/tests/lib/components/MessageInput.svelte.test.js b/ui/tests/lib/components/MessageInput.svelte.test.js
index 3dde5d7..508fb43 100644
--- a/ui/tests/lib/components/MessageInput.svelte.test.js
+++ b/ui/tests/lib/components/MessageInput.svelte.test.js
@@ -1,48 +1,37 @@
-import { flushSync, mount, unmount } from 'svelte';
-import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
+import { render, screen } from '@testing-library/svelte';
+import userEvent from '@testing-library/user-event';
+import { beforeEach, expect, test, describe, it, vi } from 'vitest';
import MessageInput from '$lib/components/MessageInput.svelte';
-import axios from 'axios';
-let component;
+const user = userEvent.setup();
-let mocks = vi.hoisted(() => ({
- post: vi.fn()
+const mocks = vi.hoisted(() => ({
+ sendMessage: vi.fn()
}));
-describe('MessageInput', () => {
- beforeEach(() => {
- vi.mock('axios', async (importActual) => {
- const actual = await importActual();
-
- const mockAxios = {
- default: {
- ...actual.default,
- create: vi.fn(() => ({
- ...actual.default.create(),
- post: mocks.post
- }))
- }
- };
-
- return mockAxios;
+describe('CreateChannelForm', async () => {
+ beforeEach(async () => {
+ render(MessageInput, {
+ sendMessage: mocks.sendMessage
});
- mocks.post.mockResolvedValue({ status: 200 });
-
- component = mount(MessageInput, {
- target: document.body // `document` exists because of jsdom
- });
- flushSync();
});
- afterEach(() => {
- unmount(component);
- });
+ describe('sends a message', async () => {
+ it('with non-empty content', async () => {
+ const input = screen.getByRole('textbox');
+ await user.type(input, 'a happy surprise');
- test('onsubmit happy path', async () => {
- // Click the button, then flush the changes so you can synchronously write expectations
- document.body.querySelector('button[type=submit]').click();
- flushSync();
+ const send = screen.getByRole('button');
+ await user.click(send);
- expect(mocks.post).toHaveBeenCalled();
+ expect(mocks.sendMessage).toHaveBeenCalledExactlyOnceWith('a happy surprise');
+ });
+
+ it('with empty content', async () => {
+ const send = screen.getByRole('button');
+ await user.click(send);
+
+ expect(mocks.sendMessage).toHaveBeenCalledExactlyOnceWith('');
+ });
});
});
diff --git a/ui/tests/lib/components/MessageRun.svelte.test.js b/ui/tests/lib/components/MessageRun.svelte.test.js
deleted file mode 100644
index 7671c52..0000000
--- a/ui/tests/lib/components/MessageRun.svelte.test.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { flushSync, mount, unmount } from 'svelte';
-import { afterEach, beforeEach, describe, expect, test } from 'vitest';
-import MessageRun from '$lib/components/MessageRun.svelte';
-
-let component;
-
-describe('MessageRun', () => {
- beforeEach(() => {
- component = mount(MessageRun, {
- target: document.body // `document` exists because of jsdom
- });
- flushSync();
- });
-
- afterEach(() => {
- unmount(component);
- });
-
- test('mounts', async () => {
- expect(component).toBeTruthy();
- });
-});