diff --git a/packages/json-render-ui/src/action-bridge.ts b/packages/json-render-ui/src/action-bridge.ts index a31a19249..6cf76a4c3 100644 --- a/packages/json-render-ui/src/action-bridge.ts +++ b/packages/json-render-ui/src/action-bridge.ts @@ -15,7 +15,7 @@ export interface JsonRenderActionBridge { handlers: Record) => Promise> /** Reactive per-action loading flags. */ loading: Record - /** The most recent action failure, or `null`. */ + /** The most recent action failure, cleared when that action is retried. */ error: { value: JsonRenderActionError | null } } @@ -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) diff --git a/packages/json-render-ui/test/action-bridge.test.ts b/packages/json-render-ui/test/action-bridge.test.ts index c63beab2b..efad4e263 100644 --- a/packages/json-render-ui/test/action-bridge.test.ts +++ b/packages/json-render-ui/test/action-bridge.test.ts @@ -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() + 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() + 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 }) + }) })