Skip to content
Open
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
4 changes: 3 additions & 1 deletion packages/json-render-ui/src/action-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface JsonRenderActionBridge {
handlers: Record<string, (params?: Record<string, unknown>) => Promise<unknown>>
/** Reactive per-action loading flags. */
loading: Record<string, boolean>
/** The most recent action failure, or `null`. */
/** The most recent action failure, cleared when that action is retried. */
error: { value: JsonRenderActionError | null }
}

Expand Down Expand Up @@ -53,6 +53,8 @@ export function createActionBridge(
error.value = { action, error: err }
throw err
}
if (error.value?.action === action)
error.value = null
loading[action] = true
try {
return await rpc.call(action, params)
Expand Down
60 changes: 60 additions & 0 deletions packages/json-render-ui/test/action-bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,64 @@ describe('action bridge', () => {
expect(call).not.toHaveBeenCalled()
expect(bridge.error.value?.action).toBe('doThing')
})

it('clears an action error when retrying and keeps it clear after success', async () => {
expect.assertions(6)
const retry = Promise.withResolvers<string>()
const call = vi.fn(async () => 'ok')
.mockRejectedValueOnce(new Error('first attempt failed'))
.mockReturnValueOnce(retry.promise)
const bridge = createActionBridge({ call })

await expect(bridge.handlers.refreshData()).rejects.toThrow('first attempt failed')
const pending = bridge.handlers.refreshData()
expect(bridge.loading.refreshData).toBe(true)
expect(bridge.error.value).toBeNull()
retry.resolve('ok')
await expect(pending).resolves.toBe('ok')
expect(bridge.error.value).toBeNull()
expect(bridge.loading.refreshData).toBe(false)
})

it('reports the new error when a retry fails', async () => {
expect.assertions(3)
const retryError = new Error('retry failed')
const call = vi.fn()
.mockRejectedValueOnce(new Error('first attempt failed'))
.mockRejectedValueOnce(retryError)
const bridge = createActionBridge({ call })

await expect(bridge.handlers.refreshData()).rejects.toThrow('first attempt failed')
await expect(bridge.handlers.refreshData()).rejects.toThrow('retry failed')
expect(bridge.error.value).toEqual({ action: 'refreshData', error: retryError })
})

it('preserves an error when another action succeeds', async () => {
expect.assertions(3)
const error = new Error('refresh failed')
const call = vi.fn(async () => 'ok').mockRejectedValueOnce(error)
const bridge = createActionBridge({ call })

await expect(bridge.handlers.refreshData()).rejects.toThrow('refresh failed')
await expect(bridge.handlers.saveData()).resolves.toBe('ok')
expect(bridge.error.value).toEqual({ action: 'refreshData', error })
})

it('preserves a failure received while a retry is pending', async () => {
expect.assertions(4)
const retry = Promise.withResolvers<string>()
const error = new Error('save failed')
const call = vi.fn(async () => 'ok')
.mockRejectedValueOnce(new Error('refresh failed'))
.mockReturnValueOnce(retry.promise)
.mockRejectedValueOnce(error)
const bridge = createActionBridge({ call })

await expect(bridge.handlers.refreshData()).rejects.toThrow('refresh failed')
const pending = bridge.handlers.refreshData()
await expect(bridge.handlers.saveData()).rejects.toThrow('save failed')
retry.resolve('ok')
await expect(pending).resolves.toBe('ok')
expect(bridge.error.value).toEqual({ action: 'saveData', error })
})
})
Loading