refactor: extract form into organism
I want to explore how I could potentially reuse components. Therefore I extract the form into a component. Given its complexity it could either be a molecule or an organism. I went with the latter here. I have to explore how I pass around Objects and Arrays in Svelte so that I can move server logic up in the component tree. When I reuse components, I potentially want to define the labels and placeholders. Therefore the hard-coded strings have to become variables. Eventually I will define the key on the template level and use the real values on page level and pass it down. That would break tests currently so I'm not doing it in this refactoring. Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
This commit is contained in:
parent
db1b8822e8
commit
dd50af1764
6 changed files with 546 additions and 200 deletions
56
src/lib/components/organisms/LoginForm.svelte
Normal file
56
src/lib/components/organisms/LoginForm.svelte
Normal file
|
@ -0,0 +1,56 @@
|
|||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
|
||||
/**
|
||||
* Form handling by SvelteKit
|
||||
*/
|
||||
export let form: unknown = null;
|
||||
|
||||
/**
|
||||
* Translation keys to enable reuse
|
||||
*/
|
||||
export let i18n: null;
|
||||
|
||||
/**
|
||||
* Available servers for signin
|
||||
*/
|
||||
export let servers: Array = [];
|
||||
</script>
|
||||
|
||||
<form class="space-y-4" name="login" method="POST" action="?/login">
|
||||
{#if form?.missing}
|
||||
<p class="error">{$_(i18n.validation.missing)}</p>
|
||||
{/if}
|
||||
{#if form?.incorrect}
|
||||
<p class="error">{$_(i18n.validation.incorrect)}</p>
|
||||
{/if}
|
||||
<label class="label">
|
||||
<span>{$_(i18n.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>{$_(i18n.fields.account.label)}</span>
|
||||
<input class="input" name="account" type="text" value={form?.account ?? ''} />
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>{$_(i18n.fields.passphrase.label)}</span>
|
||||
<input
|
||||
class="input"
|
||||
name="passphrase"
|
||||
type="password"
|
||||
placeholder={$_(i18n.fields.passphrase.placeholder)}
|
||||
/>
|
||||
</label>
|
||||
<div class="text-right">
|
||||
<a href="/" class="btn btn-bg-initial">
|
||||
{$_(i18n.reset)}
|
||||
</a>
|
||||
</div>
|
||||
<button type="submit" class="w-full btn variant-filled-primary">
|
||||
{$_(i18n.submit)}
|
||||
</button>
|
||||
</form>
|
|
@ -1,47 +1,43 @@
|
|||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
|
||||
import LoginForm from '../organisms/LoginForm.svelte';
|
||||
|
||||
/**
|
||||
* Form handling by SvelteKit
|
||||
*/
|
||||
export let form: unknown = null;
|
||||
const i18n = {
|
||||
fields: {
|
||||
account: {
|
||||
label: 'page.login.form.fields.account.label'
|
||||
},
|
||||
passphrase: {
|
||||
label: 'page.login.form.fields.passphrase.label',
|
||||
placeholder: 'page.login.form.fields.passphrase.placeholder'
|
||||
},
|
||||
server: {
|
||||
label: 'page.login.form.fields.server.label'
|
||||
}
|
||||
},
|
||||
reset: 'page.login.form.reset',
|
||||
submit: 'page.login.form.submit',
|
||||
validation: {
|
||||
incorrect: 'page.login.form.validation.incorrect',
|
||||
missing: 'page.login.form.validation.missing'
|
||||
}
|
||||
};
|
||||
const servers = {
|
||||
'1': 'fig.fr33domlover.site',
|
||||
'2': 'grape.fr33domlover.site',
|
||||
'3': 'walnut.fr33domlover.site'
|
||||
};
|
||||
</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>
|
||||
<LoginForm {form} {i18n} {servers} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -36,23 +36,24 @@
|
|||
},
|
||||
"login": {
|
||||
"form": {
|
||||
"validation": {
|
||||
"incorrect": "Accountname oder Kennwort sind falsch.",
|
||||
"missing": "Das Account-Feld ist verbindlich."
|
||||
},
|
||||
"fields": {
|
||||
"account": {
|
||||
"label": "Accountname"
|
||||
},
|
||||
"passphrase": {
|
||||
"label": "Passwort"
|
||||
"label": "Passwort",
|
||||
"placeholder": ""
|
||||
},
|
||||
"server": {
|
||||
"label": "F2-Server"
|
||||
}
|
||||
},
|
||||
"reset": "Passwort zurücksetzen",
|
||||
"submit": "Einloggen"
|
||||
"submit": "Einloggen",
|
||||
"validation": {
|
||||
"incorrect": "Accountname oder Kennwort sind falsch.",
|
||||
"missing": "Das Account-Feld ist verbindlich."
|
||||
}
|
||||
},
|
||||
"heading": "Einloggen",
|
||||
"intro": "Die Einwahldaten ausfüllen um Anvil mit deinem F2-Account zu benutzen."
|
||||
|
|
|
@ -36,23 +36,24 @@
|
|||
},
|
||||
"login": {
|
||||
"form": {
|
||||
"validation": {
|
||||
"incorrect": "Account or password wrong.",
|
||||
"missing": "The account field is required."
|
||||
},
|
||||
"fields": {
|
||||
"account": {
|
||||
"label": "Account name"
|
||||
},
|
||||
"passphrase": {
|
||||
"label": "Passphrase"
|
||||
"label": "Passphrase",
|
||||
"placeholder": "password"
|
||||
},
|
||||
"server": {
|
||||
"label": "F2 server"
|
||||
}
|
||||
},
|
||||
"reset": "Reset passphrase",
|
||||
"submit": "Log in"
|
||||
"submit": "Log in",
|
||||
"validation": {
|
||||
"incorrect": "Account or password wrong.",
|
||||
"missing": "The account field is required."
|
||||
}
|
||||
},
|
||||
"heading": "Log in",
|
||||
"intro": "To use Anvil with your F2 account, fill in your credentials."
|
||||
|
|
|
@ -36,23 +36,24 @@
|
|||
},
|
||||
"login": {
|
||||
"form": {
|
||||
"validation": {
|
||||
"incorrect": "שם המשתמש או הסיסמה שגויים.",
|
||||
"missing": "שם המשתמש חובה."
|
||||
},
|
||||
"fields": {
|
||||
"account": {
|
||||
"label": "שם משתמש"
|
||||
},
|
||||
"passphrase": {
|
||||
"label": "סיסמה"
|
||||
"label": "סיסמה",
|
||||
"placeholder": ""
|
||||
},
|
||||
"server": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"reset": "לשנות סיסמה?",
|
||||
"submit": "להיכנס"
|
||||
"submit": "להיכנס",
|
||||
"validation": {
|
||||
"incorrect": "שם המשתמש או הסיסמה שגויים.",
|
||||
"missing": "שם המשתמש חובה."
|
||||
}
|
||||
},
|
||||
"heading": "כניסה",
|
||||
"intro": ""
|
||||
|
|
291
tests/components/organisms/LoginForm.test.ts
Normal file
291
tests/components/organisms/LoginForm.test.ts
Normal file
|
@ -0,0 +1,291 @@
|
|||
import '@testing-library/jest-dom';
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
import { init, locale, register } from 'svelte-i18n';
|
||||
|
||||
import LoginForm from '../../../src/lib/components/organisms/LoginForm.svelte';
|
||||
import enMessages from '../../../src/lib/i18n/locales/en.json';
|
||||
|
||||
describe('LoginForm.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 = {};
|
||||
const i18n = {
|
||||
fields: {
|
||||
account: {
|
||||
label: enMessages.page.login.form.fields.account.label
|
||||
},
|
||||
passphrase: {
|
||||
label: enMessages.page.login.form.fields.passphrase.label
|
||||
},
|
||||
server: {
|
||||
label: enMessages.page.login.form.fields.server.label
|
||||
}
|
||||
}
|
||||
};
|
||||
const servers = [];
|
||||
|
||||
// Act
|
||||
const { container } = render(LoginForm, { form, i18n, servers });
|
||||
|
||||
// Assert
|
||||
expect(container).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have a form', () => {
|
||||
// Arrange
|
||||
const form = {};
|
||||
const i18n = {
|
||||
fields: {
|
||||
account: {
|
||||
label: enMessages.page.login.form.fields.account.label
|
||||
},
|
||||
passphrase: {
|
||||
label: enMessages.page.login.form.fields.passphrase.label
|
||||
},
|
||||
server: {
|
||||
label: enMessages.page.login.form.fields.server.label
|
||||
}
|
||||
}
|
||||
};
|
||||
const servers = [];
|
||||
|
||||
// Act
|
||||
render(LoginForm, { form, i18n, servers });
|
||||
const formElement = screen.getByRole('form');
|
||||
|
||||
// Assert
|
||||
expect(formElement).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have a server select control', () => {
|
||||
// Arrange
|
||||
const form = {};
|
||||
const i18n = {
|
||||
fields: {
|
||||
account: {
|
||||
label: enMessages.page.login.form.fields.account.label
|
||||
},
|
||||
passphrase: {
|
||||
label: enMessages.page.login.form.fields.passphrase.label
|
||||
},
|
||||
server: {
|
||||
label: enMessages.page.login.form.fields.server.label
|
||||
}
|
||||
}
|
||||
};
|
||||
const servers = [];
|
||||
|
||||
// Act
|
||||
render(LoginForm, { form, i18n, servers });
|
||||
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 = {};
|
||||
const i18n = {
|
||||
fields: {
|
||||
account: {
|
||||
label: enMessages.page.login.form.fields.account.label
|
||||
},
|
||||
passphrase: {
|
||||
label: enMessages.page.login.form.fields.passphrase.label
|
||||
},
|
||||
server: {
|
||||
label: enMessages.page.login.form.fields.server.label
|
||||
}
|
||||
}
|
||||
};
|
||||
const servers = [];
|
||||
|
||||
// Act
|
||||
render(LoginForm, { form, i18n, servers });
|
||||
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 = {};
|
||||
const i18n = {
|
||||
fields: {
|
||||
account: {
|
||||
label: enMessages.page.login.form.fields.account.label
|
||||
},
|
||||
passphrase: {
|
||||
label: enMessages.page.login.form.fields.passphrase.label
|
||||
},
|
||||
server: {
|
||||
label: enMessages.page.login.form.fields.server.label
|
||||
}
|
||||
}
|
||||
};
|
||||
const servers = [];
|
||||
|
||||
// Act
|
||||
render(LoginForm, { form, i18n, servers });
|
||||
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 = {};
|
||||
const i18n = {
|
||||
fields: {
|
||||
account: {
|
||||
label: enMessages.page.login.form.fields.account.label
|
||||
},
|
||||
passphrase: {
|
||||
label: enMessages.page.login.form.fields.passphrase.label
|
||||
},
|
||||
server: {
|
||||
label: enMessages.page.login.form.fields.server.label
|
||||
}
|
||||
},
|
||||
reset: enMessages.page.login.form.reset
|
||||
};
|
||||
const servers = [];
|
||||
|
||||
// Act
|
||||
render(LoginForm, { form, i18n, servers });
|
||||
const reset = screen.getByRole('link', { name: enMessages.page.login.form.reset });
|
||||
|
||||
// Assert
|
||||
expect(reset).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have a submit button', () => {
|
||||
// Arrange
|
||||
const form = {};
|
||||
const i18n = {
|
||||
fields: {
|
||||
account: {
|
||||
label: enMessages.page.login.form.fields.account.label
|
||||
},
|
||||
passphrase: {
|
||||
label: enMessages.page.login.form.fields.passphrase.label
|
||||
},
|
||||
server: {
|
||||
label: enMessages.page.login.form.fields.server.label
|
||||
}
|
||||
},
|
||||
submit: enMessages.page.login.form.submit
|
||||
};
|
||||
const servers = [];
|
||||
|
||||
// Act
|
||||
render(LoginForm, { form, i18n, servers });
|
||||
const submit = screen.getByRole('button', { name: enMessages.page.login.form.submit });
|
||||
|
||||
// Assert
|
||||
expect(submit).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have no validation errors', () => {
|
||||
// Arrange
|
||||
const form = {};
|
||||
const i18n = {
|
||||
fields: {
|
||||
account: {
|
||||
label: enMessages.page.login.form.fields.account.label
|
||||
},
|
||||
passphrase: {
|
||||
label: enMessages.page.login.form.fields.passphrase.label
|
||||
},
|
||||
server: {
|
||||
label: enMessages.page.login.form.fields.server.label
|
||||
}
|
||||
}
|
||||
};
|
||||
const servers = [];
|
||||
|
||||
// Act
|
||||
render(LoginForm, { form, i18n, servers });
|
||||
|
||||
// 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
|
||||
};
|
||||
const i18n = {
|
||||
fields: {
|
||||
account: {
|
||||
label: enMessages.page.login.form.fields.account.label
|
||||
},
|
||||
passphrase: {
|
||||
label: enMessages.page.login.form.fields.passphrase.label
|
||||
},
|
||||
server: {
|
||||
label: enMessages.page.login.form.fields.server.label
|
||||
}
|
||||
},
|
||||
validation: {
|
||||
missing: enMessages.page.login.form.validation.missing
|
||||
}
|
||||
};
|
||||
const servers = [];
|
||||
|
||||
// Act
|
||||
render(LoginForm, { form, i18n, servers });
|
||||
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
|
||||
};
|
||||
const i18n = {
|
||||
fields: {
|
||||
account: {
|
||||
label: enMessages.page.login.form.fields.account.label
|
||||
},
|
||||
passphrase: {
|
||||
label: enMessages.page.login.form.fields.passphrase.label
|
||||
},
|
||||
server: {
|
||||
label: enMessages.page.login.form.fields.server.label
|
||||
}
|
||||
},
|
||||
validation: {
|
||||
incorrect: enMessages.page.login.form.validation.incorrect
|
||||
}
|
||||
};
|
||||
const servers = [];
|
||||
|
||||
// Act
|
||||
render(LoginForm, { form, i18n, servers });
|
||||
const intro = screen.getByText(enMessages.page.login.form.validation.incorrect);
|
||||
|
||||
// Assert
|
||||
expect(intro).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue