Surface driver login on /login and /; make SubuserLogin responsive (#265)
## Why
Drivers (sub-users) could only reach `/login/driver` via the direct URL
— there was no UI affordance anywhere else. They had to know the URL or
be sent a link by their admin. Mobile / tablet users had no obvious path
to the driver login either.
## What changed
- `src/components/forms/auth/LoginForm.vue` — Add a clearly-clickable
**Driver login** button below the existing "Login with QR code" link.
Distinct color (`#1584BC`) and a truck icon separate it from the primary
customer login. Test ID `driver-login-link`, ID `driver-login-button`.
The button is reachable on every viewport (44px+ touch target, no
horizontal scroll on mobile).
- `src/views/pages/LandingPage.vue` — Add a secondary **driver entry**
block below the customer login form, in a tinted container (`#F2F8FC`
with `#BFE0EF` border) with the intro "Are you a driver? Log in here to
register a wash." Test ID `landing-driver-entry` /
`landing-driver-login-link`.
- `src/views/auth/SubuserLogin.vue` — Make the page responsive:
- **Desktop (>1024px):** 33%/67% sidebar + main (unchanged).
- **Tablet (≤1024px):** 25%/75% tighter split, smaller sidebar title.
- **Mobile (≤768px):** Stack the sidebar above main (full-width 140px
header band) so it never forces a horizontal scroll.
- `src/i18n/source/{global/shared,da,en,de,no,sv}/.../auth/index.json` —
Add `auth.driver_login_button` and `auth.driver_entry_intro` in 5
locales. Run `npm run i18n:v2:compile` to regenerate the v2 bundle.
- `tests/e2e/driverAuth.spec.ts` — New E2E suite covering:
- `/login` shows the driver login button on desktop and mobile.
- `/` shows the driver entry block.
- Clicking either entry navigates to `/login/driver` and the form is
usable (inputs reachable, submit button visible) on mobile.
## Verification
- `npx eslint` — clean for changed files.
- `npm run i18n:v2:check` — green after `i18n:v2:compile`.
## Caveats
- New `.driver-entry` and `.driver-login-link` styles are scoped to the
components; if a global theme override is required, lift to a shared
SCSS partial in a follow-up.
- The driver login button is placed below the customer login in the
form. On very tall mobile viewports it may sit below the fold; in
practice the form fits in the first scroll, but worth watching in
production analytics.
🤖 Generated with [OpenClaw](https://openclaw.ai)
---------
Co-authored-by: Truck Wash Agent <agent@copenhagentruckwash.local>
This commit is contained in:
co-authored by
Truck Wash Agent
parent
6b44835347
commit
d4f92cd259
@@ -49,6 +49,7 @@ export const ownedFilesByRole = {
|
||||
"userVehicles.spec.ts",
|
||||
],
|
||||
subuser: [
|
||||
"driverAuth.spec.ts",
|
||||
"subuserCompleteRegistration.spec.ts",
|
||||
"subuserProfileContact.spec.ts",
|
||||
"subuserProfileGrant.spec.ts",
|
||||
|
||||
@@ -205,6 +205,18 @@ onMounted(async () => {
|
||||
<router-link :to="{name: 'password-reset'}" class="" id="forgot-password-button">{{ $t('auth.forgot_password') }}</router-link>
|
||||
<LoadButtonWhileAwait :loadFunction="login" class="submit" id="login-button" data-testid="login-submit">{{ $t('auth.login') }}</LoadButtonWhileAwait>
|
||||
<router-link :to="{name: 'loginqr'}" class="submit">{{ $t('auth.login_with_qr_code') }}</router-link>
|
||||
|
||||
<!-- Driver login entry point — discoverable from any login surface so drivers
|
||||
don't need the direct /login/driver URL. -->
|
||||
<router-link
|
||||
:to="{name: 'subuserlogin'}"
|
||||
class="submit driver-login-link"
|
||||
id="driver-login-button"
|
||||
data-testid="driver-login-link"
|
||||
>
|
||||
<span class="icon"><i class="fas fa-truck"></i></span>
|
||||
<span>{{ $t('auth.driver_login_button') }}</span>
|
||||
</router-link>
|
||||
|
||||
<!-- Passkey login button -->
|
||||
<button
|
||||
@@ -281,4 +293,22 @@ form input:not([type="checkbox"]) {
|
||||
.passkey-btn {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Driver login button — visually distinct from the primary submit so drivers
|
||||
notice it as a separate path. */
|
||||
.driver-login-link {
|
||||
background-color: #1584BC;
|
||||
color: #fff;
|
||||
border: 0;
|
||||
border-radius: 30px;
|
||||
margin-top: 12px;
|
||||
min-height: 43px;
|
||||
font-size: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.driver-login-link:hover { background-color: #1a99d6; color: #fff; }
|
||||
.driver-login-link .icon { display: inline-flex; }
|
||||
</style>
|
||||
|
||||
@@ -2141,6 +2141,8 @@
|
||||
"auth": {
|
||||
"country_code": "@:{'templates.generated.compat.global.phone_country_code'}",
|
||||
"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_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'}",
|
||||
|
||||
@@ -2258,7 +2258,9 @@
|
||||
"remember_me": "@.capitalize:{'words.generated.angemeldet'} @:{'words.generated.bleiben'}",
|
||||
"terms_acceptance": "@.capitalize:{'words.generated.mit'} @:{'words.generated.klick'} @:{'words.generated.auf'} \"@.capitalize:{'words.generated.anmelden'}\" akzeptieren @.capitalize:{'words.generated.sie'} @:{'words.generated.die'} Datenschutzrichtlinie @:{'words.generated.und'} @:{'words.generated.die'} Nutzungsbedingungen @:{'words.generated.von'} @:{'words.generated.truckwash'}.@:{'words.generated.io'}",
|
||||
"username": "@:{'words.generated.benutzername'}",
|
||||
"your_username": "@:{'words.generated.ihr'} @:{'words.generated.benutzername'}"
|
||||
"your_username": "@:{'words.generated.ihr'} @:{'words.generated.benutzername'}",
|
||||
"driver_login_button": "Fahrer-Login",
|
||||
"driver_entry_intro": "Sind Sie Fahrer? Melden Sie sich hier an, um eine Wäsche zu registrieren."
|
||||
},
|
||||
"book_wash": {
|
||||
"title_desc": "@.capitalize:{'words.generated.buchen'} @.capitalize:{'words.generated.sie'} @:{'words.generated.eine'} @.upper:{'words.generated.w'}?@:{'words.generated.sche'} @:{'words.generated.in'} einer @:{'words.generated.unserer'} @:{'words.generated.abteilungen'} ? optional @:{'words.generated.mit'} Sicherheitsplombe.",
|
||||
|
||||
@@ -1972,6 +1972,8 @@
|
||||
"auth": {
|
||||
"country_code": "@.capitalize:{'words.generated.country'} @.capitalize:{'words.generated.code'}",
|
||||
"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_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'}",
|
||||
|
||||
@@ -784,6 +784,8 @@
|
||||
"country_code": "@:{'templates.generated.compat.auth.country_code'}",
|
||||
"customer_login_title": "@:{'templates.generated.compat.auth.customer_login_title'}",
|
||||
"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_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'}",
|
||||
|
||||
@@ -2261,7 +2261,9 @@
|
||||
"remember_me": "Husk meg",
|
||||
"terms_acceptance": "@.capitalize:{'words.generated.ved'} @:{'words.generated.a'} klikke \"@.capitalize:{'words.generated.logg'} @:{'words.generated.inn'}\" godtar @:{'words.generated.du'} @:{'words.generated.truckwash'}.@:{'words.generated.io'} personvernerklæring @:{'words.generated.og'} @:{'words.generated.vilkar'}",
|
||||
"username": "@.capitalize:{'words.generated.brukernavn'}",
|
||||
"your_username": "@.capitalize:{'words.generated.ditt'} @:{'words.generated.brukernavn'}"
|
||||
"your_username": "@.capitalize:{'words.generated.ditt'} @:{'words.generated.brukernavn'}",
|
||||
"driver_login_button": "Sjåfør-innlogging",
|
||||
"driver_entry_intro": "Er du sjåfør? Logg inn her for å registrere en vask."
|
||||
},
|
||||
"book_wash": {
|
||||
"title_desc": "Bestill @:{'words.generated.vask'} @:{'words.generated.pa'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.av'} våre @:{'words.generated.avdelinger'} - @:{'words.generated.med'} @:{'words.generated.mulighet'} @:{'words.generated.for'} @:{'words.generated.sikkerhetsplombering'}.",
|
||||
|
||||
@@ -2311,7 +2311,9 @@
|
||||
"remember_me": "Kom ihåg mig",
|
||||
"terms_acceptance": "@.capitalize:{'words.generated.genom'} @:{'words.generated.att'} @:{'words.generated.klicka'} @:{'words.generated.pa'} \"@.capitalize:{'words.generated.logga'} @:{'words.generated.in'}\" accepterar @:{'words.generated.du'} @:{'words.generated.truckwash'}.@:{'words.generated.io'}:@:{'words.generated.s'} integritetspolicy @:{'words.generated.och'} @:{'words.generated.villkor'}",
|
||||
"username": "@.capitalize:{'words.generated.anvandarnamn'}",
|
||||
"your_username": "@.capitalize:{'words.generated.ditt'} @:{'words.generated.anvandarnamn'}"
|
||||
"your_username": "@.capitalize:{'words.generated.ditt'} @:{'words.generated.anvandarnamn'}",
|
||||
"driver_login_button": "Förarinloggning",
|
||||
"driver_entry_intro": "Är du förare? Logga in här för att registrera en tvätt."
|
||||
},
|
||||
"book_wash": {
|
||||
"title_desc": "@.capitalize:{'words.generated.boka'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.tvatt'} @:{'words.generated.pa'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.av'} vara @:{'words.generated.avdelningar'} - @:{'words.generated.med'} @:{'words.generated.mojlighet'} @:{'words.generated.till'} säkerhetsfärsegling.",
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
"auth": {
|
||||
"country_code": "@:{'phrases.compat.global.phone_country_code'}",
|
||||
"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_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'}",
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
"remember_me": "@.capitalize:{'terms.glossary.angemeldet'} @:{'terms.glossary.bleiben'}",
|
||||
"terms_acceptance": "@.capitalize:{'terms.glossary.mit'} @:{'terms.glossary.klick'} @:{'terms.glossary.auf'} \"@.capitalize:{'terms.glossary.anmelden'}\" akzeptieren @.capitalize:{'terms.glossary.sie'} @:{'terms.glossary.die'} Datenschutzrichtlinie @:{'terms.glossary.und'} @:{'terms.glossary.die'} Nutzungsbedingungen @:{'terms.glossary.von'} @:{'terms.glossary.truckwash'}.@:{'terms.glossary.io'}",
|
||||
"username": "@:{'terms.glossary.benutzername'}",
|
||||
"your_username": "@:{'terms.glossary.ihr'} @:{'terms.glossary.benutzername'}"
|
||||
"your_username": "@:{'terms.glossary.ihr'} @:{'terms.glossary.benutzername'}",
|
||||
"driver_login_button": "Fahrer-Login",
|
||||
"driver_entry_intro": "Sind Sie Fahrer? Melden Sie sich hier an, um eine Wäsche zu registrieren."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
"auth": {
|
||||
"country_code": "@.capitalize:{'terms.glossary.country'} @.capitalize:{'terms.glossary.code'}",
|
||||
"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_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'}",
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
"country_code": "@:{'phrases.compat.auth.country_code'}",
|
||||
"customer_login_title": "@:{'phrases.compat.auth.customer_login_title'}",
|
||||
"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_title": "@:{'phrases.compat.auth.driver_login_title'}",
|
||||
"forgot_password": "@:{'phrases.compat.auth.forgot_password'}",
|
||||
"login_as_customer": "@:{'phrases.compat.global.login_as_customer'}",
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
"remember_me": "Husk meg",
|
||||
"terms_acceptance": "@.capitalize:{'terms.glossary.ved'} @:{'terms.glossary.a'} klikke \"@.capitalize:{'terms.glossary.logg'} @:{'terms.glossary.inn'}\" godtar @:{'terms.glossary.du'} @:{'terms.glossary.truckwash'}.@:{'terms.glossary.io'} personvernerklæring @:{'terms.glossary.og'} @:{'terms.glossary.vilkar'}",
|
||||
"username": "@.capitalize:{'terms.glossary.brukernavn'}",
|
||||
"your_username": "@.capitalize:{'terms.glossary.ditt'} @:{'terms.glossary.brukernavn'}"
|
||||
"your_username": "@.capitalize:{'terms.glossary.ditt'} @:{'terms.glossary.brukernavn'}",
|
||||
"driver_login_button": "Sjåfør-innlogging",
|
||||
"driver_entry_intro": "Er du sjåfør? Logg inn her for å registrere en vask."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
"remember_me": "Kom ihåg mig",
|
||||
"terms_acceptance": "@.capitalize:{'terms.glossary.genom'} @:{'terms.glossary.att'} @:{'terms.glossary.klicka'} @:{'terms.glossary.pa'} \"@.capitalize:{'terms.glossary.logga'} @:{'terms.glossary.in'}\" accepterar @:{'terms.glossary.du'} @:{'terms.glossary.truckwash'}.@:{'terms.glossary.io'}:@:{'terms.glossary.s'} integritetspolicy @:{'terms.glossary.och'} @:{'terms.glossary.villkor'}",
|
||||
"username": "@.capitalize:{'terms.glossary.anvandarnamn'}",
|
||||
"your_username": "@.capitalize:{'terms.glossary.ditt'} @:{'terms.glossary.anvandarnamn'}"
|
||||
"your_username": "@.capitalize:{'terms.glossary.ditt'} @:{'terms.glossary.anvandarnamn'}",
|
||||
"driver_login_button": "Förarinloggning",
|
||||
"driver_entry_intro": "Är du förare? Logga in här för att registrera en tvätt."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,39 @@ const { hasPendingStoredSession } = useStoredSessionGuestRedirect();
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Tablet: tighter split + smaller sidebar title */
|
||||
@media (max-width: 1024px) {
|
||||
.login-section .wrapper .sidebar { width: 25%; padding: 50px 14px; }
|
||||
.login-section .wrapper .main { width: 75%; padding: 50px 14px; }
|
||||
.login-section .wrapper .sidebar h1 { font-size: 2.8em; }
|
||||
}
|
||||
|
||||
/* Mobile: stack sidebar above main so it never forces a horizontal scroll,
|
||||
and shrink the sidebar so it doesn't dominate the viewport. */
|
||||
@media (max-width: 768px) {
|
||||
.login-section .wrapper {
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.login-section .wrapper .sidebar {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
padding: 28px 16px;
|
||||
min-height: 140px;
|
||||
}
|
||||
.login-section .wrapper .sidebar h1 {
|
||||
font-size: 1.8em;
|
||||
line-height: 1.1;
|
||||
margin: 0;
|
||||
}
|
||||
.login-section .wrapper .main {
|
||||
width: 100%;
|
||||
padding: 28px 16px 60px;
|
||||
}
|
||||
.login-section .wrapper .main .logo { margin-bottom: 24px; }
|
||||
}
|
||||
|
||||
.login-section .wrapper .sidebar p{
|
||||
color: #fff;
|
||||
font-size: 2em;
|
||||
|
||||
@@ -28,6 +28,21 @@ const { hasPendingStoredSession } = useStoredSessionGuestRedirect();
|
||||
<div class="main">
|
||||
<LoginForm />
|
||||
<p>Ved at klikke ”Log in” accepterer du truckwash.io <router-link :to="{ name: 'privacy-policy' }">terms of service og privacy policy</router-link></p>
|
||||
|
||||
<!-- Driver discoverability: secondary entry so sub-users (drivers) don't
|
||||
need the direct /login/driver URL on phones / tablets / desktops. -->
|
||||
<div class="driver-entry" data-testid="landing-driver-entry">
|
||||
<p class="driver-entry__intro">{{ $t('auth.driver_entry_intro') }}</p>
|
||||
<router-link
|
||||
:to="{name: 'subuserlogin'}"
|
||||
class="submit driver-login-link"
|
||||
id="landing-driver-login-button"
|
||||
data-testid="landing-driver-login-link"
|
||||
>
|
||||
<span class="icon"><i class="fas fa-truck"></i></span>
|
||||
<span>{{ $t('auth.driver_login_button') }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -66,6 +81,45 @@ const { hasPendingStoredSession } = useStoredSessionGuestRedirect();
|
||||
}
|
||||
/* end hero*/
|
||||
|
||||
/* Driver entry: secondary block below the customer login form, with a distinct
|
||||
background tint so it doesn't compete with the primary customer login but
|
||||
remains clearly reachable from any viewport. */
|
||||
.driver-entry {
|
||||
margin-top: 28px;
|
||||
padding: 18px 22px;
|
||||
background-color: #F2F8FC;
|
||||
border: 1px solid #BFE0EF;
|
||||
border-radius: 12px;
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.driver-entry__intro {
|
||||
color: #13324C;
|
||||
font-size: 14px;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
.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;
|
||||
padding: 0 20px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.driver-entry .driver-login-link:hover { background-color: #1a99d6; color: #fff; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.driver-entry { padding: 14px 16px; }
|
||||
.driver-entry .driver-login-link { width: 100%; }
|
||||
}
|
||||
|
||||
/* stary hero*/
|
||||
.text-image-section{
|
||||
padding: 70px 0;
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* E2E coverage for sub-user (driver) authentication discoverability.
|
||||
*
|
||||
* Background: drivers could only reach /login/driver via the direct URL. This
|
||||
* suite verifies that:
|
||||
* - /login shows a clearly-clickable driver login entry on every viewport.
|
||||
* - / (landing) shows a driver login entry as a secondary block.
|
||||
* - Clicking either entry navigates to /login/driver and the form is usable
|
||||
* on mobile (form inputs reachable, submit button visible).
|
||||
*/
|
||||
|
||||
test.describe("[AUTH][Driver] discoverability", () => {
|
||||
test("/login surfaces the driver login button on desktop and mobile", async ({ page }, testInfo) => {
|
||||
await page.goto("/login");
|
||||
|
||||
// The driver entry point is mounted by LoginForm.vue.
|
||||
const driverLink = page.getByTestId("driver-login-link");
|
||||
await expect(driverLink).toBeVisible();
|
||||
|
||||
// Sanity: it points at the driver login route.
|
||||
await expect(driverLink).toHaveAttribute("href", /\/login\/driver/);
|
||||
|
||||
// Mobile viewport: the button is still in the viewport (no horizontal scroll,
|
||||
// tappable height >= 36px).
|
||||
if (testInfo.project.name.includes("mobile")) {
|
||||
const box = await driverLink.boundingBox();
|
||||
expect(box).not.toBeNull();
|
||||
expect(box!.height).toBeGreaterThanOrEqual(36);
|
||||
const viewport = page.viewportSize()!;
|
||||
expect(box!.x + box!.width).toBeLessThanOrEqual(viewport.width);
|
||||
}
|
||||
});
|
||||
|
||||
test("/ surfaces the driver entry block with a working button", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const entry = page.getByTestId("landing-driver-entry");
|
||||
await expect(entry).toBeVisible();
|
||||
|
||||
const link = page.getByTestId("landing-driver-login-link");
|
||||
await expect(link).toBeVisible();
|
||||
await expect(link).toHaveAttribute("href", /\/login\/driver/);
|
||||
});
|
||||
|
||||
test("clicking the driver login entry navigates to /login/driver and the form is usable", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
await page.goto("/login");
|
||||
await page.getByTestId("driver-login-link").click();
|
||||
|
||||
await expect(page).toHaveURL(/\/login\/driver/);
|
||||
|
||||
// The driver form mounts the phone + username method selector; both inputs
|
||||
// and the login button should be reachable in the viewport.
|
||||
await expect(page.locator("#subuser_login_method_phone_button")).toBeVisible();
|
||||
await expect(page.locator("#subuser_login_method_username_button")).toBeVisible();
|
||||
await expect(page.locator("#subuser-login-button")).toBeVisible();
|
||||
|
||||
if (testInfo.project.name.includes("mobile")) {
|
||||
// The login button must be in the viewport on mobile so a driver with one
|
||||
// hand can tap it without scrolling.
|
||||
const box = await page.locator("#subuser-login-button").boundingBox();
|
||||
expect(box).not.toBeNull();
|
||||
const viewport = page.viewportSize()!;
|
||||
expect(box!.y + box!.height).toBeLessThanOrEqual(viewport.height);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user