From dd50af17640e1d4490e8134e9a68cf37399774ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Jaenisch?= Date: Wed, 28 Feb 2024 16:57:08 +0100 Subject: [PATCH] refactor: extract form into organism MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/lib/components/organisms/LoginForm.svelte | 56 ++++ src/lib/components/templates/Login.svelte | 60 ++-- src/lib/i18n/locales/de.json | 163 +++++----- src/lib/i18n/locales/en.json | 13 +- src/lib/i18n/locales/he.json | 163 +++++----- tests/components/organisms/LoginForm.test.ts | 291 ++++++++++++++++++ 6 files changed, 546 insertions(+), 200 deletions(-) create mode 100644 src/lib/components/organisms/LoginForm.svelte create mode 100644 tests/components/organisms/LoginForm.test.ts diff --git a/src/lib/components/organisms/LoginForm.svelte b/src/lib/components/organisms/LoginForm.svelte new file mode 100644 index 0000000..cfb5257 --- /dev/null +++ b/src/lib/components/organisms/LoginForm.svelte @@ -0,0 +1,56 @@ + + +
+ {#if form?.missing} +

{$_(i18n.validation.missing)}

+ {/if} + {#if form?.incorrect} +

{$_(i18n.validation.incorrect)}

+ {/if} + + + + + +
diff --git a/src/lib/components/templates/Login.svelte b/src/lib/components/templates/Login.svelte index d60b436..d19f1a4 100644 --- a/src/lib/components/templates/Login.svelte +++ b/src/lib/components/templates/Login.svelte @@ -1,47 +1,43 @@

{$_('page.login.heading')}

{$_('page.login.intro')}

-
- {#if form?.missing} -

{$_('page.login.form.validation.missing')}

- {/if} - {#if form?.incorrect} -

{$_('page.login.form.validation.incorrect')}

- {/if} - - - - - -
+
diff --git a/src/lib/i18n/locales/de.json b/src/lib/i18n/locales/de.json index 505339b..417e113 100644 --- a/src/lib/i18n/locales/de.json +++ b/src/lib/i18n/locales/de.json @@ -1,83 +1,84 @@ { - "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": "Accountname oder Kennwort sind falsch.", - "missing": "Das Account-Feld ist verbindlich." - }, - "fields": { - "account": { - "label": "Accountname" - }, - "passphrase": { - "label": "Passwort" - }, - "server": { - "label": "F2-Server" - } - }, - "reset": "Passwort zurücksetzen", - "submit": "Einloggen" - }, - "heading": "Einloggen", - "intro": "Die Einwahldaten ausfüllen um Anvil mit deinem F2-Account zu benutzen." - }, - "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": { + "fields": { + "account": { + "label": "Accountname" + }, + "passphrase": { + "label": "Passwort", + "placeholder": "" + }, + "server": { + "label": "F2-Server" + } + }, + "reset": "Passwort zurücksetzen", + "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." + }, + "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!" + } } diff --git a/src/lib/i18n/locales/en.json b/src/lib/i18n/locales/en.json index 9477cdc..2ae8919 100644 --- a/src/lib/i18n/locales/en.json +++ b/src/lib/i18n/locales/en.json @@ -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." diff --git a/src/lib/i18n/locales/he.json b/src/lib/i18n/locales/he.json index b511436..4463811 100644 --- a/src/lib/i18n/locales/he.json +++ b/src/lib/i18n/locales/he.json @@ -1,83 +1,84 @@ { - "page": { - "import_project": { - "form": { - "avatar": "תמונה", - "components": "רכיבים", - "fields": { - "avatar": { - "label": "להעלות תמונה?" - }, - "description": { - "label": "תיאור", - "placeholder": "על הפרויקט בפחות מ-100 אותיות…" - }, - "issues": { - "label": "סוגיות" - }, - "name": { - "error": "חובה", - "label": "שם", - "placeholder": "שם הפרויקט…" - }, - "pr": { - "label": "פרים" - }, - "repository": { - "hint": "אפשר לייבא קוד מקופסת גיט", - "label": "קופסת קוד", - "placeholder": "לינק לקופסת קוד של גיט" - } - }, - "submit": "ליצור פרויקט" - }, - "heading": "יצירת פרויקט", - "intro": "" - }, - "login": { - "form": { - "validation": { - "incorrect": "שם המשתמש או הסיסמה שגויים.", - "missing": "שם המשתמש חובה." - }, - "fields": { - "account": { - "label": "שם משתמש" - }, - "passphrase": { - "label": "סיסמה" - }, - "server": { - "label": "" - } - }, - "reset": "לשנות סיסמה?", - "submit": "להיכנס" - }, - "heading": "כניסה", - "intro": "" - }, - "profile": { - "activities": { - "block_or_report": "{blockElementOpen}לחסום{blockElementClose} או {reportElementOpen}לדווח{reportElementClose}", - "like": "אהבתי" - }, - "heading": "הפרופיל של", - "history": { - "activities": { - "setup": { - "description": "", - "summary": "" - } - }, - "heading": "פעילות" - }, - "projects": { - "add_or_import": "", - "empty": "עדיין אין כאן פרוייקטים.", - "heading": "פרויקטים" - } - }, - "welcome": "" - } + "page": { + "import_project": { + "form": { + "avatar": "תמונה", + "components": "רכיבים", + "fields": { + "avatar": { + "label": "להעלות תמונה?" + }, + "description": { + "label": "תיאור", + "placeholder": "על הפרויקט בפחות מ-100 אותיות…" + }, + "issues": { + "label": "סוגיות" + }, + "name": { + "error": "חובה", + "label": "שם", + "placeholder": "שם הפרויקט…" + }, + "pr": { + "label": "פרים" + }, + "repository": { + "hint": "אפשר לייבא קוד מקופסת גיט", + "label": "קופסת קוד", + "placeholder": "לינק לקופסת קוד של גיט" + } + }, + "submit": "ליצור פרויקט" + }, + "heading": "יצירת פרויקט", + "intro": "" + }, + "login": { + "form": { + "fields": { + "account": { + "label": "שם משתמש" + }, + "passphrase": { + "label": "סיסמה", + "placeholder": "" + }, + "server": { + "label": "" + } + }, + "reset": "לשנות סיסמה?", + "submit": "להיכנס", + "validation": { + "incorrect": "שם המשתמש או הסיסמה שגויים.", + "missing": "שם המשתמש חובה." + } + }, + "heading": "כניסה", + "intro": "" + }, + "profile": { + "activities": { + "block_or_report": "{blockElementOpen}לחסום{blockElementClose} או {reportElementOpen}לדווח{reportElementClose}", + "like": "אהבתי" + }, + "heading": "הפרופיל של", + "history": { + "activities": { + "setup": { + "description": "", + "summary": "" + } + }, + "heading": "פעילות" + }, + "projects": { + "add_or_import": "", + "empty": "עדיין אין כאן פרוייקטים.", + "heading": "פרויקטים" + } + }, + "welcome": "" + } } diff --git a/tests/components/organisms/LoginForm.test.ts b/tests/components/organisms/LoginForm.test.ts new file mode 100644 index 0000000..3f1a7d0 --- /dev/null +++ b/tests/components/organisms/LoginForm.test.ts @@ -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(); + }); + }); +});