From d0245e3e16f94a108304a45c544aa0de30eb0405 Mon Sep 17 00:00:00 2001 From: Antti Kettunen Date: Wed, 20 May 2026 20:38:29 +0300 Subject: [PATCH] test(webui): share shell bridge test helper - add a reusable shell bridge factory for legacy shell-backed tests - trim route and bridge fixtures down to only the overrides they need --- webui/src/app/router.test.tsx | 58 ++++--------------- webui/src/platform/shell/bridge.test.ts | 44 +++++--------- .../src/routes/artist-detail/-route.test.tsx | 21 +------ webui/src/routes/issues/-route.test.tsx | 23 +------- webui/src/test/shell-bridge.ts | 24 ++++++++ 5 files changed, 53 insertions(+), 117 deletions(-) create mode 100644 webui/src/test/shell-bridge.ts diff --git a/webui/src/app/router.test.tsx b/webui/src/app/router.test.tsx index 95f90e97..f2d1933c 100644 --- a/webui/src/app/router.test.tsx +++ b/webui/src/app/router.test.tsx @@ -4,31 +4,26 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { SHELL_PROFILE_CONTEXT_CHANGED_EVENT, - type ShellBridge, type ShellProfileContext, type ShellPageId, } from '@/platform/shell/bridge'; +import { HttpResponse, http, server } from '@/test/msw'; +import { createShellBridge } from '@/test/shell-bridge'; import { createAppQueryClient } from './query-client'; import { AppRouterProvider, createAppRouter } from './router'; -function mockIssuesFetch() { - return vi.fn(async (input: RequestInfo | URL) => { - const url = input instanceof Request ? input.url : String(input); - - if (url.includes('/api/issues/counts')) { - return new Response( - JSON.stringify({ +describe('createAppRouter', () => { + beforeEach(() => { + server.use( + http.get('/api/issues/counts', () => + HttpResponse.json({ success: true, counts: { open: 2, in_progress: 1, resolved: 0, dismissed: 0, total: 3 }, }), - { status: 200, headers: { 'Content-Type': 'application/json' } }, - ); - } - - if (url.includes('/api/issues?')) { - return new Response( - JSON.stringify({ + ), + http.get('/api/issues', () => + HttpResponse.json({ success: true, total: 1, issues: [ @@ -44,37 +39,8 @@ function mockIssuesFetch() { }, ], }), - { status: 200, headers: { 'Content-Type': 'application/json' } }, - ); - } - - throw new Error(`Unexpected fetch request: ${url}`); - }); -} - -function createShellBridge(overrides: Partial = {}): ShellBridge { - return { - getCurrentProfileContext: vi.fn(() => ({ profileId: 1, isAdmin: false })), - isPageAllowed: vi.fn(() => true), - getProfileHomePage: vi.fn<() => ShellPageId>(() => 'discover'), - resolveLegacyPath: vi.fn<(pathname: string) => ShellPageId | null>(() => 'search'), - setActivePageChrome: vi.fn(), - activateLegacyPath: vi.fn(), - navigateToArtistDetail: vi.fn(), - cancelSimilarArtistsLoad: vi.fn(), - showReactHost: vi.fn(), - navigateToArtistDetail: vi.fn(), - playLibraryTrack: vi.fn(), - startStream: vi.fn(), - showLoadingOverlay: vi.fn(), - hideLoadingOverlay: vi.fn(), - ...overrides, - }; -} - -describe('createAppRouter', () => { - beforeEach(() => { - vi.stubGlobal('fetch', mockIssuesFetch()); + ), + ); }); afterEach(() => { diff --git a/webui/src/platform/shell/bridge.test.ts b/webui/src/platform/shell/bridge.test.ts index 29546e29..80b363c2 100644 --- a/webui/src/platform/shell/bridge.test.ts +++ b/webui/src/platform/shell/bridge.test.ts @@ -1,8 +1,14 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { createShellBridge } from '@/test/shell-bridge'; + import type { ShellProfileContext } from './bridge'; -import { SHELL_PROFILE_CONTEXT_CHANGED_EVENT, bindWindowWebRouter, waitForShellContext } from './bridge'; +import { + SHELL_PROFILE_CONTEXT_CHANGED_EVENT, + bindWindowWebRouter, + waitForShellContext, +} from './bridge'; describe('waitForShellContext', () => { beforeEach(() => { @@ -10,21 +16,7 @@ describe('waitForShellContext', () => { }); it('resolves immediately when the shell already has a profile', async () => { - window.SoulSyncWebShellBridge = { - getProfileHomePage: vi.fn(() => 'discover'), - isPageAllowed: vi.fn(() => true), - activateLegacyPath: vi.fn(), - getCurrentProfileContext: vi.fn(() => ({ profileId: 2, isAdmin: true })), - resolveLegacyPath: vi.fn(() => 'issues'), - setActivePageChrome: vi.fn(), - cancelSimilarArtistsLoad: vi.fn(), - showReactHost: vi.fn(), - navigateToArtistDetail: vi.fn(), - playLibraryTrack: vi.fn(), - startStream: vi.fn(), - showLoadingOverlay: vi.fn(), - hideLoadingOverlay: vi.fn(), - } as NonNullable; + window.SoulSyncWebShellBridge = createShellBridge(); await expect(waitForShellContext()).resolves.toEqual({ bridge: window.SoulSyncWebShellBridge, @@ -37,21 +29,9 @@ describe('waitForShellContext', () => { it('waits for the legacy shell to publish profile context', async () => { const getCurrentProfileContext = vi.fn<() => ShellProfileContext | null>(() => null); - window.SoulSyncWebShellBridge = { - getProfileHomePage: vi.fn(() => 'discover'), - isPageAllowed: vi.fn(() => true), - activateLegacyPath: vi.fn(), + window.SoulSyncWebShellBridge = createShellBridge({ getCurrentProfileContext, - resolveLegacyPath: vi.fn(() => 'issues'), - setActivePageChrome: vi.fn(), - cancelSimilarArtistsLoad: vi.fn(), - showReactHost: vi.fn(), - navigateToArtistDetail: vi.fn(), - playLibraryTrack: vi.fn(), - startStream: vi.fn(), - showLoadingOverlay: vi.fn(), - hideLoadingOverlay: vi.fn(), - } as NonNullable; + }); const contextPromise = waitForShellContext(); @@ -106,7 +86,9 @@ describe('bindWindowWebRouter', () => { bindWindowWebRouter({ navigate } as never); - await expect(window.SoulSyncWebRouter?.navigateToPage('artist-detail', {} as never)).resolves.toBe(false); + await expect( + window.SoulSyncWebRouter?.navigateToPage('artist-detail', {} as never), + ).resolves.toBe(false); expect(navigate).not.toHaveBeenCalled(); }); }); diff --git a/webui/src/routes/artist-detail/-route.test.tsx b/webui/src/routes/artist-detail/-route.test.tsx index 232ff944..c1426ba9 100644 --- a/webui/src/routes/artist-detail/-route.test.tsx +++ b/webui/src/routes/artist-detail/-route.test.tsx @@ -2,25 +2,9 @@ import { createMemoryHistory } from '@tanstack/react-router'; import { render, waitFor } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type { ShellBridge, ShellPageId } from '@/platform/shell/bridge'; - import { createAppQueryClient } from '@/app/query-client'; import { AppRouterProvider, createAppRouter } from '@/app/router'; - -function createShellBridge(overrides: Partial = {}): ShellBridge { - return { - getCurrentProfileContext: vi.fn(() => ({ profileId: 2, isAdmin: false })), - isPageAllowed: vi.fn(() => true), - getProfileHomePage: vi.fn<() => ShellPageId>(() => 'discover'), - resolveLegacyPath: vi.fn<(pathname: string) => ShellPageId | null>(() => 'artist-detail'), - setActivePageChrome: vi.fn(), - activateLegacyPath: vi.fn(), - navigateToArtistDetail: vi.fn(), - cancelSimilarArtistsLoad: vi.fn(), - showReactHost: vi.fn(), - ...overrides, - }; -} +import { createShellBridge } from '@/test/shell-bridge'; function renderArtistDetailRoute(initialEntries = ['/artist-detail/library/42']) { const queryClient = createAppQueryClient(); @@ -87,7 +71,8 @@ describe('artist-detail route', () => { ); }); - const cancelSimilarArtistsLoad = window.SoulSyncWebShellBridge?.cancelSimilarArtistsLoad as ReturnType; + const cancelSimilarArtistsLoad = window.SoulSyncWebShellBridge + ?.cancelSimilarArtistsLoad as ReturnType; cancelSimilarArtistsLoad.mockClear(); unmount(); diff --git a/webui/src/routes/issues/-route.test.tsx b/webui/src/routes/issues/-route.test.tsx index 023e24b8..7a0abdcd 100644 --- a/webui/src/routes/issues/-route.test.tsx +++ b/webui/src/routes/issues/-route.test.tsx @@ -2,10 +2,9 @@ import { createMemoryHistory } from '@tanstack/react-router'; import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type { ShellBridge, ShellPageId } from '@/platform/shell/bridge'; - import { createAppQueryClient } from '@/app/query-client'; import { AppRouterProvider, createAppRouter } from '@/app/router'; +import { createShellBridge } from '@/test/shell-bridge'; function createResponse(body: unknown, ok = true, status = 200) { return new Response(JSON.stringify(body), { @@ -14,26 +13,6 @@ function createResponse(body: unknown, ok = true, status = 200) { }); } -function createShellBridge(overrides: Partial = {}): ShellBridge { - return { - getCurrentProfileContext: vi.fn(() => ({ profileId: 2, isAdmin: true })), - isPageAllowed: vi.fn(() => true), - getProfileHomePage: vi.fn<() => ShellPageId>(() => 'discover'), - resolveLegacyPath: vi.fn<(pathname: string) => ShellPageId | null>(() => 'search'), - setActivePageChrome: vi.fn(), - activateLegacyPath: vi.fn(), - navigateToArtistDetail: vi.fn(), - cancelSimilarArtistsLoad: vi.fn(), - showReactHost: vi.fn(), - navigateToArtistDetail: vi.fn(), - playLibraryTrack: vi.fn(), - startStream: vi.fn(), - showLoadingOverlay: vi.fn(), - hideLoadingOverlay: vi.fn(), - ...overrides, - }; -} - function renderIssuesRoute(initialEntries = ['/issues']) { const queryClient = createAppQueryClient(); const history = createMemoryHistory({ initialEntries }); diff --git a/webui/src/test/shell-bridge.ts b/webui/src/test/shell-bridge.ts new file mode 100644 index 00000000..0c0cbe4b --- /dev/null +++ b/webui/src/test/shell-bridge.ts @@ -0,0 +1,24 @@ +import { vi } from 'vitest'; + +import type { ShellBridge, ShellPageId } from '@/platform/shell/bridge'; + +export function createShellBridge(overrides: Partial = {}): ShellBridge { + const bridge: ShellBridge = { + getCurrentProfileContext: vi.fn(() => ({ profileId: 2, isAdmin: true })), + isPageAllowed: vi.fn(() => true), + getProfileHomePage: vi.fn<() => ShellPageId>(() => 'discover'), + resolveLegacyPath: vi.fn<(pathname: string) => ShellPageId | null>(() => 'search'), + setActivePageChrome: vi.fn(), + activateLegacyPath: vi.fn(), + cancelSimilarArtistsLoad: vi.fn(), + navigateToArtistDetail: vi.fn(), + playLibraryTrack: vi.fn(), + showReactHost: vi.fn(), + startStream: vi.fn(), + showLoadingOverlay: vi.fn(), + hideLoadingOverlay: vi.fn(), + }; + + Object.assign(bridge, overrides); + return bridge; +}