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:
André Jaenisch 2024-02-28 16:57:08 +01:00
parent db1b8822e8
commit dd50af1764
No known key found for this signature in database
GPG key ID: 5A668E771F1ED854
6 changed files with 546 additions and 200 deletions

View 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>

View file

@ -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>

View file

@ -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."

View file

@ -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."

View file

@ -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": ""

View 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();
});
});
});