diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 8c1d37aea2..959a3d1e04 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -40,6 +40,7 @@ Write the steps that test your changes. This helps reviewers check your work. - [ ] **Self-Review**: I reviewed my own code with care. - [ ] **Writing Style**: The PR title and description use Simplified Technical English (ASD-STE100): short sentences, plain words, active voice, one idea per sentence, no contractions. - [ ] **Testing**: New and/or updated tests have been added to cover my changes (if applicable). +- [ ] **Test Helpers** (test changes only): Shared test helpers (`src/test-utils/`, `@/utils/test-utils`) are used only for mechanical duplication; test intent and scenario-specific mocks remain visible. - [ ] **Visual Snapshot** (UI changes only): If a user would notice this change at a glance (layout, theme tokens, brand elements, empty/error states), I added or updated a `*.visual.tsx` snapshot in `webview-ui/`. See `webview-ui/AGENTS.md` → "When a UI change needs a snapshot". - [ ] **Documentation Impact**: I checked if my changes need documentation updates (see "Documentation Updates" section below). - [ ] **Contribution Guidelines**: I read and agree to the [Contributor Guidelines](/CONTRIBUTING.md). diff --git a/src/api/providers/__tests__/github-copilot.spec.ts b/src/api/providers/__tests__/github-copilot.spec.ts index d932e8a13e..9e3a60110e 100644 --- a/src/api/providers/__tests__/github-copilot.spec.ts +++ b/src/api/providers/__tests__/github-copilot.spec.ts @@ -119,7 +119,7 @@ const mockLanguageModelChat = { } describe("connectGitHubCopilot", () => { - beforeEach(() => vi.clearAllMocks()) + beforeEach(() => clearAllMocks()) it("authenticates through VS Code and selects only Copilot models", async () => { vi.mocked(vscode.authentication.getSession).mockResolvedValue({ @@ -413,7 +413,7 @@ describe("Copilot model capabilities", () => { } beforeEach(() => { - vi.clearAllMocks() + clearAllMocks() mockLanguageModelChat.countTokens.mockResolvedValue(10) mockLanguageModelChat.sendRequest.mockImplementation(async () => ({ stream: (async function* () { @@ -1322,7 +1322,7 @@ describe("getGitHubCopilotModels", () => { ...overrides, }) - beforeEach(() => vi.clearAllMocks()) + beforeEach(() => clearAllMocks()) it("describes each model by identity and by the capabilities the host reports", async () => { vi.mocked(vscode.lm.selectChatModels).mockResolvedValue([ diff --git a/src/api/providers/__tests__/io-intelligence.spec.ts b/src/api/providers/__tests__/io-intelligence.spec.ts index 28ce7010b7..084dbc64b3 100644 --- a/src/api/providers/__tests__/io-intelligence.spec.ts +++ b/src/api/providers/__tests__/io-intelligence.spec.ts @@ -9,6 +9,7 @@ import { ioIntelligenceDefaultModelId, providerIdentifiers } from "@roo-code/typ import { buildApiHandler } from "../../index" import { asyncStreamFrom, collectStream } from "../../../test-utils/stream" +import { clearAllMocks } from "../../../test-utils/reset" import { IOIntelligenceHandler } from "../io-intelligence" import { getModels } from "../fetchers/modelCache" @@ -42,7 +43,7 @@ const loggedText = (spy: { mock: { calls: unknown[][] } }) => describe("IOIntelligenceHandler", () => { beforeEach(() => { - vi.clearAllMocks() + clearAllMocks() vi.mocked(getModels).mockResolvedValue({ "meta-llama/Llama-3.3-70B-Instruct": { maxTokens: 8192, diff --git a/src/api/providers/__tests__/nanogpt.spec.ts b/src/api/providers/__tests__/nanogpt.spec.ts index 0a13f779c9..b02fcf843a 100644 --- a/src/api/providers/__tests__/nanogpt.spec.ts +++ b/src/api/providers/__tests__/nanogpt.spec.ts @@ -9,6 +9,7 @@ import { nanoGptDefaultModelId, providerIdentifiers } from "@roo-code/types" import { buildApiHandler } from "../../index" import { asyncStreamFrom, collectStream } from "../../../test-utils/stream" +import { clearAllMocks } from "../../../test-utils/reset" import { createReadFileTool } from "../../../core/prompts/tools/native-tools/read_file" import { NanoGptHandler } from "../nanogpt" import { getModels } from "../fetchers/modelCache" @@ -45,7 +46,7 @@ const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "H describe("NanoGptHandler", () => { beforeEach(() => { - vi.clearAllMocks() + clearAllMocks() vi.mocked(getModels).mockResolvedValue({ "model:thinking": { maxTokens: 128000, diff --git a/src/api/providers/__tests__/vscode-lm.spec.ts b/src/api/providers/__tests__/vscode-lm.spec.ts index 309155c5f1..ea3690810e 100644 --- a/src/api/providers/__tests__/vscode-lm.spec.ts +++ b/src/api/providers/__tests__/vscode-lm.spec.ts @@ -6676,7 +6676,7 @@ describe("getVsCodeLmModels", () => { ...overrides, }) - beforeEach(() => vi.clearAllMocks()) + beforeEach(() => clearAllMocks()) it("returns serializable model metadata with host-reported image support", async () => { const model = liveModel({ capabilities: { supportsImageToText: true } }) diff --git a/src/core/config/__tests__/CustomModesManager.spec.ts b/src/core/config/__tests__/CustomModesManager.spec.ts index 71ed60ee40..eddaec5621 100644 --- a/src/core/config/__tests__/CustomModesManager.spec.ts +++ b/src/core/config/__tests__/CustomModesManager.spec.ts @@ -198,7 +198,7 @@ describe("CustomModesManager", () => { const firstResult = await manager.getCustomModes() // Reset mock to verify it's not called again - vi.clearAllMocks() + clearAllMocks() // Setup mocks again for second call ;(fileExistsAtPath as Mock).mockImplementation(async (path: string) => { @@ -236,7 +236,7 @@ describe("CustomModesManager", () => { await manager.getCustomModes() // Reset mocks to track new calls - vi.clearAllMocks() + clearAllMocks() // Update a mode const updatedMode: ModeConfig = { @@ -260,7 +260,7 @@ describe("CustomModesManager", () => { await manager.updateCustomMode("mode1", updatedMode) // Reset mocks again - vi.clearAllMocks() + clearAllMocks() // Next call should read from file again (cache invalidated) await manager.getCustomModes() @@ -282,7 +282,7 @@ describe("CustomModesManager", () => { await manager.getCustomModes() // Reset mocks to track new calls - vi.clearAllMocks() + clearAllMocks() // Delete a mode await manager.deleteCustomMode("mode1") @@ -296,7 +296,7 @@ describe("CustomModesManager", () => { }) // Reset mocks again - vi.clearAllMocks() + clearAllMocks() // Next call should read from file again (cache invalidated) await manager.getCustomModes() @@ -321,7 +321,7 @@ describe("CustomModesManager", () => { await manager.getCustomModes() // Reset mocks to track new calls - vi.clearAllMocks() + clearAllMocks() // Setup for update const updatedMode: ModeConfig = { @@ -346,7 +346,7 @@ describe("CustomModesManager", () => { await manager.updateCustomMode("mode1", updatedMode) // Reset mocks again - vi.clearAllMocks() + clearAllMocks() // Setup mocks again ;(fileExistsAtPath as Mock).mockImplementation(async (path: string) => { @@ -387,7 +387,7 @@ describe("CustomModesManager", () => { await manager.getCustomModes() // Reset mock to verify it's not called again - vi.clearAllMocks() + clearAllMocks() // Setup mocks again for second call ;(fileExistsAtPath as Mock).mockImplementation(async (path: string) => { @@ -408,7 +408,7 @@ describe("CustomModesManager", () => { currentTime += 11000 // Reset mocks again - vi.clearAllMocks() + clearAllMocks() // Setup mocks again for third call ;(fileExistsAtPath as Mock).mockImplementation(async (path: string) => { diff --git a/src/eslint.config.mjs b/src/eslint.config.mjs index f0f2ce4ce2..d5c4d73c1f 100644 --- a/src/eslint.config.mjs +++ b/src/eslint.config.mjs @@ -27,6 +27,15 @@ const testPathTraversalSelectors = [ }, ] +// Test-utils rollout guardrail: lanes converted to the shared reset helper +// stay converted. Use clearAllMocks from test-utils/reset. +const testResetHelperSelector = { + selector: + "CallExpression[callee.object.name='vi'][callee.property.name='clearAllMocks'], CallExpression[callee.object.name='vitest'][callee.property.name='clearAllMocks']", + message: + "Use the shared clearAllMocks() helper from src/test-utils/reset instead of calling vi.clearAllMocks() directly.", +} + /** @type {import("eslint").Linter.Config} */ export default [ ...config, @@ -85,9 +94,6 @@ export default [ "@typescript-eslint/no-floating-promises": "error", }, }, - { - ignores: [".stryker-tmp", "webview-ui", "out"], - }, { // Tests inside __tests__/ directories must not traverse 3+ levels above // the test file. That escapes the src/ package root and makes Turbo @@ -112,4 +118,34 @@ export default [ "no-restricted-syntax": ["error", ...testPathTraversalSelectors, ...apiProviderTestSelectors], }, }, + { + // Test-utils rollout guardrail: lanes converted to the shared reset + // helpers stay converted. Use clearAllMocks from test-utils/reset. + // no-restricted-syntax replaces earlier blocks for the same files, so + // repeat the traversal selectors here. + files: [ + "core/config/__tests__/**/*.ts", + "services/code-index/**/__tests__/**/*.ts", + "integrations/terminal/**/__tests__/**/*.ts", + ], + rules: { + "no-restricted-syntax": ["error", ...testPathTraversalSelectors, testResetHelperSelector], + }, + }, + { + // Same guardrail for the API provider lane; this block also replaces the + // API provider hygiene selectors, so repeat those too. + files: ["api/providers/__tests__/**/*.ts"], + rules: { + "no-restricted-syntax": [ + "error", + ...testPathTraversalSelectors, + ...apiProviderTestSelectors, + testResetHelperSelector, + ], + }, + }, + { + ignores: [".stryker-tmp", "webview-ui", "out"], + }, ] diff --git a/src/services/code-index/__tests__/code-index-manager-registry.spec.ts b/src/services/code-index/__tests__/code-index-manager-registry.spec.ts index 0d5012a641..59e481dc6d 100644 --- a/src/services/code-index/__tests__/code-index-manager-registry.spec.ts +++ b/src/services/code-index/__tests__/code-index-manager-registry.spec.ts @@ -1,5 +1,6 @@ import * as vscode from "vscode" import { makeExtensionContext, makeTextDocument, makeTextEditor, makeUri } from "../../../test-utils/vscode" +import { clearAllMocks } from "../../../test-utils/reset" import { CodeIndexManager } from "../manager" import { CodeIndexManagerRegistry } from "../code-index-manager-registry" import { CodeIndexWorkspaceScope } from "../code-index-workspace-scope" @@ -25,7 +26,7 @@ describe("CodeIndexManagerRegistry", () => { let second: vscode.WorkspaceFolder beforeEach(() => { - vi.clearAllMocks() + clearAllMocks() context = makeExtensionContext() first = { uri: makeUri("/first"), name: "first", index: 0 } second = { uri: makeUri("/second"), name: "second", index: 1 } diff --git a/src/services/code-index/__tests__/code-index-scope.spec.ts b/src/services/code-index/__tests__/code-index-scope.spec.ts index 2a5692c817..765348aa11 100644 --- a/src/services/code-index/__tests__/code-index-scope.spec.ts +++ b/src/services/code-index/__tests__/code-index-scope.spec.ts @@ -1,4 +1,5 @@ import { makeExtensionContext } from "../../../test-utils/vscode" +import { clearAllMocks } from "../../../test-utils/reset" import { CodeIndexScope } from "../code-index-scope" import { CodeIndexStatusManager } from "../code-index-status-manager" import { CodeIndexManagerRegistry } from "../code-index-manager-registry" @@ -9,7 +10,7 @@ vi.mock("../code-index-manager-registry", () => ({ })) describe("CodeIndexScope", () => { - beforeEach(() => vi.clearAllMocks()) + beforeEach(() => clearAllMocks()) it("creates, initializes and disposes its status manager", () => { const context = makeExtensionContext() diff --git a/src/services/code-index/__tests__/code-index-status-manager.spec.ts b/src/services/code-index/__tests__/code-index-status-manager.spec.ts index f640836b3a..6d003194a6 100644 --- a/src/services/code-index/__tests__/code-index-status-manager.spec.ts +++ b/src/services/code-index/__tests__/code-index-status-manager.spec.ts @@ -6,6 +6,7 @@ import { makeExtensionContext, makeWorkspaceConfiguration, } from "../../../test-utils/vscode" +import { clearAllMocks } from "../../../test-utils/reset" import { CodeIndexManager } from "../manager" import { CodeIndexStateManager } from "../state-manager" import { CodeIndexStatusManager, type CodeIndexStatus } from "../code-index-status-manager" @@ -68,7 +69,7 @@ describe("CodeIndexStatusManager", () => { let disposeWorkspaceFolders: ReturnType void>> beforeEach(() => { - vi.clearAllMocks() + clearAllMocks() vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(makeWorkspaceConfiguration()) Object.defineProperty(vscode.window, "activeTextEditor", { configurable: true, value: undefined }) Object.defineProperty(vscode.workspace, "workspaceFolders", { diff --git a/src/services/code-index/__tests__/code-index-workspace-scope.spec.ts b/src/services/code-index/__tests__/code-index-workspace-scope.spec.ts index 417d7284c1..c29a832e93 100644 --- a/src/services/code-index/__tests__/code-index-workspace-scope.spec.ts +++ b/src/services/code-index/__tests__/code-index-workspace-scope.spec.ts @@ -1,4 +1,5 @@ import { makeExtensionContext, makeUri } from "../../../test-utils/vscode" +import { clearAllMocks } from "../../../test-utils/reset" import { CodeIndexManager } from "../manager" import { CodeIndexWorkspaceScope } from "../code-index-workspace-scope" import { CodeIndexStateManager } from "../state-manager" @@ -13,7 +14,7 @@ vi.mock("../manager", () => ({ })) describe("CodeIndexWorkspaceScope", () => { - beforeEach(() => vi.clearAllMocks()) + beforeEach(() => clearAllMocks()) it("owns a guarded workspace indexing manager and recreates it after disposal", () => { const scope = new CodeIndexWorkspaceScope("/workspace", makeUri("/workspace"), makeExtensionContext()) diff --git a/src/services/code-index/embedders/__tests__/embedder-validation-manager.spec.ts b/src/services/code-index/embedders/__tests__/embedder-validation-manager.spec.ts index 6ff11f8fa7..7128695e8f 100644 --- a/src/services/code-index/embedders/__tests__/embedder-validation-manager.spec.ts +++ b/src/services/code-index/embedders/__tests__/embedder-validation-manager.spec.ts @@ -1,6 +1,7 @@ import { TelemetryService } from "@roo-code/telemetry" import { TelemetryEventName } from "@roo-code/types" import { t } from "../../../../i18n" +import { clearAllMocks } from "../../../../test-utils/reset" import embeddings from "../../../../i18n/locales/en/embeddings.json" import { OpenAiEmbedder } from "../openai" import { EmbedderValidationManager } from "../embedder-validation-manager" @@ -14,7 +15,7 @@ vi.mock("@roo-code/telemetry", () => ({ describe("EmbedderValidationManager", () => { const manager = new EmbedderValidationManager() - beforeEach(() => vi.clearAllMocks()) + beforeEach(() => clearAllMocks()) it.each([{ valid: true }, { valid: false, error: "Invalid credentials" }])( "preserves the validation result %j without reporting an exception", diff --git a/src/services/code-index/processors/__tests__/processor-factories.spec.ts b/src/services/code-index/processors/__tests__/processor-factories.spec.ts index 4af92c14cd..1b222fb997 100644 --- a/src/services/code-index/processors/__tests__/processor-factories.spec.ts +++ b/src/services/code-index/processors/__tests__/processor-factories.spec.ts @@ -1,6 +1,7 @@ import * as vscode from "vscode" import ignore from "ignore" import { makeExtensionContext } from "../../../../test-utils/vscode" +import { clearAllMocks } from "../../../../test-utils/reset" import { Package } from "../../../../shared/package" import { CacheManager } from "../../cache-manager" import { BATCH_SEGMENT_THRESHOLD } from "../../constants" @@ -30,7 +31,7 @@ vi.mock("../scanner") vi.mock("../file-watcher") describe("processor factories", () => { - beforeEach(() => vi.clearAllMocks()) + beforeEach(() => clearAllMocks()) afterEach(() => vi.restoreAllMocks()) function dependencies() { diff --git a/src/services/code-index/vector-store/__tests__/vector-store-factory.spec.ts b/src/services/code-index/vector-store/__tests__/vector-store-factory.spec.ts index d9f9ae4b50..c8d4058602 100644 --- a/src/services/code-index/vector-store/__tests__/vector-store-factory.spec.ts +++ b/src/services/code-index/vector-store/__tests__/vector-store-factory.spec.ts @@ -1,5 +1,6 @@ import { providerIdentifiers } from "@roo-code/types/provider-identifiers" import { getDefaultModelId, getModelDimension } from "../../../../shared/embeddingModels" +import { clearAllMocks } from "../../../../test-utils/reset" import type { CodeIndexConfig } from "../../interfaces/config" import { QdrantVectorStore } from "../qdrant-client" import { VectorStoreFactory } from "../vector-store-factory" @@ -15,7 +16,7 @@ describe("VectorStoreFactory", () => { let config: CodeIndexConfig beforeEach(() => { - vi.clearAllMocks() + clearAllMocks() vi.mocked(getDefaultModelId).mockReturnValue("default-model") vi.mocked(getModelDimension).mockReturnValue(undefined) factory = new VectorStoreFactory() diff --git a/webview-ui/eslint.config.mjs b/webview-ui/eslint.config.mjs index 11a49e9b41..e7b18babca 100644 --- a/webview-ui/eslint.config.mjs +++ b/webview-ui/eslint.config.mjs @@ -46,4 +46,32 @@ export default [ "no-undef": "off", }, }, + { + // Test-utils rollout guardrail: component specs render through the shared + // renderWithExtensionState helper instead of hand-wrapping providers. + // Hook specs (including .tsx ones) keep local renderHook wrappers since + // they pass a specific QueryClient instance to the hook, and Playwright + // visual fixtures mount real providers inside stories rather than Vitest + // renders. + files: ["src/components/**/__tests__/**/*.tsx"], + ignores: [ + "src/components/**/hooks/**/__tests__/**/*.tsx", + "src/components/**/__tests__/**/*.visual.fixture.tsx", + ], + rules: { + "no-restricted-imports": [ + "error", + { + paths: [ + { + name: "@tanstack/react-query", + importNames: ["QueryClientProvider"], + message: + "Use renderWithExtensionState from @/utils/test-utils instead of hand-wrapping QueryClientProvider in component specs.", + }, + ], + }, + ], + }, + }, ] diff --git a/webview-ui/src/components/settings/providers/__tests__/LiteLLM.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/LiteLLM.spec.tsx index c6cd779658..d61176c2fa 100644 --- a/webview-ui/src/components/settings/providers/__tests__/LiteLLM.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/LiteLLM.spec.tsx @@ -1,5 +1,4 @@ -import { QueryClient, QueryClientProvider } from "@tanstack/react-query" -import { act, fireEvent, render, screen, waitFor } from "@testing-library/react" +import { act, fireEvent, screen, waitFor } from "@testing-library/react" import { allRouterModelsProvider, @@ -8,6 +7,8 @@ import { type ProviderSettings, } from "@roo-code/types" +import { createTestQueryClient, renderWithExtensionState } from "@src/utils/test-utils" + import { LiteLLM } from "../LiteLLM" const { postMessageMock, mockUseExtensionState } = vi.hoisted(() => ({ @@ -19,7 +20,8 @@ vi.mock("@src/utils/vscode", () => ({ vscode: { postMessage: postMessageMock }, })) -vi.mock("@src/context/ExtensionStateContext", () => ({ +vi.mock("@src/context/ExtensionStateContext", async (importOriginal) => ({ + ...(await importOriginal()), useExtensionState: mockUseExtensionState, })) @@ -53,7 +55,7 @@ describe("LiteLLM", () => { }) it("invalidates both LiteLLM caches after a successful model refresh", async () => { - const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + const queryClient = createTestQueryClient() const invalidateQueries = vi.spyOn(queryClient, "invalidateQueries") const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.litellm, @@ -61,14 +63,13 @@ describe("LiteLLM", () => { litellmBaseUrl: "http://localhost:4000", } - render( - - - , + renderWithExtensionState( + , + { queryClient }, ) fireEvent.click(screen.getByTestId("refresh-button")) @@ -93,15 +94,14 @@ describe("LiteLLM", () => { }) it("recognizes failed refresh responses for the canonical LiteLLM provider", () => { - const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) - render( - - - , + const queryClient = createTestQueryClient() + renderWithExtensionState( + , + { queryClient }, ) act(() => { @@ -121,21 +121,20 @@ describe("LiteLLM", () => { }) it("does not invalidate caches when a LiteLLM error and router models arrive in the same tick", () => { - const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + const queryClient = createTestQueryClient() const invalidateQueries = vi.spyOn(queryClient, "invalidateQueries") - render( - - - , + renderWithExtensionState( + , + { queryClient }, ) fireEvent.click(screen.getByTestId("refresh-button")) @@ -158,19 +157,18 @@ describe("LiteLLM", () => { }) it("ignores failed refresh responses for another provider", () => { - const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) - render( - - - , + const queryClient = createTestQueryClient() + renderWithExtensionState( + , + { queryClient }, ) fireEvent.click(screen.getByTestId("refresh-button")) @@ -205,16 +203,15 @@ describe("LiteLLM", () => { }, }, }) - const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) - - render( - - - , + const queryClient = createTestQueryClient() + + renderWithExtensionState( + , + { queryClient }, ) expect(screen.queryByText("settings:providers.enablePromptCaching")).not.toBeInTheDocument() @@ -231,19 +228,18 @@ describe("LiteLLM", () => { }, }, }) - const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) - - render( - - - , + const queryClient = createTestQueryClient() + + renderWithExtensionState( + , + { queryClient }, ) expect(screen.getByText("settings:providers.enablePromptCaching")).toBeInTheDocument() diff --git a/webview-ui/src/components/settings/providers/__tests__/Moonshot.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Moonshot.spec.tsx index 5c5d1b41b9..867daa82f9 100644 --- a/webview-ui/src/components/settings/providers/__tests__/Moonshot.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/Moonshot.spec.tsx @@ -1,8 +1,15 @@ // npx vitest src/components/settings/providers/__tests__/Moonshot.spec.tsx import React from "react" -import { render, screen, fireEvent, waitFor, act } from "@/utils/test-utils" -import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import { + render, + screen, + fireEvent, + waitFor, + act, + createTestQueryClient, + renderWithExtensionState, +} from "@/utils/test-utils" import { allRouterModelsProvider, providerIdentifiers, type ProviderSettings } from "@roo-code/types" import { Moonshot } from "../Moonshot" @@ -73,7 +80,8 @@ vi.mock("@src/utils/vscode", () => { }) // Mock useExtensionState -vi.mock("@src/context/ExtensionStateContext", () => ({ +vi.mock("@src/context/ExtensionStateContext", async (importOriginal) => ({ + ...(await importOriginal()), useExtensionState: vi.fn(), })) @@ -225,16 +233,15 @@ describe("Moonshot Component", () => { }) it("invalidates only the Moonshot and shared router-model caches after a successful refresh", async () => { - const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + const queryClient = createTestQueryClient() const invalidateQueries = vi.spyOn(queryClient, "invalidateQueries") - render( - - - , + renderWithExtensionState( + , + { queryClient }, ) const refreshButton = screen diff --git a/webview-ui/src/components/settings/providers/__tests__/Poe.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Poe.spec.tsx index 34e5d5c2d8..5ac753e008 100644 --- a/webview-ui/src/components/settings/providers/__tests__/Poe.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/Poe.spec.tsx @@ -1,5 +1,4 @@ -import { QueryClient, QueryClientProvider } from "@tanstack/react-query" -import { act, fireEvent, render, screen, waitFor } from "@testing-library/react" +import { act, fireEvent, screen, waitFor } from "@testing-library/react" import { allRouterModelsProvider, @@ -8,6 +7,8 @@ import { type ProviderSettings, } from "@roo-code/types" +import { createTestQueryClient, renderWithExtensionState } from "@src/utils/test-utils" + import { Poe } from "../Poe" const { mockUseExtensionState, postMessageMock } = vi.hoisted(() => ({ @@ -19,7 +20,8 @@ vi.mock("@src/utils/vscode", () => ({ vscode: { postMessage: postMessageMock }, })) -vi.mock("@src/context/ExtensionStateContext", () => ({ +vi.mock("@src/context/ExtensionStateContext", async (importOriginal) => ({ + ...(await importOriginal()), useExtensionState: mockUseExtensionState, })) @@ -55,19 +57,14 @@ describe("Poe", () => { const organizationAllowList: OrganizationAllowList = { allowAll: true, providers: {} } const setApiConfigurationField = vi.fn() - const renderComponent = (apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.poe }) => { - const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) - - return render( - - - , + const renderComponent = (apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.poe }) => + renderWithExtensionState( + , ) - } beforeEach(() => { vi.clearAllMocks() @@ -114,17 +111,16 @@ describe("Poe", () => { }) it("invalidates only the Poe and shared router-model caches after a successful refresh", async () => { - const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + const queryClient = createTestQueryClient() const invalidateQueries = vi.spyOn(queryClient, "invalidateQueries") - render( - - - , + renderWithExtensionState( + , + { queryClient }, ) fireEvent.click(screen.getByTestId("refresh-button")) diff --git a/webview-ui/src/components/settings/providers/__tests__/ProviderRouting.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/ProviderRouting.spec.tsx index d165ead136..84c202ac7d 100644 --- a/webview-ui/src/components/settings/providers/__tests__/ProviderRouting.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/ProviderRouting.spec.tsx @@ -1,6 +1,5 @@ -import { act, fireEvent, render, screen, waitFor } from "@testing-library/react" +import { act, fireEvent, screen, waitFor } from "@testing-library/react" import userEvent from "@testing-library/user-event" -import { QueryClientProvider } from "@tanstack/react-query" import { providerIdentifiers, @@ -10,7 +9,7 @@ import { } from "@roo-code/types" import { vscode } from "@src/utils/vscode" -import { createTestQueryClient } from "@src/utils/test-utils" +import { render } from "@src/utils/test-utils" import { Unbound } from "../Unbound" import { VercelAiGateway } from "../VercelAiGateway" @@ -48,8 +47,7 @@ vi.mock("../../ModelPicker", () => ({ ModelPicker: modelPickerMock })) vi.mock("@src/components/common/VSCodeButtonLink", () => ({ VSCodeButtonLink: () => null })) // The Copilot model list lives in the shared query cache, so every render needs a client. -const renderWithQuery = (ui: React.ReactElement) => - render({ui}) +const renderWithQuery = (ui: React.ReactElement) => render(ui) describe("provider model routing", () => { const organizationAllowList: OrganizationAllowList = { allowAll: true, providers: {} } diff --git a/webview-ui/src/components/ui/hooks/__tests__/useOpenRouterModelProviders.spec.tsx b/webview-ui/src/components/ui/hooks/__tests__/useOpenRouterModelProviders.spec.tsx index 7b3dda7e24..dcf0b5829d 100644 --- a/webview-ui/src/components/ui/hooks/__tests__/useOpenRouterModelProviders.spec.tsx +++ b/webview-ui/src/components/ui/hooks/__tests__/useOpenRouterModelProviders.spec.tsx @@ -1,17 +1,17 @@ import React from "react" import axios from "axios" -import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import { QueryClientProvider } from "@tanstack/react-query" import { renderHook, waitFor } from "@testing-library/react" import { z } from "zod" +import { createTestQueryClient } from "@src/utils/test-utils" + import { useOpenRouterModelProviders } from "../useOpenRouterModelProviders" vi.mock("axios") const wrapper = ({ children }: React.PropsWithChildren) => ( - - {children} - + {children} ) describe("useOpenRouterModelProviders", () => { diff --git a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts index 62af5fb6fa..4955c3c031 100644 --- a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts +++ b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts @@ -1,7 +1,9 @@ // npx vitest src/components/ui/hooks/__tests__/useSelectedModel.spec.ts import React from "react" -import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import { QueryClientProvider } from "@tanstack/react-query" + +import { createTestQueryClient } from "@/utils/test-utils" import { renderHook } from "@testing-library/react" import type { Mock } from "vitest" @@ -101,13 +103,7 @@ const createLocalModelsResult = ( ): ReturnType => createQueryResult(data, options) as ReturnType const createWrapper = () => { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, - }) + const queryClient = createTestQueryClient() return ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children) } diff --git a/webview-ui/src/components/ui/hooks/__tests__/useZooGatewayRouterModelsSync.spec.ts b/webview-ui/src/components/ui/hooks/__tests__/useZooGatewayRouterModelsSync.spec.ts index a4675d25f7..c1d2a3aaae 100644 --- a/webview-ui/src/components/ui/hooks/__tests__/useZooGatewayRouterModelsSync.spec.ts +++ b/webview-ui/src/components/ui/hooks/__tests__/useZooGatewayRouterModelsSync.spec.ts @@ -2,6 +2,8 @@ import React from "react" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" + +import { createTestQueryClient } from "@/utils/test-utils" import { renderHook, waitFor } from "@testing-library/react" import type { Mock } from "vitest" @@ -40,7 +42,7 @@ const renderSyncHook = (queryClient: QueryClient) => React.createElement(QueryClientProvider, { client: queryClient }, children), }) -const makeQueryClient = () => new QueryClient({ defaultOptions: { queries: { retry: false } } }) +const makeQueryClient = () => createTestQueryClient() beforeEach(() => { vi.clearAllMocks()