feat(ui): finalize shared shells and loading states
This commit is contained in:
@@ -65,6 +65,7 @@ test('legacy reimbursement approval and archive centers are no longer accessible
|
||||
assert.equal(canAccessAppView(adminUser, 'requests'), false)
|
||||
assert.equal(canAccessAppView(adminUser, 'approval'), false)
|
||||
assert.equal(canAccessAppView(adminUser, 'archive'), false)
|
||||
assert.equal(canAccessAppView(adminUser, 'logs'), false)
|
||||
assert.equal(canAccessAppView(adminUser, 'documents'), true)
|
||||
})
|
||||
|
||||
|
||||
56
web/tests/minimum-visible-state.test.mjs
Normal file
56
web/tests/minimum-visible-state.test.mjs
Normal file
@@ -0,0 +1,56 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { effectScope, nextTick, ref } from 'vue'
|
||||
|
||||
import { useMinimumVisibleState } from '../src/composables/useMinimumVisibleState.js'
|
||||
|
||||
function wait(ms) {
|
||||
return new Promise((resolve) => globalThis.setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
test('minimum visible state stays visible after a fast loading toggle', async () => {
|
||||
const scope = effectScope()
|
||||
const state = scope.run(() => {
|
||||
const loading = ref(false)
|
||||
const visible = useMinimumVisibleState(loading, { minVisibleMs: 35 })
|
||||
return { loading, visible }
|
||||
})
|
||||
|
||||
state.loading.value = true
|
||||
await nextTick()
|
||||
assert.equal(state.visible.value, true)
|
||||
|
||||
state.loading.value = false
|
||||
await nextTick()
|
||||
assert.equal(state.visible.value, true)
|
||||
|
||||
await wait(50)
|
||||
assert.equal(state.visible.value, false)
|
||||
scope.stop()
|
||||
})
|
||||
|
||||
test('minimum visible state cancels a pending hide when loading restarts', async () => {
|
||||
const scope = effectScope()
|
||||
const state = scope.run(() => {
|
||||
const loading = ref(false)
|
||||
const visible = useMinimumVisibleState(loading, { minVisibleMs: 40 })
|
||||
return { loading, visible }
|
||||
})
|
||||
|
||||
state.loading.value = true
|
||||
await nextTick()
|
||||
state.loading.value = false
|
||||
await nextTick()
|
||||
await wait(10)
|
||||
|
||||
state.loading.value = true
|
||||
await nextTick()
|
||||
await wait(40)
|
||||
assert.equal(state.visible.value, true)
|
||||
|
||||
state.loading.value = false
|
||||
await nextTick()
|
||||
await wait(50)
|
||||
assert.equal(state.visible.value, false)
|
||||
scope.stop()
|
||||
})
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
} from '../src/composables/useNavigation.js'
|
||||
|
||||
function testDerivesViewFromRouteName() {
|
||||
assert.equal(resolveAppViewFromRoute({ name: 'app-log-detail', meta: {} }), 'logs')
|
||||
assert.equal(resolveAppViewFromRoute({ name: 'app-log-detail', meta: {} }), 'settings')
|
||||
assert.equal(resolveAppViewFromRoute({ name: 'app-request-detail', meta: {} }), 'documents')
|
||||
assert.equal(resolveAppViewFromRoute({ name: 'app-policies', meta: { appView: 'logs' } }), 'policies')
|
||||
assert.equal(resolveAppViewFromRoute({ name: 'app-policies', meta: { appView: 'settings' } }), 'policies')
|
||||
}
|
||||
|
||||
function testFallsBackToValidMeta() {
|
||||
@@ -19,7 +19,7 @@ function testFallsBackToValidMeta() {
|
||||
}
|
||||
|
||||
function testResolvesMainRouteNames() {
|
||||
assert.equal(resolveTargetRouteName('logs'), 'app-logs')
|
||||
assert.equal(resolveTargetRouteName('logs'), 'app-settings')
|
||||
assert.equal(resolveTargetRouteName('policies'), 'app-policies')
|
||||
assert.equal(resolveTargetRouteName('requests'), 'app-overview')
|
||||
assert.equal(resolveTargetRouteName('approval'), 'app-overview')
|
||||
@@ -31,7 +31,8 @@ function testLegacyCentersAreRemovedFromNavigation() {
|
||||
assert.equal(appViews.includes('requests'), false)
|
||||
assert.equal(appViews.includes('approval'), false)
|
||||
assert.equal(appViews.includes('archive'), false)
|
||||
assert.equal(navItems.some((item) => ['requests', 'approval', 'archive'].includes(item.id)), false)
|
||||
assert.equal(appViews.includes('logs'), false)
|
||||
assert.equal(navItems.some((item) => ['requests', 'approval', 'archive', 'logs'].includes(item.id)), false)
|
||||
}
|
||||
|
||||
function run() {
|
||||
|
||||
34
web/tests/refresh-interval-controls.test.mjs
Normal file
34
web/tests/refresh-interval-controls.test.mjs
Normal file
@@ -0,0 +1,34 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const refreshOptions = readFileSync(new URL('../src/utils/refreshIntervalOptions.js', import.meta.url), 'utf8')
|
||||
const logsView = readFileSync(new URL('../src/views/LogsView.vue', import.meta.url), 'utf8')
|
||||
const logsScript = readFileSync(new URL('../src/views/scripts/LogsView.js', import.meta.url), 'utf8')
|
||||
const workRecords = readFileSync(
|
||||
new URL('../src/components/audit/DigitalEmployeeWorkRecords.vue', import.meta.url),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
test('shared refresh interval options default to 60 seconds', () => {
|
||||
assert.match(refreshOptions, /DEFAULT_REFRESH_INTERVAL_MS\s*=\s*60000/)
|
||||
for (const value of [1000, 3000, 5000, 10000, 30000, 60000, 180000]) {
|
||||
assert.match(refreshOptions, new RegExp(`value:\\s*${value}`))
|
||||
}
|
||||
})
|
||||
|
||||
test('system logs list exposes refresh interval control', () => {
|
||||
assert.match(logsScript, /refreshInterval\s*=\s*ref\(DEFAULT_REFRESH_INTERVAL_MS\)/)
|
||||
assert.match(logsScript, /window\.setInterval\([\s\S]*refreshInterval\.value/)
|
||||
assert.match(logsView, /刷新时间 \{\{ refreshIntervalLabel \}\}/)
|
||||
assert.match(logsView, /v-for="option in refreshIntervalOptions"/)
|
||||
assert.doesNotMatch(logsView, /刷新日志/)
|
||||
})
|
||||
|
||||
test('digital employee work records expose refresh interval control', () => {
|
||||
assert.match(workRecords, /refreshInterval\s*=\s*ref\(DEFAULT_REFRESH_INTERVAL_MS\)/)
|
||||
assert.match(workRecords, /refreshIntervalPickerOptions\s*=\s*REFRESH_INTERVAL_OPTIONS/)
|
||||
assert.match(workRecords, /window\.setInterval\([\s\S]*refreshInterval\.value/)
|
||||
assert.match(workRecords, /刷新时间 \$\{refreshIntervalLabel\}/)
|
||||
assert.doesNotMatch(workRecords, /AGENT_RUN_POLL_INTERVAL_MS/)
|
||||
})
|
||||
22
web/tests/settings-system-logs-section.test.mjs
Normal file
22
web/tests/settings-system-logs-section.test.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
const settingsModel = readFileSync(new URL('../src/utils/settingsModelHelper.js', import.meta.url), 'utf8')
|
||||
const settingsView = readFileSync(new URL('../src/views/SettingsView.vue', import.meta.url), 'utf8')
|
||||
const settingsScript = readFileSync(new URL('../src/views/scripts/SettingsView.js', import.meta.url), 'utf8')
|
||||
const router = readFileSync(new URL('../src/router/index.js', import.meta.url), 'utf8')
|
||||
const logDetailView = readFileSync(new URL('../src/views/LogDetailView.vue', import.meta.url), 'utf8')
|
||||
|
||||
test('system logs are nested under system settings instead of sidebar navigation', () => {
|
||||
assert.match(settingsModel, /id:\s*'systemLogs'[\s\S]*label:\s*'系统日志'/)
|
||||
assert.match(settingsView, /activeSection === 'systemLogs'/)
|
||||
assert.match(settingsView, /<LogsView v-else class="settings-logs-view" \/>/)
|
||||
assert.match(settingsScript, /import LogsView from '\.\.\/LogsView\.vue'/)
|
||||
})
|
||||
|
||||
test('log detail keeps the settings context and legacy logs URLs redirect', () => {
|
||||
assert.match(router, /path:\s*'\/app\/settings\/logs\/:logKind\/:logId'[\s\S]*appView:\s*'settings'/)
|
||||
assert.match(router, /path:\s*'\/app\/logs'[\s\S]*section:\s*'systemLogs'/)
|
||||
assert.match(logDetailView, /router\.push\(\{ name:\s*'app-settings', query:\s*\{ section:\s*'systemLogs' \} \}\)/)
|
||||
})
|
||||
Reference in New Issue
Block a user