Files
dodo/frontend/src/TodayEnvironment.test.ts
T
bboysoul 978c4b87ed
ci / gitleaks (push) Successful in 8s
ci / docker (push) Successful in 6m49s
feat: unify task and habit pages with Today
2026-09-18 10:51:57 +08:00

106 lines
7.3 KiB
TypeScript

import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
const app = readFileSync('src/App.vue', 'utf8')
const css = readFileSync('src/style.css', 'utf8')
describe('Today environment integration', () => {
it('loads environment independently alongside Today task work', () => {
expect(app).toContain("import TodayEnvironmentStrip, { type TodayEnvironment } from './components/TodayEnvironmentStrip.vue'")
expect(app).toContain('let todayEnvironmentLoadToken = 0')
expect(app).toContain("api('/today/environment')")
expect(app).toContain('void loadTodayEnvironment()')
expect(app).toContain('++todayEnvironmentLoadToken')
expect(app).toContain("activeView.value !== 'today'")
expect(app).toContain('<TodayEnvironmentStrip :environment="todayEnvironment" :loading="todayEnvironmentLoading" :failed="todayEnvironmentError" />')
})
it('keeps environment failures local and retains old values during refresh', () => {
const loader = app.slice(app.indexOf('async function loadTodayEnvironment'), app.indexOf('async function loadOverdueTasks'))
expect(loader).toContain('todayEnvironmentLoading.value = true')
expect(loader).not.toContain('todayEnvironment.value = null')
expect(loader).not.toContain('fail(')
expect(loader).not.toContain('toast(')
expect(loader).toContain('todayEnvironmentError.value = true')
})
it('refreshes Today environment on Shanghai date rollover and page restoration', () => {
expect(app).toContain("import { shanghaiDateKey, useTaskDueClock, watchShanghaiDateRollover } from './lib/task-due-clock'")
expect(app).toContain('watchShanghaiDateRollover(taskDueNowMs, handleTodayEnvironmentResume)')
expect(app).toContain('todayEnvironmentDateKey')
expect(app).toContain("document.addEventListener('visibilitychange', handleTodayEnvironmentResume)")
expect(app).toContain("window.addEventListener('focus', handleTodayEnvironmentResume)")
expect(app).toContain("window.addEventListener('pageshow', handleTodayEnvironmentResume)")
expect(app).toContain("document.removeEventListener('visibilitychange', handleTodayEnvironmentResume)")
expect(app).toContain("window.removeEventListener('focus', handleTodayEnvironmentResume)")
expect(app).toContain("window.removeEventListener('pageshow', handleTodayEnvironmentResume)")
expect(app).toContain('if (document.visibilityState === \'hidden\' || activeView.value !== \'today\') return')
expect(app).toContain('if (todayEnvironmentDateKey.value !== shanghaiDateKey()) void loadTodayEnvironment()')
})
it('keeps mobile environment copy to two rows including refresh states', () => {
expect(css).toMatch(/@media\(max-width:720px\)\{\.today-environment\{[^}]*grid-template-rows:auto auto/)
expect(css).not.toMatch(/\.today-board\{[^}]*grid-template-rows:/)
expect(css).not.toContain('@container(max-width:720px){.today-environment')
expect(css).not.toContain('@container(min-width:721px){.today-environment')
expect(css).not.toContain('.today-environment__refreshing')
})
it('renders the approved plain-list composition without a board or progress tracks', () => {
expect(app).toContain("<main :class=\"{'today-main':activeView==='today','list-main':activeView==='tasks'||activeView==='habits'}\"")
expect(app).toContain('<section v-if="activeView===\'today\'" class="today-context" aria-label="今日概览">')
expect(app).toContain('<p class="today-remaining">还有 {{ todayTaskRemaining + todayHabitRemaining }} 项待完成</p>')
expect(app).not.toContain('class="today-board"')
expect(app).not.toContain('today-board-title')
expect(app).not.toContain('today-board__progress')
expect(app).not.toContain('today-task-track')
expect(app).not.toContain('today-habit-track')
expect(app).not.toContain('role="progressbar"')
expect(app).toContain("'today-active': activeView==='today'")
expect(css).toContain('.shell.today-active .unified-fab{box-shadow:0 8px 18px rgba(174,65,29,.22)}')
expect(css).not.toContain('\n.unified-fab{box-shadow:0 8px 18px rgba(174,65,29,.22)}')
expect(css).toContain('main.today-main{padding-left:max(44px,calc((100% - 1080px)/2));padding-right:max(44px,calc((100% - 1080px)/2))}')
expect(css).toContain('.today-environment{grid-template-columns:repeat(3,minmax(0,1fr));')
expect(css).toContain('@media(max-width:930px){main.today-main{padding-left:max(44px,calc((100% - 630px)/2));padding-right:max(44px,calc((100% - 630px)/2))}}')
expect(css).toContain('@media(max-width:720.98px){main.today-main{padding-left:29px!important;padding-right:29px!important}')
expect(css).toContain('.today-context{margin:0 0 14px;border-bottom:0}')
expect(css).toContain('.today-remaining{margin:0 0 24px;color:var(--muted);font-size:13px}')
expect(css).not.toContain('.today-board{')
expect(css).not.toContain('.today-board__progress{')
expect(css).not.toContain('.today-track{')
expect(css).not.toContain('.today-track-rail{')
})
it('renders the approved plain-list DOM in prototype order without duplicating the Today title', () => {
const main = app.slice(app.indexOf('<main :class='), app.indexOf('</main>'))
const environment = main.indexOf('<TodayEnvironmentStrip')
const title = main.indexOf('<h1 class="today-page-title">今天</h1>')
const remaining = main.indexOf('<p class="today-remaining">')
const filter = main.indexOf('<CompletedFilterPill v-model="showCompleted" class="today-inline-filter" />')
const overdue = main.indexOf('id="today-overdue-heading"')
expect(environment).toBeGreaterThan(-1)
expect(title).toBeGreaterThan(environment)
expect(remaining).toBeGreaterThan(title)
expect(filter).toBeGreaterThan(remaining)
expect(overdue).toBeGreaterThan(filter)
expect(main.match(/>今天<\/h1>/g)).toHaveLength(1)
expect(main).toContain("<div v-if=\"!['today','tasks','habits'].includes(activeView)\" class=\"topbar-title\"><h1")
expect(main).toContain("<div v-if=\"['today', 'tasks', 'upcoming', 'habits'].includes(activeView)\" class=\"topbar-actions\">")
expect(main).toContain('<CompletedFilterPill v-if="activeView===\'upcoming\'" v-model="showCompleted" class="topbar-filter" />')
expect(main).toContain('aria-label="刷新当前页面"')
})
it('keeps the editorial environment layout readable at desktop and narrow widths', () => {
expect(css).toMatch(/\.today-environment\{[^}]*grid-column:1\/-1/)
expect(css).toMatch(/@media\(min-width:721px\)\{\.today-environment\{[^}]*grid-template-columns:minmax\(0,1\.25fr\) minmax\(0,1fr\) minmax\(0,1fr\)/)
expect(css).toMatch(/\.today-environment__calendar strong\{[^}]*font-size:23px/)
expect(css).toMatch(/@media\(max-width:720px\)\{\.today-environment\{[^}]*grid-template-columns:minmax\(0,1fr\) minmax\(0,1fr\)[^}]*grid-template-rows:auto auto/)
expect(css).toMatch(/\.today-environment__calendar\{[^}]*grid-column:1\/-1[^}]*grid-row:1/)
expect(css).toMatch(/\.today-environment__weather\{[^}]*grid-column:1[^}]*grid-row:2/)
expect(css).toMatch(/\.today-environment__gold\{[^}]*grid-column:2[^}]*grid-row:2/)
expect(css).toMatch(/\.today-environment__weather,\.today-environment__gold\{[^}]*flex-direction:column/)
expect(css).not.toMatch(/\.today-environment[^}]*overflow-x:auto/)
expect(css).not.toMatch(/@media\(max-width:720px\)[\s\S]*?\.today-environment[^}]*text-overflow:ellipsis/)
})
})