summaryrefslogtreecommitdiff
path: root/ui
diff options
context:
space:
mode:
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();
- });
-});