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/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/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..4adf24445 --- /dev/null +++ b/packages/test-utils/src/test-store.ts @@ -0,0 +1,189 @@ +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, 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"; + +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?: DirectoryJSON; + + /** + * 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?: UCDStoreManifest; + + /** + * 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) + * + * @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 { + /** + * The created UCD store instance + */ + store: UCDStore; + + /** + * Path to the test directory (only present when using structure/manifest or basePath) + */ + 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 + * + * @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 { + // 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 (mockApi) { + const mockConfig: MockStoreConfig = { + baseUrl, + versions, + responses: mockApi === true ? undefined : 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": mockConfig.responses?.["/api/v1/files/.ucd-store.json"] ?? options.manifest, + }; + } + + mockStoreApi(mockConfig); + } + + let storePath: string | undefined; + + const structure: DirectoryJSON = { ...options.structure }; + if (options.manifest) { + structure[".ucd-store.json"] = JSON.stringify(options.manifest); + } + + if (options.basePath) { + storePath = options.basePath; + } else { + storePath = await testdir(structure, options.testdirsOptions); + } + + const fs = options.fs || await loadNodeBridge(storePath); + + const store = createUCDStore({ + fs, + basePath: storePath || "", + baseUrl, + versions, + globalFilters: options.globalFilters, + }); + + if (options.autoInit !== false) { + await store.init(); + } + + return { store, storePath }; +} 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(); + }); + }); +}); 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..171de2145 --- /dev/null +++ b/packages/test-utils/test/test-store.test.ts @@ -0,0 +1,387 @@ +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 { 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 () => { + 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"); + }); + + 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", () => { + 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(); + + 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); + }); + + 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", () => { + 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"); + }); + + 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", () => { + 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"], + })); + }); + }); + + 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(""); + }); + }); +}); 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; 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