diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7960d608eb..fd0984f097 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -38,6 +38,7 @@ Detail the steps to test your changes. This helps reviewers verify your work. - [ ] **Scope**: My changes are focused on the linked issue (one major feature/fix per PR). - [ ] **Self-Review**: I have performed a thorough self-review of my code. - [ ] **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've 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 have considered if my changes require documentation updates (see "Documentation Updates" section below). - [ ] **Contribution Guidelines**: I have read and agree to the [Contributor Guidelines](/CONTRIBUTING.md). 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 65965eb8d5..81ddf97fdd 100644 --- a/src/eslint.config.mjs +++ b/src/eslint.config.mjs @@ -45,6 +45,27 @@ export default [ "@typescript-eslint/no-floating-promises": "error", }, }, + { + // Test-utils rollout guardrail: lanes converted to the shared reset + // helpers stay converted. Use clearAllMocks from test-utils/reset. + files: [ + "api/providers/__tests__/**/*.ts", + "core/config/__tests__/**/*.ts", + "services/code-index/**/__tests__/**/*.ts", + "integrations/terminal/**/__tests__/**/*.ts", + ], + rules: { + "no-restricted-syntax": [ + "error", + { + 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.", + }, + ], + }, + }, { ignores: ["webview-ui", "out"], }, diff --git a/webview-ui/eslint.config.mjs b/webview-ui/eslint.config.mjs index db76f49211..043356e20c 100644 --- a/webview-ui/eslint.config.mjs +++ b/webview-ui/eslint.config.mjs @@ -43,4 +43,26 @@ export default [ "no-undef": "off", }, }, + { + // Test-utils rollout guardrail: component specs render through the shared + // renderWithExtensionState helper instead of hand-wrapping providers. + // Scoped to .tsx component specs; .ts hook specs keep local renderHook + // wrappers since they pass a specific QueryClient instance to the hook. + files: ["src/components/**/__tests__/**/*.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/ui/hooks/__tests__/useSelectedModel.spec.ts b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts index 5fca23ba8e..9e30ad9132 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" @@ -38,13 +40,7 @@ const mockUseRouterModels = useRouterModels as Mock const mockUseOpenRouterModelProviders = useOpenRouterModelProviders as Mock 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()