feat: strengthen backup and mobile workflows
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { backupFileSnapshot, isCurrentBackupSnapshot, shouldCommitBackupPreflight } from './backup-preflight-state'
|
||||
|
||||
describe('backup preflight identity', () => {
|
||||
it('rejects a preflight result after the selected file changes even when the old request finishes last', () => {
|
||||
const oldFile = new File(['old'], 'old.zip', { lastModified: 10 })
|
||||
const newFile = new File(['new'], 'new.zip', { lastModified: 20 })
|
||||
const started = backupFileSnapshot(oldFile, 'merge')
|
||||
|
||||
expect(isCurrentBackupSnapshot(started, newFile, 'merge')).toBe(false)
|
||||
})
|
||||
|
||||
it('does not let a token for one filename authorize another file with matching metadata', () => {
|
||||
const first = new File(['same'], 'first.zip', { lastModified: 10 })
|
||||
const second = new File(['same'], 'second.zip', { lastModified: 10 })
|
||||
|
||||
expect(isCurrentBackupSnapshot(backupFileSnapshot(first, 'merge'), second, 'merge')).toBe(false)
|
||||
})
|
||||
|
||||
it('invalidates a preflight when restore mode changes', () => {
|
||||
const file = new File(['zip'], 'backup.zip', { lastModified: 10 })
|
||||
|
||||
expect(isCurrentBackupSnapshot(backupFileSnapshot(file, 'merge'), file, 'replace')).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects an older generation even when its file snapshot still matches', () => {
|
||||
const file = new File(['zip'], 'backup.zip', { lastModified: 10 })
|
||||
expect(shouldCommitBackupPreflight(1, 2, backupFileSnapshot(file, 'merge'), file, 'merge')).toBe(false)
|
||||
expect(shouldCommitBackupPreflight(2, 2, backupFileSnapshot(file, 'merge'), file, 'merge')).toBe(true)
|
||||
})
|
||||
|
||||
it('binds identity to the exact File object as well as name size and modified time', () => {
|
||||
const first = new File(['same'], 'backup.zip', { lastModified: 10 })
|
||||
const replacement = new File(['same'], 'backup.zip', { lastModified: 10 })
|
||||
|
||||
expect(isCurrentBackupSnapshot(backupFileSnapshot(first, 'merge'), replacement, 'merge')).toBe(false)
|
||||
expect(isCurrentBackupSnapshot(backupFileSnapshot(first, 'merge'), first, 'merge')).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { BackupMode } from '../api'
|
||||
|
||||
export type BackupFileSnapshot = {
|
||||
file: File
|
||||
name: string
|
||||
size: number
|
||||
lastModified: number
|
||||
mode: BackupMode
|
||||
}
|
||||
|
||||
export function backupFileSnapshot(file: File, mode: BackupMode): BackupFileSnapshot {
|
||||
return { file, name: file.name, size: file.size, lastModified: file.lastModified, mode }
|
||||
}
|
||||
|
||||
export function isCurrentBackupSnapshot(snapshot: BackupFileSnapshot, file: File | null, mode: BackupMode) {
|
||||
return Boolean(file)
|
||||
&& snapshot.file === file
|
||||
&& snapshot.name === file!.name
|
||||
&& snapshot.size === file!.size
|
||||
&& snapshot.lastModified === file!.lastModified
|
||||
&& snapshot.mode === mode
|
||||
}
|
||||
|
||||
export function shouldCommitBackupPreflight(
|
||||
generation: number,
|
||||
currentGeneration: number,
|
||||
snapshot: BackupFileSnapshot,
|
||||
file: File | null,
|
||||
mode: BackupMode,
|
||||
) {
|
||||
return generation === currentGeneration && isCurrentBackupSnapshot(snapshot, file, mode)
|
||||
}
|
||||
|
||||
export function isLegacyBackup(file: File) {
|
||||
return !file.name.toLowerCase().endsWith('.zip')
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { nextDialogFocusIndex } from './list-purge'
|
||||
|
||||
describe('archived list purge dialog behavior', () => {
|
||||
it('wraps Tab focus between the cancel and destructive actions', () => {
|
||||
expect(nextDialogFocusIndex(0, 2, true)).toBe(1)
|
||||
expect(nextDialogFocusIndex(1, 2, false)).toBe(0)
|
||||
})
|
||||
|
||||
it('leaves focus alone while moving between interior controls', () => {
|
||||
expect(nextDialogFocusIndex(1, 3, true)).toBeNull()
|
||||
expect(nextDialogFocusIndex(1, 3, false)).toBeNull()
|
||||
expect(nextDialogFocusIndex(0, 0, false)).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,6 +0,0 @@
|
||||
export function nextDialogFocusIndex(currentIndex: number, controlCount: number, shiftKey: boolean) {
|
||||
if (controlCount < 2) return null
|
||||
if (shiftKey && currentIndex === 0) return controlCount - 1
|
||||
if (!shiftKey && currentIndex === controlCount - 1) return 0
|
||||
return null
|
||||
}
|
||||
Reference in New Issue
Block a user