refactor: use a Component for the route

This way I can expose the implementation to Storybook and add tests to
it.

Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
This commit is contained in:
André Jaenisch 2024-02-22 13:34:38 +01:00
parent 31a7512a0b
commit c5e75a034a
No known key found for this signature in database
GPG key ID: 5A668E771F1ED854
8 changed files with 523 additions and 88 deletions

View file

@ -0,0 +1,10 @@
<script lang="ts">
import LoginTemplate from '../templates/Login.svelte';
/**
* Form handling by SvelteKit
*/
export let form: unknown = null;
</script>
<LoginTemplate {form} />

View file

@ -0,0 +1,47 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
/**
* Form handling by SvelteKit
*/
export let form: unknown = null;
</script>
<div class="w-full max-w-md h-full mx-auto flex justify-center items-center py-10">
<div class="space-y-10">
<h1 class="h1 font-bold">{$_('page.login.heading')}</h1>
<p>{$_('page.login.intro')}</p>
<form class="space-y-4" name="login" method="POST" action="?/login">
{#if form?.missing}
<p class="error">{$_('page.login.form.validation.missing')}</p>
{/if}
{#if form?.incorrect}
<p class="error">{$_('page.login.form.validation.incorrect')}</p>
{/if}
<label class="label">
<span>{$_('page.login.form.fields.server.label')}</span>
<select class="select" name="server">
<option value="1">fig.fr33domlover.site</option>
<option value="2">grape.fr33domlover.site</option>
<option value="3">walnut.fr33domlover.site</option>
</select>
</label>
<label class="label">
<span>{$_('page.login.form.fields.account.label')}</span>
<input class="input" name="account" type="text" value={form?.account ?? ''} />
</label>
<label class="label">
<span>{$_('page.login.form.fields.passphrase.label')}</span>
<input class="input" name="passphrase" type="password" placeholder="password" />
</label>
<div class="text-right">
<a href="/" class="btn btn-bg-initial">
{$_('page.login.form.reset')}
</a>
</div>
<button type="submit" class="w-full btn variant-filled-primary">
{$_('page.login.form.submit')}
</button>
</form>
</div>
</div>

View file

@ -1,60 +1,83 @@
{
"page": {
"import_project": {
"form": {
"avatar": "Avatar",
"components": "Komponenten",
"fields": {
"avatar": {
"label": "Bild hochladen"
},
"description": {
"label": "Beschreibung",
"placeholder": "Beschreibe das Projekt in unter 100 Zeichen..."
},
"issues": {
"label": "Issues"
},
"name": {
"error": "Dieses Feld ist erforderlich",
"label": "Name",
"placeholder": "Name des Projekts..."
},
"pr": {
"label": "Pullrequest"
},
"repository": {
"hint": "optional: von Git-Repository importieren",
"label": "Repository",
"placeholder": "URL des Git-Repositorys"
}
},
"submit": "Projekt erstellen"
},
"heading": "Projekt erstellen",
"intro": "Neues F2-Projekt zu Anvil hinzufügen."
},
"profile": {
"activities": {
"block_or_report": "{blockElementOpen}blockieren{blockElementClose} oder {reportElementOpen}melden{reportElementClose}",
"like": "Gefällt mir"
},
"heading": "Profil für",
"history": {
"activities": {
"setup": {
"description": "Das F2-Konto @{username}@{instance} wurde erfolgreich innerhalb von {created_with} erstellt",
"summary": "Kontoeinstellungen"
}
},
"heading": "Aktivitäten"
},
"projects": {
"add_or_import": "{addElementOpen}Ein Projekt hinzufügen{addElementClose} oder {importElementOpen}Ein Projekt importieren{importElementClose}.",
"empty": "Bisher keine Projekte hinzugefügt.",
"heading": "Projekte"
}
},
"welcome": "Willkommen bei Anvil!"
}
"page": {
"import_project": {
"form": {
"avatar": "Avatar",
"components": "Komponenten",
"fields": {
"avatar": {
"label": "Bild hochladen"
},
"description": {
"label": "Beschreibung",
"placeholder": "Beschreibe das Projekt in unter 100 Zeichen..."
},
"issues": {
"label": "Issues"
},
"name": {
"error": "Dieses Feld ist erforderlich",
"label": "Name",
"placeholder": "Name des Projekts..."
},
"pr": {
"label": "Pullrequest"
},
"repository": {
"hint": "optional: von Git-Repository importieren",
"label": "Repository",
"placeholder": "URL des Git-Repositorys"
}
},
"submit": "Projekt erstellen"
},
"heading": "Projekt erstellen",
"intro": "Neues F2-Projekt zu Anvil hinzufügen."
},
"login": {
"form": {
"validation": {
"incorrect": "",
"missing": ""
},
"fields": {
"account": {
"label": ""
},
"passphrase": {
"label": ""
},
"server": {
"label": ""
}
},
"reset": "",
"submit": ""
},
"heading": "",
"intro": ""
},
"profile": {
"activities": {
"block_or_report": "{blockElementOpen}blockieren{blockElementClose} oder {reportElementOpen}melden{reportElementClose}",
"like": "Gefällt mir"
},
"heading": "Profil für",
"history": {
"activities": {
"setup": {
"description": "Das F2-Konto @{username}@{instance} wurde erfolgreich innerhalb von {created_with} erstellt",
"summary": "Kontoeinstellungen"
}
},
"heading": "Aktivitäten"
},
"projects": {
"add_or_import": "{addElementOpen}Ein Projekt hinzufügen{addElementClose} oder {importElementOpen}Ein Projekt importieren{importElementClose}.",
"empty": "Bisher keine Projekte hinzugefügt.",
"heading": "Projekte"
}
},
"welcome": "Willkommen bei Anvil!"
}
}

View file

@ -34,6 +34,29 @@
"heading": "Create project",
"intro": "Add a new F2 project to Anvil."
},
"login": {
"form": {
"validation": {
"incorrect": "Account or password wrong.",
"missing": "The account field is required."
},
"fields": {
"account": {
"label": "Account name"
},
"passphrase": {
"label": "Passphrase"
},
"server": {
"label": "F2 server"
}
},
"reset": "Reset passphrase",
"submit": "Log in"
},
"heading": "Log in",
"intro": "To use Anvil with your F2 account, fill in your credentials."
},
"profile": {
"activities": {
"block_or_report": "{blockElementOpen}block{blockElementClose} or {reportElementOpen}report{reportElementClose}",

View file

@ -34,6 +34,29 @@
"heading": "",
"intro": ""
},
"login": {
"form": {
"validation": {
"incorrect": "",
"missing": ""
},
"fields": {
"account": {
"label": ""
},
"passphrase": {
"label": ""
},
"server": {
"label": ""
}
},
"reset": "",
"submit": ""
},
"heading": "",
"intro": ""
},
"profile": {
"activities": {
"block_or_report": "",

View file

@ -1,35 +1,8 @@
<script>
import Login from '$lib/components/pages/Login.svelte';
/** @type {import('./$types').ActionData} */
export let form;
</script>
<div class="w-full max-w-md h-full mx-auto flex justify-center items-center py-10">
<div class="space-y-10">
<h1 class="h1 font-bold">Log in</h1>
<p>To use Anvil with your F2 account, fill in your credentials.</p>
<form class="space-y-4" method="POST" action="?/login">
{#if form?.missing}<p class="error">The account field is required.</p>{/if}
{#if form?.incorrect}<p class="error">Account or password wrong.</p>{/if}
<label class="label">
<span>F2 server</span>
<select class="select" title="F2 server" name="server">
<option value="1">fig.fr33domlover.site</option>
<option value="2">grape.fr33domlover.site</option>
<option value="3">walnut.fr33domlover.site</option>
</select>
</label>
<label class="label">
<span>Account name</span>
<input class="input" name="account" type="text" value={form?.account ?? ''} />
</label>
<label class="label">
<span>Passphrase</span>
<input class="input" name="passphrase" type="password" placeholder="password" />
</label>
<div class="text-right">
<a href="/" class="btn btn-bg-initial">Reset passphrase</a>
</div>
<button type="submit" class="w-full btn variant-filled-primary"> Log in </button>
</form>
</div>
</div>
<Login {form} />

View file

@ -0,0 +1,168 @@
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/svelte';
import { init, locale, register } from 'svelte-i18n';
import Login from '../../../src/lib/components/pages/Login.svelte';
import enMessages from '../../../src/lib/i18n/locales/en.json';
describe('Login.svelte', () => {
beforeEach(() => {
register('en', () => import('../../../src/lib/i18n/locales/en.json'));
init({ fallbackLocale: 'en', initialLocale: 'en' });
locale.set('en');
});
it('should mount', () => {
// Arrange
const form = {};
// Act
const { container } = render(Login, { form });
// Assert
expect(container).toBeTruthy();
});
it('should have a h1', () => {
// Arrange
const form = {};
// Act
render(Login, { form });
const h1 = screen.getByRole('heading', { level: 1 });
// Assert
expect(h1).toBeInTheDocument();
expect(h1).toHaveTextContent(enMessages.page.login.heading);
});
it('should have an intro', () => {
// Arrange
const form = {};
// Act
render(Login, { form });
const intro = screen.getByText(enMessages.page.login.intro);
// Assert
expect(intro).toBeInTheDocument();
});
it('should have a form', () => {
// Arrange
const form = {};
// Act
render(Login, { form });
const formElement = screen.getByRole('form');
// Assert
expect(formElement).toBeInTheDocument();
});
it('should have a server select control', () => {
// Arrange
const form = {};
// Act
render(Login, { form });
const server = screen.getByLabelText(enMessages.page.login.form.fields.server.label);
// Assert
expect(server).toBeInTheDocument();
});
it('should have a account input control', () => {
// Arrange
const form = {};
// Act
render(Login, { form });
const account = screen.getByLabelText(enMessages.page.login.form.fields.account.label);
// Assert
expect(account).toBeInTheDocument();
});
it('should have a passphrase input control', () => {
// Arrange
const form = {};
// Act
render(Login, { form });
const passphrase = screen.getByLabelText(enMessages.page.login.form.fields.passphrase.label);
// Assert
expect(passphrase).toBeInTheDocument();
});
it('should have a reset passphrase link', () => {
// Arrange
const form = {};
// Act
render(Login, { form });
const reset = screen.getByRole('link', { name: enMessages.page.login.form.reset });
// Assert
expect(reset).toBeInTheDocument();
});
it('should have a submit button', () => {
// Arrange
const form = {};
// Act
render(Login, { form });
const submit = screen.getByRole('button', { name: enMessages.page.login.form.submit });
// Assert
expect(submit).toBeInTheDocument();
});
it('should have no validation errors', () => {
// Arrange
const form = {};
// Act
render(Login, { form });
// Assert
expect(
screen.queryByText(enMessages.page.login.form.validation.incorrect)
).not.toBeInTheDocument();
expect(
screen.queryByText(enMessages.page.login.form.validation.missing)
).not.toBeInTheDocument();
});
describe('when validation failed', () => {
it('should show an error on required inputs', () => {
// Arrange
const form = {
missing: true
};
// Act
render(Login, { form });
const intro = screen.getByText(enMessages.page.login.form.validation.missing);
// Assert
expect(intro).toBeInTheDocument();
});
it('should show an error on invalid inputs', () => {
// Arrange
const form = {
incorrect: true
};
// Act
render(Login, { form });
const intro = screen.getByText(enMessages.page.login.form.validation.incorrect);
// Assert
expect(intro).toBeInTheDocument();
});
});
});

View file

@ -0,0 +1,168 @@
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/svelte';
import { init, locale, register } from 'svelte-i18n';
import Login from '../../../src/lib/components/templates/Login.svelte';
import enMessages from '../../../src/lib/i18n/locales/en.json';
describe('Login.svelte', () => {
beforeEach(() => {
register('en', () => import('../../../src/lib/i18n/locales/en.json'));
init({ fallbackLocale: 'en', initialLocale: 'en' });
locale.set('en');
});
it('should mount', () => {
// Arrange
const form = {};
// Act
const { container } = render(Login, { form });
// Assert
expect(container).toBeTruthy();
});
it('should have a h1', () => {
// Arrange
const form = {};
// Act
render(Login, { form });
const h1 = screen.getByRole('heading', { level: 1 });
// Assert
expect(h1).toBeInTheDocument();
expect(h1).toHaveTextContent(enMessages.page.login.heading);
});
it('should have an intro', () => {
// Arrange
const form = {};
// Act
render(Login, { form });
const intro = screen.getByText(enMessages.page.login.intro);
// Assert
expect(intro).toBeInTheDocument();
});
it('should have a form', () => {
// Arrange
const form = {};
// Act
render(Login, { form });
const formElement = screen.getByRole('form');
// Assert
expect(formElement).toBeInTheDocument();
});
it('should have a server select control', () => {
// Arrange
const form = {};
// Act
render(Login, { form });
const server = screen.getByLabelText(enMessages.page.login.form.fields.server.label);
// Assert
expect(server).toBeInTheDocument();
});
it('should have a account input control', () => {
// Arrange
const form = {};
// Act
render(Login, { form });
const account = screen.getByLabelText(enMessages.page.login.form.fields.account.label);
// Assert
expect(account).toBeInTheDocument();
});
it('should have a passphrase input control', () => {
// Arrange
const form = {};
// Act
render(Login, { form });
const passphrase = screen.getByLabelText(enMessages.page.login.form.fields.passphrase.label);
// Assert
expect(passphrase).toBeInTheDocument();
});
it('should have a reset passphrase link', () => {
// Arrange
const form = {};
// Act
render(Login, { form });
const reset = screen.getByRole('link', { name: enMessages.page.login.form.reset });
// Assert
expect(reset).toBeInTheDocument();
});
it('should have a submit button', () => {
// Arrange
const form = {};
// Act
render(Login, { form });
const submit = screen.getByRole('button', { name: enMessages.page.login.form.submit });
// Assert
expect(submit).toBeInTheDocument();
});
it('should have no validation errors', () => {
// Arrange
const form = {};
// Act
render(Login, { form });
// Assert
expect(
screen.queryByText(enMessages.page.login.form.validation.incorrect)
).not.toBeInTheDocument();
expect(
screen.queryByText(enMessages.page.login.form.validation.missing)
).not.toBeInTheDocument();
});
describe('when validation failed', () => {
it('should show an error on required inputs', () => {
// Arrange
const form = {
missing: true
};
// Act
render(Login, { form });
const intro = screen.getByText(enMessages.page.login.form.validation.missing);
// Assert
expect(intro).toBeInTheDocument();
});
it('should show an error on invalid inputs', () => {
// Arrange
const form = {
incorrect: true
};
// Act
render(Login, { form });
const intro = screen.getByText(enMessages.page.login.form.validation.incorrect);
// Assert
expect(intro).toBeInTheDocument();
});
});
});