opsdash-app/opsdash/test/useDashboardPersistence.test.ts
blade34242 32c5b95894
All checks were successful
Nextcloud Server Tests / version-consistency (push) Successful in 32s
Nextcloud Server Tests / matrix-config (push) Successful in 27s
Nextcloud Server Tests / Nextcloud stable30 / PHP 8.2 (stable30, 8.2) (push) Successful in 15m47s
Nextcloud Server Tests / Nextcloud stable31 / PHP 8.2 (stable31, 8.2) (push) Successful in 16m10s
Nextcloud Server Tests / Nextcloud stable31 / PHP 8.3 (stable31, 8.3) (push) Successful in 15m58s
Nextcloud Server Tests / Nextcloud stable32 / PHP 8.2 (stable32, 8.2) (push) Successful in 15m55s
Nextcloud Server Tests / Nextcloud stable32 / PHP 8.3 (stable32, 8.3) (push) Successful in 16m23s
Nextcloud Server Tests / Nextcloud stable33 / PHP 8.2 (stable33, 8.2) (push) Successful in 17m14s
Nextcloud Server Tests / Nextcloud stable33 / PHP 8.3 (stable33, 8.3) (push) Successful in 16m23s
Refine recap delivery scheduling
2026-05-15 14:01:57 +07:00

452 lines
13 KiB
TypeScript

import { ref } from 'vue'
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { useDashboardPersistence } from '../composables/useDashboardPersistence'
import { createDefaultTargetsConfig } from '../src/services/targets'
import { createDefaultDeckSettings, createDefaultReportingConfig } from '../src/services/reporting'
import persistFixture from './fixtures-v2/persist-response.json'
import persistQaFixture from './fixtures-v2/persist-qa.json'
import persistWeekOffset from './fixtures-v2/persist-week-offset1.json'
import persistReportingDeck from './fixtures-v2/persist-reporting-deck.json'
import { createDefaultWidgetTabs } from '../src/services/widgetsRegistry'
function createPersistence(overrides: Partial<Parameters<typeof useDashboardPersistence>[0]> = {}) {
const selected = ref<string[]>([])
const groupsById = ref<Record<string, number>>({})
const targetsWeek = ref<Record<string, number>>({})
const targetsMonth = ref<Record<string, number>>({})
const targetsConfig = ref(createDefaultTargetsConfig())
const themePreference =
overrides.themePreference ?? ref<'auto' | 'light' | 'dark'>('auto')
const onboardingState = overrides.onboardingState ?? ref<any>(null)
const route = vi.fn<(name: 'persist') => string>().mockReturnValue('/persist')
const postJson = vi.fn().mockResolvedValue({})
const notifyError = vi.fn()
const notifySuccess = vi.fn()
const onReload = vi.fn().mockResolvedValue(undefined)
const persistence = useDashboardPersistence({
route,
postJson,
notifyError,
notifySuccess,
onReload,
selected,
groupsById,
targetsWeek,
targetsMonth,
targetsConfig,
themePreference,
onboardingState,
...overrides,
})
return {
route,
postJson,
notifyError,
notifySuccess,
onReload,
selected,
groupsById,
targetsWeek,
targetsMonth,
targetsConfig,
themePreference,
onboardingState,
...persistence,
}
}
describe('useDashboardPersistence', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.runOnlyPendingTimers()
vi.useRealTimers()
vi.clearAllMocks()
})
it('posts current payload and updates selection + config', async () => {
const persistedConfig = createDefaultTargetsConfig()
persistedConfig.totalHours = 99
const postJson = vi.fn().mockResolvedValue({
read: ['cal-1', 'cal-2'],
targets_config_read: persistedConfig,
})
const notifySuccess = vi.fn()
const notifyError = vi.fn()
const onReload = vi.fn().mockResolvedValue(undefined)
const {
queueSave,
selected,
groupsById,
targetsWeek,
targetsMonth,
targetsConfig,
isSaving,
} = createPersistence({
postJson,
notifySuccess,
notifyError,
onReload,
})
selected.value = ['cal-1']
groupsById.value = { 'cal-1': 2 }
targetsWeek.value = { 'cal-1': 12 }
targetsMonth.value = { 'cal-1': 48 }
targetsConfig.value.totalHours = 64
const configSnapshot = targetsConfig.value
queueSave(true)
expect(isSaving.value).toBe(false)
await vi.runOnlyPendingTimersAsync()
expect(postJson).toHaveBeenCalledWith('/persist', expect.objectContaining({
cals: ['cal-1'],
groups: { 'cal-1': 2 },
targets_week: { 'cal-1': 12 },
targets_month: { 'cal-1': 48 },
}))
const sentConfig = postJson.mock.calls[0][1]?.targets_config
expect(sentConfig).toMatchObject(configSnapshot)
expect(sentConfig).not.toBe(configSnapshot)
expect(selected.value).toEqual(['cal-1', 'cal-2'])
expect(targetsConfig.value.totalHours).toBe(99)
expect(isSaving.value).toBe(false)
expect(notifySuccess).toHaveBeenCalledWith('Selection saved')
expect(notifyError).not.toHaveBeenCalled()
expect(onReload).toHaveBeenCalledTimes(1)
})
it('debounces rapid queueSave calls', async () => {
const postJson = vi.fn().mockResolvedValue({})
const notifySuccess = vi.fn()
const { queueSave } = createPersistence({
postJson,
notifySuccess,
})
queueSave(false)
vi.advanceTimersByTime(200)
queueSave(false)
await vi.runOnlyPendingTimersAsync()
expect(postJson).toHaveBeenCalledTimes(1)
expect(notifySuccess).toHaveBeenCalledTimes(1)
})
it('applies server-provided balance UI toggles', async () => {
const serverConfig = createDefaultTargetsConfig()
serverConfig.balance.ui.showNotes = false
const postJson = vi.fn().mockResolvedValue({
targets_config_read: serverConfig,
})
const { queueSave, targetsConfig } = createPersistence({
postJson,
notifySuccess: vi.fn(),
})
targetsConfig.value.balance.ui.showNotes = true
queueSave(false)
await vi.runOnlyPendingTimersAsync()
expect(postJson).toHaveBeenCalledTimes(1)
expect(targetsConfig.value.balance.ui.showNotes).toBe(false)
})
it('applies balance index basis from server config', async () => {
const serverConfig = createDefaultTargetsConfig()
serverConfig.balance.index.basis = 'calendar'
const postJson = vi.fn().mockResolvedValue({
targets_config_read: serverConfig,
})
const { queueSave, targetsConfig } = createPersistence({
postJson,
notifySuccess: vi.fn(),
})
targetsConfig.value.balance.index.basis = 'category'
queueSave(false)
await vi.runOnlyPendingTimersAsync()
expect(postJson).toHaveBeenCalledTimes(1)
expect(targetsConfig.value.balance.index.basis).toBe('calendar')
})
it('persists theme preference when provided', async () => {
const postJson = vi.fn().mockResolvedValue({
theme_preference_read: 'dark',
})
const { queueSave, themePreference } = createPersistence({
postJson,
themePreference: ref<'auto' | 'light' | 'dark'>('light'),
})
queueSave(false)
await vi.runOnlyPendingTimersAsync()
expect(postJson).toHaveBeenCalledWith('/persist', expect.objectContaining({
theme_preference: 'light',
}))
expect(themePreference.value).toBe('dark')
})
it('persists onboarding state and applies server read-back', async () => {
const onboardingState = ref<any>({
completed: true,
version: 1,
strategy: 'full_granular',
completed_at: '2026-02-21T00:00:00.000Z',
dashboardMode: 'pro',
})
const postJson = vi.fn().mockResolvedValue({
onboarding_read: {
completed: true,
version: 1,
strategy: 'total_plus_categories',
completed_at: '2026-02-21T00:00:00.000Z',
dashboardMode: 'quick',
},
})
const { queueSave } = createPersistence({
postJson,
onboardingState,
})
queueSave(false)
await vi.runOnlyPendingTimersAsync()
expect(postJson).toHaveBeenCalledWith('/persist', expect.objectContaining({
onboarding: expect.objectContaining({
strategy: 'full_granular',
dashboardMode: 'pro',
}),
}))
expect(onboardingState.value.strategy).toBe('total_plus_categories')
expect(onboardingState.value.dashboardMode).toBe('quick')
})
it('does not downgrade release notes seen version from stale save read-back', async () => {
const onboardingState = ref<any>({
completed: true,
version: 1,
strategy: 'total_only',
completed_at: '2026-04-01T00:00:00.000Z',
dashboardMode: 'standard',
releaseNotesSeenVersion: '0.7.5',
})
const postJson = vi.fn().mockResolvedValue({
onboarding_read: {
completed: true,
version: 1,
strategy: 'total_only',
completed_at: '2026-04-01T00:00:00.000Z',
dashboardMode: 'standard',
releaseNotesSeenVersion: '0.7.4',
},
})
const { queueSave } = createPersistence({
postJson,
onboardingState,
})
queueSave(false)
await vi.runOnlyPendingTimersAsync()
expect(onboardingState.value.releaseNotesSeenVersion).toBe('0.7.5')
})
it('replays persist response fixture without dropping UI flags', async () => {
const postJson = vi.fn().mockResolvedValue(persistFixture)
const themePref = ref<'auto' | 'light' | 'dark'>('auto')
const { queueSave, selected, targetsConfig, themePreference } = createPersistence({
postJson,
themePreference: themePref,
})
targetsConfig.value.balance.ui.showNotes = true
queueSave(false)
await vi.runOnlyPendingTimersAsync()
expect(selected.value).toEqual(persistFixture.saved)
expect(themePreference.value).toBe('dark')
expect(targetsConfig.value.categories).toHaveLength(
persistFixture.targets_config_read.categories.length,
)
expect(targetsConfig.value.balance.ui.showNotes).toBe(true)
})
it('handles minimal QA persist fixture', async () => {
const postJson = vi.fn().mockResolvedValue(persistQaFixture)
const { queueSave, selected } = createPersistence({ postJson })
queueSave(false)
await vi.runOnlyPendingTimersAsync()
expect(selected.value).toEqual(['opsdash-focus'])
})
it('replays week offset persist fixture', async () => {
const postJson = vi.fn().mockResolvedValue(persistWeekOffset)
const initialWeek = { ...persistWeekOffset.targets_week_read }
const targetsWeek = ref<Record<string, number>>({ ...initialWeek })
const { queueSave, selected } = createPersistence({
postJson,
targetsWeek,
})
queueSave(false)
await vi.runOnlyPendingTimersAsync()
expect(selected.value).toEqual(persistWeekOffset.saved)
expect(targetsWeek.value).toEqual(initialWeek)
})
it('applies reporting + Deck settings from persist fixture', async () => {
const postJson = vi.fn().mockResolvedValue(persistReportingDeck)
const reportingConfig = ref(createDefaultReportingConfig())
reportingConfig.value.enabled = false
const deckSettings = ref(createDefaultDeckSettings())
deckSettings.value.defaultFilter = 'all'
deckSettings.value.hiddenBoards = []
const { queueSave } = createPersistence({
postJson,
reportingConfig,
deckSettings,
})
queueSave(false)
await vi.runOnlyPendingTimersAsync()
expect(reportingConfig.value.modes.week).toEqual({
enabled: true,
delivery: 'checkpoint_final',
sendTimeLocal: '06:00',
})
expect(reportingConfig.value.modes.month).toEqual({
enabled: false,
delivery: 'final',
sendTimeLocal: '18:00',
})
expect(reportingConfig.value.notifyEmail).toBe(false)
expect(reportingConfig.value.notifyNotification).toBe(true)
expect(deckSettings.value.enabled).toBe(false)
expect(deckSettings.value.defaultFilter).toBe('mine')
expect(deckSettings.value.hiddenBoards).toEqual([42])
})
it('persists and normalizes widget layout', async () => {
const initialTabs = createDefaultWidgetTabs('standard')
const widgetTabs = ref(initialTabs)
const postJson = vi.fn().mockResolvedValue({
widgets_read: [
{
type: 'notes',
layout: { width: 'half', height: 'l', order: 5 },
options: { mode: 'month' },
},
],
})
const { queueSave } = createPersistence({
postJson,
widgetTabs,
})
queueSave(false)
await vi.runOnlyPendingTimersAsync()
expect(postJson).toHaveBeenCalledWith('/persist', expect.objectContaining({
widgets: initialTabs,
}))
expect(widgetTabs.value.tabs).toHaveLength(1)
expect(widgetTabs.value.tabs[0].widgets).toEqual(initialTabs.tabs[0].widgets)
})
it('keeps empty tab widgets from persistence payloads', async () => {
const initialTabs = createDefaultWidgetTabs('standard')
const widgetTabs = ref(initialTabs)
const postJson = vi.fn().mockResolvedValue({
widgets_read: {
tabs: [
{ id: 'tab-empty', label: 'Empty', widgets: [] },
{ id: 'tab-full', label: 'Full', widgets: [
{ type: 'note_editor', layout: { width: 'half', height: 'm', order: 1 }, options: {} },
] },
],
defaultTabId: 'tab-empty',
},
})
const { queueSave } = createPersistence({
postJson,
widgetTabs,
})
queueSave(false)
await vi.runOnlyPendingTimersAsync()
expect(widgetTabs.value.tabs).toHaveLength(2)
expect(widgetTabs.value.defaultTabId).toBe('tab-empty')
expect(widgetTabs.value.tabs[0].widgets).toEqual([])
expect(widgetTabs.value.tabs[1].widgets[0].type).toBe('note_editor')
})
it('migrates chart filters from persisted widget tabs', async () => {
const initialTabs = createDefaultWidgetTabs('standard')
const widgetTabs = ref(initialTabs)
const postJson = vi.fn().mockResolvedValue({
widgets_read: {
tabs: [
{
id: 'tab-a',
label: 'Alpha',
widgets: [
{
type: 'chart_pie',
layout: { width: 'half', height: 'm', order: 1 },
options: { scope: 'calendar', calendarFilter: ['cal-1'] },
},
],
},
],
defaultTabId: 'tab-a',
},
})
const { queueSave } = createPersistence({
postJson,
widgetTabs,
})
queueSave(false)
await vi.runOnlyPendingTimersAsync()
const widget = widgetTabs.value.tabs[0].widgets[0]
expect(widget.options.filterMode).toBe('calendar')
expect(widget.options.filterIds).toEqual(['cal-1'])
})
})