diff --git a/packages/sdk-embed/package.json b/packages/sdk-embed/package.json index 0c732bce52e..150f7992910 100644 --- a/packages/sdk-embed/package.json +++ b/packages/sdk-embed/package.json @@ -23,6 +23,7 @@ "dist" ], "scripts": { + "test": "vitest run", "build": "tsup", "typecheck": "tsc --noEmit" }, @@ -39,6 +40,7 @@ "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "tsup": "^8.0.0", - "typescript": "^5.7.3" + "typescript": "^5.7.3", + "vitest": "~2.1.9" } } diff --git a/packages/sdk-embed/src/vanilla/cap-embed.test.ts b/packages/sdk-embed/src/vanilla/cap-embed.test.ts new file mode 100644 index 00000000000..1aa1729c4be --- /dev/null +++ b/packages/sdk-embed/src/vanilla/cap-embed.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; +import { createEmbedUrl } from "./cap-embed"; + +describe("createEmbedUrl", () => { + it("builds a default Cap embed URL with SDK and public-key markers", () => { + const url = new URL( + createEmbedUrl({ videoId: "video-123", publicKey: "pk_test" }), + ); + + expect(url.origin).toBe("https://cap.so"); + expect(url.pathname).toBe("/embed/video-123"); + expect(url.searchParams.get("sdk")).toBe("1"); + expect(url.searchParams.get("pk")).toBe("pk_test"); + }); + + it("includes autoplay and branding options when provided", () => { + const url = new URL( + createEmbedUrl({ + videoId: "video-123", + publicKey: "pk_live", + apiBase: "https://app.example.com", + autoplay: true, + branding: { + logoUrl: "https://cdn.example.com/logo.png", + accentColor: "#ff00aa", + }, + }), + ); + + expect(url.origin).toBe("https://app.example.com"); + expect(url.searchParams.get("autoplay")).toBe("1"); + expect(url.searchParams.get("logo")).toBe( + "https://cdn.example.com/logo.png", + ); + expect(url.searchParams.get("accent")).toBe("#ff00aa"); + }); +}); diff --git a/packages/sdk-embed/vitest.config.ts b/packages/sdk-embed/vitest.config.ts new file mode 100644 index 00000000000..b9379103131 --- /dev/null +++ b/packages/sdk-embed/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts"], + exclude: ["**/node_modules/**", "**/dist/**"], + }, +}); diff --git a/packages/sdk-recorder/package.json b/packages/sdk-recorder/package.json index 7fef2785793..88bd60ba06b 100644 --- a/packages/sdk-recorder/package.json +++ b/packages/sdk-recorder/package.json @@ -19,6 +19,7 @@ "dist" ], "scripts": { + "test": "vitest run", "build": "tsup", "typecheck": "tsc --noEmit" }, @@ -35,6 +36,7 @@ "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "tsup": "^8.0.0", - "typescript": "^5.7.3" + "typescript": "^5.7.3", + "vitest": "~2.1.9" } } diff --git a/packages/sdk-recorder/src/core/mime-types.test.ts b/packages/sdk-recorder/src/core/mime-types.test.ts new file mode 100644 index 00000000000..d3a8385977e --- /dev/null +++ b/packages/sdk-recorder/src/core/mime-types.test.ts @@ -0,0 +1,25 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { getSupportedMimeType } from "./mime-types"; + +describe("getSupportedMimeType", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("returns the first supported preferred MIME type", () => { + vi.stubGlobal("MediaRecorder", { + isTypeSupported: (mimeType: string) => + mimeType === "video/webm;codecs=vp8,opus", + }); + + expect(getSupportedMimeType()).toBe("video/webm;codecs=vp8,opus"); + }); + + it("returns an empty string when the browser supports none of the preferred types", () => { + vi.stubGlobal("MediaRecorder", { + isTypeSupported: () => false, + }); + + expect(getSupportedMimeType()).toBe(""); + }); +}); diff --git a/packages/sdk-recorder/src/index.ts b/packages/sdk-recorder/src/index.ts index 0f7eb10e1fb..8017b277e45 100644 --- a/packages/sdk-recorder/src/index.ts +++ b/packages/sdk-recorder/src/index.ts @@ -84,7 +84,9 @@ export class CapRecorder { const set = this.listeners.get(event); if (set) set.add(handler as EventHandler); return () => { - this.listeners.get(event)?.delete(handler); + this.listeners + .get(event) + ?.delete(handler as EventHandler); }; } diff --git a/packages/sdk-recorder/vitest.config.ts b/packages/sdk-recorder/vitest.config.ts new file mode 100644 index 00000000000..b9379103131 --- /dev/null +++ b/packages/sdk-recorder/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts"], + exclude: ["**/node_modules/**", "**/dist/**"], + }, +}); diff --git a/packages/web-api-contract/package.json b/packages/web-api-contract/package.json index 9b22385f99e..49d6a4b7d67 100644 --- a/packages/web-api-contract/package.json +++ b/packages/web-api-contract/package.json @@ -4,8 +4,14 @@ "main": "./src/index.ts", "types": "./src/index.ts", "type": "module", + "scripts": { + "test": "vitest run" + }, "dependencies": { "@ts-rest/core": "^3.52.1", "zod": "^3.25.76" + }, + "devDependencies": { + "vitest": "~2.1.9" } } diff --git a/packages/web-api-contract/src/index.test.ts b/packages/web-api-contract/src/index.test.ts new file mode 100644 index 00000000000..42961da7287 --- /dev/null +++ b/packages/web-api-contract/src/index.test.ts @@ -0,0 +1,110 @@ +import { describe, expect, it } from "vitest"; +import { + contract, + DesktopOrganization, + licenseContract, + Notification, + OrganizationBrandingPatchBody, + orgCustomDomainContract, +} from "./index"; + +describe("desktop contract schemas", () => { + it("parses organization branding payloads with uploaded logos", () => { + const parsed = OrganizationBrandingPatchBody.parse({ + brandColors: { + primary: "#111111", + secondary: null, + accent: "#BADA55", + background: "#ffffff", + }, + logo: { + action: "upload", + contentType: "image/png", + data: "base64-logo", + }, + }); + + expect(parsed.logo?.action).toBe("upload"); + expect( + OrganizationBrandingPatchBody.safeParse({ + brandColors: { + primary: "111111", + secondary: null, + accent: null, + background: null, + }, + }).success, + ).toBe(false); + }); + + it("validates desktop organizations with nullable brand fields", () => { + expect( + DesktopOrganization.parse({ + id: "org_123", + name: "Cap Team", + ownerId: "user_123", + role: "owner", + canEditBrand: true, + iconUrl: null, + brandColors: { + primary: null, + secondary: "#123ABC", + accent: null, + background: "#000000", + }, + }), + ).toMatchObject({ role: "owner", canEditBrand: true }); + }); +}); + +describe("notification contract schema", () => { + it("coerces notification timestamps and rejects unknown notification types", () => { + const parsed = Notification.parse({ + id: "notification_123", + readAt: null, + createdAt: "2026-05-14T20:00:00.000Z", + type: "comment", + videoId: "video_123", + author: { id: "user_123", name: "Ada", avatar: null }, + comment: { id: "comment_123", content: "Looks good" }, + }); + + expect(parsed.createdAt).toBeInstanceOf(Date); + expect(Notification.safeParse({ ...parsed, type: "mention" }).success).toBe( + false, + ); + }); +}); + +describe("route contracts", () => { + it("keeps core video and notification routes stable", () => { + expect(contract.video.getTranscribeStatus).toMatchObject({ + method: "GET", + path: "/video/transcribe/status", + }); + expect(contract.video.delete).toMatchObject({ + method: "DELETE", + path: "/video/delete", + }); + expect(contract.notifications.get).toMatchObject({ + method: "GET", + path: "/notifications", + }); + }); + + it("keeps commercial license and custom domain routes stable", () => { + expect(licenseContract.activateCommercialLicense).toMatchObject({ + method: "POST", + path: "/commercial/activate", + }); + expect( + licenseContract.createCommercialCheckoutUrl.body.safeParse({ + type: "monthly", + }).success, + ).toBe(false); + expect(orgCustomDomainContract.getOrgCustomDomain).toMatchObject({ + method: "GET", + path: "/org-custom-domain", + }); + }); +}); diff --git a/packages/web-api-contract/vitest.config.ts b/packages/web-api-contract/vitest.config.ts new file mode 100644 index 00000000000..b9379103131 --- /dev/null +++ b/packages/web-api-contract/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts"], + exclude: ["**/node_modules/**", "**/dist/**"], + }, +}); diff --git a/packages/web-domain/package.json b/packages/web-domain/package.json index e96e70bc883..bc758a19d9d 100644 --- a/packages/web-domain/package.json +++ b/packages/web-domain/package.json @@ -8,6 +8,7 @@ "main": "./dist/index.js" }, "scripts": { + "test": "vitest run", "build": "tsdown", "generate-openapi": "node scripts/generate-openapi.ts" }, @@ -18,6 +19,7 @@ "effect": "^3.18.4" }, "devDependencies": { - "@effect/platform-node": "^0.98.3" + "@effect/platform-node": "^0.98.3", + "vitest": "~2.1.9" } } diff --git a/packages/web-domain/src/Policy.test.ts b/packages/web-domain/src/Policy.test.ts new file mode 100644 index 00000000000..fa1afc14d87 --- /dev/null +++ b/packages/web-domain/src/Policy.test.ts @@ -0,0 +1,28 @@ +import { Cause, Effect, Option } from "effect"; +import { describe, expect, it } from "vitest"; +import { publicPolicy, withPublicPolicy } from "./Policy"; + +describe("publicPolicy", () => { + it("allows an effect when the public predicate succeeds", async () => { + const result = await Effect.runPromise( + Effect.succeed("allowed").pipe( + withPublicPolicy( + publicPolicy((user) => Effect.succeed(Option.isNone(user))), + ), + ), + ); + + expect(result).toBe("allowed"); + }); + + it("fails with PolicyDeniedError when the public predicate denies access", async () => { + const exit = await Effect.runPromiseExit( + publicPolicy(() => Effect.succeed(false)), + ); + + expect(exit._tag).toBe("Failure"); + if (exit._tag === "Failure") { + expect(Cause.pretty(exit.cause)).toContain("PolicyDenied"); + } + }); +}); diff --git a/packages/web-domain/vitest.config.ts b/packages/web-domain/vitest.config.ts new file mode 100644 index 00000000000..b9379103131 --- /dev/null +++ b/packages/web-domain/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts"], + exclude: ["**/node_modules/**", "**/dist/**"], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce77b06b4f8..2b6dbc2cecc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1089,6 +1089,9 @@ importers: typescript: specifier: ^5.7.3 version: 5.8.3 + vitest: + specifier: ~2.1.9 + version: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0) packages/sdk-recorder: dependencies: @@ -1108,6 +1111,9 @@ importers: typescript: specifier: ^5.7.3 version: 5.8.3 + vitest: + specifier: ~2.1.9 + version: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0) packages/tsconfig: {} @@ -1332,6 +1338,10 @@ importers: zod: specifier: ^3.25.76 version: 3.25.76 + devDependencies: + vitest: + specifier: ~2.1.9 + version: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0) packages/web-api-contract-effect: dependencies: @@ -1420,6 +1430,9 @@ importers: '@effect/platform-node': specifier: ^0.98.3 version: 0.98.3(@effect/cluster@0.50.4(@effect/platform@0.92.1(effect@3.18.4))(@effect/rpc@0.71.0(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4))(@effect/sql@0.44.2(@effect/experimental@0.56.0(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4)(ioredis@5.6.1))(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4))(@effect/workflow@0.11.3(@effect/platform@0.92.1(effect@3.18.4))(@effect/rpc@0.71.0(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4))(effect@3.18.4))(effect@3.18.4))(@effect/platform@0.92.1(effect@3.18.4))(@effect/rpc@0.71.0(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4))(@effect/sql@0.44.2(@effect/experimental@0.56.0(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4)(ioredis@5.6.1))(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4))(effect@3.18.4) + vitest: + specifier: ~2.1.9 + version: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0) packages: