Files
client-web/src/navigation/workspaceTabs.test.mjs
T
2026-08-07 10:45:15 +08:00

86 lines
4.9 KiB
JavaScript

import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
const storeSource = readFileSync(new URL('./workspace-tabs/workspaceTabs.ts', import.meta.url), 'utf8')
const unsavedSource = readFileSync(new URL('./workspace-tabs/unsavedWork.ts', import.meta.url), 'utf8')
const tabsSource = readFileSync(new URL('../components/WorkspaceTabs.vue', import.meta.url), 'utf8')
const layoutSource = readFileSync(new URL('../layouts/AppLayout.vue', import.meta.url), 'utf8')
const tabStyles = readFileSync(new URL('../styles/workspace-tabs.css', import.meta.url), 'utf8')
test('workspace tabs are isolated by account, restored from session storage, and capped without evicting protected work', () => {
assert.match(storeSource, /cc:client-workspace-tabs:\$\{ownerId\}/)
assert.match(storeSource, /sessionStorage\.getItem/)
assert.match(storeSource, /sessionStorage\.setItem/)
assert.match(storeSource, /MAX_WORKSPACE_TABS = 12/)
assert.match(storeSource, /MAX_CACHED_WORKSPACE_VIEWS = 24/)
assert.match(storeSource, /!protectedKeys\.has\(tab\.key\)/)
assert.match(storeSource, /sort\(\(left, right\) => left\.lastVisitedAt - right\.lastVisitedAt\)/)
})
test('workspace tabs exclude public and forbidden routes and use one tab per route name', () => {
assert.match(storeSource, /excludedRouteNames = new Set\(\['login', 'register', 'terms', 'privacy', 'forbidden'\]\)/)
assert.match(storeSource, /const routeName = String\(route\.name\)/)
assert.match(storeSource, /key: routeName/)
assert.match(storeSource, /tabs\.value\.find\(\(tab\) => tab\.key === routeName\)/)
})
test('closing tabs invalidates their cached instance and returns the most recently visited fallback', () => {
assert.match(storeSource, /function bumpCacheEpoch/)
assert.match(storeSource, /for \(const key of keys\) bumpCacheEpoch\(key\)/)
assert.match(storeSource, /sort\(\(left, right\) => right\.lastVisitedAt - left\.lastVisitedAt\)/)
assert.match(storeSource, /return \{ closedActive, fallbackPath:/)
})
test('the shell renders cached route views below an accessible workspace tab strip', () => {
assert.match(layoutSource, /<WorkspaceTabs[\s\S]*:inert="mobileMenuOpen"/)
assert.match(layoutSource, /<KeepAlive :max="MAX_CACHED_WORKSPACE_VIEWS">/)
assert.match(layoutSource, /workspace\.cacheKey\(viewRoute\.name\)/)
assert.match(layoutSource, /scrollPositions\.set\(previousRouteName, window\.scrollY\)/)
assert.match(layoutSource, /window\.scrollTo\(0, scrollPositions\.get\(routeName\) \?\? 0\)/)
assert.match(tabsSource, /role="tablist"/)
assert.match(tabsSource, /role="tab"/)
assert.match(tabsSource, /:aria-selected=/)
assert.match(tabsSource, /关闭当前/)
assert.match(tabsSource, /关闭其他/)
assert.match(tabsSource, /关闭右侧/)
assert.match(tabsSource, /关闭全部/)
})
test('unsaved work blocks destructive closes and browser unload without blocking cached tab switches', () => {
assert.match(unsavedSource, /dirtyRouteKeys/)
assert.match(tabsSource, /unsaved\.findDirty\(keys\)/)
assert.match(tabsSource, /放弃未保存的修改/)
assert.match(layoutSource, /window\.addEventListener\('beforeunload', handleBeforeUnload\)/)
assert.match(layoutSource, /if \(!unsaved\.hasDirty\) return/)
assert.doesNotMatch(tabsSource, /beforeEach|onBeforeRouteLeave/)
})
test('unsaved registrations use instance ownership so stale cached views cannot replace or remove a reopened view', () => {
assert.match(unsavedSource, /const owners = new Map<string, symbol>\(\)/)
assert.match(unsavedSource, /function claim\(key: string, owner: symbol\)/)
assert.match(unsavedSource, /if \(owner && owners\.get\(entry\.key\) !== owner\) return/)
assert.match(unsavedSource, /if \(owner && owners\.get\(key\) !== owner\) return/)
assert.match(unsavedSource, /store\.claim\(options\.key, owner\)/)
assert.match(unsavedSource, /store\.remove\(options\.key, owner\)/)
})
test('unsaved registration only tracks the dirty ref and cannot subscribe to its own store writes', () => {
assert.match(unsavedSource, /watch\(options\.dirty, \(dirty\) =>/)
assert.match(unsavedSource, /\}, \{ immediate: true \}\)/)
assert.doesNotMatch(unsavedSource, /watchEffect/)
assert.match(unsavedSource, /current\.routeKey === entry\.routeKey[\s\S]*current\.dirty === entry\.dirty\) return/)
})
test('the tab strip keeps touch targets and horizontal navigation usable on narrow screens', () => {
assert.match(tabStyles, /overflow-x: auto/)
assert.match(tabStyles, /scrollbar-width: none/)
assert.match(tabStyles, /::-webkit-scrollbar \{ display: none; width: 0; height: 0; \}/)
assert.match(tabsSource, /@wheel="handleTabWheel"/)
assert.match(tabsSource, /scroller\.scrollLeft = Math\.max/)
assert.match(tabStyles, /@media \(max-width: 720px\)/)
assert.match(tabStyles, /\.workspace-tab__close \{ width: 40px; height: 40px;/)
assert.match(tabStyles, /\.workspace-tabs__more \{ width: 48px; height: 48px;/)
assert.match(tabStyles, /prefers-reduced-motion: reduce/)
})