1.1.1: fullbak
This commit is contained in:
1 parent
d59201100b
commit
01c13981c4
6 files changed
+443
No files matched your search
@@ -1943,6 +1943,102 @@ export function registerAllTests(): void {
|
||||
})
|
||||
})
|
||||
|
||||
suite('ruwbak — multi-project backup roundtrip', () => {
|
||||
// A .ruwbak is a zip holding one manifest.json + one packaged
|
||||
// .ruw per project. Covers the "back up everything in browser
|
||||
// storage" / "restore a backup" flow exposed from the welcome
|
||||
// dialog.
|
||||
|
||||
test('pack + unpack preserves each project with its metadata', async () => {
|
||||
const { packBackup, unpackBackup } = await import('@/lib/backupFile')
|
||||
const a = makeFreshStore()
|
||||
a.addAsset({ kind: 'rect', name: 'A', width: 10, height: 10, fill: '#000' })
|
||||
const projectA = JSON.parse(JSON.stringify(a.projectForSave))
|
||||
const b = makeFreshStore()
|
||||
b.addAsset({ kind: 'rect', name: 'B', width: 20, height: 20, fill: '#fff' })
|
||||
const projectB = JSON.parse(JSON.stringify(b.projectForSave))
|
||||
const bundle = packBackup([
|
||||
{ id: 'id-a', name: 'Alpha', savedAt: 1, project: projectA },
|
||||
{ id: 'id-b', name: 'Beta', savedAt: 2, project: projectB },
|
||||
])
|
||||
const out = unpackBackup(bundle)
|
||||
expect(out.length).toBe(2)
|
||||
expect(out[0].id).toBe('id-a')
|
||||
expect(out[0].name).toBe('Alpha')
|
||||
expect(out[0].savedAt).toBe(1)
|
||||
expect(out[0].project.assets.some((x) => x.name === 'A')).toBe(true)
|
||||
expect(out[1].id).toBe('id-b')
|
||||
expect(out[1].project.assets.some((x) => x.name === 'B')).toBe(true)
|
||||
})
|
||||
|
||||
test('backup survives asset data through the inner .ruw bundles', async () => {
|
||||
const { packBackup, unpackBackup } = await import('@/lib/backupFile')
|
||||
const s = makeFreshStore()
|
||||
const imgSrc = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX///+nxBvIAAAAC0lEQVQI12NgAAIAAAUAAeImBZsAAAAASUVORK5CYII='
|
||||
s.addAsset({
|
||||
kind: 'image', name: 'pixel', src: imgSrc, width: 1, height: 1,
|
||||
})
|
||||
const project = JSON.parse(JSON.stringify(s.projectForSave))
|
||||
const bundle = packBackup([
|
||||
{ id: 'only', name: 'Sole', savedAt: 0, project },
|
||||
])
|
||||
const [rec] = unpackBackup(bundle)
|
||||
const img = rec.project.assets.find((a) => a.name === 'pixel')! as {
|
||||
src: string
|
||||
}
|
||||
// Image round-trips all the way: through inner .ruw asset
|
||||
// extraction + rehydration via the outer backup zip.
|
||||
expect(img.src).toBe(imgSrc)
|
||||
})
|
||||
|
||||
test('unpackBackup rejects non-zip + wrong magic + missing files', async () => {
|
||||
const { unpackBackup, packBackup } = await import('@/lib/backupFile')
|
||||
// Non-zip input.
|
||||
let err: unknown
|
||||
try { unpackBackup(new Uint8Array([0x7b, 0x7d])) } catch (e) { err = e }
|
||||
expect((err as Error).message).toContain('ZIP')
|
||||
// Wrong-magic manifest.
|
||||
const { zipStore } = await import('@/lib/zipUtil')
|
||||
const badManifest = zipStore([{
|
||||
name: 'manifest.json',
|
||||
data: new TextEncoder().encode(JSON.stringify({ magic: 'nope', version: 1, projects: [] })),
|
||||
}])
|
||||
err = null
|
||||
try { unpackBackup(badManifest) } catch (e) { err = e }
|
||||
expect((err as Error).message.toLowerCase()).toContain('magic')
|
||||
// Missing referenced project file.
|
||||
const s = makeFreshStore()
|
||||
s.addAsset({ kind: 'rect', name: 'X', width: 10, height: 10, fill: '#fff' })
|
||||
const goodBundle = packBackup([
|
||||
{ id: 'missing', name: 'Lost', savedAt: 1, project: JSON.parse(JSON.stringify(s.projectForSave)) },
|
||||
])
|
||||
// Strip the inner .ruw to simulate corruption.
|
||||
const { unzipStore } = await import('@/lib/zipUtil')
|
||||
const entries = unzipStore(goodBundle).filter(
|
||||
(e) => !e.name.startsWith('projects/'),
|
||||
)
|
||||
const corrupt = zipStore(entries)
|
||||
err = null
|
||||
try { unpackBackup(corrupt) } catch (e) { err = e }
|
||||
expect((err as Error).message.toLowerCase()).toContain('missing')
|
||||
})
|
||||
|
||||
test('duplicate ids in the input get unique file paths inside the zip', async () => {
|
||||
const { packBackup } = await import('@/lib/backupFile')
|
||||
const { unzipStore } = await import('@/lib/zipUtil')
|
||||
const s = makeFreshStore()
|
||||
s.addAsset({ kind: 'rect', name: 'X', width: 10, height: 10, fill: '#fff' })
|
||||
const project = JSON.parse(JSON.stringify(s.projectForSave))
|
||||
const bundle = packBackup([
|
||||
{ id: 'dup', name: 'A', savedAt: 1, project },
|
||||
{ id: 'dup', name: 'B', savedAt: 2, project },
|
||||
])
|
||||
const files = unzipStore(bundle).map((e) => e.name).filter((n) => n.startsWith('projects/'))
|
||||
// Two distinct project files, one suffixed to avoid collision.
|
||||
expect(new Set(files).size).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
suite('editor state — save / load roundtrip', () => {
|
||||
// The store's editor block carries canvas zoom/pan, overlay
|
||||
// toggles and timeline display settings. It rides along with the
|
||||
@@ -2842,6 +2938,29 @@ export function registerAllTests(): void {
|
||||
const back = await getStoredProject('definitely-missing-id-42')
|
||||
expect(back).toBeNull()
|
||||
})
|
||||
|
||||
test('listAllProjects returns every record — no 8-recents cap', async () => {
|
||||
// Backup flow needs the full dump, not just the top 8 that
|
||||
// the welcome dialog shows.
|
||||
const { listAllProjects } = await import('@/lib/projectStore')
|
||||
const s = makeFreshStore()
|
||||
const ids: string[] = []
|
||||
const prefix = `test-backup-${Date.now()}-`
|
||||
for (let i = 0; i < 10; i++) ids.push(`${prefix}${i}`)
|
||||
try {
|
||||
for (const id of ids) {
|
||||
await saveProjectToStore(s.project, { id, name: id })
|
||||
await tick(1)
|
||||
}
|
||||
const all = await listAllProjects()
|
||||
// At least our 10 show up, even though listRecentProjects
|
||||
// would only return 8.
|
||||
const seen = ids.filter((id) => all.some((r) => r.id === id))
|
||||
expect(seen.length).toBe(10)
|
||||
} finally {
|
||||
for (const id of ids) await deleteStoredProject(id).catch(() => {})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
suite('ui — WelcomeDialog', () => {
|
||||
|
||||
Reference in new issue
Block a user