diff options
Diffstat (limited to 'ui')
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">➕</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}>🗑️</button> + <button onclick={ondelete}>🗑️</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">»</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(); - }); -}); |
