diff --git a/src/i18n/generated/da-v2.json b/src/i18n/generated/da-v2.json index a0977c39..64e67006 100644 --- a/src/i18n/generated/da-v2.json +++ b/src/i18n/generated/da-v2.json @@ -2143,6 +2143,7 @@ "customer_login_title": "@:{'templates.generated.compat.global.login'}", "driver_entry_intro": "@.capitalize:{'words.generated.er'} @:{'words.generated.du'} @:{'words.generated.chauffør'}? @.capitalize:{'words.generated.log'} @:{'words.generated.ind'} @:{'words.generated.her'} @:{'words.generated.for'} @:{'words.generated.at'} @:{'words.generated.fa'} @:{'words.generated.adgang'} @:{'words.generated.til'} @:{'words.generated.dine'} @:{'words.generated.vaske'}.", "driver_login_button": "@.capitalize:{'words.generated.log'} @:{'words.generated.ind'} @.lower:{'words.generated.som'} @:{'words.generated.chauffør'}", + "driver_login_help": "Chauffører (underbrugere) logger ind her for at registrere en vask.", "driver_login_title": "@.capitalize:{'words.generated.chauffør'} @.capitalize:{'words.generated.login'}", "forgot_password": "Glemt @:{'words.generated.adgangskode'}", "operator_tagline": "@.capitalize:{'words.generated.truckwash'}.@:{'words.generated.io'} – @:{'words.generated.digitale'} @:{'words.generated.løsninger'} @:{'words.generated.til'} @:{'words.generated.lastbilvask'}", diff --git a/src/i18n/generated/de-v2.json b/src/i18n/generated/de-v2.json index 38b3b872..474d85e0 100644 --- a/src/i18n/generated/de-v2.json +++ b/src/i18n/generated/de-v2.json @@ -2260,6 +2260,7 @@ "username": "@:{'words.generated.benutzername'}", "your_username": "@:{'words.generated.ihr'} @:{'words.generated.benutzername'}", "driver_login_button": "Fahrer-Login", + "driver_login_help": "Fahrer (Unterbenutzer) melden sich hier an, um eine Wäsche zu registrieren.", "driver_entry_intro": "Sind Sie Fahrer? Melden Sie sich hier an, um eine Wäsche zu registrieren." }, "book_wash": { diff --git a/src/i18n/generated/en-v2.json b/src/i18n/generated/en-v2.json index 857c1dce..1c71289d 100644 --- a/src/i18n/generated/en-v2.json +++ b/src/i18n/generated/en-v2.json @@ -1974,6 +1974,7 @@ "customer_login_title": "@.capitalize:{'words.generated.customer'} @.capitalize:{'words.generated.login'}", "driver_entry_intro": "@.capitalize:{'words.generated.are'} @:{'words.generated.you'} @:{'words.generated.a'} @:{'words.generated.driver'}? @.capitalize:{'words.generated.log'} @:{'words.generated.in'} @:{'words.generated.here'} @:{'words.generated.to'} @:{'words.generated.register'} @:{'words.generated.a'} @:{'words.generated.wash'}.", "driver_login_button": "@.capitalize:{'words.generated.driver'} @.capitalize:{'words.generated.login'}", + "driver_login_help": "Drivers (sub-users) log in here to register a wash.", "driver_login_title": "@.capitalize:{'words.generated.driver'} @.capitalize:{'words.generated.login'}", "forgot_password": "Forgot @.capitalize:{'words.generated.password'}", "operator_tagline": "@.capitalize:{'words.generated.truckwash'}.@:{'words.generated.io'} - @:{'words.generated.digital'} @:{'words.generated.solutions'} @:{'words.generated.for'} @:{'words.generated.truck'} @:{'words.generated.washing'}", diff --git a/src/i18n/generated/global-v2.json b/src/i18n/generated/global-v2.json index 35d40d52..48fc9091 100644 --- a/src/i18n/generated/global-v2.json +++ b/src/i18n/generated/global-v2.json @@ -786,6 +786,7 @@ "customer_number": "@:{'templates.generated.compat.objects.columns.customer_number'}", "driver_entry_intro": "@:{'templates.generated.compat.auth.driver_entry_intro'}", "driver_login_button": "@:{'templates.generated.compat.auth.driver_login_button'}", + "driver_login_help": "@:{'templates.generated.compat.auth.driver_login_help'}", "driver_login_title": "@:{'templates.generated.compat.auth.driver_login_title'}", "forgot_password": "@:{'templates.generated.compat.auth.forgot_password'}", "login_as_customer": "@:{'templates.generated.compat.global.login_as_customer'}", diff --git a/src/i18n/generated/no-v2.json b/src/i18n/generated/no-v2.json index d35245ff..0fc554dc 100644 --- a/src/i18n/generated/no-v2.json +++ b/src/i18n/generated/no-v2.json @@ -2263,6 +2263,7 @@ "username": "@.capitalize:{'words.generated.brukernavn'}", "your_username": "@.capitalize:{'words.generated.ditt'} @:{'words.generated.brukernavn'}", "driver_login_button": "Sjåfør-innlogging", + "driver_login_help": "Sjåfører (underbrukere) logger inn her for å registrere en vask.", "driver_entry_intro": "Er du sjåfør? Logg inn her for å registrere en vask." }, "book_wash": { diff --git a/src/i18n/generated/sv-v2.json b/src/i18n/generated/sv-v2.json index 8fd8ae72..9f6e61a6 100644 --- a/src/i18n/generated/sv-v2.json +++ b/src/i18n/generated/sv-v2.json @@ -2313,6 +2313,7 @@ "username": "@.capitalize:{'words.generated.anvandarnamn'}", "your_username": "@.capitalize:{'words.generated.ditt'} @:{'words.generated.anvandarnamn'}", "driver_login_button": "Förarinloggning", + "driver_login_help": "Förare (underanvändare) loggar in här för att registrera en tvätt.", "driver_entry_intro": "Är du förare? Logga in här för att registrera en tvätt." }, "book_wash": { diff --git a/src/i18n/source/da/phrases/compat/auth/index.json b/src/i18n/source/da/phrases/compat/auth/index.json index 7a146785..2d97e706 100644 --- a/src/i18n/source/da/phrases/compat/auth/index.json +++ b/src/i18n/source/da/phrases/compat/auth/index.json @@ -5,6 +5,7 @@ "customer_login_title": "@:{'phrases.compat.global.login'}", "driver_entry_intro": "@.capitalize:{'terms.glossary.er'} @:{'terms.glossary.du'} @:{'terms.glossary.chauffør'}? @.capitalize:{'terms.glossary.log'} @:{'terms.glossary.ind'} @:{'terms.glossary.her'} @:{'terms.glossary.for'} @:{'terms.glossary.at'} @:{'terms.glossary.fa'} @:{'terms.glossary.adgang'} @:{'terms.glossary.til'} @:{'terms.glossary.dine'} @:{'terms.glossary.vaske'}.", "driver_login_button": "@.capitalize:{'terms.glossary.log'} @:{'terms.glossary.ind'} @.lower:{'terms.glossary.som'} @:{'terms.glossary.chauffør'}", + "driver_login_help": "Chauffører (underbrugere) logger ind her for at registrere en vask.", "driver_login_title": "@.capitalize:{'terms.glossary.chauffør'} @.capitalize:{'terms.glossary.login'}", "forgot_password": "Glemt @:{'terms.glossary.adgangskode'}", "operator_tagline": "@.capitalize:{'terms.glossary.truckwash'}.@:{'terms.glossary.io'} – @:{'terms.glossary.digitale'} @:{'terms.glossary.løsninger'} @:{'terms.glossary.til'} @:{'terms.glossary.lastbilvask'}", diff --git a/src/i18n/source/de/phrases/compat/auth/index.json b/src/i18n/source/de/phrases/compat/auth/index.json index 15d3360b..9fd4ce10 100644 --- a/src/i18n/source/de/phrases/compat/auth/index.json +++ b/src/i18n/source/de/phrases/compat/auth/index.json @@ -12,6 +12,7 @@ "username": "@:{'terms.glossary.benutzername'}", "your_username": "@:{'terms.glossary.ihr'} @:{'terms.glossary.benutzername'}", "driver_login_button": "Fahrer-Login", + "driver_login_help": "Fahrer (Unterbenutzer) melden sich hier an, um eine Wäsche zu registrieren.", "driver_entry_intro": "Sind Sie Fahrer? Melden Sie sich hier an, um eine Wäsche zu registrieren." } } diff --git a/src/i18n/source/en/phrases/compat/auth/index.json b/src/i18n/source/en/phrases/compat/auth/index.json index f58ec9f8..a5e5ccd6 100644 --- a/src/i18n/source/en/phrases/compat/auth/index.json +++ b/src/i18n/source/en/phrases/compat/auth/index.json @@ -5,6 +5,7 @@ "customer_login_title": "@.capitalize:{'terms.glossary.customer'} @.capitalize:{'terms.glossary.login'}", "driver_entry_intro": "@.capitalize:{'terms.glossary.are'} @:{'terms.glossary.you'} @:{'terms.glossary.a'} @:{'terms.glossary.driver'}? @.capitalize:{'terms.glossary.log'} @:{'terms.glossary.in'} @:{'terms.glossary.here'} @:{'terms.glossary.to'} @:{'terms.glossary.register'} @:{'terms.glossary.a'} @:{'terms.glossary.wash'}.", "driver_login_button": "@.capitalize:{'terms.glossary.driver'} @.capitalize:{'terms.glossary.login'}", + "driver_login_help": "Drivers (sub-users) log in here to register a wash.", "driver_login_title": "@.capitalize:{'terms.glossary.driver'} @.capitalize:{'terms.glossary.login'}", "forgot_password": "Forgot @.capitalize:{'terms.glossary.password'}", "operator_tagline": "@.capitalize:{'terms.glossary.truckwash'}.@:{'terms.glossary.io'} - @:{'terms.glossary.digital'} @:{'terms.glossary.solutions'} @:{'terms.glossary.for'} @:{'terms.glossary.truck'} @:{'terms.glossary.washing'}", diff --git a/src/i18n/source/global/shared/auth/index.json b/src/i18n/source/global/shared/auth/index.json index 9e6d24f7..acba67d4 100644 --- a/src/i18n/source/global/shared/auth/index.json +++ b/src/i18n/source/global/shared/auth/index.json @@ -5,6 +5,7 @@ "customer_number": "@:{'phrases.compat.objects.columns.customer_number'}", "driver_entry_intro": "@:{'phrases.compat.auth.driver_entry_intro'}", "driver_login_button": "@:{'phrases.compat.auth.driver_login_button'}", + "driver_login_help": "@:{'phrases.compat.auth.driver_login_help'}", "driver_login_title": "@:{'phrases.compat.auth.driver_login_title'}", "forgot_password": "@:{'phrases.compat.auth.forgot_password'}", "login_as_customer": "@:{'phrases.compat.global.login_as_customer'}", diff --git a/src/i18n/source/no/phrases/compat/auth/index.json b/src/i18n/source/no/phrases/compat/auth/index.json index 03b32a2c..92d89c24 100644 --- a/src/i18n/source/no/phrases/compat/auth/index.json +++ b/src/i18n/source/no/phrases/compat/auth/index.json @@ -12,6 +12,7 @@ "username": "@.capitalize:{'terms.glossary.brukernavn'}", "your_username": "@.capitalize:{'terms.glossary.ditt'} @:{'terms.glossary.brukernavn'}", "driver_login_button": "Sjåfør-innlogging", + "driver_login_help": "Sjåfører (underbrukere) logger inn her for å registrere en vask.", "driver_entry_intro": "Er du sjåfør? Logg inn her for å registrere en vask." } } diff --git a/src/i18n/source/sv/phrases/compat/auth/index.json b/src/i18n/source/sv/phrases/compat/auth/index.json index e0dc9cda..b378a811 100644 --- a/src/i18n/source/sv/phrases/compat/auth/index.json +++ b/src/i18n/source/sv/phrases/compat/auth/index.json @@ -12,6 +12,7 @@ "username": "@.capitalize:{'terms.glossary.anvandarnamn'}", "your_username": "@.capitalize:{'terms.glossary.ditt'} @:{'terms.glossary.anvandarnamn'}", "driver_login_button": "Förarinloggning", + "driver_login_help": "Förare (underanvändare) loggar in här för att registrera en tvätt.", "driver_entry_intro": "Är du förare? Logga in här för att registrera en tvätt." } } diff --git a/src/views/auth/Login.vue b/src/views/auth/Login.vue index fa79cc5b..7b71ae51 100644 --- a/src/views/auth/Login.vue +++ b/src/views/auth/Login.vue @@ -31,6 +31,23 @@ const { hasPendingStoredSession } = useStoredSessionGuestRedirect();

