From 2ddc52c0b04954bf3e4baac716a5a0162f994ef0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Apr 2026 20:51:00 +0100 Subject: [PATCH] test: fix root test and typecheck gates --- convex/search.test.ts | 14 ++-- src/__tests__/skill-detail-page.test.tsx | 29 ++++---- tsconfig.json | 4 + vitest.config.ts | 25 +++++++ vitest.setup.ts | 95 +++++++++++++++++++++++- 5 files changed, 146 insertions(+), 21 deletions(-) diff --git a/convex/search.test.ts b/convex/search.test.ts index a8c0ac8b..938f0bb1 100644 --- a/convex/search.test.ts +++ b/convex/search.test.ts @@ -17,14 +17,16 @@ vi.mock("./lib/badges", () => ({ Boolean(skill.badges?.highlighted), })); -type WrappedHandler = { - _handler: ( - ctx: unknown, - args: unknown, - ) => Promise>; +type WrappedHandler = { + _handler: (ctx: unknown, args: unknown) => Promise>; }; -const searchSkillsHandler = (searchSkills as unknown as WrappedHandler)._handler; +const searchSkillsHandler = ( + searchSkills as unknown as WrappedHandler<{ + skill: { slug: string; _id: string }; + score: number; + }> +)._handler; const lexicalFallbackSkillsHandler = (lexicalFallbackSkills as unknown as WrappedHandler)._handler; const hydrateResultsHandler = ( hydrateResults as unknown as { diff --git a/src/__tests__/skill-detail-page.test.tsx b/src/__tests__/skill-detail-page.test.tsx index 404e045f..c9216d9e 100644 --- a/src/__tests__/skill-detail-page.test.tsx +++ b/src/__tests__/skill-detail-page.test.tsx @@ -8,7 +8,6 @@ const useAuthStatusMock = vi.fn(); process.env.VITE_CONVEX_URL = process.env.VITE_CONVEX_URL ?? "https://example.convex.cloud"; - vi.mock("../components/UserBadge", () => ({ UserBadge: () => null, })); @@ -229,10 +228,12 @@ describe("SkillDetailPage", () => { ); expect(screen.getByRole("heading", { name: "CLI Commands" })).toBeTruthy(); - expect(screen.getByText("openclaw skills install steipete/weather")).toBeTruthy(); + expect(screen.getByText("openclaw skills install weather")).toBeTruthy(); expect(screen.getByText("npx clawhub@latest install weather")).toBeTruthy(); expect(screen.getByText(/After install, inspect the skill metadata/i)).toBeTruthy(); - expect(installHeading.compareDocumentPosition(scanDisclaimer) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + expect( + installHeading.compareDocumentPosition(scanDisclaimer) & Node.DOCUMENT_POSITION_FOLLOWING, + ).toBeTruthy(); }); it("does not refetch readme when SSR data already matches the latest version", async () => { @@ -324,17 +325,17 @@ describe("SkillDetailPage", () => { useQueryMock.mockImplementation((_fn: unknown, args: unknown) => { if (args === "skip") return undefined; if (args && typeof args === "object" && "skillId" in args) return []; - return { - skill: { - _id: "skills:1", - slug: "weather", - displayName: "Weather", - summary: "Get current weather.", - ownerUserId: "users:1", - ownerPublisherId: "publishers:steipete", - tags: {}, - stats: { stars: 0, downloads: 0 }, - }, + return { + skill: { + _id: "skills:1", + slug: "weather", + displayName: "Weather", + summary: "Get current weather.", + ownerUserId: "users:1", + ownerPublisherId: "publishers:steipete", + tags: {}, + stats: { stars: 0, downloads: 0 }, + }, owner: { _id: "publishers:steipete", _creationTime: 0, diff --git a/tsconfig.json b/tsconfig.json index 5c4b5f52..894f38f6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,10 @@ /* Bundler mode */ "moduleResolution": "bundler", + "paths": { + "convex": ["./node_modules/convex"], + "convex/*": ["./node_modules/convex/*"] + }, "allowImportingTsExtensions": true, "verbatimModuleSyntax": false, "noEmit": true, diff --git a/vitest.config.ts b/vitest.config.ts index 862a6253..c88b13db 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,8 +1,33 @@ +import { createRequire } from "node:module"; import { defineConfig } from "vitest/config"; +const require = createRequire(import.meta.url); +const testingLibraryRequire = createRequire(require.resolve("@testing-library/react")); + export default defineConfig({ + resolve: { + dedupe: ["react", "react-dom"], + alias: [ + { + find: "react/jsx-dev-runtime", + replacement: testingLibraryRequire.resolve("react/jsx-dev-runtime"), + }, + { + find: "react/jsx-runtime", + replacement: testingLibraryRequire.resolve("react/jsx-runtime"), + }, + { find: "react-dom/client", replacement: testingLibraryRequire.resolve("react-dom/client") }, + { find: "react-dom", replacement: testingLibraryRequire.resolve("react-dom") }, + { find: "react", replacement: testingLibraryRequire.resolve("react") }, + ], + }, test: { environment: "jsdom", + environmentOptions: { + jsdom: { + url: "http://localhost/", + }, + }, globals: true, setupFiles: ["./vitest.setup.ts"], testTimeout: 15_000, diff --git a/vitest.setup.ts b/vitest.setup.ts index 10ae2a30..4474240d 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -1 +1,94 @@ -// Vitest setup (intentionally minimal for now) +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +const storageData = new WeakMap>(); + +function getStorageData(storage: Storage) { + let data = storageData.get(storage); + if (!data) { + data = new Map(); + storageData.set(storage, data); + } + return data; +} + +function installLocalStorageShim() { + if (typeof window === "undefined" || typeof Storage === "undefined") return; + if (typeof window.localStorage?.clear === "function") return; + + Object.defineProperties(Storage.prototype, { + length: { + configurable: true, + get() { + return getStorageData(this as Storage).size; + }, + }, + clear: { + configurable: true, + value() { + getStorageData(this as Storage).clear(); + }, + }, + getItem: { + configurable: true, + value(key: string) { + return getStorageData(this as Storage).get(String(key)) ?? null; + }, + }, + key: { + configurable: true, + value(index: number) { + return Array.from(getStorageData(this as Storage).keys())[index] ?? null; + }, + }, + removeItem: { + configurable: true, + value(key: string) { + getStorageData(this as Storage).delete(String(key)); + }, + }, + setItem: { + configurable: true, + value(key: string, value: string) { + getStorageData(this as Storage).set(String(key), String(value)); + }, + }, + }); + + const localStorage = Object.create(Storage.prototype) as Storage; + storageData.set(localStorage, new Map()); + Object.defineProperty(window, "localStorage", { + configurable: true, + value: localStorage, + }); +} + +installLocalStorageShim(); + +function proxyReactInternals() { + try { + const rootReact = require("react"); + const testingLibraryRequire = createRequire(require.resolve("@testing-library/react")); + const rendererReact = testingLibraryRequire("react"); + const rootInternals = rootReact.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE; + const rendererInternals = + rendererReact.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE; + if (!rootInternals || !rendererInternals || rootInternals === rendererInternals) return; + + for (const key of ["H", "A", "T", "S", "V"] as const) { + Object.defineProperty(rootInternals, key, { + configurable: true, + get() { + return rendererInternals[key]; + }, + set(value) { + rendererInternals[key] = value; + }, + }); + } + } catch { + // Best effort for package-manager layouts that install duplicate React copies. + } +} + +proxyReactInternals();