From 85f8e06b767fa3a1e0c87edfeefa2810cfa6a426 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sat, 4 Oct 2025 14:11:36 +0200 Subject: [PATCH 01/10] feat(test-utils): add `createTestStore` helper function for simplified test setup - Introduces `createTestStore` to streamline store creation in tests. - Combines test directory setup, store creation, and optional API mocking. - Supports custom file structures and various filesystem bridges. - Provides auto-initialization and returns both store and storePath for easy assertions. --- .changeset/clever-horses-shout.md | 57 ++++++++ packages/test-utils/src/index.ts | 4 +- packages/test-utils/src/test-store.ts | 179 ++++++++++++++++++++++++++ 3 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 .changeset/clever-horses-shout.md create mode 100644 packages/test-utils/src/test-store.ts diff --git a/.changeset/clever-horses-shout.md b/.changeset/clever-horses-shout.md new file mode 100644 index 000000000..d9aaa00ba --- /dev/null +++ b/.changeset/clever-horses-shout.md @@ -0,0 +1,57 @@ +--- +"@ucdjs/test-utils": minor +--- + +Add `createTestStore` helper function for simplified test setup + +Added a new `createTestStore` helper function that simplifies common store creation patterns in tests. It combines testdir setup, store creation, and optional API mocking into a single function. + +**Features:** +- Auto-creates testdir with custom file structure +- Supports any filesystem bridge (Node, HTTP, custom) +- Optional built-in API mocking via `mockApi` parameter +- Auto-initialization (configurable via `autoInit`) +- Returns both store and storePath for easy assertions + +**Usage Examples:** + +```typescript +import { createTestStore } from '@ucdjs/test-utils'; + +// Simple Node store with testdir +const { store, storePath } = await createTestStore({ + structure: { "15.0.0": { "file.txt": "content" } }, + versions: ["15.0.0"] +}); + +// With API mocking +const { store } = await createTestStore({ + structure: { "15.0.0": { "file.txt": "content" } }, + mockApi: true // uses default mockStoreApi configuration +}); + +// Custom API responses +const { store } = await createTestStore({ + structure: { "15.0.0": { "file.txt": "content" } }, + mockApi: { + responses: { + "/api/v1/versions": customVersions, + } + } +}); + +// Custom filesystem bridge (HTTP) +const { store } = await createTestStore({ + fs: HTTPFileSystemBridge({ baseUrl: "https://api.ucdjs.dev" }), + versions: ["15.0.0"] +}); + +// Manual initialization control +const { store } = await createTestStore({ + structure: { "15.0.0": { "file.txt": "content" } }, + autoInit: false // don't auto-initialize +}); +await store.init(); // initialize manually +``` + +This helper is fully optional and works alongside the existing `mockStoreApi` function for maximum flexibility. diff --git a/packages/test-utils/src/index.ts b/packages/test-utils/src/index.ts index cd7e95e25..7716d5af7 100644 --- a/packages/test-utils/src/index.ts +++ b/packages/test-utils/src/index.ts @@ -1,3 +1,5 @@ export { isLinux, isMac, isUnix, isWindows } from "./conditions"; -export { mockStoreApi as setupMockStore } from "./mock-store"; +export { mockStoreApi, mockStoreApi as setupMockStore } from "./mock-store"; export type { MockStoreConfig, StoreEndpointConfig, StoreEndpoints } from "./mock-store"; +export { createTestStore } from "./test-store"; +export type { CreateTestStoreOptions, CreateTestStoreResult } from "./test-store"; diff --git a/packages/test-utils/src/test-store.ts b/packages/test-utils/src/test-store.ts new file mode 100644 index 000000000..2a19070f4 --- /dev/null +++ b/packages/test-utils/src/test-store.ts @@ -0,0 +1,179 @@ +import type { FileSystemBridge } from "@ucdjs/fs-bridge"; +import type { PathFilterOptions } from "@ucdjs/shared"; +import type { UCDStore } from "@ucdjs/ucd-store"; +import type { MockStoreConfig } from "./mock-store"; +import { testdir } from "vitest-testdirs"; +import { mockStoreApi } from "./mock-store"; + +export interface CreateTestStoreOptions { + /** + * File structure to create in testdir (only used when no custom fs is provided) + * @example + * { + * "15.0.0": { + * "ArabicShaping.txt": "content" + * } + * } + */ + structure?: Record; + + /** + * Store manifest content (only used when no custom fs is provided) + * Will be written to .ucd-store.json + * @example + * { + * "15.0.0": "15.0.0" + * } + */ + manifest?: Record; + + /** + * Unicode versions to use in the store + */ + versions?: string[]; + + /** + * Base URL for the Unicode API + * @default "https://api.ucdjs.dev" + */ + baseUrl?: string; + + /** + * Base path for the store + * When using structure/manifest, this will be set to the testdir path + */ + basePath?: string; + + /** + * Global filters to apply when fetching Unicode data + */ + globalFilters?: PathFilterOptions; + + /** + * Custom filesystem bridge + * If not provided and structure/manifest are given, a Node.js bridge will be created + * If not provided and no structure/manifest, a Node.js bridge with basePath will be created + */ + fs?: FileSystemBridge; + + /** + * Whether to automatically initialize the store + * @default true + */ + autoInit?: boolean; + + /** + * Optional API mocking configuration + * - `true`: Use default mockStoreApi configuration + * - `MockStoreConfig`: Custom mockStoreApi configuration + * - `undefined`/`false`: Don't setup API mocking (useful when mocking is done in beforeEach) + */ + mockApi?: boolean | MockStoreConfig; +} + +export interface CreateTestStoreResult { + /** + * The created UCD store instance + */ + store: UCDStore; + + /** + * Path to the test directory (only present when using structure/manifest or basePath) + */ + storePath?: string; +} + +/** + * Creates a test UCD store with optional file structure and API mocking + * + * @example + * // Simple Node store with testdir + * const { store, storePath } = await createTestStore({ + * structure: { "15.0.0": { "file.txt": "content" } }, + * versions: ["15.0.0"] + * }); + * + * @example + * // With API mocking + * const { store } = await createTestStore({ + * structure: { "15.0.0": { "file.txt": "content" } }, + * mockApi: true + * }); + * + * @example + * // Custom filesystem bridge + * const { store } = await createTestStore({ + * fs: HTTPFileSystemBridge({ baseUrl: "https://api.ucdjs.dev" }), + * versions: ["15.0.0"] + * }); + */ +export async function createTestStore( + options: CreateTestStoreOptions = {}, +): Promise { + // Setup API mocking if requested + if (options.mockApi) { + if (options.mockApi === true) { + mockStoreApi(); + } else { + mockStoreApi(options.mockApi); + } + } + + let storePath: string | undefined; + let fs: FileSystemBridge; + + if (options.fs) { + // Custom bridge provided - use as-is + fs = options.fs; + storePath = options.basePath; + } else if (options.structure || options.manifest) { + // Auto-create testdir + Node bridge + const testdirStructure: Record = { + ...options.structure, + }; + + if (options.manifest) { + testdirStructure[".ucd-store.json"] = JSON.stringify(options.manifest); + } + + storePath = await testdir(testdirStructure); + + // Dynamically import Node bridge + const NodeFileSystemBridge = await import("@ucdjs/fs-bridge/bridges/node").then((m) => m.default); + if (!NodeFileSystemBridge) { + throw new Error("Node.js FileSystemBridge could not be loaded"); + } + + fs = NodeFileSystemBridge({ basePath: storePath }); + } else { + // Default Node bridge with optional basePath + const NodeFileSystemBridge = await import("@ucdjs/fs-bridge/bridges/node").then((m) => m.default); + if (!NodeFileSystemBridge) { + throw new Error("Node.js FileSystemBridge could not be loaded"); + } + + storePath = options.basePath || ""; + fs = NodeFileSystemBridge({ basePath: storePath }); + } + + // Create the store using the generic factory + const { createUCDStore } = await import("@ucdjs/ucd-store"); + + const store = createUCDStore({ + fs, + basePath: storePath || "", + baseUrl: options.baseUrl, + versions: options.versions, + globalFilters: options.globalFilters, + }); + + // Auto-initialize unless explicitly disabled + if (options.autoInit !== false) { + await store.init(); + } + + return { + store, + storePath, + }; +} From b432a231cc97fce4d022a455e30ccd6cf0616bd3 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 5 Oct 2025 05:56:25 +0200 Subject: [PATCH 02/10] refactor(test-utils): update `CreateTestStoreOptions` types and improve API mocking * Changed `structure` and `manifest` types to `DirectoryJSON` and `UCDStoreManifest` respectively. * Updated `mockApi` type to exclude `versions` and `baseUrl` from `MockStoreConfig`. * Introduced `loadNodeBridge` function for better handling of Node.js FileSystemBridge loading. * Enhanced API mocking logic to include manifest in mocked responses. --- packages/test-utils/src/test-store.ts | 78 +++++++++++++-------------- 1 file changed, 38 insertions(+), 40 deletions(-) diff --git a/packages/test-utils/src/test-store.ts b/packages/test-utils/src/test-store.ts index 2a19070f4..7ae750ae8 100644 --- a/packages/test-utils/src/test-store.ts +++ b/packages/test-utils/src/test-store.ts @@ -1,6 +1,8 @@ import type { FileSystemBridge } from "@ucdjs/fs-bridge"; +import type { UCDStoreManifest } from "@ucdjs/schemas"; import type { PathFilterOptions } from "@ucdjs/shared"; import type { UCDStore } from "@ucdjs/ucd-store"; +import type { DirectoryJSON } from "vitest-testdirs"; import type { MockStoreConfig } from "./mock-store"; import { testdir } from "vitest-testdirs"; import { mockStoreApi } from "./mock-store"; @@ -15,7 +17,7 @@ export interface CreateTestStoreOptions { * } * } */ - structure?: Record; + structure?: DirectoryJSON; /** * Store manifest content (only used when no custom fs is provided) @@ -25,7 +27,7 @@ export interface CreateTestStoreOptions { * "15.0.0": "15.0.0" * } */ - manifest?: Record; + manifest?: UCDStoreManifest; /** * Unicode versions to use in the store @@ -68,7 +70,7 @@ export interface CreateTestStoreOptions { * - `MockStoreConfig`: Custom mockStoreApi configuration * - `undefined`/`false`: Don't setup API mocking (useful when mocking is done in beforeEach) */ - mockApi?: boolean | MockStoreConfig; + mockApi?: boolean | Omit; } export interface CreateTestStoreResult { @@ -83,6 +85,14 @@ export interface CreateTestStoreResult { storePath?: string; } +async function loadNodeBridge(basePath: string): Promise { + const NodeFileSystemBridge = await import("@ucdjs/fs-bridge/bridges/node").then((m) => m.default); + if (!NodeFileSystemBridge) { + throw new Error("Node.js FileSystemBridge could not be loaded"); + } + return NodeFileSystemBridge({ basePath }); +} + /** * Creates a test UCD store with optional file structure and API mocking * @@ -110,70 +120,58 @@ export interface CreateTestStoreResult { export async function createTestStore( options: CreateTestStoreOptions = {}, ): Promise { - // Setup API mocking if requested + const versions = options.versions ?? ["16.0.0", "15.1.0", "15.0.0"]; + const baseUrl = options.baseUrl ?? "https://api.ucdjs.dev"; + if (options.mockApi) { - if (options.mockApi === true) { - mockStoreApi(); - } else { - mockStoreApi(options.mockApi); + const mockConfig: MockStoreConfig = { + baseUrl, + versions, + responses: options.mockApi === true ? undefined : options.mockApi.responses, + }; + + // Include manifest in mocked responses so the store can read it + if (options.manifest) { + mockConfig.responses = { + ...mockConfig.responses, + "/api/v1/files/.ucd-store.json": options.manifest, + }; } + + mockStoreApi(mockConfig); } let storePath: string | undefined; let fs: FileSystemBridge; if (options.fs) { - // Custom bridge provided - use as-is fs = options.fs; storePath = options.basePath; } else if (options.structure || options.manifest) { - // Auto-create testdir + Node bridge - const testdirStructure: Record = { - ...options.structure, - }; - + const structure: DirectoryJSON = { ...options.structure }; if (options.manifest) { - testdirStructure[".ucd-store.json"] = JSON.stringify(options.manifest); - } - - storePath = await testdir(testdirStructure); - - // Dynamically import Node bridge - const NodeFileSystemBridge = await import("@ucdjs/fs-bridge/bridges/node").then((m) => m.default); - if (!NodeFileSystemBridge) { - throw new Error("Node.js FileSystemBridge could not be loaded"); + structure[".ucd-store.json"] = JSON.stringify(options.manifest); } - fs = NodeFileSystemBridge({ basePath: storePath }); + storePath = await testdir(structure); + fs = await loadNodeBridge(storePath); } else { - // Default Node bridge with optional basePath - const NodeFileSystemBridge = await import("@ucdjs/fs-bridge/bridges/node").then((m) => m.default); - if (!NodeFileSystemBridge) { - throw new Error("Node.js FileSystemBridge could not be loaded"); - } - storePath = options.basePath || ""; - fs = NodeFileSystemBridge({ basePath: storePath }); + fs = await loadNodeBridge(storePath); } - // Create the store using the generic factory const { createUCDStore } = await import("@ucdjs/ucd-store"); - const store = createUCDStore({ fs, basePath: storePath || "", - baseUrl: options.baseUrl, - versions: options.versions, + baseUrl, + versions, globalFilters: options.globalFilters, }); - // Auto-initialize unless explicitly disabled if (options.autoInit !== false) { await store.init(); } - return { - store, - storePath, - }; + return { store, storePath }; } From 7bd9490f82187c0a334ef56dbaa57f5a80e62d10 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 5 Oct 2025 06:01:06 +0200 Subject: [PATCH 03/10] chore(test-utils): add missing dependency --- packages/test-utils/package.json | 2 ++ pnpm-lock.yaml | 3 +++ 2 files changed, 5 insertions(+) diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 235bf840c..00d861d95 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -42,6 +42,7 @@ "typecheck": "tsc --noEmit" }, "peerDependencies": { + "@ucdjs/fs-bridge": "workspace:*", "@ucdjs/ucd-store": "workspace:*", "vitest-testdirs": "catalog:testing" }, @@ -55,6 +56,7 @@ "@luxass/eslint-config": "catalog:linting", "@ucdjs-tooling/tsconfig": "workspace:*", "@ucdjs-tooling/tsdown-config": "workspace:*", + "@ucdjs/fs-bridge": "workspace:*", "@ucdjs/ucd-store": "workspace:*", "eslint": "catalog:linting", "publint": "catalog:dev", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 930520c08..577c85ddc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -847,6 +847,9 @@ importers: '@ucdjs-tooling/tsdown-config': specifier: workspace:* version: link:../../tooling/tsdown-config + '@ucdjs/fs-bridge': + specifier: workspace:* + version: link:../fs-bridge '@ucdjs/ucd-store': specifier: workspace:* version: link:../ucd-store From 41eddf72c1f6b8bb376513aa852744d062583dbc Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 5 Oct 2025 06:14:51 +0200 Subject: [PATCH 04/10] refactor(ucd-store): improve formatting of method signatures and error handling * Reformatted method signatures for `getFileTree`, `getFilePaths`, and `getFile` for better readability. * Enhanced error handling in manifest validation to improve clarity and maintainability. --- packages/ucd-store/src/store.ts | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/packages/ucd-store/src/store.ts b/packages/ucd-store/src/store.ts index 1e3774148..da469c759 100644 --- a/packages/ucd-store/src/store.ts +++ b/packages/ucd-store/src/store.ts @@ -119,7 +119,10 @@ export class UCDStore { return this.#manifestPath; } - async getFileTree(version: string, extraFilters?: Pick): Promise> { + async getFileTree( + version: string, + extraFilters?: Pick, + ): Promise> { return tryCatch(async () => { if (!this.#initialized) { throw new UCDStoreNotInitializedError(); @@ -142,7 +145,10 @@ export class UCDStore { }); } - async getFilePaths(version: string, extraFilters?: Pick): Promise> { + async getFilePaths( + version: string, + extraFilters?: Pick, + ): Promise> { return tryCatch(async () => { if (!this.#initialized) { throw new UCDStoreNotInitializedError(); @@ -162,7 +168,11 @@ export class UCDStore { }); } - async getFile(version: string, filePath: string, extraFilters?: Pick): Promise> { + async getFile( + version: string, + filePath: string, + extraFilters?: Pick, + ): Promise> { return tryCatch(async () => { if (!this.#initialized) { throw new UCDStoreNotInitializedError(); @@ -435,17 +445,26 @@ export class UCDStore { const manifestData = await this.#fs.read(this.#manifestPath); if (!manifestData) { - throw new UCDStoreInvalidManifestError(this.#manifestPath, "store manifest is empty"); + throw new UCDStoreInvalidManifestError( + this.#manifestPath, + "store manifest is empty", + ); } const jsonData = safeJsonParse(manifestData); if (!jsonData) { - throw new UCDStoreInvalidManifestError(this.#manifestPath, "store manifest is not a valid JSON"); + throw new UCDStoreInvalidManifestError( + this.#manifestPath, + "store manifest is not a valid JSON", + ); } const parsedManifest = UCDStoreManifestSchema.safeParse(jsonData); if (!parsedManifest.success) { - throw new UCDStoreInvalidManifestError(this.#manifestPath, `store manifest is not a valid JSON: ${parsedManifest.error.message}`); + throw new UCDStoreInvalidManifestError( + this.#manifestPath, + `store manifest is not a valid JSON: ${parsedManifest.error.message}`, + ); } return parsedManifest.data; From 0a57a58c308d6a849f4f2c148909d51a486fa337 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 5 Oct 2025 06:15:05 +0200 Subject: [PATCH 05/10] test(test-utils): add comprehensive tests for `mockStoreApi` * Implement tests for basic setup, version configuration, response configuration, mixed configuration, endpoint handlers, and edge cases. * Ensure functionality for default and custom configurations, including error handling for disabled endpoints. * Validate response data structure and behavior for various scenarios. --- packages/test-utils/test/mock-store.test.ts | 262 ++++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 packages/test-utils/test/mock-store.test.ts diff --git a/packages/test-utils/test/mock-store.test.ts b/packages/test-utils/test/mock-store.test.ts new file mode 100644 index 000000000..9c9d2b0f0 --- /dev/null +++ b/packages/test-utils/test/mock-store.test.ts @@ -0,0 +1,262 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { mockStoreApi } from "../src/mock-store"; + +describe("mockStoreApi", () => { + beforeEach(() => { + vi.resetModules(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe("basic setup", () => { + it("should set up default handlers when called with no config", async () => { + mockStoreApi(); + + const response = await fetch("https://api.ucdjs.dev/api/v1/versions"); + expect(response.ok).toBe(true); + const data = await response.json(); + expect(Array.isArray(data)).toBe(true); + }); + + it("should set up handlers with custom config", async () => { + mockStoreApi({ + baseUrl: "https://custom.api.com", + versions: ["14.0.0"], + }); + + const response = await fetch("https://custom.api.com/api/v1/versions"); + expect(response.ok).toBe(true); + const data = await response.json(); + expect(Array.isArray(data)).toBe(true); + }); + + it("should normalize baseUrl by removing trailing slash", async () => { + mockStoreApi({ + baseUrl: "https://api.example.com/", + }); + + const response = await fetch("https://api.example.com/api/v1/versions"); + expect(response.ok).toBe(true); + }); + }); + + describe("version configuration", () => { + it("should use default versions when none provided", async () => { + mockStoreApi(); + + const response = await fetch("https://api.ucdjs.dev/api/v1/versions"); + expect(response.ok).toBe(true); + const data = await response.json(); + expect(data.length).toBeGreaterThan(0); + }); + + it("should use custom versions when provided", async () => { + mockStoreApi({ + versions: ["13.0.0", "12.1.0"], + }); + + const response = await fetch("https://api.ucdjs.dev/api/v1/versions"); + expect(response.ok).toBe(true); + const data = await response.json(); + expect(data.length).toBeGreaterThan(0); + }); + }); + + describe("response configuration", () => { + it("should enable all default endpoints when no responses config provided", async () => { + mockStoreApi(); + + const versionsResponse = await fetch("https://api.ucdjs.dev/api/v1/versions"); + expect(versionsResponse.ok).toBe(true); + + const manifestResponse = await fetch("https://api.ucdjs.dev/api/v1/files/.ucd-store.json"); + expect(manifestResponse.ok).toBe(true); + }); + + it("should disable endpoints when responses is set to false", async () => { + try { + mockStoreApi({ + responses: { + "/api/v1/versions": false, + }, + }); + + const res = await fetch("https://api.ucdjs.dev/api/v1/versions"); + expect(res.ok).toBe(true); + expect(res.status).toBe(200); + + expect.fail( + "mockStoreApi should have thrown an error, since /versions mocked is disabled\n" + + "And MSW should throw have blocked it", + ); + } catch (err) { + const msg = (err as Error).message; + expect(msg).toBe("[MSW] Cannot bypass a request when using the \"error\" strategy for the \"onUnhandledRequest\" option."); + } + }); + + it("should accept custom response data", async () => { + mockStoreApi({ + responses: { + "/api/v1/versions": [ + { + version: "15.0.0", + documentationUrl: "https://example.com", + date: null, + url: "https://example.com", + mappedUcdVersion: null, + type: "stable", + }, + ], + }, + }); + + const response = await fetch("https://api.ucdjs.dev/api/v1/versions"); + expect(response.ok).toBe(true); + const data = await response.json(); + expect(data).toEqual([ + { + version: "15.0.0", + documentationUrl: "https://example.com", + date: null, + url: "https://example.com", + mappedUcdVersion: null, + type: "stable", + }, + ]); + }); + + it("should accept custom manifest response", async () => { + mockStoreApi({ + responses: { + "/api/v1/files/.ucd-store.json": { + "15.0.0": "15.0.0", + "14.0.0": "14.0.0", + }, + }, + }); + + const response = await fetch("https://api.ucdjs.dev/api/v1/files/.ucd-store.json"); + expect(response.ok).toBe(true); + const data = await response.json(); + expect(data).toEqual({ + "15.0.0": "15.0.0", + "14.0.0": "14.0.0", + }); + }); + + it("should accept true for default responses", async () => { + mockStoreApi({ + responses: { + "/api/v1/versions": true, + "/api/v1/versions/:version/file-tree": true, + "/api/v1/files/.ucd-store.json": true, + "/api/v1/files/:wildcard": true, + }, + }); + + const response = await fetch("https://api.ucdjs.dev/api/v1/versions"); + expect(response.ok).toBe(true); + }); + }); + + describe("mixed configuration", () => { + it("should handle mix of enabled and disabled endpoints", () => { + expect(() => mockStoreApi({ + responses: { + "/api/v1/versions": true, + "/api/v1/versions/:version/file-tree": false, + "/api/v1/files/.ucd-store.json": { + "15.0.0": "15.0.0", + }, + "/api/v1/files/:wildcard": true, + }, + })).not.toThrow(); + }); + + it("should handle custom baseUrl with custom responses", () => { + expect(() => mockStoreApi({ + baseUrl: "https://custom.api.com", + versions: ["14.0.0", "13.0.0"], + responses: { + "/api/v1/versions": true, + "/api/v1/files/.ucd-store.json": { + "14.0.0": "14.0.0", + "13.0.0": "13.0.0", + }, + }, + })).not.toThrow(); + }); + }); + + describe("endpoint handlers", () => { + it("should set up versions endpoint when enabled", async () => { + mockStoreApi({ + responses: { + "/api/v1/versions": true, + }, + }); + + const response = await fetch("https://api.ucdjs.dev/api/v1/versions"); + expect(response.ok).toBe(true); + const data = await response.json(); + expect(Array.isArray(data)).toBe(true); + }); + + it("should set up file-tree endpoint when enabled", async () => { + mockStoreApi({ + responses: { + "/api/v1/versions/:version/file-tree": true, + }, + versions: ["15.0.0"], + }); + + const response = await fetch("https://api.ucdjs.dev/api/v1/versions/15.0.0/file-tree"); + expect(response.ok).toBe(true); + }); + + it("should set up files endpoint when enabled", async () => { + mockStoreApi({ + responses: { + "/api/v1/files/:wildcard": true, + }, + }); + + const response = await fetch("https://api.ucdjs.dev/api/v1/files/test.txt"); + expect(response.ok).toBe(true); + }); + + it("should set up manifest endpoint when enabled", async () => { + mockStoreApi({ + responses: { + "/api/v1/files/.ucd-store.json": true, + }, + }); + + const response = await fetch("https://api.ucdjs.dev/api/v1/files/.ucd-store.json"); + expect(response.ok).toBe(true); + }); + }); + + describe("edge cases", () => { + it("should handle empty versions array", () => { + expect(() => mockStoreApi({ + versions: [], + })).not.toThrow(); + }); + + it("should handle empty responses object", () => { + expect(() => mockStoreApi({ + responses: {}, + })).not.toThrow(); + }); + + it("should handle baseUrl without protocol", () => { + expect(() => mockStoreApi({ + baseUrl: "api.example.com", + })).not.toThrow(); + }); + }); +}); From b7d25926107a82b932ee29949bc69511609a33f2 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 5 Oct 2025 06:49:01 +0200 Subject: [PATCH 06/10] fix(test-utils): enhance `createTestStore` with new options and tests - Added `testdirsOptions` to customize test directory creation. - Improved handling of `mockApi` option with default value. - Updated version inference logic based on provided manifest. - Introduced comprehensive tests for `createTestStore` covering various scenarios. --- packages/test-utils/src/test-store.ts | 46 +++++++++++++++++---------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/packages/test-utils/src/test-store.ts b/packages/test-utils/src/test-store.ts index 7ae750ae8..c8d3958a5 100644 --- a/packages/test-utils/src/test-store.ts +++ b/packages/test-utils/src/test-store.ts @@ -2,7 +2,7 @@ import type { FileSystemBridge } from "@ucdjs/fs-bridge"; import type { UCDStoreManifest } from "@ucdjs/schemas"; import type { PathFilterOptions } from "@ucdjs/shared"; import type { UCDStore } from "@ucdjs/ucd-store"; -import type { DirectoryJSON } from "vitest-testdirs"; +import type { DirectoryJSON, TestdirOptions } from "vitest-testdirs"; import type { MockStoreConfig } from "./mock-store"; import { testdir } from "vitest-testdirs"; import { mockStoreApi } from "./mock-store"; @@ -69,8 +69,16 @@ export interface CreateTestStoreOptions { * - `true`: Use default mockStoreApi configuration * - `MockStoreConfig`: Custom mockStoreApi configuration * - `undefined`/`false`: Don't setup API mocking (useful when mocking is done in beforeEach) + * + * @default true */ mockApi?: boolean | Omit; + + /** + * Options to pass to testdir when creating the test directory + * Only used when structure or manifest are provided and no custom fs is given + */ + testdirsOptions?: TestdirOptions; } export interface CreateTestStoreResult { @@ -87,9 +95,11 @@ export interface CreateTestStoreResult { async function loadNodeBridge(basePath: string): Promise { const NodeFileSystemBridge = await import("@ucdjs/fs-bridge/bridges/node").then((m) => m.default); + if (!NodeFileSystemBridge) { throw new Error("Node.js FileSystemBridge could not be loaded"); } + return NodeFileSystemBridge({ basePath }); } @@ -120,14 +130,20 @@ async function loadNodeBridge(basePath: string): Promise { export async function createTestStore( options: CreateTestStoreOptions = {}, ): Promise { - const versions = options.versions ?? ["16.0.0", "15.1.0", "15.0.0"]; + // Infer versions from manifest if provided, otherwise use explicit versions or defaults + const versions = options.versions ?? ( + options.manifest + ? Object.keys(options.manifest) + : ["16.0.0", "15.1.0", "15.0.0"] + ); const baseUrl = options.baseUrl ?? "https://api.ucdjs.dev"; + const mockApi = options.mockApi ?? true; - if (options.mockApi) { + if (mockApi) { const mockConfig: MockStoreConfig = { baseUrl, versions, - responses: options.mockApi === true ? undefined : options.mockApi.responses, + responses: mockApi === true ? undefined : mockApi.responses, }; // Include manifest in mocked responses so the store can read it @@ -142,24 +158,20 @@ export async function createTestStore( } let storePath: string | undefined; - let fs: FileSystemBridge; - if (options.fs) { - fs = options.fs; - storePath = options.basePath; - } else if (options.structure || options.manifest) { - const structure: DirectoryJSON = { ...options.structure }; - if (options.manifest) { - structure[".ucd-store.json"] = JSON.stringify(options.manifest); - } + const structure: DirectoryJSON = { ...options.structure }; + if (options.manifest) { + structure[".ucd-store.json"] = JSON.stringify(options.manifest); + } - storePath = await testdir(structure); - fs = await loadNodeBridge(storePath); + if (options.basePath) { + storePath = options.basePath; } else { - storePath = options.basePath || ""; - fs = await loadNodeBridge(storePath); + storePath = await testdir(structure, options.testdirsOptions); } + const fs = options.fs || await loadNodeBridge(storePath); + const { createUCDStore } = await import("@ucdjs/ucd-store"); const store = createUCDStore({ fs, From f74ee6be1a0695c51159953d81b4903925f030f8 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 5 Oct 2025 06:49:06 +0200 Subject: [PATCH 07/10] test(test-utils): add comprehensive tests for `createTestStore` - Implement tests for basic store creation scenarios. - Validate behavior for auto-initialization and version handling. - Test custom filesystem bridge integration and global filters application. --- packages/test-utils/test/test-store.test.ts | 166 ++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 packages/test-utils/test/test-store.test.ts diff --git a/packages/test-utils/test/test-store.test.ts b/packages/test-utils/test/test-store.test.ts new file mode 100644 index 000000000..0497cc4ec --- /dev/null +++ b/packages/test-utils/test/test-store.test.ts @@ -0,0 +1,166 @@ +import { defineFileSystemBridge } from "@ucdjs/fs-bridge"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createTestStore } from "../src/test-store"; + +describe("createTestStore", () => { + describe("basic store creation", () => { + it("should create a store with default configuration", async () => { + const { store } = await createTestStore(); + + expect(store).toBeDefined(); + expect(store.init).toBeDefined(); + expect(store.getFile).toBeDefined(); + }); + + it("should auto-initialize by default", async () => { + const { store } = await createTestStore(); + + expect(store.initialized).toBe(true); + }); + + it("should skip initialization when autoInit is false", async () => { + const { store } = await createTestStore({ autoInit: false }); + + expect(store.initialized).toBe(false); + }); + + it("should use default versions when none provided", async () => { + const { store } = await createTestStore(); + + expect(store.versions).toEqual(["16.0.0", "15.1.0", "15.0.0"]); + }); + + it("should use custom versions when provided", async () => { + const { store } = await createTestStore({ + versions: ["14.0.0", "13.0.0"], + }); + + expect(store.versions).toEqual(["14.0.0", "13.0.0"]); + }); + + it("should use custom baseUrl when provided", async () => { + const { store } = await createTestStore({ + baseUrl: "https://custom.api.com", + }); + + expect(store.baseUrl).toBe("https://custom.api.com"); + }); + }); + + describe("with structure", () => { + it("should create testdir with provided structure", async () => { + const { store, storePath } = await createTestStore({ + structure: { + "15.0.0": { + "UnicodeData.txt": "test content", + }, + }, + }); + + expect(storePath).toBeDefined(); + expect(typeof storePath).toBe("string"); + expect(store).toBeDefined(); + }); + + it("should create manifest file when manifest is provided", async () => { + const manifest = { + "15.0.0": "15.0.0", + }; + + const { store, storePath } = await createTestStore({ + manifest, + structure: { + "15.0.0": {}, + }, + testdirsOptions: { + cleanup: false, + }, + }); + + expect(storePath).toBeDefined(); + + // Read the manifest using the hidden method + const readManifest = await store["~readManifest"](); + expect(readManifest).toEqual(manifest); + }); + + it("should create testdir with just manifest", async () => { + const manifest = { + "15.0.0": "15.0.0", + }; + + const { store, storePath } = await createTestStore({ + manifest, + }); + + expect(storePath).toBeDefined(); + expect(store).toBeDefined(); + + const readManifest = await store["~readManifest"](); + expect(readManifest).toEqual(manifest); + }); + }); + + describe("with custom filesystem bridge", () => { + it("should use custom fs bridge when provided", async () => { + const mockFs = defineFileSystemBridge({ + setup() { + return { + read: vi.fn().mockResolvedValue("mock content"), + write: vi.fn().mockResolvedValue(undefined), + listdir: vi.fn().mockResolvedValue([]), + exists: vi.fn().mockResolvedValue(true), + mkdir: vi.fn().mockResolvedValue(undefined), + rm: vi.fn().mockResolvedValue(undefined), + }; + }, + })(); + + const { store } = await createTestStore({ + fs: mockFs, + autoInit: false, + }); + + expect(store).toBeDefined(); + expect(store.fs).toBe(mockFs); + }); + + it("should use basePath when custom fs is provided", async () => { + const mockFs = defineFileSystemBridge({ + setup() { + return { + read: vi.fn().mockResolvedValue(""), + write: vi.fn().mockResolvedValue(undefined), + listdir: vi.fn().mockResolvedValue([]), + exists: vi.fn().mockResolvedValue(true), + mkdir: vi.fn().mockResolvedValue(undefined), + rm: vi.fn().mockResolvedValue(undefined), + }; + }, + })(); + + const { storePath } = await createTestStore({ + fs: mockFs, + basePath: "/custom/path", + autoInit: false, + }); + + expect(storePath).toBe("/custom/path"); + }); + }); + + describe("with global filters", () => { + it("should apply global filters when provided", async () => { + const { store } = await createTestStore({ + globalFilters: { + include: ["**/*.txt"], + }, + }); + + expect(store.filter).toBeDefined(); + expect(store.filter.patterns()).toEqual(expect.objectContaining({ + include: ["**/*.txt"], + })); + }); + }); +}); From 752a1aa2bb8826e2e8cc9f9fd76975186450eccb Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 5 Oct 2025 06:50:44 +0200 Subject: [PATCH 08/10] chore: lint --- packages/test-utils/test/test-store.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/test-utils/test/test-store.test.ts b/packages/test-utils/test/test-store.test.ts index 0497cc4ec..68fe3c824 100644 --- a/packages/test-utils/test/test-store.test.ts +++ b/packages/test-utils/test/test-store.test.ts @@ -1,5 +1,5 @@ import { defineFileSystemBridge } from "@ucdjs/fs-bridge"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { createTestStore } from "../src/test-store"; describe("createTestStore", () => { From 00e6cc60d723449a5f4e696f13d81b1d63c65132 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 5 Oct 2025 07:48:56 +0200 Subject: [PATCH 09/10] test(test-utils): enhance `createTestStore` tests with additional scenarios * Added tests for handling empty structure and manifest objects. * Verified behavior when no options are provided, ensuring proper initialization. * Included checks for custom filesystem bridge and API mocking configurations. * Improved test coverage for return values and directory creation logic. --- packages/test-utils/test/test-store.test.ts | 225 +++++++++++++++++++- 1 file changed, 223 insertions(+), 2 deletions(-) diff --git a/packages/test-utils/test/test-store.test.ts b/packages/test-utils/test/test-store.test.ts index 68fe3c824..171de2145 100644 --- a/packages/test-utils/test/test-store.test.ts +++ b/packages/test-utils/test/test-store.test.ts @@ -1,7 +1,23 @@ +import type { UnicodeVersionList } from "@ucdjs/schemas"; +import fsp from "node:fs/promises"; +import os from "node:os"; +import { join } from "node:path"; import { defineFileSystemBridge } from "@ucdjs/fs-bridge"; -import { describe, expect, it, vi } from "vitest"; +import { HttpResponse } from "msw"; +import { describe, expect, it, onTestFinished, vi } from "vitest"; import { createTestStore } from "../src/test-store"; +async function createOSTemporaryFolder() { + const osTmpDir = os.tmpdir(); + const tmpUCDStoreFolder = join(osTmpDir, `tmp-ucd-store-test-${Date.now()}`); + + await fsp.mkdir(tmpUCDStoreFolder, { recursive: true }).catch(() => { + expect.fail("Failed to create temporary directory for test store"); + }); + + return tmpUCDStoreFolder; +} + describe("createTestStore", () => { describe("basic store creation", () => { it("should create a store with default configuration", async () => { @@ -45,6 +61,16 @@ describe("createTestStore", () => { expect(store.baseUrl).toBe("https://custom.api.com"); }); + + it("should create empty testdir when no options provided", async () => { + const { store, storePath } = await createTestStore(); + + expect(storePath).toBeDefined(); + expect(storePath).not.toBe(""); + expect(typeof storePath).toBe("string"); + expect(store).toBeDefined(); + expect(store.initialized).toBe(true); + }); }); describe("with structure", () => { @@ -79,7 +105,6 @@ describe("createTestStore", () => { expect(storePath).toBeDefined(); - // Read the manifest using the hidden method const readManifest = await store["~readManifest"](); expect(readManifest).toEqual(manifest); }); @@ -99,6 +124,55 @@ describe("createTestStore", () => { const readManifest = await store["~readManifest"](); expect(readManifest).toEqual(manifest); }); + + it("should handle empty structure object", async () => { + const { store, storePath } = await createTestStore({ + structure: {}, + }); + + expect(storePath).toBeDefined(); + expect(storePath).not.toBe(""); + expect(typeof storePath).toBe("string"); + expect(store).toBeDefined(); + expect(store.initialized).toBe(true); + }); + + it("should handle empty manifest object", async () => { + const { store, storePath } = await createTestStore({ + manifest: {}, + }); + + expect(storePath).toBeDefined(); + expect(typeof storePath).toBe("string"); + expect(store).toBeDefined(); + + const readManifest = await store["~readManifest"](); + expect(readManifest).toEqual({}); + }); + + it("should pass testdirsOptions to testdir", async () => { + const { store, storePath } = await createTestStore({ + structure: { + "15.0.0": { + "UnicodeData.txt": "test content", + }, + }, + testdirsOptions: { + cleanup: false, + }, + }); + + onTestFinished(async () => { + await fsp.rm(storePath!, { recursive: true, force: true }); + }); + + expect(storePath).toBeDefined(); + expect(typeof storePath).toBe("string"); + expect(store).toBeDefined(); + + const dirStat = await fsp.stat(storePath!); + expect(dirStat.isDirectory()).toBe(true); + }); }); describe("with custom filesystem bridge", () => { @@ -147,6 +221,64 @@ describe("createTestStore", () => { expect(storePath).toBe("/custom/path"); }); + + it("should not create testdir when basePath is provided", async () => { + const tmpUCDStoreFolder = await createOSTemporaryFolder(); + + onTestFinished(async () => { + await fsp.rm(tmpUCDStoreFolder, { recursive: true, force: true }); + }); + + const { store, storePath } = await createTestStore({ + basePath: tmpUCDStoreFolder, + structure: { + "15.0.0": { + "UnicodeData.txt": "test content", + }, + }, + autoInit: false, + }); + + expect(store).toBeDefined(); + expect(storePath).toBe(tmpUCDStoreFolder); + + const dirStat = await fsp.stat(tmpUCDStoreFolder); + expect(dirStat.isDirectory()).toBe(true); + + const entries = await fsp.readdir(tmpUCDStoreFolder); + expect(entries).toEqual([]); + }); + + it("should ignore manifest when basePath is provided", async () => { + const tmpUCDStoreFolder = await createOSTemporaryFolder(); + + onTestFinished(async () => { + await fsp.rm(tmpUCDStoreFolder, { recursive: true, force: true }); + }); + + const { store, storePath } = await createTestStore({ + basePath: tmpUCDStoreFolder, + manifest: { + "15.0.0": "15.0.0", + }, + autoInit: false, + }); + + expect(store).toBeDefined(); + expect(storePath).toBe(tmpUCDStoreFolder); + + const dirStat = await fsp.stat(tmpUCDStoreFolder); + expect(dirStat.isDirectory()).toBe(true); + + const entries = await fsp.readdir(tmpUCDStoreFolder); + expect(entries).toEqual([]); + + // NOTE: reading manifest should return empty object + // But this will throw an error, since the manifest doesn't exist. + // Which is the expected behavior, and what we are testing for. + const readManifest = await store["~readManifest"]().catch(() => ({})); + expect(readManifest).toEqual({}); + }); }); describe("with global filters", () => { @@ -163,4 +295,93 @@ describe("createTestStore", () => { })); }); }); + + describe("with API mocking", () => { + it("should not set up API mocking when mockApi is false", async () => { + try { + const { store } = await createTestStore({ + mockApi: false, + }); + + expect(store).toBeDefined(); + expect(store.initialized).toBe(false); + + expect.fail( + "mockStoreApi should have thrown an error, since /versions mocked is disabled\n" + + "And MSW should throw have blocked it", + ); + } catch (err) { + const msg = (err as Error).message; + expect(msg).toBe("[MSW] Cannot bypass a request when using the \"error\" strategy for the \"onUnhandledRequest\" option."); + } + }); + + it("should use custom mock config when provided", async () => { + let called = 0; + const { store } = await createTestStore({ + versions: ["15.0.0"], + mockApi: { + responses: { + "/api/v1/versions": () => { + called += 1; + return HttpResponse.json([ + { + version: "15.0.0", + documentationUrl: "https://example.com", + date: null, + url: "https://example.com", + mappedUcdVersion: null, + type: "stable", + }, + ] as UnicodeVersionList); + }, + }, + }, + }); + + expect(store).toBeDefined(); + expect(store.initialized).toBe(true); + expect(store.versions).toEqual(["15.0.0"]); + + expect(called).toBe(1); + }); + + it("should include manifest in mocked responses when manifest provided", async () => { + const manifest = { + "15.0.0": "15.0.0", + }; + + const { store } = await createTestStore({ + manifest, + mockApi: true, + }); + + expect(store).toBeDefined(); + expect(store.initialized).toBe(true); + + const fetchedManifest = await store["~readManifest"](); + expect(fetchedManifest).toEqual(manifest); + + const { data: fetchedData } = await store.client.GET("/api/v1/files/.ucd-store.json"); + expect(fetchedData).toEqual(manifest); + }); + }); + + describe("return value", () => { + it("should return both store and storePath", async () => { + const testStore = await createTestStore({ + structure: { + "15.0.0": { + "UnicodeData.txt": "test content", + }, + }, + }); + + expect(testStore).toBeDefined(); + expect(testStore.store).toBeDefined(); + expect(testStore.storePath).toBeDefined(); + expect(typeof testStore.storePath).toBe("string"); + expect(testStore.storePath).not.toBe(""); + }); + }); }); From 3a39fc087d9c6b863b4110b5071f854886cb646a Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 5 Oct 2025 08:01:06 +0200 Subject: [PATCH 10/10] fix(test-utils): update manifest handling in `createTestStore` * Improved the handling of the manifest option in `createTestStore` to ensure existing responses are preserved. * Removed unnecessary import of `createUCDStore` as it is now imported at the top of the file. --- packages/test-utils/src/test-store.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/test-utils/src/test-store.ts b/packages/test-utils/src/test-store.ts index c8d3958a5..4adf24445 100644 --- a/packages/test-utils/src/test-store.ts +++ b/packages/test-utils/src/test-store.ts @@ -4,6 +4,7 @@ import type { PathFilterOptions } from "@ucdjs/shared"; import type { UCDStore } from "@ucdjs/ucd-store"; import type { DirectoryJSON, TestdirOptions } from "vitest-testdirs"; import type { MockStoreConfig } from "./mock-store"; +import { createUCDStore } from "@ucdjs/ucd-store"; import { testdir } from "vitest-testdirs"; import { mockStoreApi } from "./mock-store"; @@ -150,7 +151,7 @@ export async function createTestStore( if (options.manifest) { mockConfig.responses = { ...mockConfig.responses, - "/api/v1/files/.ucd-store.json": options.manifest, + "/api/v1/files/.ucd-store.json": mockConfig.responses?.["/api/v1/files/.ucd-store.json"] ?? options.manifest, }; } @@ -172,7 +173,6 @@ export async function createTestStore( const fs = options.fs || await loadNodeBridge(storePath); - const { createUCDStore } = await import("@ucdjs/ucd-store"); const store = createUCDStore({ fs, basePath: storePath || "",