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
53 changes: 53 additions & 0 deletions playwright/e2e/loading-scene.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { expect } from '@playwright/test'
import { test } from '../support/fixtures/random-user'
import {
createWhiteboard,
openFilesApp,
waitForCanvas,
} from '../support/utils'

test.beforeEach(async ({ page }) => {
await page.addInitScript(() => {
;(window as any).__whiteboardTest = true
;(window as any).__whiteboardTestHooks = {
blockInitialData: true,
}
})
await openFilesApp(page)
})

test('resolves stale initial data promises', async ({ page }) => {
test.setTimeout(90000)
await createWhiteboard(page)
await waitForCanvas(page)

await expect.poll(
async () => page.evaluate(() => Boolean((window as any).__whiteboardTestHooks?.pendingInitialData)),
{
timeout: 10000,
interval: 200,
},
).toBeTruthy()

const loadingScene = page.getByText(/Loading scene/i)
await expect(loadingScene).toBeVisible({ timeout: 5000 })

await page.evaluate(() => {
const hooks = (window as any).__whiteboardTestHooks
if (!hooks?.whiteboardConfigStore) {
throw new Error('Whiteboard test hooks not available')
}
const store = hooks.whiteboardConfigStore
const pending = hooks.pendingInitialData
store.getState().resetInitialDataPromise()
hooks.restoreResolveInitialData?.()
store.getState().resolveInitialData(pending)
})

await expect(loadingScene).toBeHidden({ timeout: 5000 })
})
71 changes: 67 additions & 4 deletions src/stores/useWhiteboardConfigStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ import { create } from 'zustand'
import { createResolvablePromise } from '../utils/createResolvablePromise'
import type { ExcalidrawInitialDataState } from '@excalidraw/excalidraw/types/types'

type InitialDataPromise = ReturnType<typeof createResolvablePromise>

interface WhiteboardConfigState {
// Core state
fileId: number
fileName: string
publicSharingToken: string | null
isReadOnly: boolean // Single source of truth for read-only state, determined by JWT
isEmbedded: boolean
initialDataPromise: ReturnType<typeof createResolvablePromise>
initialDataPromise: InitialDataPromise
pendingInitialDataPromises: InitialDataPromise[]
collabBackendUrl: string // URL of the collaboration backend server
isVersionPreview: boolean
versionSource: string | null
Expand Down Expand Up @@ -61,6 +64,7 @@ export const useWhiteboardConfigStore = create<WhiteboardConfigState>()((set, ge
isReadOnly: false,
isEmbedded: false,
initialDataPromise: createResolvablePromise(),
pendingInitialDataPromises: [],
collabBackendUrl: '', // Will be initialized from initial state
isVersionPreview: false,
versionSource: null,
Expand All @@ -85,12 +89,24 @@ export const useWhiteboardConfigStore = create<WhiteboardConfigState>()((set, ge
},

resolveInitialData: (data: ExcalidrawInitialDataState) => {
// Resolve the promise with the data
get().initialDataPromise.resolve(data)
const { initialDataPromise, pendingInitialDataPromises } = get()
pendingInitialDataPromises.forEach((promise) => {
promise.resolve(data)
})
initialDataPromise.resolve(data)
if (pendingInitialDataPromises.length > 0) {
set({ pendingInitialDataPromises: [] })
}
},

resetInitialDataPromise: () =>
set({ initialDataPromise: createResolvablePromise() }),
set((state) => ({
pendingInitialDataPromises: [
...state.pendingInitialDataPromises,
state.initialDataPromise,
],
initialDataPromise: createResolvablePromise(),
})),

// Reset the entire store to its initial state
resetStore: () => {
Expand All @@ -109,6 +125,7 @@ export const useWhiteboardConfigStore = create<WhiteboardConfigState>()((set, ge
// Reset these values
isReadOnly: false,
initialDataPromise: createResolvablePromise(),
pendingInitialDataPromises: [],
zenModeEnabled: false,
gridModeEnabled: false,
})
Expand All @@ -128,3 +145,49 @@ export const useWhiteboardConfigStore = create<WhiteboardConfigState>()((set, ge
set({ isReadOnly: readOnly })
},
}))

type WhiteboardTestHooks = {
whiteboardConfigStore?: typeof useWhiteboardConfigStore
blockInitialData?: boolean
pendingInitialData?: ExcalidrawInitialDataState | null
originalResolveInitialData?: (data: ExcalidrawInitialDataState) => void
restoreResolveInitialData?: () => void
}

declare global {
interface Window {
__whiteboardTest?: boolean
__whiteboardTestHooks?: WhiteboardTestHooks
}
}

const attachTestHooks = () => {
if (typeof window === 'undefined' || !window.__whiteboardTest) {
return
}

window.__whiteboardTestHooks = window.__whiteboardTestHooks || {}
const hooks = window.__whiteboardTestHooks
hooks.whiteboardConfigStore = useWhiteboardConfigStore

if (!hooks.blockInitialData || hooks.originalResolveInitialData) {
return
}

hooks.pendingInitialData = null
hooks.originalResolveInitialData = useWhiteboardConfigStore.getState().resolveInitialData
useWhiteboardConfigStore.setState({
resolveInitialData: (data: ExcalidrawInitialDataState) => {
hooks.pendingInitialData = data
},
})
hooks.restoreResolveInitialData = () => {
const original = hooks.originalResolveInitialData
if (!original) {
return
}
useWhiteboardConfigStore.setState({ resolveInitialData: original })
}
}

attachTestHooks()
Loading