|
1 | 1 | import type { AxiosResponse } from 'axios' |
2 | 2 | import { AxiosError } from 'axios' |
3 | 3 | import { beforeEach, describe, expect, it, vi } from 'vitest' |
4 | | -import { nextTick, ref } from 'vue' |
| 4 | +import { ref } from 'vue' |
5 | 5 |
|
6 | 6 | import { mergeCustomNodesI18n } from '@/i18n' |
7 | 7 | import { useSettingStore } from '@/platform/settings/settingStore' |
@@ -66,6 +66,16 @@ const mockDistributionTypes = vi.hoisted(() => ({ |
66 | 66 | })) |
67 | 67 | vi.mock('@/platform/distribution/types', () => mockDistributionTypes) |
68 | 68 |
|
| 69 | +const mockCaptureException = vi.hoisted(() => vi.fn()) |
| 70 | +vi.mock('@sentry/vue', () => ({ |
| 71 | + captureException: mockCaptureException |
| 72 | +})) |
| 73 | + |
| 74 | +const mockAddError = vi.hoisted(() => vi.fn()) |
| 75 | +vi.mock('@datadog/browser-rum', () => ({ |
| 76 | + datadogRum: { addError: mockAddError } |
| 77 | +})) |
| 78 | + |
69 | 79 | function requestFailure(status: number) { |
70 | 80 | const error = new AxiosError(`Request failed with status code ${status}`) |
71 | 81 | error.response = { status } as AxiosResponse |
@@ -131,31 +141,77 @@ describe('bootstrapStore', () => { |
131 | 141 | describe('cloud mode', () => { |
132 | 142 | beforeEach(() => { |
133 | 143 | mockDistributionTypes.isCloud = true |
| 144 | + mockCaptureException.mockReset() |
| 145 | + mockAddError.mockReset() |
134 | 146 | }) |
135 | 147 |
|
136 | | - it('waits for Firebase auth before loading stores', async () => { |
| 148 | + it('waits for Firebase init before loading stores, then proceeds regardless of auth state', async () => { |
137 | 149 | const store = useBootstrapStore() |
138 | 150 | const settingStore = useSettingStore() |
139 | 151 | const bootstrapPromise = store.startStoreBootstrap() |
140 | 152 |
|
141 | 153 | expect(store.isI18nReady).toBe(false) |
142 | 154 | expect(settingStore.isReady).toBe(false) |
143 | 155 |
|
144 | | - // Firebase initialized but user not yet authenticated |
| 156 | + // Firebase resolves with no user (signed-out) — bootstrap must unblock. |
| 157 | + // Previously it also waited for isAuthenticated, which made every |
| 158 | + // signed-out load wait 35s and fire a false Sentry timeout. |
145 | 159 | mockIsAuthInitialized.value = true |
146 | | - await nextTick() |
147 | | - |
148 | | - expect(store.isI18nReady).toBe(false) |
149 | | - expect(settingStore.isReady).toBe(false) |
150 | | - |
151 | | - // User authenticates (e.g. signs in on login page) |
152 | | - mockIsAuthAuthenticated.value = true |
153 | 160 | await bootstrapPromise |
154 | 161 |
|
155 | 162 | await vi.waitFor(() => { |
156 | 163 | expect(store.isI18nReady).toBe(true) |
157 | 164 | expect(settingStore.isReady).toBe(true) |
158 | 165 | }) |
159 | 166 | }) |
| 167 | + |
| 168 | + it('retries once and proceeds if Firebase init resolves during the backoff', async () => { |
| 169 | + vi.useFakeTimers() |
| 170 | + try { |
| 171 | + const store = useBootstrapStore() |
| 172 | + const settingStore = useSettingStore() |
| 173 | + const bootstrapPromise = store.startStoreBootstrap() |
| 174 | + |
| 175 | + // First wait times out with Firebase still not initialized. |
| 176 | + await vi.advanceTimersByTimeAsync(16_001) |
| 177 | + expect(settingStore.isReady).toBe(false) |
| 178 | + |
| 179 | + // Firebase resolves during the retry backoff. |
| 180 | + mockIsAuthInitialized.value = true |
| 181 | + await vi.advanceTimersByTimeAsync(3_001) |
| 182 | + await bootstrapPromise |
| 183 | + |
| 184 | + expect(settingStore.isReady).toBe(true) |
| 185 | + expect(mockCaptureException).not.toHaveBeenCalled() |
| 186 | + } finally { |
| 187 | + vi.useRealTimers() |
| 188 | + } |
| 189 | + }) |
| 190 | + |
| 191 | + it('gives up after a second timeout, reports it, and continues bootstrap unauthenticated', async () => { |
| 192 | + vi.useFakeTimers() |
| 193 | + try { |
| 194 | + const store = useBootstrapStore() |
| 195 | + const settingStore = useSettingStore() |
| 196 | + const bootstrapPromise = store.startStoreBootstrap() |
| 197 | + |
| 198 | + // Firebase never resolves through the initial wait, the backoff, or the retry. |
| 199 | + await vi.advanceTimersByTimeAsync(16_000 + 3_000 + 16_001) |
| 200 | + await bootstrapPromise |
| 201 | + |
| 202 | + expect(mockCaptureException).toHaveBeenCalledOnce() |
| 203 | + expect(mockCaptureException).toHaveBeenCalledWith(expect.any(Error), { |
| 204 | + tags: { error_type: 'bootstrap_auth_wait_timeout' } |
| 205 | + }) |
| 206 | + expect(mockAddError).toHaveBeenCalledOnce() |
| 207 | + expect(mockAddError).toHaveBeenCalledWith(expect.any(Error), { |
| 208 | + error_type: 'bootstrap_auth_wait_timeout' |
| 209 | + }) |
| 210 | + // Bootstrap must not stay stuck: stores load even when Firebase never fires. |
| 211 | + expect(settingStore.isReady).toBe(true) |
| 212 | + } finally { |
| 213 | + vi.useRealTimers() |
| 214 | + } |
| 215 | + }) |
160 | 216 | }) |
161 | 217 | }) |
0 commit comments