diff --git a/.gitignore b/.gitignore index ef9e9e932044ff3ebdce288a1be02c1092007930..eb047408b6d9c495191482f0b2b618d7f23f2957 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ packages/server/prisma/dev.db packages/client/public/**/*.css packages/client/public/**/*.js +packages/client/coverage # build directory /build diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ea73d4d7988b448144c86889c3bc7aca7d0129b1..e9e856be9e3a818bdb43d30741176f0e9245fe1d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,7 +9,7 @@ include: inputs: wiki_token: $CI_PUSH_TOKEN -eslint: +lint:eslint: stage: lint rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" @@ -23,38 +23,43 @@ eslint: reports: codequality: gl-codequality.json -check outdated: - stage: test +lint:markdown: + stage: lint rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" + changes: + - "**/*.md" image: node:24-alpine - allow_failure: true before_script: + - apk update && apk add git - corepack enable yarn && corepack prepare --activate - - yarn workspaces focus --all script: - - npm outdated + - git diff --name-only $CI_MERGE_REQUEST_DIFF_BASE_SHA...HEAD | grep '\.md$' | xargs yarn dlx -p markdownlint-cli markdownlint -lint markdown: - stage: lint +check outdated: + stage: test rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - changes: - - "**/*.md" image: node:24-alpine + allow_failure: true before_script: - - apk update && apk add git - corepack enable yarn && corepack prepare --activate + - yarn workspaces focus --all script: - - git diff --name-only $CI_MERGE_REQUEST_DIFF_BASE_SHA...HEAD | grep '\.md$' | xargs yarn dlx -p markdownlint-cli markdownlint + - npm outdated -jest server: +test:server: stage: test rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" changes: - packages/server/src/**/* image: node:24-alpine + cache: + key: yarn-packages + paths: + - .yarn/cache + - node_modules services: - name: postgres:14-alpine alias: postgres @@ -82,6 +87,37 @@ jest server: coverage_format: cobertura path: packages/server/coverage/cobertura-coverage.xml +test:client: + stage: test + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + changes: + - packages/client/**/* + image: mcr.microsoft.com/playwright:v1.60.0-jammy + cache: + key: yarn-packages + paths: + - .yarn/cache + - node_modules + before_script: + - corepack enable yarn && corepack prepare --activate + script: + - yarn workspaces focus --all + - yarn build-api-schema + - yarn workspace @sc07-canvas/lib build + - yarn workspace @sc07-canvas/client vitest run + --config vitest.config.ts + --browser.headless + --coverage + --coverage.reporter=text-summary + --coverage.reporter=cobertura + coverage: /All\sfiles.*?\|\s+[\d\.]+/ + artifacts: + reports: + coverage_report: + coverage_format: cobertura + path: packages/client/coverage/cobertura-coverage.xml + deploy: stage: deploy trigger: diff --git a/package.json b/package.json index 31e8d419d019969fc41b7cef6880507e70342a7b..14b70518a34fefad4b03345894434a5d8db26ed4 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "markdownlint-cli": "^0.48.0", "nodemon": "^3.1.11", "openapi-typescript": "^7.13.0", + "playwright": "^1.60.0", "prettier": "^3.8.1", "tailwindcss": "^4.1.18", "ts-node": "^10.9.2", diff --git a/packages/client/package.json b/packages/client/package.json index 394cfa6914dcdedfed5a2b244492e19cc343c766..57ec1127e807cda6bb1b18eeec2b59d784f9b735 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -6,7 +6,8 @@ "scripts": { "build": "vite build", "dev": "vite serve", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest" }, "type": "module", "license": "MIT", @@ -44,6 +45,8 @@ "@tsconfig/vite-react": "^7.0.2", "@types/lodash.throttle": "^4.1.9", "@types/socket.io-client": "^3.0.0", + "@vitest/browser-playwright": "^4.1.8", + "@vitest/coverage-istanbul": "4.1.8", "eslint": "^9.39.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-react": "^7.37.5", @@ -51,6 +54,7 @@ "postcss": "^8.5.6", "sass": "^1.97.3", "typescript-eslint": "^8.56.0", - "vite": "^7.3.1" + "vite": "^7.3.1", + "vitest": "^4.1.8" } } diff --git a/packages/client/src/lib/__tests__/banner.test.ts b/packages/client/src/lib/__tests__/banner.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..6818d31024659c0191d5df848b835c08a976bc34 --- /dev/null +++ b/packages/client/src/lib/__tests__/banner.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; + +describe("banner", () => { + let consoleSpy: ReturnType; + + beforeEach(() => { + consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + }); + + afterEach(() => { + consoleSpy.mockRestore(); + }); + + it("printBanner calls console.log", async () => { + // Dynamic import so the template literal with __BUILD_YEAR__ resolves + const { printBanner } = await import("../banner"); + printBanner(); + expect(consoleSpy).toHaveBeenCalled(); + }); + + it("printBanner passes formatting arguments", async () => { + const { printBanner } = await import("../banner"); + printBanner(); + // First arg should be the banner string, rest are formatting strings + const call = consoleSpy.mock.calls[0]; + expect(call[0]).toContain("canvas.fediverse.events"); + // Should have at least 2 arguments (string + format specifiers) + expect(call.length).toBeGreaterThanOrEqual(2); + }); + + it("banner includes copyright text", async () => { + const { printBanner } = await import("../banner"); + printBanner(); + const call = consoleSpy.mock.calls[0]; + expect(call[0]).toMatch(/Copyright/); + }); +}); \ No newline at end of file diff --git a/packages/client/src/lib/__tests__/canvasRenderer.test.ts b/packages/client/src/lib/__tests__/canvasRenderer.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..426ba2a62783acb02ddacaf9baa3502b69b958d1 --- /dev/null +++ b/packages/client/src/lib/__tests__/canvasRenderer.test.ts @@ -0,0 +1,549 @@ +/** + * Tests for CanvasRenderer — runs in browser (Playwright Chromium + Firefox). + * + * No mocks or stubs needed: real document.createElement('canvas'), + * CanvasRenderingContext2D, requestAnimationFrame, and getImageData are + * all available in the browser environment. + */ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { CanvasRenderer, type CanvasPixel } from "../canvasRenderer"; + +/* ─── Helpers ─── */ + +function makeCanvas(w = 100, h = 100): HTMLCanvasElement { + const c = document.createElement("canvas"); + c.width = w; + c.height = h; + return c; +} + +function ctxOf(r: CanvasRenderer): CanvasRenderingContext2D { + const ctx = (r as any).ctx as CanvasRenderingContext2D | undefined; + if (!ctx) throw new Error("No canvas context set"); + return ctx; +} + +function pixelColor(ctx: CanvasRenderingContext2D, x: number, y: number) { + const [r, g, b] = ctx.getImageData(x, y, 1, 1).data; + return { r, g, b }; +} + +function hexColor(ctx: CanvasRenderingContext2D, x: number, y: number) { + const { r, g, b } = pixelColor(ctx, x, y); + return "#" + [r, g, b].map((c) => c.toString(16).padStart(2, "0")).join(""); +} + +function pixelColorAlpha(ctx: CanvasRenderingContext2D, x: number, y: number) { + const [r, g, b, a] = ctx.getImageData(x, y, 1, 1).data; + return { r, g, b, a }; +} + +function blankCtxOf(r: CanvasRenderer): CanvasRenderingContext2D { + const ctx = (r as any).blank_ctx as CanvasRenderingContext2D | undefined; + if (!ctx) throw new Error("No blank canvas context set"); + return ctx; +} + +/** + * Attach a canvas to the renderer as "main" and call setSize (which triggers + * drawFull synchronously since ctx is available). + */ +function setup(r: CanvasRenderer, w = 100, h = 100): HTMLCanvasElement { + const c = makeCanvas(); + r.useCanvas(c, "main"); + r.setSize(w, h); + return c; +} + +/* ─── Tests ─── */ + +describe("CanvasRenderer", () => { + let renderer: CanvasRenderer; + + beforeEach(() => { + renderer = new CanvasRenderer(); + }); + + afterEach(() => { + renderer.removeAllListeners(); + }); + + /* ─────── Constructor ─────── */ + + describe("constructor", () => { + it("creates an instance", () => { + expect(renderer).toBeInstanceOf(CanvasRenderer); + }); + + it("initialises empty pixel maps", () => { + const r = renderer as any; + expect(r.pixels.size).toBe(0); + expect(r.allPixels.size).toBe(0); + }); + + it("initialises dimensions to zero", () => { + const r = renderer as any; + expect(r.dimentions.width).toBe(0); + expect(r.dimentions.height).toBe(0); + }); + + it("detects it is not inside a WorkerGlobalScope", () => { + const r = renderer as any; + expect(r.isWorker).toBe(false); + }); + }); + + /* ─────── useCanvas ─────── */ + + describe("useCanvas", () => { + it("stores a reference and context for the main canvas", () => { + const c = makeCanvas(); + renderer.useCanvas(c, "main"); + const r = renderer as any; + expect(r.canvas).toBe(c); + expect(r.ctx).toBeTruthy(); + }); + + it("stores a reference and context for the blank canvas", () => { + const c = makeCanvas(); + renderer.useCanvas(c, "blank"); + const r = renderer as any; + expect(r.blank).toBe(c); + expect(r.blank_ctx).toBeTruthy(); + }); + + it("sets canvas width/height from internal dimensions", () => { + renderer.setSize(50, 60); + const c = makeCanvas(); + renderer.useCanvas(c, "main"); + expect(c.width).toBe(50); + expect(c.height).toBe(60); + }); + + it("throws when getContext returns null", () => { + const c = makeCanvas(); + vi.spyOn(c, "getContext").mockReturnValue(null); + expect(() => renderer.useCanvas(c, "main")).toThrow( + "Unable to get canvas context", + ); + }); + }); + + /* ─────── removeCanvas ─────── */ + + describe("removeCanvas", () => { + it("removes the blank canvas reference", () => { + const c = makeCanvas(); + renderer.useCanvas(c, "blank"); + renderer.removeCanvas("blank"); + const r = renderer as any; + expect(r.blank).toBeUndefined(); + expect(r.blank_ctx).toBeUndefined(); + }); + + it("throws when trying to remove the main canvas", () => { + expect(() => renderer.removeCanvas("main")).toThrow( + "Cannot remove main canvas", + ); + }); + }); + + /* ─────── usePixel / usePixels ─────── */ + + describe("usePixel", () => { + it("stores a pixel in both pending and allPixels maps", () => { + renderer.usePixel({ x: 5, y: 10, hex: "ff0000" }); + const r = renderer as any; + expect(r.pixels.get("5,10")).toBe("ff0000"); + expect(r.allPixels.get("5,10")).toBe("ff0000"); + }); + + it("overwrites an existing pixel at the same coordinate", () => { + renderer.usePixel({ x: 5, y: 10, hex: "ff0000" }); + renderer.usePixel({ x: 5, y: 10, hex: "00ff00" }); + const r = renderer as any; + expect(r.pixels.get("5,10")).toBe("00ff00"); + }); + + it("accepts 'null' as a valid hex value", () => { + renderer.usePixel({ x: 0, y: 0, hex: "null" }); + const r = renderer as any; + expect(r.pixels.get("0,0")).toBe("null"); + }); + }); + + describe("usePixels", () => { + it("stores multiple pixels in both maps", () => { + const pixels: CanvasPixel[] = [ + { x: 0, y: 0, hex: "ff0000" }, + { x: 1, y: 1, hex: "00ff00" }, + ]; + renderer.usePixels(pixels); + const r = renderer as any; + expect(r.pixels.size).toBe(2); + expect(r.allPixels.size).toBe(2); + }); + + it("is a no-op for an empty array", () => { + renderer.usePixels([]); + const r = renderer as any; + expect(r.pixels.size).toBe(0); + }); + + it("ignores the replace parameter (same as usePixel)", () => { + renderer.usePixels([{ x: 0, y: 0, hex: "ff0000" }], true); + const r = renderer as any; + expect(r.pixels.get("0,0")).toBe("ff0000"); + }); + }); + + /* ─────── setSize ─────── */ + + describe("setSize", () => { + it("updates internal dimensions", () => { + renderer.setSize(200, 150); + const r = renderer as any; + expect(r.dimentions.width).toBe(200); + expect(r.dimentions.height).toBe(150); + }); + + it("resizes the attached main canvas", () => { + const c = makeCanvas(); + renderer.useCanvas(c, "main"); + renderer.setSize(200, 150); + expect(c.width).toBe(200); + expect(c.height).toBe(150); + }); + + it("resizes the attached blank canvas", () => { + const c = makeCanvas(); + renderer.useCanvas(c, "blank"); + renderer.setSize(200, 150); + expect(c.width).toBe(200); + expect(c.height).toBe(150); + }); + + it("emits ready event", () => { + const fn = vi.fn(); + renderer.on("ready", fn); + renderer.setSize(100, 100); + expect(fn).toHaveBeenCalledTimes(1); + }); + }); + + /* ─────── draw ─────── */ + + describe("draw", () => { + it("draws pending pixels onto the canvas", () => { + const canvas = setup(renderer); + const ctx = ctxOf(renderer); + + renderer.usePixel({ x: 5, y: 5, hex: "ff0000" }); + renderer.usePixel({ x: 10, y: 10, hex: "00ff00" }); + renderer.draw(); + + expect(hexColor(ctx, 5, 5)).toBe("#ff0000"); + expect(hexColor(ctx, 10, 10)).toBe("#00ff00"); + }); + + it("clears the pending queue after drawing", () => { + setup(renderer); + renderer.usePixel({ x: 0, y: 0, hex: "ff0000" }); + renderer.draw(); + const r = renderer as any; + expect(r.pixels.size).toBe(0); + }); + + it("draws 'null' hex as white (#ffffff)", () => { + const canvas = setup(renderer); + const ctx = ctxOf(renderer); + + renderer.usePixel({ x: 15, y: 15, hex: "null" }); + renderer.draw(); + + const { r, g, b } = pixelColor(ctx, 15, 15); + expect(r).toBe(255); + expect(g).toBe(255); + expect(b).toBe(255); + }); + + it("is a no-op when there are no pending pixels", () => { + setup(renderer); + expect(() => renderer.draw()).not.toThrow(); + }); + + it("does not crash when ctx is undefined and pixels queue is empty", () => { + // No canvas attached — ctx is undefined + expect(() => renderer.draw()).not.toThrow(); + }); + + it("tracks draw timing statistics across multiple calls", () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + setup(renderer); + + // Draw enough pixels so performance.now() registers a real time diff + // (the pixel loop is never a no-op when there are pixels to draw). + for (let i = 0; i < 50; i++) { + for (let j = 0; j < 10; j++) { + renderer.usePixel({ x: i, y: j, hex: "ff0000" }); + } + } + // First draw: pushes a diff value to drawTimes + renderer.draw(); + warnSpy.mockClear(); + + // Second draw: drawTimes.length > 0 → exercises the reduce callback + renderer.draw(); + expect(() => renderer.draw()).not.toThrow(); + warnSpy.mockRestore(); + }); + }); + + /* ─────── drawFull (triggered by setSize) ─────── */ + + describe("drawFull (triggered by setSize)", () => { + it("draws all accumulated pixels", () => { + renderer.usePixel({ x: 10, y: 10, hex: "ff0000" }); + const canvas = setup(renderer); // calls setSize → tryDrawFull → drawFull + const ctx = ctxOf(renderer); + + expect(hexColor(ctx, 10, 10)).toBe("#ff0000"); + }); + + it("fills the entire canvas with white first", () => { + const canvas = setup(renderer); + const ctx = ctxOf(renderer); + + // After setup → drawFull, whole canvas should be white + const { r, g, b } = pixelColor(ctx, 50, 50); + expect(r).toBe(255); + expect(g).toBe(255); + expect(b).toBe(255); + }); + + it("draws 'null' hex pixels as white via drawFull", () => { + renderer.usePixel({ x: 3, y: 3, hex: "null" }); + setup(renderer); // calls setSize → tryDrawFull → drawFull + + const ctx = ctxOf(renderer); + const { r, g, b } = pixelColor(ctx, 3, 3); + expect(r).toBe(255); + expect(g).toBe(255); + expect(b).toBe(255); + }); + }); + + /* ─────── startRender / stopRender ─────── */ + + describe("startRender / stopRender", () => { + it("calls drawFull and sets _stopRender = false", () => { + const canvas = setup(renderer); + renderer.usePixel({ x: 20, y: 20, hex: "ff0000" }); + + renderer.startRender(); + + // drawFull runs synchronously inside startRender (ctx exists) + const ctx = ctxOf(renderer); + expect(hexColor(ctx, 20, 20)).toBe("#ff0000"); + expect((renderer as any)._stopRender).toBe(false); + }); + + it("stopRender sets _stopRender = true", () => { + renderer.startRender(); + renderer.stopRender(); + expect((renderer as any)._stopRender).toBe(true); + }); + + it("does not throw when called before startRender", () => { + expect(() => renderer.stopRender()).not.toThrow(); + }); + + it("prevents tryDrawFull from drawing", () => { + setup(renderer); + renderer.startRender(); + renderer.stopRender(); + + const ctx = ctxOf(renderer); + + // After stop, setSize → tryDrawFull checks _stopRender and returns + // early. Setting canvas.width/height inside setSize clears the canvas + // to transparent black, but drawFull never re-runs. + renderer.usePixel({ x: 0, y: 0, hex: "ff0000" }); + renderer.setSize(100, 100); + + // Canvas was cleared by resize, but drawFull was blocked + // Default cleared state is transparent black (0, 0, 0) + const { r, g, b } = pixelColor(ctx, 0, 0); + expect(r).toBe(0); + expect(g).toBe(0); + expect(b).toBe(0); + + // The pixel IS stored in the maps though + expect((renderer as any).allPixels.get("0,0")).toBe("ff0000"); + }); + }); + + /* ─────── Edge cases ─────── */ + + describe("edge cases", () => { + it("removing an un-attached blank canvas does not throw", () => { + expect(() => renderer.removeCanvas("blank")).not.toThrow(); + }); + + it("setSize with zero dimensions still emits ready", () => { + const fn = vi.fn(); + renderer.on("ready", fn); + renderer.setSize(0, 0); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it("multiple setSize calls resize the canvas each time", () => { + const c = makeCanvas(); + renderer.useCanvas(c, "main"); + renderer.setSize(50, 50); + expect(c.width).toBe(50); + expect(c.height).toBe(50); + renderer.setSize(200, 100); + expect(c.width).toBe(200); + expect(c.height).toBe(100); + }); + + it("allPixels persists across draw calls", () => { + setup(renderer); + renderer.usePixel({ x: 0, y: 0, hex: "ff0000" }); + renderer.draw(); + renderer.usePixel({ x: 1, y: 1, hex: "00ff00" }); + renderer.draw(); + const r = renderer as any; + expect(r.allPixels.size).toBe(2); + expect(r.allPixels.get("0,0")).toBe("ff0000"); + expect(r.allPixels.get("1,1")).toBe("00ff00"); + }); + + it("setSize is idempotent with same dimensions", () => { + const fn = vi.fn(); + renderer.on("ready", fn); + setup(renderer, 100, 100); + expect(fn).toHaveBeenCalledTimes(1); + renderer.setSize(100, 100); + expect(fn).toHaveBeenCalledTimes(2); + }); + }); + + /* ─────── Blank canvas / drawBlank ─────── */ + + describe("blank canvas (drawBlank via startRender)", () => { + it("draws semi-transparent green where hex is 'null'", () => { + const main = makeCanvas(); + const blank = makeCanvas(); + renderer.useCanvas(main, "main"); + renderer.useCanvas(blank, "blank"); + renderer.setSize(100, 100); + + renderer.usePixel({ x: 5, y: 5, hex: "null" }); + renderer.usePixel({ x: 10, y: 10, hex: "ff0000" }); // should NOT appear on blank + + // startRender calls tryDrawBlank → drawBlank (blank_ctx is set) + renderer.startRender(); + renderer.stopRender(); + + const bCtx = blankCtxOf(renderer); + + // Null pixel at (5,5) should be rgba(0,140,0,0.5) on the blank canvas + // Note: getImageData may be off by 1 due to premultiplied-alpha precision + const nil = pixelColorAlpha(bCtx, 5, 5); + expect(nil.r).toBe(0); + expect(nil.g).toBeGreaterThanOrEqual(130); + expect(nil.g).toBeLessThanOrEqual(140); + expect(nil.b).toBe(0); + // Alpha = Math.round(0.5 * 255) = 127 or 128 + expect(nil.a).toBeGreaterThanOrEqual(120); + expect(nil.a).toBeLessThanOrEqual(130); + + // Non-null pixel at (10,10) should NOT appear on blank + const solid = pixelColorAlpha(bCtx, 10, 10); + expect(solid.a).toBe(0); // transparent / untouched + }); + + it("clears the blank canvas before each drawBlank call", () => { + const main = makeCanvas(); + const blank = makeCanvas(100, 100); + renderer.useCanvas(main, "main"); + renderer.useCanvas(blank, "blank"); + renderer.setSize(100, 100); + + // Add a null pixel, render once + renderer.usePixel({ x: 5, y: 5, hex: "null" }); + renderer.startRender(); + renderer.stopRender(); + + const bCtx = blankCtxOf(renderer); + expect(pixelColorAlpha(bCtx, 5, 5).a).toBeGreaterThan(0); + + // Remove that pixel from allPixels and re-render + renderer.startRender(); + renderer.stopRender(); + + // Remove the pixel and restart + (renderer as any).allPixels.delete("5,5"); + renderer.startRender(); + renderer.stopRender(); + + // Pixel should now be transparent (cleared by drawBlank's clearRect) + expect(pixelColorAlpha(bCtx, 5, 5).a).toBe(0); + }); + + it("is a no-op when _stopRender is set", () => { + renderer.startRender(); + renderer.stopRender(); + + // second startRender returns immediately from tryDrawFull/tryDrawBlank + renderer.startRender(); // _stopRender is still false now... + // but stopRender is called to clean up + renderer.stopRender(); + // No crash = pass + }); + }); + + /* ─────── Events ─────── */ + + describe("events", () => { + it("emits ready only from setSize", () => { + const fn = vi.fn(); + renderer.on("ready", fn); + + setup(renderer); + expect(fn).toHaveBeenCalledTimes(1); + + renderer.usePixel({ x: 0, y: 0, hex: "ff0000" }); + renderer.draw(); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it("supports multiple ready listeners", () => { + const fn1 = vi.fn(); + const fn2 = vi.fn(); + renderer.on("ready", fn1); + renderer.on("ready", fn2); + renderer.setSize(100, 100); + expect(fn1).toHaveBeenCalledTimes(1); + expect(fn2).toHaveBeenCalledTimes(1); + }); + + it("removeListener works", () => { + const fn = vi.fn(); + renderer.on("ready", fn); + renderer.removeListener("ready", fn); + renderer.setSize(100, 100); + expect(fn).not.toHaveBeenCalled(); + }); + + it("removeAllListeners clears all listeners", () => { + const fn = vi.fn(); + renderer.on("ready", fn); + renderer.removeAllListeners(); + renderer.setSize(100, 100); + expect(fn).not.toHaveBeenCalled(); + }); + }); +}); \ No newline at end of file diff --git a/packages/client/src/lib/__tests__/canvasUtils.test.ts b/packages/client/src/lib/__tests__/canvasUtils.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..502748d6486b7c6127f405132c2bcf207ef3d919 --- /dev/null +++ b/packages/client/src/lib/__tests__/canvasUtils.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from "vitest"; +import { CanvasUtils } from "../canvas.utils"; + +describe("CanvasUtils", () => { + describe("canvasToPanZoomTransform", () => { + it("returns transform centered on the middle of the canvas", () => { + const result = CanvasUtils.canvasToPanZoomTransform(50, 50, [100, 100]); + expect(result.transformX).toBe(0); + expect(result.transformY).toBe(0); + }); + + it("returns positive transform when coordinates are in the top-left", () => { + const result = CanvasUtils.canvasToPanZoomTransform(0, 0, [100, 100]); + expect(result.transformX).toBe(50); + expect(result.transformY).toBe(50); + }); + + it("returns negative transform when coordinates are in the bottom-right", () => { + const result = CanvasUtils.canvasToPanZoomTransform(100, 100, [100, 100]); + expect(result.transformX).toBe(-50); + expect(result.transformY).toBe(-50); + }); + + it("works with non-square canvas dimensions", () => { + const result = CanvasUtils.canvasToPanZoomTransform(200, 50, [400, 100]); + expect(result.transformX).toBe(0); + expect(result.transformY).toBe(0); + }); + + it("handles large coordinates", () => { + const result = CanvasUtils.canvasToPanZoomTransform( + 5000, + 3000, + [10000, 6000], + ); + expect(result.transformX).toBe(0); + expect(result.transformY).toBe(0); + }); + }); +}); \ No newline at end of file diff --git a/packages/client/src/lib/__tests__/constants.test.ts b/packages/client/src/lib/__tests__/constants.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..67b458a30fc47bb0d6467b5f17eadfdb03c89b5c --- /dev/null +++ b/packages/client/src/lib/__tests__/constants.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect } from "vitest"; +import { + RECOMMENDED_INSTANCES, + DISCORD_INVITE, + MATRIX_ALIAS, + MATRIX_INVITE, + MATRIX_URL, +} from "../constants"; + +describe("constants", () => { + describe("RECOMMENDED_INSTANCES", () => { + it("each instance has required fields", () => { + for (const instance of RECOMMENDED_INSTANCES) { + expect(instance.name).toBeTruthy(); + expect(instance.url).toMatch(/^https:\/\//); + expect(instance.software).toBeTruthy(); + expect(instance.software.name).toBeTruthy(); + expect(instance.software.like).toBeTruthy(); + } + }); + + it("all instance URLs are valid https URLs", () => { + for (const instance of RECOMMENDED_INSTANCES) { + expect(() => new URL(instance.url)).not.toThrow(); + expect(instance.url.startsWith("https://")).toBe(true); + } + }); + }); + + describe("DISCORD_INVITE", () => { + it("is a valid discord.gg URL", () => { + expect(DISCORD_INVITE).toMatch(/^https:\/\/discord\.gg\//); + }); + }); + + describe("MATRIX_ALIAS", () => { + it("valid matrix alias", () => { + expect(MATRIX_ALIAS.startsWith("#")).toBe(true); + expect(MATRIX_ALIAS.includes(":")); + }); + }); + + describe("MATRIX_INVITE", () => { + it("is a matrix.to URL", () => { + expect(MATRIX_INVITE).toMatch(/^https:\/\/matrix\.to\/#\//); + }); + + it("includes the alias", () => { + expect(MATRIX_INVITE).toContain(MATRIX_ALIAS); + }); + }); + + describe("MATRIX_URL", () => { + it("is a matrix:r/ URI", () => { + expect(MATRIX_URL).toMatch(/^matrix:r\//); + }); + }); +}); diff --git a/packages/client/src/lib/__tests__/keybinds.test.ts b/packages/client/src/lib/__tests__/keybinds.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..752ddf955ed94d8590e339b237523bda3e192394 --- /dev/null +++ b/packages/client/src/lib/__tests__/keybinds.test.ts @@ -0,0 +1,249 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; + +// KeybindManager is a singleton whose constructor registers document listeners. +// We test handleInteraction directly — it's the pure keybind-matching logic. + +describe("KeybindManager", () => { + let manager: typeof import("../keybinds").KeybindManager; + let emitSpy: ReturnType; + + beforeEach(async () => { + // Dynamic import to avoid interference from other tests + const mod = await import("../keybinds"); + manager = mod.KeybindManager; + emitSpy = vi.spyOn(manager, "emit").mockImplementation(() => true); + }); + + afterEach(() => { + emitSpy.mockRestore(); + }); + + /* ─── Keyboard keybinds ─── */ + + it("TOGGLE_TEMPLATE on pressing T", () => { + manager.handleInteraction( + { key: "KeyT" }, + { clientX: -1, clientY: -1 }, + ); + expect(emitSpy).toHaveBeenCalledWith("TOGGLE_TEMPLATE", { + clientX: -1, + clientY: -1, + }); + }); + + it("TOGGLE_TEMPLATE with extra shift modifier still matches", () => { + // Keybind doesn't specify shift, so shift doesn't block the match + manager.handleInteraction( + { key: "KeyT", shift: true }, + { clientX: -1, clientY: -1 }, + ); + expect(emitSpy).toHaveBeenCalledWith("TOGGLE_TEMPLATE", { + clientX: -1, + clientY: -1, + }); + }); + + it("TOGGLE_BLANK on pressing V", () => { + manager.handleInteraction( + { key: "KeyV" }, + { clientX: -1, clientY: -1 }, + ); + expect(emitSpy).toHaveBeenCalledWith("TOGGLE_BLANK", { + clientX: -1, + clientY: -1, + }); + }); + + it("TOGGLE_BLANK on pressing B (alternative keybind)", () => { + manager.handleInteraction( + { key: "KeyB" }, + { clientX: -1, clientY: -1 }, + ); + expect(emitSpy).toHaveBeenCalledWith("TOGGLE_BLANK", { + clientX: -1, + clientY: -1, + }); + }); + + it("TOGGLE_HEATMAP on pressing H", () => { + manager.handleInteraction( + { key: "KeyH" }, + { clientX: -1, clientY: -1 }, + ); + expect(emitSpy).toHaveBeenCalledWith("TOGGLE_HEATMAP", { + clientX: -1, + clientY: -1, + }); + }); + + it("TOGGLE_GRID on pressing G", () => { + manager.handleInteraction( + { key: "KeyG" }, + { clientX: -1, clientY: -1 }, + ); + expect(emitSpy).toHaveBeenCalledWith("TOGGLE_GRID", { + clientX: -1, + clientY: -1, + }); + }); + + it("TOGGLE_MOD_MENU on pressing M", () => { + manager.handleInteraction( + { key: "KeyM" }, + { clientX: -1, clientY: -1 }, + ); + expect(emitSpy).toHaveBeenCalledWith("TOGGLE_MOD_MENU", { + clientX: -1, + clientY: -1, + }); + }); + + it("DESELECT_COLOR on pressing Escape", () => { + manager.handleInteraction( + { key: "Escape" }, + { clientX: -1, clientY: -1 }, + ); + expect(emitSpy).toHaveBeenCalledWith("DESELECT_COLOR", { + clientX: -1, + clientY: -1, + }); + }); + + it("SNAPSHOT on pressing P", () => { + manager.handleInteraction( + { key: "KeyP" }, + { clientX: -1, clientY: -1 }, + ); + expect(emitSpy).toHaveBeenCalledWith("SNAPSHOT", { + clientX: -1, + clientY: -1, + }); + }); + + it("KEYBINDS on pressing Shift+/", () => { + manager.handleInteraction( + { key: "Slash", shift: true }, + { clientX: -1, clientY: -1 }, + ); + expect(emitSpy).toHaveBeenCalledWith("KEYBINDS", { + clientX: -1, + clientY: -1, + }); + }); + + it("Slash without shift does not trigger KEYBINDS", () => { + manager.handleInteraction( + { key: "Slash" }, + { clientX: -1, clientY: -1 }, + ); + expect(emitSpy).not.toHaveBeenCalled(); + }); + + /* ─── Mouse keybinds ─── */ + + it("PIXEL_WHOIS on Shift+LCLICK", () => { + manager.handleInteraction( + { key: "LCLICK", shift: true }, + { clientX: 100, clientY: 200 }, + ); + expect(emitSpy).toHaveBeenCalledWith("PIXEL_WHOIS", { + clientX: 100, + clientY: 200, + }); + }); + + it("PIXEL_WHOIS on LONG_PRESS", () => { + manager.handleInteraction( + { key: "LONG_PRESS" }, + { clientX: 50, clientY: 60 }, + ); + expect(emitSpy).toHaveBeenCalledWith("PIXEL_WHOIS", { + clientX: 50, + clientY: 60, + }); + }); + + it("plain LCLICK does not trigger PIXEL_WHOIS", () => { + manager.handleInteraction( + { key: "LCLICK" }, + { clientX: -1, clientY: -1 }, + ); + expect(emitSpy).not.toHaveBeenCalledWith( + "PIXEL_WHOIS", + expect.anything(), + ); + }); + + it("TEMPLATE_MOVE on Alt+LCLICK", () => { + manager.handleInteraction( + { key: "LCLICK", alt: true }, + { clientX: 30, clientY: 40 }, + ); + expect(emitSpy).toHaveBeenCalledWith("TEMPLATE_MOVE", { + clientX: 30, + clientY: 40, + }); + }); + + it("PICK_COLOR on MCLICK (middle click)", () => { + manager.handleInteraction( + { key: "MCLICK" }, + { clientX: -1, clientY: -1 }, + ); + expect(emitSpy).toHaveBeenCalledWith("PICK_COLOR", { + clientX: -1, + clientY: -1, + }); + }); + + it("MOD_SELECT on Ctrl+LCLICK", () => { + manager.handleInteraction( + { key: "LCLICK", ctrl: true }, + { clientX: 10, clientY: 20 }, + ); + expect(emitSpy).toHaveBeenCalledWith("MOD_SELECT", { + clientX: 10, + clientY: 20, + }); + }); + + /* ─── Modifier-specific tests ─── */ + + it("Shift+LCLICK does not also trigger TEMPLATE_MOVE (no alt)", () => { + manager.handleInteraction( + { key: "LCLICK", shift: true }, + { clientX: -1, clientY: -1 }, + ); + // PIXEL_WHOIS requires shift + expect(emitSpy).toHaveBeenCalledWith("PIXEL_WHOIS", expect.anything()); + // TEMPLATE_MOVE requires alt — should not fire + expect(emitSpy).not.toHaveBeenCalledWith( + "TEMPLATE_MOVE", + expect.anything(), + ); + // MOD_SELECT requires ctrl — should not fire + expect(emitSpy).not.toHaveBeenCalledWith( + "MOD_SELECT", + expect.anything(), + ); + }); + + /* ─── Unknown / non-matching ─── */ + + it("unknown key returns false and emits nothing", () => { + const result = manager.handleInteraction( + { key: "F12" }, + { clientX: -1, clientY: -1 }, + ); + expect(result).toBe(false); + expect(emitSpy).not.toHaveBeenCalled(); + }); + + it("returns true when at least one keybind matches", () => { + const result = manager.handleInteraction( + { key: "KeyT" }, + { clientX: -1, clientY: -1 }, + ); + expect(result).toBe(true); + }); +}); \ No newline at end of file diff --git a/packages/client/src/lib/__tests__/utils.test.ts b/packages/client/src/lib/__tests__/utils.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..ed1e383b5c9caef0a3def561b83451e63a87f0a3 --- /dev/null +++ b/packages/client/src/lib/__tests__/utils.test.ts @@ -0,0 +1,42 @@ +import { describe, it, expect } from "vitest"; +import { rgbToHex } from "../utils"; + +describe("rgbToHex", () => { + it('returns "#FF0000" for red', () => { + expect(rgbToHex(255, 0, 0)).toBe("#FF0000"); + }); + + it('returns "#00FF00" for green', () => { + expect(rgbToHex(0, 255, 0)).toBe("#00FF00"); + }); + + it('returns "#0000FF" for blue', () => { + expect(rgbToHex(0, 0, 255)).toBe("#0000FF"); + }); + + it('returns "#000000" for black', () => { + expect(rgbToHex(0, 0, 0)).toBe("#000000"); + }); + + it('returns "#FFFFFF" for white', () => { + expect(rgbToHex(255, 255, 255)).toBe("#FFFFFF"); + }); + + it("handles leading zeros in components", () => { + expect(rgbToHex(15, 15, 15)).toBe("#0F0F0F"); + }); + + it("returns uppercase hex", () => { + const result = rgbToHex(171, 205, 239); + expect(result).toBe("#ABCDEF"); + expect(result).toEqual(result.toUpperCase()); + }); + + it("pads single-digit hex components with leading zero", () => { + expect(rgbToHex(1, 2, 3)).toBe("#010203"); + }); + + it("handles mid-range values", () => { + expect(rgbToHex(128, 128, 128)).toBe("#808080"); + }); +}); \ No newline at end of file diff --git a/packages/client/vitest.config.ts b/packages/client/vitest.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..3c3e6fb755da336adbb62ab62ab0faf202101b3b --- /dev/null +++ b/packages/client/vitest.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from "vitest/config"; +import { playwright } from "@vitest/browser-playwright"; +import react from "@vitejs/plugin-react"; + +const BUILD_YEAR = new Date().getFullYear(); + +export default defineConfig({ + plugins: [react()], + define: { + __COMMIT_HASH__: JSON.stringify("test"), + __BUILD_YEAR__: JSON.stringify(BUILD_YEAR), + __SENTRY_DSN__: JSON.stringify(null), + __DEV_CHAT__: false, + }, + test: { + include: ["src/**/*.{test,spec}.?(c|m)[jt]s?(x)"], + browser: { + enabled: true, + provider: playwright(), + instances: [{ browser: "chromium" }, { browser: "firefox" }], + }, + coverage: { + provider: "istanbul", + }, + }, +}); diff --git a/yarn.lock b/yarn.lock index 719a620a024b5791302995c4c8ecfae50fc61c75..7cb7a077dec657e066ad35e1bc80d211ecbb60e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -314,6 +314,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-string-parser@npm:7.29.7" + checksum: 10c0/194bc0f1716e396d5ffde56ad6119745fb9557662c98611590e5e454906783a4ccb21ce93056b8eb69a4909044834e45d96e50ac695bbe9e3221648fe033c06c + languageName: node + linkType: hard + "@babel/helper-string-parser@npm:^8.0.0-rc.3, @babel/helper-string-parser@npm:^8.0.0-rc.4": version: 8.0.0-rc.4 resolution: "@babel/helper-string-parser@npm:8.0.0-rc.4" @@ -335,6 +342,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-validator-identifier@npm:7.29.7" + checksum: 10c0/4795354e7ae0dcafa72de1cd04ec51252dc1498517170beaf019e03effc5b7bf13c6b21a3949a77e07b8125be7f106ed1131350d8ebd4566ae874094a726d62b + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^8.0.0-rc.3, @babel/helper-validator-identifier@npm:^8.0.0-rc.4": version: 8.0.0-rc.4 resolution: "@babel/helper-validator-identifier@npm:8.0.0-rc.4" @@ -392,6 +406,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.29.3": + version: 7.29.7 + resolution: "@babel/parser@npm:7.29.7" + dependencies: + "@babel/types": "npm:^7.29.7" + bin: + parser: ./bin/babel-parser.js + checksum: 10c0/65133038f80b54a714d6027cb77cee3f9a6b5c4c6842ce674301e13947cbcbfa8055e63acaf1b84c085d34226a14425b2c2b97b829e0e226d2e8f1299942a51d + languageName: node + linkType: hard + "@babel/parser@npm:^8.0.0-beta.4, @babel/parser@npm:^8.0.0-rc.3": version: 8.0.0-rc.4 resolution: "@babel/parser@npm:8.0.0-rc.4" @@ -1690,6 +1715,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/types@npm:7.29.7" + dependencies: + "@babel/helper-string-parser": "npm:^7.29.7" + "@babel/helper-validator-identifier": "npm:^7.29.7" + checksum: 10c0/b6623994c69717fa27294f5fa46d59140338e2d86c6c1c13085c84ef7d53086ee357fbf4fe9abe3dd3da75734dc77c4c0df2f90fb29e667558bb3b3fb705e88f + languageName: node + linkType: hard + "@babel/types@npm:^8.0.0-rc.3, @babel/types@npm:^8.0.0-rc.4": version: 8.0.0-rc.4 resolution: "@babel/types@npm:8.0.0-rc.4" @@ -1707,6 +1742,13 @@ __metadata: languageName: node linkType: hard +"@blazediff/core@npm:1.9.1": + version: 1.9.1 + resolution: "@blazediff/core@npm:1.9.1" + checksum: 10c0/fd45cdd0544002341d74831a179ef693a81414abd348c1ff0c01086c0ea03f5e5ee284c4e16c2e6fb3670c265f90a3d85752b9360320efa9a835928e604dae77 + languageName: node + linkType: hard + "@bufbuild/protobuf@npm:^2.5.0": version: 2.12.0 resolution: "@bufbuild/protobuf@npm:2.12.0" @@ -4155,7 +4197,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.5": +"@jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.13, @jridgewell/gen-mapping@npm:^0.3.5": version: 0.3.13 resolution: "@jridgewell/gen-mapping@npm:0.3.13" dependencies: @@ -4189,6 +4231,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/trace-mapping@npm:0.3.31, @jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25, @jridgewell/trace-mapping@npm:^0.3.28": + version: 0.3.31 + resolution: "@jridgewell/trace-mapping@npm:0.3.31" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.1.0" + "@jridgewell/sourcemap-codec": "npm:^1.4.14" + checksum: 10c0/4b30ec8cd56c5fd9a661f088230af01e0c1a3888d11ffb6b47639700f71225be21d1f7e168048d6d4f9449207b978a235c07c8f15c07705685d16dc06280e9d9 + languageName: node + linkType: hard + "@jridgewell/trace-mapping@npm:0.3.9": version: 0.3.9 resolution: "@jridgewell/trace-mapping@npm:0.3.9" @@ -4199,16 +4251,6 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25, @jridgewell/trace-mapping@npm:^0.3.28": - version: 0.3.31 - resolution: "@jridgewell/trace-mapping@npm:0.3.31" - dependencies: - "@jridgewell/resolve-uri": "npm:^3.1.0" - "@jridgewell/sourcemap-codec": "npm:^1.4.14" - checksum: 10c0/4b30ec8cd56c5fd9a661f088230af01e0c1a3888d11ffb6b47639700f71225be21d1f7e168048d6d4f9449207b978a235c07c8f15c07705685d16dc06280e9d9 - languageName: node - linkType: hard - "@matrix-org/matrix-sdk-crypto-wasm@npm:^15.3.0": version: 15.3.0 resolution: "@matrix-org/matrix-sdk-crypto-wasm@npm:15.3.0" @@ -6267,6 +6309,13 @@ __metadata: languageName: node linkType: hard +"@oxc-project/types@npm:=0.133.0": + version: 0.133.0 + resolution: "@oxc-project/types@npm:0.133.0" + checksum: 10c0/70c57ba58644f7ec217b670c301801f4d06995f4ccdba6b2bd106ea3e5ee49d616573e6ef8d55530b87571a960696543687f3850e87ad173d3f88965c30cdd63 + languageName: node + linkType: hard + "@oxc-project/types@npm:=0.93.0": version: 0.93.0 resolution: "@oxc-project/types@npm:0.93.0" @@ -6441,6 +6490,13 @@ __metadata: languageName: node linkType: hard +"@polka/url@npm:^1.0.0-next.24": + version: 1.0.0-next.29 + resolution: "@polka/url@npm:1.0.0-next.29" + checksum: 10c0/0d58e081844095cb029d3c19a659bfefd09d5d51a2f791bc61eba7ea826f13d6ee204a8a448c2f5a855c17df07b37517373ff916dd05801063c0568ae9937684 + languageName: node + linkType: hard + "@prisma/adapter-pg@npm:^7.4.0": version: 7.4.0 resolution: "@prisma/adapter-pg@npm:7.4.0" @@ -10977,6 +11033,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-android-arm64@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-android-arm64@npm:1.0.3" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@rolldown/binding-darwin-arm64@npm:1.0.0-beta.41": version: 1.0.0-beta.41 resolution: "@rolldown/binding-darwin-arm64@npm:1.0.0-beta.41" @@ -10991,6 +11054,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-darwin-arm64@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-darwin-arm64@npm:1.0.3" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@rolldown/binding-darwin-x64@npm:1.0.0-beta.41": version: 1.0.0-beta.41 resolution: "@rolldown/binding-darwin-x64@npm:1.0.0-beta.41" @@ -11005,6 +11075,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-darwin-x64@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-darwin-x64@npm:1.0.3" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@rolldown/binding-freebsd-x64@npm:1.0.0-beta.41": version: 1.0.0-beta.41 resolution: "@rolldown/binding-freebsd-x64@npm:1.0.0-beta.41" @@ -11019,6 +11096,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-freebsd-x64@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-freebsd-x64@npm:1.0.3" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-beta.41": version: 1.0.0-beta.41 resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-beta.41" @@ -11033,6 +11117,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.3" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@rolldown/binding-linux-arm64-gnu@npm:1.0.0-beta.41": version: 1.0.0-beta.41 resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.0-beta.41" @@ -11047,6 +11138,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-linux-arm64-gnu@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.3" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@rolldown/binding-linux-arm64-musl@npm:1.0.0-beta.41": version: 1.0.0-beta.41 resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.0-beta.41" @@ -11061,6 +11159,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-linux-arm64-musl@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.3" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.18": version: 1.0.0-rc.18 resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.18" @@ -11068,6 +11173,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-linux-ppc64-gnu@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.3" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + "@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.18": version: 1.0.0-rc.18 resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.18" @@ -11075,6 +11187,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-linux-s390x-gnu@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.3" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + "@rolldown/binding-linux-x64-gnu@npm:1.0.0-beta.41": version: 1.0.0-beta.41 resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.0-beta.41" @@ -11089,6 +11208,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-linux-x64-gnu@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.3" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@rolldown/binding-linux-x64-musl@npm:1.0.0-beta.41": version: 1.0.0-beta.41 resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.0-beta.41" @@ -11103,6 +11229,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-linux-x64-musl@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.3" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@rolldown/binding-openharmony-arm64@npm:1.0.0-beta.41": version: 1.0.0-beta.41 resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.0-beta.41" @@ -11117,6 +11250,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-openharmony-arm64@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.3" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + "@rolldown/binding-wasm32-wasi@npm:1.0.0-beta.41": version: 1.0.0-beta.41 resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.0-beta.41" @@ -11137,6 +11277,17 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-wasm32-wasi@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.3" + dependencies: + "@emnapi/core": "npm:1.10.0" + "@emnapi/runtime": "npm:1.10.0" + "@napi-rs/wasm-runtime": "npm:^1.1.4" + conditions: cpu=wasm32 + languageName: node + linkType: hard + "@rolldown/binding-win32-arm64-msvc@npm:1.0.0-beta.41": version: 1.0.0-beta.41 resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.0-beta.41" @@ -11151,6 +11302,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-win32-arm64-msvc@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.3" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@rolldown/binding-win32-ia32-msvc@npm:1.0.0-beta.41": version: 1.0.0-beta.41 resolution: "@rolldown/binding-win32-ia32-msvc@npm:1.0.0-beta.41" @@ -11172,6 +11330,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-win32-x64-msvc@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.3" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@rolldown/pluginutils@npm:1.0.0-beta.41": version: 1.0.0-beta.41 resolution: "@rolldown/pluginutils@npm:1.0.0-beta.41" @@ -11193,6 +11358,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/pluginutils@npm:^1.0.0": + version: 1.0.1 + resolution: "@rolldown/pluginutils@npm:1.0.1" + checksum: 10c0/99d9b06d90196823e4d8c841f258db7a16e5dbba5824a2962b05d907b79f1ba929d56f22dd744fd530936e568c865ee56a719dc31e57e13bc0a8eb4764a8d8dd + languageName: node + linkType: hard + "@rollup/pluginutils@npm:^5.1.4": version: 5.3.0 resolution: "@rollup/pluginutils@npm:5.3.0" @@ -11551,6 +11723,8 @@ __metadata: "@tsconfig/vite-react": "npm:^7.0.2" "@types/lodash.throttle": "npm:^4.1.9" "@types/socket.io-client": "npm:^3.0.0" + "@vitest/browser-playwright": "npm:^4.1.8" + "@vitest/coverage-istanbul": "npm:4.1.8" altcha-lib: "npm:^1.4.1" date-fns: "npm:^4.1.0" eslint: "npm:^9.39.2" @@ -11573,6 +11747,7 @@ __metadata: use-sound: "npm:^5.0.0" vite: "npm:^7.3.1" vite-plugin-banner: "npm:^0.8.1" + vitest: "npm:^4.1.8" languageName: unknown linkType: soft @@ -12186,7 +12361,7 @@ __metadata: languageName: node linkType: hard -"@standard-schema/spec@npm:^1.0.0": +"@standard-schema/spec@npm:^1.0.0, @standard-schema/spec@npm:^1.1.0": version: 1.1.0 resolution: "@standard-schema/spec@npm:1.1.0" checksum: 10c0/d90f55acde4b2deb983529c87e8025fa693de1a5e8b49ecc6eb84d1fd96328add0e03d7d551442156c7432fd78165b2c26ff561b970a9a881f046abb78d6a526 @@ -12794,6 +12969,16 @@ __metadata: languageName: node linkType: hard +"@types/chai@npm:^5.2.2": + version: 5.2.3 + resolution: "@types/chai@npm:5.2.3" + dependencies: + "@types/deep-eql": "npm:*" + assertion-error: "npm:^2.0.1" + checksum: 10c0/e0ef1de3b6f8045a5e473e867c8565788c444271409d155588504840ad1a53611011f85072188c2833941189400228c1745d78323dac13fcede9c2b28bacfb2f + languageName: node + linkType: hard + "@types/connect@npm:*, @types/connect@npm:3.4.38": version: 3.4.38 resolution: "@types/connect@npm:3.4.38" @@ -12828,6 +13013,13 @@ __metadata: languageName: node linkType: hard +"@types/deep-eql@npm:*": + version: 4.0.2 + resolution: "@types/deep-eql@npm:4.0.2" + checksum: 10c0/bf3f811843117900d7084b9d0c852da9a044d12eb40e6de73b552598a6843c21291a8a381b0532644574beecd5e3491c5ff3a0365ab86b15d59862c025384844 + languageName: node + linkType: hard + "@types/estree@npm:1.0.8, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.6": version: 1.0.8 resolution: "@types/estree@npm:1.0.8" @@ -13633,6 +13825,143 @@ __metadata: languageName: node linkType: hard +"@vitest/browser-playwright@npm:^4.1.8": + version: 4.1.8 + resolution: "@vitest/browser-playwright@npm:4.1.8" + dependencies: + "@vitest/browser": "npm:4.1.8" + "@vitest/mocker": "npm:4.1.8" + tinyrainbow: "npm:^3.1.0" + peerDependencies: + playwright: "*" + vitest: 4.1.8 + peerDependenciesMeta: + playwright: + optional: false + checksum: 10c0/aa2a9b7a9614f6a4860271e1eef76873a1970a534e59c5ffc7147d13611cbcab38f57f1de418ebaeb89b2c3e675be3b4f17425ac6d0198a1eecf68fe9e517857 + languageName: node + linkType: hard + +"@vitest/browser@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/browser@npm:4.1.8" + dependencies: + "@blazediff/core": "npm:1.9.1" + "@vitest/mocker": "npm:4.1.8" + "@vitest/utils": "npm:4.1.8" + magic-string: "npm:^0.30.21" + pngjs: "npm:^7.0.0" + sirv: "npm:^3.0.2" + tinyrainbow: "npm:^3.1.0" + ws: "npm:^8.19.0" + peerDependencies: + vitest: 4.1.8 + checksum: 10c0/9e78c4b2273f5defd43e822361ec9c6f2804e9144fc5679a69171a588476c945a1ae3de313ad77239683490a3df703766257efc8a509c1f244c4b8f3b9ab4fe6 + languageName: node + linkType: hard + +"@vitest/coverage-istanbul@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/coverage-istanbul@npm:4.1.8" + dependencies: + "@babel/core": "npm:^7.29.0" + "@istanbuljs/schema": "npm:^0.1.3" + "@jridgewell/gen-mapping": "npm:^0.3.13" + "@jridgewell/trace-mapping": "npm:0.3.31" + istanbul-lib-coverage: "npm:^3.2.2" + istanbul-lib-report: "npm:^3.0.1" + istanbul-reports: "npm:^3.2.0" + magicast: "npm:^0.5.2" + obug: "npm:^2.1.1" + tinyrainbow: "npm:^3.1.0" + peerDependencies: + vitest: 4.1.8 + checksum: 10c0/cb4d1df51fbe078391f17da0d09f2e7d765aa001bbaebaa6665ae9d356d9fd34f62b3739da175492b566cd2f35a3fd9d1cda61f35544f99b88611566931a772e + languageName: node + linkType: hard + +"@vitest/expect@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/expect@npm:4.1.8" + dependencies: + "@standard-schema/spec": "npm:^1.1.0" + "@types/chai": "npm:^5.2.2" + "@vitest/spy": "npm:4.1.8" + "@vitest/utils": "npm:4.1.8" + chai: "npm:^6.2.2" + tinyrainbow: "npm:^3.1.0" + checksum: 10c0/f7bf6c720d2427c3bd0b35472ebd84d963be7d09ecf52a0fb05e8c4d5d0c9ee164a8c28eee6360947be1b245b47faefab54560cb98e5cb678c1c1074260b9149 + languageName: node + linkType: hard + +"@vitest/mocker@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/mocker@npm:4.1.8" + dependencies: + "@vitest/spy": "npm:4.1.8" + estree-walker: "npm:^3.0.3" + magic-string: "npm:^0.30.21" + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + checksum: 10c0/f8cb2b8b55dc2cba0b2399aeee528b0187042f22cbc2d50a4fd6141f5aa246ebc41700f45dd1d73eca44ddfb57dcde48b2eb317bfbb1198f5ab2cc4fd04b2ea0 + languageName: node + linkType: hard + +"@vitest/pretty-format@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/pretty-format@npm:4.1.8" + dependencies: + tinyrainbow: "npm:^3.1.0" + checksum: 10c0/553c456692a4b9ae13cd116c234c74b4495e0f1a0d5c51ffc3fab8ea085e3550769967e29db79bdac0cf127b1bf88b7f70bfba3dcc72be6bddf834433e30cc91 + languageName: node + linkType: hard + +"@vitest/runner@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/runner@npm:4.1.8" + dependencies: + "@vitest/utils": "npm:4.1.8" + pathe: "npm:^2.0.3" + checksum: 10c0/706808a4b7b95ea9a9268fc152dd39e15a9a754f37c7990aea167486a9094caa913dae454771ae02c18dccfabd667f8cc38eed33a1307a79d32a89878b5bcce1 + languageName: node + linkType: hard + +"@vitest/snapshot@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/snapshot@npm:4.1.8" + dependencies: + "@vitest/pretty-format": "npm:4.1.8" + "@vitest/utils": "npm:4.1.8" + magic-string: "npm:^0.30.21" + pathe: "npm:^2.0.3" + checksum: 10c0/ba4c32112491d42d24986f921c50ede5edbdb4b7eafa16c72cf8d2c9ecc44121fdb3d9365236747a9841f0d6776affc6457470fcbb082df9dbc28c24792a0c6d + languageName: node + linkType: hard + +"@vitest/spy@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/spy@npm:4.1.8" + checksum: 10c0/3c10c0325a09d16bc0e77c0be96c47c15416186e33332880c0d1dd0a51d51a866091067b81f2a2ef6fb422a7760e6cf15c04d91a0eca4d59f62e8c8401fa53fc + languageName: node + linkType: hard + +"@vitest/utils@npm:4.1.8": + version: 4.1.8 + resolution: "@vitest/utils@npm:4.1.8" + dependencies: + "@vitest/pretty-format": "npm:4.1.8" + convert-source-map: "npm:^2.0.0" + tinyrainbow: "npm:^3.1.0" + checksum: 10c0/acda9d3d640c1ebc81afb358ac30589d7d7d583af81e2d09419f0af9cbe41f3ce0b90527326943bf0da51614be5fc31afcd32259f6beb32b3417999d6ef380f3 + languageName: node + linkType: hard + "@volar/language-core@npm:2.4.28, @volar/language-core@npm:~2.4.11": version: 2.4.28 resolution: "@volar/language-core@npm:2.4.28" @@ -14117,6 +14446,13 @@ __metadata: languageName: node linkType: hard +"assertion-error@npm:^2.0.1": + version: 2.0.1 + resolution: "assertion-error@npm:2.0.1" + checksum: 10c0/bbbcb117ac6480138f8c93cf7f535614282dea9dc828f540cdece85e3c665e8f78958b96afac52f29ff883c72638e6a87d469ecc9fe5bc902df03ed24a55dba8 + languageName: node + linkType: hard + "ast-kit@npm:^3.0.0-beta.1": version: 3.0.0-beta.1 resolution: "ast-kit@npm:3.0.0-beta.1" @@ -14652,6 +14988,13 @@ __metadata: languageName: node linkType: hard +"chai@npm:^6.2.2": + version: 6.2.2 + resolution: "chai@npm:6.2.2" + checksum: 10c0/e6c69e5f0c11dffe6ea13d0290936ebb68fcc1ad688b8e952e131df6a6d5797d5e860bc55cef1aca2e950c3e1f96daf79e9d5a70fb7dbaab4e46355e2635ed53 + languageName: node + linkType: hard + "chalk@npm:^4.0.0, chalk@npm:^4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" @@ -15765,6 +16108,13 @@ __metadata: languageName: node linkType: hard +"es-module-lexer@npm:^2.0.0": + version: 2.1.0 + resolution: "es-module-lexer@npm:2.1.0" + checksum: 10c0/93bcf2454fa72d67fe3ccd0abef8ce7933f5840a319513418a643dd8e9c6aa8f49709cecfae02ded722805dd327232d30723a807cc52e6809d6ac697c62c29fb + languageName: node + linkType: hard + "es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": version: 1.1.1 resolution: "es-object-atoms@npm:1.1.1" @@ -16395,6 +16745,13 @@ __metadata: languageName: node linkType: hard +"expect-type@npm:^1.3.0": + version: 1.3.0 + resolution: "expect-type@npm:1.3.0" + checksum: 10c0/8412b3fe4f392c420ab41dae220b09700e4e47c639a29ba7ba2e83cc6cffd2b4926f7ac9e47d7e277e8f4f02acda76fd6931cb81fd2b382fa9477ef9ada953fd + languageName: node + linkType: hard + "expect@npm:30.2.0, expect@npm:^30.0.0": version: 30.2.0 resolution: "expect@npm:30.2.0" @@ -16785,6 +17142,16 @@ __metadata: languageName: node linkType: hard +"fsevents@npm:2.3.2": + version: 2.3.2 + resolution: "fsevents@npm:2.3.2" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/be78a3efa3e181cda3cf7a4637cb527bcebb0bd0ea0440105a3bb45b86f9245b307dc10a2507e8f4498a7d4ec349d1910f4d73e4d4495b16103106e07eee735b + conditions: os=darwin + languageName: node + linkType: hard + "fsevents@npm:^2.3.3, fsevents@npm:~2.3.2, fsevents@npm:~2.3.3": version: 2.3.3 resolution: "fsevents@npm:2.3.3" @@ -16795,6 +17162,15 @@ __metadata: languageName: node linkType: hard +"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin": + version: 2.3.2 + resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin::version=2.3.2&hash=df0bf1" + dependencies: + node-gyp: "npm:latest" + conditions: os=darwin + languageName: node + linkType: hard + "fsevents@patch:fsevents@npm%3A^2.3.3#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" @@ -17881,7 +18257,7 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0": +"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0, istanbul-lib-coverage@npm:^3.2.2": version: 3.2.2 resolution: "istanbul-lib-coverage@npm:3.2.2" checksum: 10c0/6c7ff2106769e5f592ded1fb418f9f73b4411fd5a084387a5410538332b6567cd1763ff6b6cadca9b9eb2c443cce2f7ea7d7f1b8d315f9ce58539793b1e0922b @@ -17901,7 +18277,7 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-report@npm:^3.0.0": +"istanbul-lib-report@npm:^3.0.0, istanbul-lib-report@npm:^3.0.1": version: 3.0.1 resolution: "istanbul-lib-report@npm:3.0.1" dependencies: @@ -17923,7 +18299,7 @@ __metadata: languageName: node linkType: hard -"istanbul-reports@npm:^3.1.3": +"istanbul-reports@npm:^3.1.3, istanbul-reports@npm:^3.2.0": version: 3.2.0 resolution: "istanbul-reports@npm:3.2.0" dependencies: @@ -18744,7 +19120,7 @@ __metadata: languageName: node linkType: hard -"lightningcss@npm:1.32.0, lightningcss@npm:^1.30.1": +"lightningcss@npm:1.32.0, lightningcss@npm:^1.30.1, lightningcss@npm:^1.32.0": version: 1.32.0 resolution: "lightningcss@npm:1.32.0" dependencies: @@ -19007,6 +19383,17 @@ __metadata: languageName: node linkType: hard +"magicast@npm:^0.5.2": + version: 0.5.3 + resolution: "magicast@npm:0.5.3" + dependencies: + "@babel/parser": "npm:^7.29.3" + "@babel/types": "npm:^7.29.0" + source-map-js: "npm:^1.2.1" + checksum: 10c0/e288c027ae5f2a794a59148cb114f4b60f1d5c03090de6c60b4d187f12d1de9158779cd7c39cea391609f4f10cd7ea737929f25f7ce44f7a96ba96ec1a477e39 + languageName: node + linkType: hard + "make-dir@npm:^4.0.0": version: 4.0.0 resolution: "make-dir@npm:4.0.0" @@ -19729,6 +20116,13 @@ __metadata: languageName: node linkType: hard +"mrmime@npm:^2.0.0": + version: 2.0.1 + resolution: "mrmime@npm:2.0.1" + checksum: 10c0/af05afd95af202fdd620422f976ad67dc18e6ee29beb03dd1ce950ea6ef664de378e44197246df4c7cdd73d47f2e7143a6e26e473084b9e4aa2095c0ad1e1761 + languageName: node + linkType: hard + "ms@npm:2.0.0": version: 2.0.0 resolution: "ms@npm:2.0.0" @@ -19828,6 +20222,15 @@ __metadata: languageName: node linkType: hard +"nanoid@npm:^3.3.12": + version: 3.3.12 + resolution: "nanoid@npm:3.3.12" + bin: + nanoid: bin/nanoid.cjs + checksum: 10c0/ba142b7b39e11e80c16dd74b0365d407880c87c1cf7e1480956981ae940ee36060fa5b6f092cd1e315184dd19244c657bd017d03327bd3c62247d691c5e8edfb + languageName: node + linkType: hard + "napi-postinstall@npm:^0.3.0": version: 0.3.4 resolution: "napi-postinstall@npm:0.3.4" @@ -20623,6 +21026,30 @@ __metadata: languageName: node linkType: hard +"playwright-core@npm:1.60.0": + version: 1.60.0 + resolution: "playwright-core@npm:1.60.0" + bin: + playwright-core: cli.js + checksum: 10c0/99ccd43923b6e9355e0723b7fe221e6326efd4687f8dafff951313662aea11db51f542a9c2122c704c445fb9baae1c9ec9fa6f895126bbddd9fe92313f6942c9 + languageName: node + linkType: hard + +"playwright@npm:^1.60.0": + version: 1.60.0 + resolution: "playwright@npm:1.60.0" + dependencies: + fsevents: "npm:2.3.2" + playwright-core: "npm:1.60.0" + dependenciesMeta: + fsevents: + optional: true + bin: + playwright: cli.js + checksum: 10c0/714ad76d85b4865d7e43c0012f9039800c1485373388973ed39d79339cee5ad467052d1e2f1eaeca107a1cb6e65342186a8578a4c3504853d84c3a691250d5db + languageName: node + linkType: hard + "pluralize@npm:^8.0.0": version: 8.0.0 resolution: "pluralize@npm:8.0.0" @@ -20630,6 +21057,13 @@ __metadata: languageName: node linkType: hard +"pngjs@npm:^7.0.0": + version: 7.0.0 + resolution: "pngjs@npm:7.0.0" + checksum: 10c0/0d4c7a0fd476a9c33df7d0a2a73e1d56537628a668841f6995c2bca070cf30819f9254a64363266bc14ef2fee47659dd3b4f2b18eec7ab65143015139f497b38 + languageName: node + linkType: hard + "possible-typed-array-names@npm:^1.0.0": version: 1.1.0 resolution: "possible-typed-array-names@npm:1.1.0" @@ -20644,6 +21078,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:^8.5.15": + version: 8.5.15 + resolution: "postcss@npm:8.5.15" + dependencies: + nanoid: "npm:^3.3.12" + picocolors: "npm:^1.1.1" + source-map-js: "npm:^1.2.1" + checksum: 10c0/7f2e63ae22fbe43aace1bf652bd99da4e90737c64194d49e51ddc9cd0f9e51ff2861a7d734379b494deffa03a880a5c65eec70bc29ee9ebaa7136dde3eee8f31 + languageName: node + linkType: hard + "postcss@npm:^8.5.6": version: 8.5.6 resolution: "postcss@npm:8.5.6" @@ -21518,6 +21963,64 @@ __metadata: languageName: node linkType: hard +"rolldown@npm:1.0.3": + version: 1.0.3 + resolution: "rolldown@npm:1.0.3" + dependencies: + "@oxc-project/types": "npm:=0.133.0" + "@rolldown/binding-android-arm64": "npm:1.0.3" + "@rolldown/binding-darwin-arm64": "npm:1.0.3" + "@rolldown/binding-darwin-x64": "npm:1.0.3" + "@rolldown/binding-freebsd-x64": "npm:1.0.3" + "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.3" + "@rolldown/binding-linux-arm64-gnu": "npm:1.0.3" + "@rolldown/binding-linux-arm64-musl": "npm:1.0.3" + "@rolldown/binding-linux-ppc64-gnu": "npm:1.0.3" + "@rolldown/binding-linux-s390x-gnu": "npm:1.0.3" + "@rolldown/binding-linux-x64-gnu": "npm:1.0.3" + "@rolldown/binding-linux-x64-musl": "npm:1.0.3" + "@rolldown/binding-openharmony-arm64": "npm:1.0.3" + "@rolldown/binding-wasm32-wasi": "npm:1.0.3" + "@rolldown/binding-win32-arm64-msvc": "npm:1.0.3" + "@rolldown/binding-win32-x64-msvc": "npm:1.0.3" + "@rolldown/pluginutils": "npm:^1.0.0" + dependenciesMeta: + "@rolldown/binding-android-arm64": + optional: true + "@rolldown/binding-darwin-arm64": + optional: true + "@rolldown/binding-darwin-x64": + optional: true + "@rolldown/binding-freebsd-x64": + optional: true + "@rolldown/binding-linux-arm-gnueabihf": + optional: true + "@rolldown/binding-linux-arm64-gnu": + optional: true + "@rolldown/binding-linux-arm64-musl": + optional: true + "@rolldown/binding-linux-ppc64-gnu": + optional: true + "@rolldown/binding-linux-s390x-gnu": + optional: true + "@rolldown/binding-linux-x64-gnu": + optional: true + "@rolldown/binding-linux-x64-musl": + optional: true + "@rolldown/binding-openharmony-arm64": + optional: true + "@rolldown/binding-wasm32-wasi": + optional: true + "@rolldown/binding-win32-arm64-msvc": + optional: true + "@rolldown/binding-win32-x64-msvc": + optional: true + bin: + rolldown: ./bin/cli.mjs + checksum: 10c0/5f9dd47b7abf203b16bc600db68542f245e974c800e59ff50b76157d1dada1403657690435b036fabca88e93d13a67c31abe5cfaa6f61ce33717f61720204cdf + languageName: node + linkType: hard + "rolldown@npm:^1.0.0-rc.18": version: 1.0.0-rc.18 resolution: "rolldown@npm:1.0.0-rc.18" @@ -22035,6 +22538,7 @@ __metadata: nodemon: "npm:^3.1.11" openapi-fetch: "npm:^0.16.0" openapi-typescript: "npm:^7.13.0" + playwright: "npm:^1.60.0" prettier: "npm:^3.8.1" react: "npm:^19.2.4" react-dom: "npm:^19.2.4" @@ -22256,6 +22760,13 @@ __metadata: languageName: node linkType: hard +"siginfo@npm:^2.0.0": + version: 2.0.0 + resolution: "siginfo@npm:2.0.0" + checksum: 10c0/3def8f8e516fbb34cb6ae415b07ccc5d9c018d85b4b8611e3dc6f8be6d1899f693a4382913c9ed51a06babb5201639d76453ab297d1c54a456544acf5c892e34 + languageName: node + linkType: hard + "signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" @@ -22288,6 +22799,17 @@ __metadata: languageName: node linkType: hard +"sirv@npm:^3.0.2": + version: 3.0.2 + resolution: "sirv@npm:3.0.2" + dependencies: + "@polka/url": "npm:^1.0.0-next.24" + mrmime: "npm:^2.0.0" + totalist: "npm:^3.0.0" + checksum: 10c0/5930e4397afdb14fbae13751c3be983af4bda5c9aadec832607dc2af15a7162f7d518c71b30e83ae3644b9a24cea041543cc969e5fe2b80af6ce8ea3174b2d04 + languageName: node + linkType: hard + "slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" @@ -22447,6 +22969,13 @@ __metadata: languageName: node linkType: hard +"stackback@npm:0.0.2": + version: 0.0.2 + resolution: "stackback@npm:0.0.2" + checksum: 10c0/89a1416668f950236dd5ac9f9a6b2588e1b9b62b1b6ad8dff1bfc5d1a15dbf0aafc9b52d2226d00c28dffff212da464eaeebfc6b7578b9d180cef3e3782c5983 + languageName: node + linkType: hard + "standard-as-callback@npm:^2.1.0": version: 2.1.0 resolution: "standard-as-callback@npm:2.1.0" @@ -22468,6 +22997,13 @@ __metadata: languageName: node linkType: hard +"std-env@npm:^4.0.0-rc.1": + version: 4.1.0 + resolution: "std-env@npm:4.1.0" + checksum: 10c0/2e14b6b490db34cb969a48d9cf7c35bca4a47653914aac2814221baae7b867a5b15940d133625c391621971f98cd2266a5dc7036669960e883f1081db2a56558 + languageName: node + linkType: hard + "stop-iteration-iterator@npm:^1.1.0": version: 1.1.0 resolution: "stop-iteration-iterator@npm:1.1.0" @@ -22916,6 +23452,13 @@ __metadata: languageName: node linkType: hard +"tinybench@npm:^2.9.0": + version: 2.9.0 + resolution: "tinybench@npm:2.9.0" + checksum: 10c0/c3500b0f60d2eb8db65250afe750b66d51623057ee88720b7f064894a6cb7eb93360ca824a60a31ab16dab30c7b1f06efe0795b352e37914a9d4bad86386a20c + languageName: node + linkType: hard + "tinyexec@npm:^1.0.2": version: 1.0.2 resolution: "tinyexec@npm:1.0.2" @@ -22933,6 +23476,16 @@ __metadata: languageName: node linkType: hard +"tinyglobby@npm:^0.2.17": + version: 0.2.17 + resolution: "tinyglobby@npm:0.2.17" + dependencies: + fdir: "npm:^6.5.0" + picomatch: "npm:^4.0.4" + checksum: 10c0/7f7bb0f197c88bc4b20c231e0deca4240ca3bf313a88f5a7fee93a872b84966a4d50220947c0455ad07a60b3b360961c5b7fd979222aeb716a9f99b412002e4c + languageName: node + linkType: hard + "tinyglobby@npm:~0.2.15": version: 0.2.16 resolution: "tinyglobby@npm:0.2.16" @@ -22943,6 +23496,13 @@ __metadata: languageName: node linkType: hard +"tinyrainbow@npm:^3.1.0": + version: 3.1.0 + resolution: "tinyrainbow@npm:3.1.0" + checksum: 10c0/f11cf387a26c5c9255bec141a90ac511b26172981b10c3e50053bc6700ea7d2336edcc4a3a21dbb8412fe7c013477d2ba4d7e4877800f3f8107be5105aad6511 + languageName: node + linkType: hard + "tmpl@npm:1.0.5": version: 1.0.5 resolution: "tmpl@npm:1.0.5" @@ -22966,6 +23526,13 @@ __metadata: languageName: node linkType: hard +"totalist@npm:^3.0.0": + version: 3.0.1 + resolution: "totalist@npm:3.0.1" + checksum: 10c0/4bb1fadb69c3edbef91c73ebef9d25b33bbf69afe1e37ce544d5f7d13854cda15e47132f3e0dc4cafe300ddb8578c77c50a65004d8b6e97e77934a69aa924863 + languageName: node + linkType: hard + "touch@npm:^3.1.0": version: 3.1.1 resolution: "touch@npm:3.1.1" @@ -23718,6 +24285,63 @@ __metadata: languageName: node linkType: hard +"vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0": + version: 8.0.16 + resolution: "vite@npm:8.0.16" + dependencies: + fsevents: "npm:~2.3.3" + lightningcss: "npm:^1.32.0" + picomatch: "npm:^4.0.4" + postcss: "npm:^8.5.15" + rolldown: "npm:1.0.3" + tinyglobby: "npm:^0.2.17" + peerDependencies: + "@types/node": ^20.19.0 || >=22.12.0 + "@vitejs/devtools": ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 + jiti: ">=1.21.0" + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: ">=0.54.8" + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + "@vitejs/devtools": + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + bin: + vite: bin/vite.js + checksum: 10c0/d75be3fbe2f63e6a8145325970338afaf0dd4d96ba9175c13f9a286fd5f95afc489401b693e4fa6c0899a4dd0e137be91cdf9401a40a635563911ad5036e3467 + languageName: node + linkType: hard + "vite@npm:^7.3.1": version: 7.3.1 resolution: "vite@npm:7.3.1" @@ -23829,6 +24453,74 @@ __metadata: languageName: node linkType: hard +"vitest@npm:^4.1.8": + version: 4.1.8 + resolution: "vitest@npm:4.1.8" + dependencies: + "@vitest/expect": "npm:4.1.8" + "@vitest/mocker": "npm:4.1.8" + "@vitest/pretty-format": "npm:4.1.8" + "@vitest/runner": "npm:4.1.8" + "@vitest/snapshot": "npm:4.1.8" + "@vitest/spy": "npm:4.1.8" + "@vitest/utils": "npm:4.1.8" + es-module-lexer: "npm:^2.0.0" + expect-type: "npm:^1.3.0" + magic-string: "npm:^0.30.21" + obug: "npm:^2.1.1" + pathe: "npm:^2.0.3" + picomatch: "npm:^4.0.3" + std-env: "npm:^4.0.0-rc.1" + tinybench: "npm:^2.9.0" + tinyexec: "npm:^1.0.2" + tinyglobby: "npm:^0.2.15" + tinyrainbow: "npm:^3.1.0" + vite: "npm:^6.0.0 || ^7.0.0 || ^8.0.0" + why-is-node-running: "npm:^2.3.0" + peerDependencies: + "@edge-runtime/vm": "*" + "@opentelemetry/api": ^1.9.0 + "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 + "@vitest/browser-playwright": 4.1.8 + "@vitest/browser-preview": 4.1.8 + "@vitest/browser-webdriverio": 4.1.8 + "@vitest/coverage-istanbul": 4.1.8 + "@vitest/coverage-v8": 4.1.8 + "@vitest/ui": 4.1.8 + happy-dom: "*" + jsdom: "*" + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@opentelemetry/api": + optional: true + "@types/node": + optional: true + "@vitest/browser-playwright": + optional: true + "@vitest/browser-preview": + optional: true + "@vitest/browser-webdriverio": + optional: true + "@vitest/coverage-istanbul": + optional: true + "@vitest/coverage-v8": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vite: + optional: false + bin: + vitest: vitest.mjs + checksum: 10c0/f459c500f8818c7a2318cd23228b4e5c0b5efb25bf8e5cb7f116c6d26e51482b2f800a8bb19837c0b5f0d05c51519edbf502bc8ceb5bd86868e8facf1d2c498e + languageName: node + linkType: hard + "vscode-uri@npm:^3.0.8": version: 3.1.0 resolution: "vscode-uri@npm:3.1.0" @@ -23959,6 +24651,18 @@ __metadata: languageName: node linkType: hard +"why-is-node-running@npm:^2.3.0": + version: 2.3.0 + resolution: "why-is-node-running@npm:2.3.0" + dependencies: + siginfo: "npm:^2.0.0" + stackback: "npm:0.0.2" + bin: + why-is-node-running: cli.js + checksum: 10c0/1cde0b01b827d2cf4cb11db962f3958b9175d5d9e7ac7361d1a7b0e2dc6069a263e69118bd974c4f6d0a890ef4eedfe34cf3d5167ec14203dbc9a18620537054 + languageName: node + linkType: hard + "winston-transport@npm:^4.9.0": version: 4.9.0 resolution: "winston-transport@npm:4.9.0" @@ -24042,6 +24746,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:^8.19.0": + version: 8.21.0 + resolution: "ws@npm:8.21.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10c0/ef4a243476283fc49bc7550966c4af4aa0eef56273837211e700de3b664e08604a760cdddcb5ba43c049140e74ccfec5b0ee0bb439e08c2adf9138902fdde5f9 + languageName: node + linkType: hard + "ws@npm:~8.18.3": version: 8.18.3 resolution: "ws@npm:8.18.3"