Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions src/renderer/composables/spaces/drawings/__tests__/useDrawings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { computed, nextTick, ref } from 'vue'

const invoke = vi.fn()
const appStore = new Map<string, unknown>()
const markPersistedStorageMutation = vi.fn()
const markUserEdit = vi.fn()
const routerPush = vi.fn()

vi.stubGlobal('ref', ref)
vi.stubGlobal('computed', computed)
vi.stubGlobal('nextTick', nextTick)

vi.mock('@/electron', () => ({
ipc: {
invoke,
},
store: {
app: {
get: (key: string) => appStore.get(key),
set: (key: string, value: unknown) => appStore.set(key, value),
},
},
}))

vi.mock('@/router', () => ({
router: {
push: routerPush,
},
RouterName: {
drawingsSpace: 'drawings-space',
},
}))

vi.mock('@/composables/useDonations', () => ({
useDonations: () => ({
incrementCreated: vi.fn(),
}),
}))

vi.mock('@/composables/useStorageMutation', () => ({
markPersistedStorageMutation,
markUserEdit,
}))

describe('useDrawings', () => {
const drawing = {
id: 'Sketch',
name: 'Sketch',
createdAt: 1,
updatedAt: 1,
}
const otherDrawing = {
id: 'Other',
name: 'Other',
createdAt: 1,
updatedAt: 1,
}
const emptyContent = '{"type":"excalidraw","elements":[]}'
const otherContent = '{"type":"excalidraw","elements":[{"id":"other"}]}'
const updatedContent = '{"type":"excalidraw","elements":[{"id":"shape"}]}'

beforeEach(() => {
vi.resetModules()
vi.clearAllMocks()
appStore.clear()
vi.stubGlobal('window', {
dispatchEvent: vi.fn(),
})
vi.stubGlobal(
'CustomEvent',
class {
detail: unknown
type: string

constructor(type: string, init?: { detail?: unknown }) {
this.type = type
this.detail = init?.detail
}
},
)
})

it('keeps active drawing content current while an IPC save is pending', async () => {
let resolveWrite: ((value: { updatedAt: number }) => void) | undefined

invoke.mockImplementation((channel: string) => {
if (channel === 'spaces:drawings:list') {
return Promise.resolve([drawing])
}

if (channel === 'spaces:drawings:read') {
return Promise.resolve(emptyContent)
}

if (channel === 'spaces:drawings:write') {
return new Promise<{ updatedAt: number }>((resolve) => {
resolveWrite = resolve
})
}

return Promise.resolve(null)
})

const { useDrawings } = await import('../useDrawings')
const drawings = useDrawings()

await drawings.init()

expect(drawings.activeDrawingContent.value).toBe(emptyContent)

const savePromise = drawings.saveDrawingContent(drawing.id, updatedContent)

expect(drawings.activeDrawingContent.value).toBe(updatedContent)
expect(markUserEdit).toHaveBeenCalledTimes(1)

resolveWrite?.({ updatedAt: 2 })
await savePromise

expect(drawings.activeDrawingContent.value).toBe(updatedContent)
})

it('uses pending content when a drawing is reselected before its save finishes', async () => {
let resolveWrite: ((value: { updatedAt: number }) => void) | undefined
const savedContentById: Record<string, string> = {
[drawing.id]: emptyContent,
[otherDrawing.id]: otherContent,
}

invoke.mockImplementation((channel: string, payload?: { id?: string }) => {
if (channel === 'spaces:drawings:list') {
return Promise.resolve([drawing, otherDrawing])
}

if (channel === 'spaces:drawings:read' && payload?.id) {
return Promise.resolve(savedContentById[payload.id])
}

if (channel === 'spaces:drawings:write') {
return new Promise<{ updatedAt: number }>((resolve) => {
resolveWrite = resolve
})
}

return Promise.resolve(null)
})

const { useDrawings } = await import('../useDrawings')
const drawings = useDrawings()

await drawings.init()

const savePromise = drawings.saveDrawingContent(drawing.id, updatedContent)

await drawings.selectDrawing(otherDrawing.id)
await drawings.selectDrawing(drawing.id)

expect(drawings.activeDrawingContent.value).toBe(updatedContent)

savedContentById[drawing.id] = updatedContent
resolveWrite?.({ updatedAt: 2 })
await savePromise
})
})
34 changes: 29 additions & 5 deletions src/renderer/composables/spaces/drawings/useDrawings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ let initialized = false
let lastSavedContent: string | null = null
let inFlightSaves = 0
let loadContentToken = 0
const pendingContentByDrawingId = new Map<string, string>()

const activeDrawing = computed(() => {
return drawings.value.find(item => item.id === activeDrawingId.value)
Expand Down Expand Up @@ -83,8 +84,11 @@ async function loadActiveDrawingContent() {
return
}

activeDrawingContent.value = typeof content === 'string' ? content : null
lastSavedContent = activeDrawingContent.value
const savedContent = typeof content === 'string' ? content : null

activeDrawingContent.value
= pendingContentByDrawingId.get(id) ?? savedContent
lastSavedContent = savedContent
sceneRevision.value += 1
}

Expand Down Expand Up @@ -251,6 +255,13 @@ export function useDrawings() {
persistViewports()
}

const pendingContent = pendingContentByDrawingId.get(id)

if (pendingContent && record.id !== id) {
pendingContentByDrawingId.delete(id)
pendingContentByDrawingId.set(record.id, pendingContent)
}

if (activeDrawingId.value === id) {
activeDrawingId.value = record.id
persistSelection()
Expand Down Expand Up @@ -296,6 +307,7 @@ export function useDrawings() {
persistViewports()
}

pendingContentByDrawingId.delete(id)
notifyDrawingsChanged(id)

if (activeDrawingId.value === id) {
Expand All @@ -310,18 +322,30 @@ export function useDrawings() {
return
}

if (id === activeDrawingId.value && content === lastSavedContent) {
return
const isActiveDrawing = id === activeDrawingId.value

if (isActiveDrawing) {
activeDrawingContent.value = content

if (content === lastSavedContent) {
return
}
}

pendingContentByDrawingId.set(id, content)
markUserEdit()
markPersistedStorageMutation()
inFlightSaves += 1

try {
const record = await ipc.invoke('spaces:drawings:write', { id, content })
const pendingContent = pendingContentByDrawingId.get(id)

if (pendingContent === content) {
pendingContentByDrawingId.delete(id)
}

if (id === activeDrawingId.value) {
if (id === activeDrawingId.value && pendingContent === content) {
lastSavedContent = content
}

Expand Down