{{ $t('auth.terms_acceptance') }}

+ + +
+

{{ $t('auth.driver_login_help') }}

+ +
@@ -158,6 +175,55 @@ const { hasPendingStoredSession } = useStoredSessionGuestRedirect(); text-decoration: none; } +/* Driver entry block — page-level, always visible on every viewport. + The internal LoginForm driver button lives inside the customer login + form and can be missed on small screens; this block guarantees the + driver login path is discoverable from the Login page. */ +.login-section .wrapper .main .driver-entry { + max-width: 400px; + width: 100%; + margin-top: 24px; + padding: 16px 20px; + background-color: #F2F8FC; + border: 1px solid #BFE0EF; + border-radius: 12px; + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; +} + +.login-section .wrapper .main .driver-entry__help { + max-width: none; + margin: 0; + font-size: 14px; + color: #13324C; + text-align: center; + line-height: 1.4; +} + +.login-section .wrapper .main .driver-entry .driver-login-link { + background-color: #1584BC; + color: #fff; + border: 0; + border-radius: 30px; + min-height: 43px; + font-size: 16px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + text-decoration: none; + margin: 0; + width: auto; + padding: 0 24px; +} + +.login-section .wrapper .main .driver-entry .driver-login-link:hover { + background-color: #1a99d6; + color: #fff; +} + @media (max-width: 1250px) { .login-section .wrapper .sidebar h1{ font-size: 2.3em; @@ -187,5 +253,20 @@ const { hasPendingStoredSession } = useStoredSessionGuestRedirect(); padding: 70px 10px; } } + +/* Mobile (<768px): ensure the driver entry block is always visible and + takes full width so it's impossible to miss on phones. */ +@media (max-width: 767px) { + .login-section .wrapper .main .driver-entry { + margin-top: 20px; + padding: 14px 16px; + width: 100%; + } + + .login-section .wrapper .main .driver-entry .driver-login-link { + width: 100%; + padding: 0 16px; + } +} /*end login-section*/ diff --git a/tests/unit/login-driver-entry.spec.js b/tests/unit/login-driver-entry.spec.js new file mode 100644 index 00000000..5329fb99 --- /dev/null +++ b/tests/unit/login-driver-entry.spec.js @@ -0,0 +1,134 @@ +// @vitest-environment jsdom + +import { defineComponent, h, nextTick } from "vue"; +import { mount } from "@vue/test-utils"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + hasStoredSessionToken: vi.fn(), +})); + +vi.mock("@/services/sessionStorage.js", () => ({ + hasStoredSessionToken: mocks.hasStoredSessionToken, +})); + +vi.mock("@/composables/useStoredSessionGuestRedirect.js", () => ({ + useStoredSessionGuestRedirect: () => ({ + hasPendingStoredSession: mocks.hasStoredSessionToken(), + }), +})); + +vi.mock("@/components/forms/auth/LoginForm.vue", () => ({ + default: defineComponent({ + name: "MockLoginForm", + setup() { + return () => h("form", { "data-testid": "login-form" }); + }, + }), +})); + +vi.mock("@/views/errors/ConnectivityIssue.vue", () => ({ + default: defineComponent({ + name: "MockConnectivityIssue", + setup(_, { slots }) { + return () => h("div", { "data-testid": "connectivity-wrapper" }, slots.default?.()); + }, + }), +})); + +vi.mock("@/components/global/BrandedLoadingScreen.vue", () => ({ + default: defineComponent({ + name: "MockBrandedLoadingScreen", + props: { + testId: { type: String, default: "branded-loading-screen" }, + }, + setup(props) { + return () => h("div", { "data-testid": props.testId }); + }, + }), +})); + +import Login from "@/views/auth/Login.vue"; + +const routerLinkStub = defineComponent({ + name: "RouterLinkStub", + props: { + to: { + type: [String, Object], + default: "", + }, + }, + setup(_, { slots, attrs }) { + return () => h("a", attrs, slots.default?.()); + }, +}); + +const mountLogin = () => + mount(Login, { + global: { + mocks: { + $t: (key) => key, + }, + stubs: { + RouterLink: routerLinkStub, + }, + }, + }); + +describe("Login.vue — driver entry block (TRU-61)", () => { + beforeEach(() => { + mocks.hasStoredSessionToken.mockReset(); + mocks.hasStoredSessionToken.mockReturnValue(false); + }); + + afterEach(() => { + document.body.innerHTML = ""; + }); + + it("renders the page-level driver entry block on the customer login page", async () => { + const wrapper = mountLogin(); + await nextTick(); + + const entry = wrapper.find('[data-testid="login-driver-entry"]'); + expect(entry.exists()).toBe(true); + }); + + it("renders a router-link to the subuser login route inside the driver entry block", async () => { + const wrapper = mountLogin(); + await nextTick(); + + const link = wrapper.find('[data-testid="login-driver-login-link"]'); + expect(link.exists()).toBe(true); + }); + + it("renders the driver login button with the localized driver_login_button label", async () => { + const wrapper = mountLogin(); + await nextTick(); + + const link = wrapper.find('[data-testid="login-driver-login-link"]'); + expect(link.text()).toContain("auth.driver_login_button"); + }); + + it("renders the driver login help text inside the entry block", async () => { + const wrapper = mountLogin(); + await nextTick(); + + const entry = wrapper.find('[data-testid="login-driver-entry"]'); + expect(entry.text()).toContain("auth.driver_login_help"); + }); + + it("places the driver entry block in the main content area (not the sidebar)", async () => { + const wrapper = mountLogin(); + await nextTick(); + + // The sidebar should NOT contain the driver entry — it must be in main + const html = wrapper.html(); + const entryHtml = wrapper.find('[data-testid="login-driver-entry"]').html(); + // Find the sidebar's outer container and ensure the entry is not inside it + const sidebarMatch = html.match(/