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:
parent
31a7512a0b
commit
c5e75a034a
8 changed files with 523 additions and 88 deletions
10
src/lib/components/pages/Login.svelte
Normal file
10
src/lib/components/pages/Login.svelte
Normal 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} />
|
47
src/lib/components/templates/Login.svelte
Normal file
47
src/lib/components/templates/Login.svelte
Normal 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>
|
|
@ -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!"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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": "",
|
||||
|
|
|
@ -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} />
|
||||
|
|
168
tests/components/pages/Login.test.ts
Normal file
168
tests/components/pages/Login.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
168
tests/components/templates/Login.test.ts
Normal file
168
tests/components/templates/Login.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue