feat: apply standard login page design
This commit is contained in:
@@ -213,10 +213,37 @@ test('settings match the approved paper-ledger geometry and action hierarchy', a
|
|||||||
expect(logoutBox!.width).toBeGreaterThanOrEqual(88)
|
expect(logoutBox!.width).toBeGreaterThanOrEqual(88)
|
||||||
expect(logoutBox!.height).toBeGreaterThanOrEqual(isMobileContract ? 44 : 40)
|
expect(logoutBox!.height).toBeGreaterThanOrEqual(isMobileContract ? 44 : 40)
|
||||||
allowExpectedError(page, 'requestfailed: POST http://127.0.0.1:5173/api/v1/auth/logout net::ERR_ABORTED')
|
allowExpectedError(page, 'requestfailed: POST http://127.0.0.1:5173/api/v1/auth/logout net::ERR_ABORTED')
|
||||||
|
await page.route('**/api/v1/auth/logout', route => route.fulfill({ status: 204 }))
|
||||||
await logout.click()
|
await logout.click()
|
||||||
await expect(page.getByRole('button', { name: '登录', exact: true })).toBeVisible()
|
await expect(page.getByRole('button', { name: '登录', exact: true })).toBeVisible()
|
||||||
await expect(page.getByText('欢迎回来,继续把生活理顺。')).toBeVisible()
|
await expect(page.getByRole('heading', { name: '欢迎回来', exact: true })).toBeVisible()
|
||||||
expect(await page.request.get('/api/v1/me')).not.toBeOK()
|
await expect(page.getByText('登录后继续处理今天的事项。')).toBeVisible()
|
||||||
|
await expect(page.getByText('登录后继续你的清单')).toBeVisible()
|
||||||
|
const authGeometry = await page.locator('.auth-card').evaluate(element => {
|
||||||
|
const card = element as HTMLElement
|
||||||
|
const cardRect = card.getBoundingClientRect()
|
||||||
|
const inputRects = [...card.querySelectorAll<HTMLElement>('input')].map(input => input.getBoundingClientRect())
|
||||||
|
const buttonRect = card.querySelector<HTMLElement>('button.primary')!.getBoundingClientRect()
|
||||||
|
return {
|
||||||
|
viewportWidth: innerWidth,
|
||||||
|
documentOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||||||
|
card: { left: cardRect.left, right: cardRect.right, width: cardRect.width },
|
||||||
|
brand: {
|
||||||
|
fontSize: getComputedStyle(card.querySelector<HTMLElement>('.auth-brand .brand')!).fontSize,
|
||||||
|
logoWidth: card.querySelector<HTMLElement>('.auth-brand .brand-logo')!.getBoundingClientRect().width,
|
||||||
|
},
|
||||||
|
inputHeights: inputRects.map(rect => rect.height),
|
||||||
|
buttonHeight: buttonRect.height,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(authGeometry.documentOverflow).toBe(0)
|
||||||
|
const expectedCardWidth = Math.min(400, authGeometry.viewportWidth - (isMobileContract ? 36 : 48))
|
||||||
|
expect(authGeometry.card.width).toBeCloseTo(expectedCardWidth, 0)
|
||||||
|
expect(authGeometry.card.left).toBeCloseTo((authGeometry.viewportWidth - expectedCardWidth) / 2, 0)
|
||||||
|
expect(authGeometry.card.right).toBeCloseTo((authGeometry.viewportWidth + expectedCardWidth) / 2, 0)
|
||||||
|
expect(authGeometry.brand).toEqual({ fontSize: '28px', logoWidth: 34 })
|
||||||
|
expect(authGeometry.inputHeights.every(height => height >= 46)).toBeTruthy()
|
||||||
|
expect(authGeometry.buttonHeight).toBeGreaterThanOrEqual(46)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('task, habit, countdown, memo, action and confirmation overlays share the modal contract', async ({ page }, testInfo) => {
|
test('task, habit, countdown, memo, action and confirmation overlays share the modal contract', async ({ page }, testInfo) => {
|
||||||
|
|||||||
@@ -1511,13 +1511,15 @@ onUnmounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="!authReady" class="center loading-brand"><img class="loading-logo" src="/dodo-logo.svg" alt=""><span class="loader" /><span>正在打开 dodo…</span></div>
|
<div v-if="!authReady" class="center loading-brand"><img class="loading-logo" src="/dodo-logo.svg" alt=""><span class="loader" /><span>正在打开 dodo…</span></div>
|
||||||
<div v-else-if="!authenticated" class="auth-shell">
|
<div v-else-if="!authenticated" class="auth-shell">
|
||||||
<section class="auth-card">
|
<form class="auth-card" aria-labelledby="auth-title" @submit.prevent="submitAuth">
|
||||||
<div class="brand brand-lockup"><img class="brand-logo" src="/dodo-logo.svg" alt=""><span class="brand-wordmark">dodo</span></div><p>{{ initialized ? '欢迎回来,继续把生活理顺。' : '创建你的 dodo' }}</p>
|
<div class="auth-brand"><div class="brand brand-lockup"><img class="brand-logo" src="/dodo-logo.svg" alt=""><span class="brand-wordmark">dodo</span></div></div>
|
||||||
|
<div class="auth-heading"><h1 id="auth-title">{{ initialized ? '欢迎回来' : '创建你的 dodo' }}</h1><p>{{ initialized ? '登录后继续处理今天的事项。' : '创建账号后开始整理你的事项。' }}</p></div>
|
||||||
<label>用户名<input v-model="username" autocomplete="username" placeholder="你的用户名"></label>
|
<label>用户名<input v-model="username" autocomplete="username" placeholder="你的用户名"></label>
|
||||||
<label>密码<input v-model="password" type="password" autocomplete="current-password" placeholder="至少 12 位" @keyup.enter="submitAuth"></label>
|
<label>密码<input v-model="password" type="password" autocomplete="current-password" placeholder="至少 12 位"></label>
|
||||||
<button class="primary" @click="submitAuth">{{ initialized ? '登录' : '开始使用' }}</button>
|
<button type="submit" class="primary">{{ initialized ? '登录' : '开始使用' }}</button>
|
||||||
<small v-if="error" role="alert">{{ error }}</small>
|
<small v-if="error" role="alert">{{ error }}</small>
|
||||||
</section>
|
<p v-if="initialized" class="auth-footnote">登录后继续你的清单</p>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="shell" :class="{ 'today-active': activeView==='today', 'sidebar-collapsed': sidebarCollapsed, 'detail-open': Boolean(selectedTask), 'memo-detail-open': activeView==='memos' && memoDetailOpen, 'mobile-sidebar-open': mobileSidebar }">
|
<div v-else class="shell" :class="{ 'today-active': activeView==='today', 'sidebar-collapsed': sidebarCollapsed, 'detail-open': Boolean(selectedTask), 'memo-detail-open': activeView==='memos' && memoDetailOpen, 'mobile-sidebar-open': mobileSidebar }">
|
||||||
<div v-if="mobileSidebar || mobileDetail" class="scrim" @click="mobileSidebar=false;mobileDetail=false" />
|
<div v-if="mobileSidebar || mobileDetail" class="scrim" @click="mobileSidebar=false;mobileDetail=false" />
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -481,7 +481,27 @@ describe('completion feedback motion', () => {
|
|||||||
expect(css).toContain('@media(prefers-reduced-motion:reduce){.task-row.just-completed,.habit-row.just-completed,.task-row.just-completed .task-check-mark,.habit-row.just-completed .task-check-mark{animation:none}.task-row.completion-exiting,.habit-row.completion-exiting{animation:none}}')
|
expect(css).toContain('@media(prefers-reduced-motion:reduce){.task-row.just-completed,.habit-row.just-completed,.task-row.just-completed .task-check-mark,.habit-row.just-completed .task-check-mark{animation:none}.task-row.completion-exiting,.habit-row.completion-exiting{animation:none}}')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('syncs the browser IANA timezone before loading user task data', () => {
|
it('stacks the desktop calendar copy to align with weather and gold', () => {
|
||||||
|
expect(css).toContain('@media(min-width:721px){.today-environment__calendar{flex-direction:column;align-items:flex-start;gap:0}.today-environment__calendar small{margin-top:2px;font-size:10.5px}}')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses the approved internal-standard login composition and material tokens', () => {
|
||||||
|
expect(app).toContain('class="auth-brand"')
|
||||||
|
expect(app).toContain('class="brand brand-lockup"')
|
||||||
|
expect(app).toContain('<h1 id="auth-title">{{ initialized ? \'欢迎回来\' : \'创建你的 dodo\' }}</h1>')
|
||||||
|
expect(app).toContain("{{ initialized ? '登录后继续处理今天的事项。' : '创建账号后开始整理你的事项。' }}")
|
||||||
|
expect(app).toContain('<p v-if="initialized" class="auth-footnote">登录后继续你的清单</p>')
|
||||||
|
expect(app).toContain('<form class="auth-card" aria-labelledby="auth-title" @submit.prevent="submitAuth">')
|
||||||
|
expect(app).toContain('<button type="submit" class="primary">')
|
||||||
|
expect(css).toMatch(/\.auth-card\{[^}]*width:min\(400px,100%\)[^}]*padding:36px[^}]*border:1px solid var\(--border-cream\)[^}]*border-radius:var\(--radius-card\)/)
|
||||||
|
expect(css).toContain('.auth-brand .brand{gap:9px;font-size:28px;letter-spacing:-2px}')
|
||||||
|
expect(css).toContain('.auth-brand .brand-logo{width:34px;height:34px}')
|
||||||
|
expect(css).toMatch(/\.auth-card input\{[^}]*min-height:46px[^}]*background:var\(--surface-raised\)/)
|
||||||
|
expect(css).toContain('.auth-card>.primary{min-height:46px}')
|
||||||
|
expect(css).toMatch(/@media\(max-width:930px\)\{\.auth-shell\{padding:18px\}\.auth-card\{padding:30px 24px\}/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('syncs browser IANA timezone before loading user task data', () => {
|
||||||
expect(app).toContain("Intl.DateTimeFormat().resolvedOptions().timeZone")
|
expect(app).toContain("Intl.DateTimeFormat().resolvedOptions().timeZone")
|
||||||
expect(app).toContain("await api('/me', { method: 'PATCH', body: JSON.stringify({ timezone }) })")
|
expect(app).toContain("await api('/me', { method: 'PATCH', body: JSON.stringify({ timezone }) })")
|
||||||
const bootstrapBlock = app.slice(app.indexOf('async function bootstrap()'), app.indexOf('async function submitAuth()'))
|
const bootstrapBlock = app.slice(app.indexOf('async function bootstrap()'), app.indexOf('async function submitAuth()'))
|
||||||
|
|||||||
Reference in New Issue
Block a user