diff --git a/.pnpm-store/v11/index.db b/.pnpm-store/v11/index.db new file mode 100644 index 0000000000..3dd0ca39ec Binary files /dev/null and b/.pnpm-store/v11/index.db differ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 98b8f1b63f..aa5fac4710 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,8 +8,8 @@ overrides: '@headlessui/react': ^2.2.4 '@tiptap/core': ^3.0.0 '@tiptap/pm': ^3.0.0 - vitest: 4.1.2 - '@vitest/runner': 4.1.2 + vitest: 4.1.7 + '@vitest/runner': 4.1.7 '@y/prosemirror>lib0': 1.0.0-rc.13 patchedDependencies: @@ -59,8 +59,8 @@ importers: specifier: ^5.9.3 version: 5.9.3 vitest: - specifier: 4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + specifier: 4.1.7 + version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(@vitest/browser-playwright@4.1.7)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) wait-on: specifier: 9.0.5 version: 9.0.5 @@ -159,7 +159,7 @@ importers: version: 3.1.18 '@polar-sh/better-auth': specifier: ^1.6.4 - version: 1.8.3(@polar-sh/sdk@0.42.5)(@stripe/react-stripe-js@4.0.2(@stripe/stripe-js@7.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@stripe/stripe-js@7.9.0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-auth@1.4.22(better-sqlite3@12.8.0)(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.51.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(pg@8.20.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.5.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))))(react-dom@19.2.5(react@19.2.5))(react-is@19.2.4)(react@19.2.5)(redux@5.0.1)(zod@4.3.6) + version: 1.8.3(@polar-sh/sdk@0.42.5)(@stripe/react-stripe-js@4.0.2(@stripe/stripe-js@7.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@stripe/stripe-js@7.9.0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-auth@1.4.22(better-sqlite3@12.8.0)(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.51.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(pg@8.20.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vitest@4.1.7))(react-dom@19.2.5(react@19.2.5))(react-is@19.2.4)(react@19.2.5)(redux@5.0.1)(zod@4.3.6) '@polar-sh/sdk': specifier: ^0.42.2 version: 0.42.5 @@ -249,7 +249,7 @@ importers: version: 6.0.5(zod@4.3.6) better-auth: specifier: ~1.4.15 - version: 1.4.22(better-sqlite3@12.8.0)(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.51.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(pg@8.20.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.5.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 1.4.22(better-sqlite3@12.8.0)(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.51.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(pg@8.20.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vitest@4.1.7) better-sqlite3: specifier: ^12.6.2 version: 12.8.0 @@ -4904,8 +4904,8 @@ importers: specifier: ^1.8.1 version: 1.8.1(eslint@8.57.1)(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: - specifier: 4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + specifier: 4.1.7 + version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(@vitest/browser-playwright@4.1.7)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) packages/core: dependencies: @@ -5013,8 +5013,8 @@ importers: specifier: ^1.8.1 version: 1.8.1(eslint@8.57.1)(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: - specifier: 4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + specifier: 4.1.7 + version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(@vitest/browser-playwright@4.1.7)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) y-prosemirror: specifier: ^1.3.7 version: 1.3.7(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8)(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30) @@ -5205,8 +5205,8 @@ importers: specifier: ^0.10.0 version: 0.10.0(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: - specifier: 4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + specifier: 4.1.7 + version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(@vitest/browser-playwright@4.1.7)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) packages/server-util: dependencies: @@ -5266,8 +5266,8 @@ importers: specifier: ^1.8.1 version: 1.8.1(eslint@8.57.1)(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: - specifier: 4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(jsdom@25.0.1(canvas@2.11.2))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + specifier: 4.1.7 + version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(@vitest/browser-playwright@4.1.7)(jsdom@25.0.1(canvas@2.11.2))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) packages/shadcn: dependencies: @@ -5481,8 +5481,8 @@ importers: specifier: ^6.0.1 version: 6.0.1(babel-plugin-react-compiler@1.0.0)(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/runner': - specifier: 4.1.2 - version: 4.1.2 + specifier: 4.1.7 + version: 4.1.7 eslint: specifier: ^8.57.1 version: 8.57.1 @@ -5520,8 +5520,8 @@ importers: specifier: ^0.10.0 version: 0.10.0(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: - specifier: 4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + specifier: 4.1.7 + version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(@vitest/browser-playwright@4.1.7)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) packages/xl-ai-server: dependencies: @@ -5584,8 +5584,8 @@ importers: specifier: ^0.10.0 version: 0.10.0(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: - specifier: 4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + specifier: 4.1.7 + version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(@vitest/browser-playwright@4.1.7)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) packages/xl-docx-exporter: dependencies: @@ -5636,8 +5636,8 @@ importers: specifier: ^1.8.1 version: 1.8.1(eslint@8.57.1)(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: - specifier: 4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + specifier: 4.1.7 + version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(@vitest/browser-playwright@4.1.7)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) xml-formatter: specifier: ^3.6.7 version: 3.7.0 @@ -5697,8 +5697,8 @@ importers: specifier: ^1.8.1 version: 1.8.1(eslint@8.57.1)(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: - specifier: 4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + specifier: 4.1.7 + version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(@vitest/browser-playwright@4.1.7)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) packages/xl-multi-column: dependencies: @@ -5764,8 +5764,8 @@ importers: specifier: ^1.8.1 version: 1.8.1(eslint@8.57.1)(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: - specifier: 4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(jsdom@25.0.1(canvas@2.11.2))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + specifier: 4.1.7 + version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(@vitest/browser-playwright@4.1.7)(jsdom@25.0.1(canvas@2.11.2))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) packages/xl-odt-exporter: dependencies: @@ -5816,8 +5816,8 @@ importers: specifier: ^1.8.1 version: 1.8.1(eslint@8.57.1)(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: - specifier: 4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + specifier: 4.1.7 + version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(@vitest/browser-playwright@4.1.7)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) xml-formatter: specifier: ^3.6.7 version: 3.7.0 @@ -5889,8 +5889,8 @@ importers: specifier: ^1.8.1 version: 1.8.1(eslint@8.57.1)(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: - specifier: 4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + specifier: 4.1.7 + version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(@vitest/browser-playwright@4.1.7)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) playground: dependencies: @@ -6128,6 +6128,21 @@ importers: '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(babel-plugin-react-compiler@1.0.0)(vite@8.0.8(@types/node@20.19.37)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/browser': + specifier: 4.1.7 + version: 4.1.7(msw@2.11.5(@types/node@20.19.37)(typescript@5.9.3))(vite@8.0.8(@types/node@20.19.37)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.7) + '@vitest/browser-playwright': + specifier: 4.1.7 + version: 4.1.7(msw@2.11.5(@types/node@20.19.37)(typescript@5.9.3))(playwright@1.51.1)(vite@8.0.8(@types/node@20.19.37)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.7) + '@y/protocols': + specifier: ^1.0.6-rc.1 + version: 1.0.6-rc.1(@y/y@14.0.0-rc.16) + '@y/y': + specifier: ^14.0.0-rc.16 + version: 14.0.0-rc.16 eslint: specifier: ^8.57.1 version: 8.57.1 @@ -6153,8 +6168,11 @@ importers: specifier: ^1.8.1 version: 1.8.1(eslint@8.57.1)(vite@8.0.8(@types/node@20.19.37)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: - specifier: 4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@20.19.37)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@20.19.37)(typescript@5.9.3))(vite@8.0.8(@types/node@20.19.37)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + specifier: 4.1.7 + version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@20.19.37)(@vitest/browser-playwright@4.1.7)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@20.19.37)(typescript@5.9.3))(vite@8.0.8(@types/node@20.19.37)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest-browser-react: + specifier: ^2.2.0 + version: 2.2.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vitest@4.1.7) packages: @@ -7138,6 +7156,9 @@ packages: '@better-fetch/fetch@1.1.21': resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==} + '@blazediff/core@1.9.1': + resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==} + '@bramus/specificity@2.4.2': resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} hasBin: true @@ -11391,11 +11412,22 @@ packages: babel-plugin-react-compiler: optional: true - '@vitest/expect@4.1.2': - resolution: {integrity: sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==} + '@vitest/browser-playwright@4.1.7': + resolution: {integrity: sha512-OlTlJej7YN6VwV7zJJoNeaCsctF+JXpzpZ4oBHUbrQFfIq+0KW2f07rprCLh9N/zRIZ0v4Mchn1QDDmWMUhPKw==} + peerDependencies: + playwright: '*' + vitest: 4.1.7 + + '@vitest/browser@4.1.7': + resolution: {integrity: sha512-N2JFGfXoEGVAut+kHeru9dD4BUMq/q5xDvBARNl0tUsly3m5KglLOu8VO/6MkDfOlgxXTycojkt6gBKsuyR+IQ==} + peerDependencies: + vitest: 4.1.7 + + '@vitest/expect@4.1.7': + resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==} - '@vitest/mocker@4.1.2': - resolution: {integrity: sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==} + '@vitest/mocker@4.1.7': + resolution: {integrity: sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -11405,20 +11437,20 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.2': - resolution: {integrity: sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==} + '@vitest/pretty-format@4.1.7': + resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==} - '@vitest/runner@4.1.2': - resolution: {integrity: sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==} + '@vitest/runner@4.1.7': + resolution: {integrity: sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==} - '@vitest/snapshot@4.1.2': - resolution: {integrity: sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==} + '@vitest/snapshot@4.1.7': + resolution: {integrity: sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==} - '@vitest/spy@4.1.2': - resolution: {integrity: sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==} + '@vitest/spy@4.1.7': + resolution: {integrity: sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==} - '@vitest/utils@4.1.2': - resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==} + '@vitest/utils@4.1.7': + resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==} '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -11861,7 +11893,7 @@ packages: react-dom: ^18.0.0 || ^19.0.0 solid-js: ^1.0.0 svelte: ^4.0.0 || ^5.0.0 - vitest: 4.1.2 + vitest: 4.1.7 vue: ^3.0.0 peerDependenciesMeta: '@lynx-js/react': @@ -14999,6 +15031,10 @@ packages: resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==} engines: {node: '>=12.13.0'} + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -16603,21 +16639,37 @@ packages: yaml: optional: true + vitest-browser-react@2.2.0: + resolution: {integrity: sha512-oY3KM6305kwJMa6nHo92vVtkOsih7mjEf12dLKuphaF+9ywWPEc+qanIBd394SZ6m5LadVEaG6dicvvizOzmjA==} + peerDependencies: + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + vitest: 4.1.7 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + vitest-tsconfig-paths@3.4.1: resolution: {integrity: sha512-CnRpA/jcqgZfnkk0yvwFW92UmIpf03wX/wLiQBNWAcOG7nv6Sdz3GsPESAMEqbVy8kHBoWB3XeNamu6PUrFZLA==} - vitest@4.1.2: - resolution: {integrity: sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==} + vitest@4.1.7: + resolution: {integrity: sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true 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.2 - '@vitest/browser-preview': 4.1.2 - '@vitest/browser-webdriverio': 4.1.2 - '@vitest/ui': 4.1.2 + '@vitest/browser-playwright': 4.1.7 + '@vitest/browser-preview': 4.1.7 + '@vitest/browser-webdriverio': 4.1.7 + '@vitest/coverage-istanbul': 4.1.7 + '@vitest/coverage-v8': 4.1.7 + '@vitest/ui': 4.1.7 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -16634,6 +16686,10 @@ packages: optional: true '@vitest/browser-webdriverio': optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true '@vitest/ui': optional: true happy-dom: @@ -18423,6 +18479,8 @@ snapshots: '@better-fetch/fetch@1.1.21': {} + '@blazediff/core@1.9.1': {} + '@bramus/specificity@2.4.2': dependencies: css-tree: 3.2.1 @@ -20011,11 +20069,11 @@ snapshots: dependencies: playwright: 1.51.1 - '@polar-sh/better-auth@1.8.3(@polar-sh/sdk@0.42.5)(@stripe/react-stripe-js@4.0.2(@stripe/stripe-js@7.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@stripe/stripe-js@7.9.0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-auth@1.4.22(better-sqlite3@12.8.0)(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.51.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(pg@8.20.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.5.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))))(react-dom@19.2.5(react@19.2.5))(react-is@19.2.4)(react@19.2.5)(redux@5.0.1)(zod@4.3.6)': + '@polar-sh/better-auth@1.8.3(@polar-sh/sdk@0.42.5)(@stripe/react-stripe-js@4.0.2(@stripe/stripe-js@7.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@stripe/stripe-js@7.9.0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-auth@1.4.22(better-sqlite3@12.8.0)(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.51.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(pg@8.20.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vitest@4.1.7))(react-dom@19.2.5(react@19.2.5))(react-is@19.2.4)(react@19.2.5)(redux@5.0.1)(zod@4.3.6)': dependencies: '@polar-sh/checkout': 0.2.0(@stripe/react-stripe-js@4.0.2(@stripe/stripe-js@7.9.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@stripe/stripe-js@7.9.0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react-is@19.2.4)(react@19.2.5)(redux@5.0.1) '@polar-sh/sdk': 0.42.5 - better-auth: 1.4.22(better-sqlite3@12.8.0)(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.51.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(pg@8.20.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.5.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))) + better-auth: 1.4.22(better-sqlite3@12.8.0)(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.51.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(pg@8.20.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vitest@4.1.7) zod: 4.3.6 transitivePeerDependencies: - '@stripe/react-stripe-js' @@ -22883,27 +22941,121 @@ snapshots: optionalDependencies: babel-plugin-react-compiler: 1.0.0 - '@vitest/expect@4.1.2': + '@vitest/browser-playwright@4.1.7(msw@2.11.5(@types/node@20.19.37)(typescript@5.9.3))(playwright@1.51.1)(vite@8.0.8(@types/node@20.19.37)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.7)': + dependencies: + '@vitest/browser': 4.1.7(msw@2.11.5(@types/node@20.19.37)(typescript@5.9.3))(vite@8.0.8(@types/node@20.19.37)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.7) + '@vitest/mocker': 4.1.7(msw@2.11.5(@types/node@20.19.37)(typescript@5.9.3))(vite@8.0.8(@types/node@20.19.37)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + playwright: 1.51.1 + tinyrainbow: 3.1.0 + vitest: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@20.19.37)(@vitest/browser-playwright@4.1.7)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@20.19.37)(typescript@5.9.3))(vite@8.0.8(@types/node@20.19.37)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser-playwright@4.1.7(msw@2.11.5(@types/node@25.5.0)(typescript@5.9.3))(playwright@1.51.1)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.7)': + dependencies: + '@vitest/browser': 4.1.7(msw@2.11.5(@types/node@25.5.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.7) + '@vitest/mocker': 4.1.7(msw@2.11.5(@types/node@25.5.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + playwright: 1.51.1 + tinyrainbow: 3.1.0 + vitest: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(@vitest/browser-playwright@4.1.7)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.5.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true + + '@vitest/browser-playwright@4.1.7(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(playwright@1.51.1)(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.7)': + dependencies: + '@vitest/browser': 4.1.7(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.7) + '@vitest/mocker': 4.1.7(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + playwright: 1.51.1 + tinyrainbow: 3.1.0 + vitest: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(@vitest/browser-playwright@4.1.7)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true + + '@vitest/browser@4.1.7(msw@2.11.5(@types/node@20.19.37)(typescript@5.9.3))(vite@8.0.8(@types/node@20.19.37)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.7)': + dependencies: + '@blazediff/core': 1.9.1 + '@vitest/mocker': 4.1.7(msw@2.11.5(@types/node@20.19.37)(typescript@5.9.3))(vite@8.0.8(@types/node@20.19.37)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/utils': 4.1.7 + magic-string: 0.30.21 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.0 + vitest: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@20.19.37)(@vitest/browser-playwright@4.1.7)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@20.19.37)(typescript@5.9.3))(vite@8.0.8(@types/node@20.19.37)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + ws: 8.20.0 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser@4.1.7(msw@2.11.5(@types/node@25.5.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.7)': + dependencies: + '@blazediff/core': 1.9.1 + '@vitest/mocker': 4.1.7(msw@2.11.5(@types/node@25.5.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/utils': 4.1.7 + magic-string: 0.30.21 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.0 + vitest: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(@vitest/browser-playwright@4.1.7)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.5.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + ws: 8.20.0 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true + + '@vitest/browser@4.1.7(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.7)': + dependencies: + '@blazediff/core': 1.9.1 + '@vitest/mocker': 4.1.7(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/utils': 4.1.7 + magic-string: 0.30.21 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.0 + vitest: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(@vitest/browser-playwright@4.1.7)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + ws: 8.20.0 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true + + '@vitest/expect@4.1.7': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.2 - '@vitest/utils': 4.1.2 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.2(msw@2.11.5(@types/node@20.19.37)(typescript@5.9.3))(vite@8.0.8(@types/node@20.19.37)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.1.7(msw@2.11.5(@types/node@20.19.37)(typescript@5.9.3))(vite@8.0.8(@types/node@20.19.37)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@vitest/spy': 4.1.2 + '@vitest/spy': 4.1.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.11.5(@types/node@20.19.37)(typescript@5.9.3) vite: 8.0.8(@types/node@20.19.37)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/mocker@4.1.2(msw@2.11.5(@types/node@25.5.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.1.7(msw@2.11.5(@types/node@25.5.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@vitest/spy': 4.1.2 + '@vitest/spy': 4.1.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: @@ -22911,36 +23063,36 @@ snapshots: vite: 8.0.8(@types/node@25.5.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3) optional: true - '@vitest/mocker@4.1.2(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.1.7(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@vitest/spy': 4.1.2 + '@vitest/spy': 4.1.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.11.5(@types/node@25.9.0)(typescript@5.9.3) vite: 8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/pretty-format@4.1.2': + '@vitest/pretty-format@4.1.7': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.2': + '@vitest/runner@4.1.7': dependencies: - '@vitest/utils': 4.1.2 + '@vitest/utils': 4.1.7 pathe: 2.0.3 - '@vitest/snapshot@4.1.2': + '@vitest/snapshot@4.1.7': dependencies: - '@vitest/pretty-format': 4.1.2 - '@vitest/utils': 4.1.2 + '@vitest/pretty-format': 4.1.7 + '@vitest/utils': 4.1.7 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.2': {} + '@vitest/spy@4.1.7': {} - '@vitest/utils@4.1.2': + '@vitest/utils@4.1.7': dependencies: - '@vitest/pretty-format': 4.1.2 + '@vitest/pretty-format': 4.1.7 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -23445,7 +23597,7 @@ snapshots: baseline-browser-mapping@2.10.17: {} - better-auth@1.4.22(better-sqlite3@12.8.0)(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.51.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(pg@8.20.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.5.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))): + better-auth@1.4.22(better-sqlite3@12.8.0)(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.51.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(pg@8.20.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vitest@4.1.7): dependencies: '@better-auth/core': 1.4.22(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.2.2)(kysely@0.28.17)(nanostores@1.2.0) '@better-auth/telemetry': 1.4.22(@better-auth/core@1.4.22(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.2.2)(kysely@0.28.17)(nanostores@1.2.0)) @@ -23465,7 +23617,7 @@ snapshots: pg: 8.20.0 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - vitest: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.5.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(@vitest/browser-playwright@4.1.7)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.5.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) better-call@1.1.8(zod@4.3.6): dependencies: @@ -24837,7 +24989,7 @@ snapshots: estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 esutils@2.0.3: {} @@ -26422,7 +26574,7 @@ snapshots: micromark-extension-mdx-expression@3.0.1: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 devlop: 1.1.0 micromark-factory-mdx-expression: 2.0.3 micromark-factory-space: 2.0.1 @@ -26433,7 +26585,7 @@ snapshots: micromark-extension-mdx-jsx@3.0.2: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 devlop: 1.1.0 estree-util-is-identifier-name: 3.0.0 micromark-factory-mdx-expression: 2.0.3 @@ -26450,7 +26602,7 @@ snapshots: micromark-extension-mdxjs-esm@3.0.0: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 micromark-util-character: 2.1.1 @@ -26486,7 +26638,7 @@ snapshots: micromark-factory-mdx-expression@2.0.3: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 devlop: 1.1.0 micromark-factory-space: 2.0.1 micromark-util-character: 2.1.1 @@ -26550,7 +26702,7 @@ snapshots: micromark-util-events-to-acorn@2.0.3: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/unist': 3.0.3 devlop: 1.1.0 estree-util-visit: 2.0.0 @@ -27307,6 +27459,8 @@ snapshots: pngjs@6.0.0: {} + pngjs@7.0.0: {} + possible-typed-array-names@1.1.0: {} postcss-selector-parser@7.1.1: @@ -27780,7 +27934,7 @@ snapshots: recma-parse@1.0.0: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 esast-util-from-js: 2.0.1 unified: 11.0.5 vfile: 6.0.3 @@ -29156,6 +29310,15 @@ snapshots: tsx: 4.21.0 yaml: 2.8.3 + vitest-browser-react@2.2.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vitest@4.1.7): + dependencies: + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + vitest: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@20.19.37)(@vitest/browser-playwright@4.1.7)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@20.19.37)(typescript@5.9.3))(vite@8.0.8(@types/node@20.19.37)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + vitest-tsconfig-paths@3.4.1: dependencies: debug: 4.4.3 @@ -29165,15 +29328,15 @@ snapshots: transitivePeerDependencies: - supports-color - vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@20.19.37)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@20.19.37)(typescript@5.9.3))(vite@8.0.8(@types/node@20.19.37)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@20.19.37)(@vitest/browser-playwright@4.1.7)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@20.19.37)(typescript@5.9.3))(vite@8.0.8(@types/node@20.19.37)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: - '@vitest/expect': 4.1.2 - '@vitest/mocker': 4.1.2(msw@2.11.5(@types/node@20.19.37)(typescript@5.9.3))(vite@8.0.8(@types/node@20.19.37)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/pretty-format': 4.1.2 - '@vitest/runner': 4.1.2 - '@vitest/snapshot': 4.1.2 - '@vitest/spy': 4.1.2 - '@vitest/utils': 4.1.2 + '@vitest/expect': 4.1.7 + '@vitest/mocker': 4.1.7(msw@2.11.5(@types/node@20.19.37)(typescript@5.9.3))(vite@8.0.8(@types/node@20.19.37)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.7 + '@vitest/runner': 4.1.7 + '@vitest/snapshot': 4.1.7 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 es-module-lexer: 2.1.0 expect-type: 1.3.0 magic-string: 0.30.21 @@ -29190,19 +29353,20 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/node': 20.19.37 + '@vitest/browser-playwright': 4.1.7(msw@2.11.5(@types/node@20.19.37)(typescript@5.9.3))(playwright@1.51.1)(vite@8.0.8(@types/node@20.19.37)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.7) jsdom: 29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0) transitivePeerDependencies: - msw - vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.5.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.5.0)(@vitest/browser-playwright@4.1.7)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.5.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: - '@vitest/expect': 4.1.2 - '@vitest/mocker': 4.1.2(msw@2.11.5(@types/node@25.5.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/pretty-format': 4.1.2 - '@vitest/runner': 4.1.2 - '@vitest/snapshot': 4.1.2 - '@vitest/spy': 4.1.2 - '@vitest/utils': 4.1.2 + '@vitest/expect': 4.1.7 + '@vitest/mocker': 4.1.7(msw@2.11.5(@types/node@25.5.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.7 + '@vitest/runner': 4.1.7 + '@vitest/snapshot': 4.1.7 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 es-module-lexer: 2.1.0 expect-type: 1.3.0 magic-string: 0.30.21 @@ -29219,20 +29383,21 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/node': 25.5.0 + '@vitest/browser-playwright': 4.1.7(msw@2.11.5(@types/node@25.5.0)(typescript@5.9.3))(playwright@1.51.1)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.7) jsdom: 29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0) transitivePeerDependencies: - msw optional: true - vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(jsdom@25.0.1(canvas@2.11.2))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(@vitest/browser-playwright@4.1.7)(jsdom@25.0.1(canvas@2.11.2))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: - '@vitest/expect': 4.1.2 - '@vitest/mocker': 4.1.2(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/pretty-format': 4.1.2 - '@vitest/runner': 4.1.2 - '@vitest/snapshot': 4.1.2 - '@vitest/spy': 4.1.2 - '@vitest/utils': 4.1.2 + '@vitest/expect': 4.1.7 + '@vitest/mocker': 4.1.7(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.7 + '@vitest/runner': 4.1.7 + '@vitest/snapshot': 4.1.7 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 es-module-lexer: 2.1.0 expect-type: 1.3.0 magic-string: 0.30.21 @@ -29249,19 +29414,20 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/node': 25.9.0 + '@vitest/browser-playwright': 4.1.7(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(playwright@1.51.1)(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.7) jsdom: 25.0.1(canvas@2.11.2) transitivePeerDependencies: - msw - vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(@vitest/browser-playwright@4.1.7)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: - '@vitest/expect': 4.1.2 - '@vitest/mocker': 4.1.2(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/pretty-format': 4.1.2 - '@vitest/runner': 4.1.2 - '@vitest/snapshot': 4.1.2 - '@vitest/spy': 4.1.2 - '@vitest/utils': 4.1.2 + '@vitest/expect': 4.1.7 + '@vitest/mocker': 4.1.7(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.7 + '@vitest/runner': 4.1.7 + '@vitest/snapshot': 4.1.7 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 es-module-lexer: 2.1.0 expect-type: 1.3.0 magic-string: 0.30.21 @@ -29278,6 +29444,7 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/node': 25.9.0 + '@vitest/browser-playwright': 4.1.7(msw@2.11.5(@types/node@25.9.0)(typescript@5.9.3))(playwright@1.51.1)(vite@8.0.8(@types/node@25.9.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.7) jsdom: 29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0) transitivePeerDependencies: - msw diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1f6cd63d35..2a5eb50b3c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -17,8 +17,8 @@ overrides: "@headlessui/react": "^2.2.4" "@tiptap/core": "^3.0.0" "@tiptap/pm": "^3.0.0" - "vitest": "4.1.2" - "@vitest/runner": "4.1.2" + "vitest": "4.1.7" + "@vitest/runner": "4.1.7" "@y/prosemirror>lib0": "1.0.0-rc.13" allowBuilds: "@parcel/watcher": true diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000000..362e4126db --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,6 @@ +# vitest-browser auto-saved debug screenshots on test failure (separate +# from `toMatchScreenshot` reference shots, which use `*-chromium-darwin.png`). +src/browser/**/__screenshots__/**/*-1.png + +# vitest-browser attachments (debug artifacts saved during test runs). +.vitest-attachments diff --git a/tests/.vitest-attachments/src/browser/y-prosemirror/propChanges.test.tsx/prop-change-image-source-actual-chromium-darwin.png b/tests/.vitest-attachments/src/browser/y-prosemirror/propChanges.test.tsx/prop-change-image-source-actual-chromium-darwin.png new file mode 100644 index 0000000000..37b96b3b1d Binary files /dev/null and b/tests/.vitest-attachments/src/browser/y-prosemirror/propChanges.test.tsx/prop-change-image-source-actual-chromium-darwin.png differ diff --git a/tests/package.json b/tests/package.json index ea7da9e138..a276b22593 100644 --- a/tests/package.json +++ b/tests/package.json @@ -7,6 +7,8 @@ "lint": "eslint src --max-warnings 0", "playwright": "playwright test", "test": "vitest --run", + "test:browser": "vitest --run --config ./vite.config.browser.ts", + "test:browser:updateSnaps": "vitest --run --config ./vite.config.browser.ts -u", "test:updateSnaps": "docker run --rm -e RUN_IN_DOCKER=true --network host -v $(pwd)/..:/work/ -w /work/tests -it mcr.microsoft.com/playwright:v1.51.1-noble npx playwright test -u", "test-ct": "playwright test -c playwright-ct.config.ts --headed", "test-ct:updateSnaps": "docker run --rm -e RUN_IN_DOCKER=true --network host -v $(pwd)/..:/work/ -w /work/tests -it mcr.microsoft.com/playwright:v1.51.1-noble npx playwright test -c playwright-ct.config.ts -u", @@ -24,6 +26,11 @@ "@types/node": "^20.19.22", "@types/react": "^19.2.3", "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "@vitest/browser": "4.1.7", + "@vitest/browser-playwright": "4.1.7", + "@y/protocols": "^1.0.6-rc.1", + "@y/y": "^14.0.0-rc.16", "eslint": "^8.57.1", "htmlfy": "^0.6.7", "react": "^19.2.5", @@ -32,7 +39,8 @@ "rimraf": "^5.0.10", "vite": "^8.0.8", "vite-plugin-eslint": "^1.8.1", - "vitest": "^4.1.2" + "vitest": "4.1.7", + "vitest-browser-react": "^2.2.0" }, "eslintConfig": { "extends": [ diff --git a/tests/src/browser/vitestSetup.ts b/tests/src/browser/vitestSetup.ts new file mode 100644 index 0000000000..87db382be8 --- /dev/null +++ b/tests/src/browser/vitestSetup.ts @@ -0,0 +1,13 @@ +import { afterEach, beforeEach } from "vitest"; + +// Make BlockNote's `UniqueID` extension emit deterministic, incrementing +// numeric IDs instead of UUIDs. Snapshots that pick up auto-generated +// block ids (e.g. a trailing paragraph BlockNote injects after an image +// or heading) stay stable across runs. +beforeEach(() => { + (window as Window & { __TEST_OPTIONS?: any }).__TEST_OPTIONS = {}; +}); + +afterEach(() => { + delete (window as Window & { __TEST_OPTIONS?: any }).__TEST_OPTIONS; +}); diff --git a/tests/src/browser/y-prosemirror/__screenshots__/basicText.concurrent.test.tsx/concurrent-bold-vs-italic-chromium-darwin.png b/tests/src/browser/y-prosemirror/__screenshots__/basicText.concurrent.test.tsx/concurrent-bold-vs-italic-chromium-darwin.png new file mode 100644 index 0000000000..73757b0623 Binary files /dev/null and b/tests/src/browser/y-prosemirror/__screenshots__/basicText.concurrent.test.tsx/concurrent-bold-vs-italic-chromium-darwin.png differ diff --git a/tests/src/browser/y-prosemirror/__screenshots__/basicText.concurrent.test.tsx/concurrent-typo-fix-vs-delete-chromium-darwin.png b/tests/src/browser/y-prosemirror/__screenshots__/basicText.concurrent.test.tsx/concurrent-typo-fix-vs-delete-chromium-darwin.png new file mode 100644 index 0000000000..0988bdb493 Binary files /dev/null and b/tests/src/browser/y-prosemirror/__screenshots__/basicText.concurrent.test.tsx/concurrent-typo-fix-vs-delete-chromium-darwin.png differ diff --git a/tests/src/browser/y-prosemirror/__screenshots__/basicText.test.tsx/suggestion-mode-add-bold-chromium-darwin.png b/tests/src/browser/y-prosemirror/__screenshots__/basicText.test.tsx/suggestion-mode-add-bold-chromium-darwin.png new file mode 100644 index 0000000000..b410bde1ed Binary files /dev/null and b/tests/src/browser/y-prosemirror/__screenshots__/basicText.test.tsx/suggestion-mode-add-bold-chromium-darwin.png differ diff --git a/tests/src/browser/y-prosemirror/__screenshots__/basicText.test.tsx/suggestion-mode-add-italic-to-bold-chromium-darwin.png b/tests/src/browser/y-prosemirror/__screenshots__/basicText.test.tsx/suggestion-mode-add-italic-to-bold-chromium-darwin.png new file mode 100644 index 0000000000..20049426e4 Binary files /dev/null and b/tests/src/browser/y-prosemirror/__screenshots__/basicText.test.tsx/suggestion-mode-add-italic-to-bold-chromium-darwin.png differ diff --git a/tests/src/browser/y-prosemirror/__screenshots__/basicText.test.tsx/suggestion-mode-remove-bold-chromium-darwin.png b/tests/src/browser/y-prosemirror/__screenshots__/basicText.test.tsx/suggestion-mode-remove-bold-chromium-darwin.png new file mode 100644 index 0000000000..b81e90f852 Binary files /dev/null and b/tests/src/browser/y-prosemirror/__screenshots__/basicText.test.tsx/suggestion-mode-remove-bold-chromium-darwin.png differ diff --git a/tests/src/browser/y-prosemirror/__screenshots__/basicText.test.tsx/suggestion-mode-universe-chromium-darwin.png b/tests/src/browser/y-prosemirror/__screenshots__/basicText.test.tsx/suggestion-mode-universe-chromium-darwin.png new file mode 100644 index 0000000000..1082952788 Binary files /dev/null and b/tests/src/browser/y-prosemirror/__screenshots__/basicText.test.tsx/suggestion-mode-universe-chromium-darwin.png differ diff --git a/tests/src/browser/y-prosemirror/__screenshots__/propChanges.concurrent.test.tsx/concurrent-textColor-vs-backgroundColor-chromium-darwin.png b/tests/src/browser/y-prosemirror/__screenshots__/propChanges.concurrent.test.tsx/concurrent-textColor-vs-backgroundColor-chromium-darwin.png new file mode 100644 index 0000000000..b9cbae43a5 Binary files /dev/null and b/tests/src/browser/y-prosemirror/__screenshots__/propChanges.concurrent.test.tsx/concurrent-textColor-vs-backgroundColor-chromium-darwin.png differ diff --git a/tests/src/browser/y-prosemirror/__screenshots__/propChanges.test.tsx/prop-change-heading-level-chromium-darwin.png b/tests/src/browser/y-prosemirror/__screenshots__/propChanges.test.tsx/prop-change-heading-level-chromium-darwin.png new file mode 100644 index 0000000000..692857d858 Binary files /dev/null and b/tests/src/browser/y-prosemirror/__screenshots__/propChanges.test.tsx/prop-change-heading-level-chromium-darwin.png differ diff --git a/tests/src/browser/y-prosemirror/__screenshots__/propChanges.test.tsx/prop-change-image-source-chromium-darwin.png b/tests/src/browser/y-prosemirror/__screenshots__/propChanges.test.tsx/prop-change-image-source-chromium-darwin.png new file mode 100644 index 0000000000..fb3717a0b7 Binary files /dev/null and b/tests/src/browser/y-prosemirror/__screenshots__/propChanges.test.tsx/prop-change-image-source-chromium-darwin.png differ diff --git a/tests/src/browser/y-prosemirror/__screenshots__/propChanges.test.tsx/prop-change-image-width-chromium-darwin.png b/tests/src/browser/y-prosemirror/__screenshots__/propChanges.test.tsx/prop-change-image-width-chromium-darwin.png new file mode 100644 index 0000000000..f8197139f1 Binary files /dev/null and b/tests/src/browser/y-prosemirror/__screenshots__/propChanges.test.tsx/prop-change-image-width-chromium-darwin.png differ diff --git a/tests/src/browser/y-prosemirror/__screenshots__/propChanges.test.tsx/prop-change-text-alignment-chromium-darwin.png b/tests/src/browser/y-prosemirror/__screenshots__/propChanges.test.tsx/prop-change-text-alignment-chromium-darwin.png new file mode 100644 index 0000000000..d0095a28f3 Binary files /dev/null and b/tests/src/browser/y-prosemirror/__screenshots__/propChanges.test.tsx/prop-change-text-alignment-chromium-darwin.png differ diff --git a/tests/src/browser/y-prosemirror/basicText.concurrent.test.tsx b/tests/src/browser/y-prosemirror/basicText.concurrent.test.tsx new file mode 100644 index 0000000000..e51227a903 --- /dev/null +++ b/tests/src/browser/y-prosemirror/basicText.concurrent.test.tsx @@ -0,0 +1,250 @@ +/* eslint-disable testing-library/render-result-naming-convention */ +/** + * Vitest browser-mode tests for two-user concurrent suggestion edits. + * Each test sets up three side-by-side editors (User A, User B, + * Merged) backed by `baseDoc` + `suggestionDocA`/`B`/`Merged`, applies + * independent suggestion edits from A and B, calls `sync()` to fan + * both updates into the merged doc, and snapshots the converged state. + * + * TODO: BlockNote's `mapAttributionToMark` (YSync.ts) hashes user IDs + * from the attribution data to pick a color from a fixed palette, but + * `Y.Attributions()` ships empty and nothing in the editor pipeline + * populates it from the editor's `user` / awareness. Result: every + * mark in every test renders as `userColorPalette[0]` (#30bced), + * regardless of which user actually made the edit. In the merged + * snapshots below we therefore cannot tell A's marks from B's. Decide + * whether the attribution layer should automatically tag writes with + * the local awareness user, or whether tests should construct an + * `Attributions` instance with pre-registered client-id → user-id + * mappings. + */ +import { expect, test } from "vitest"; + +import { setupConcurrentSuggestionTest } from "./fixtures/concurrentSuggestionFixture.js"; +import { + editorHtml, + waitForSuggestion, + ydocXml, +} from "./fixtures/suggestionFixture.js"; + +// Concurrent text edits on overlapping range: A fixes a typo while B +// deletes the whole word. After CRDT merge, snapshot what the merged +// editor ends up displaying. +test("concurrent: A fixes typo, B deletes the word", async () => { + const { + userA, + userB, + merged, + baseDoc, + suggestionDocA, + suggestionDocB, + suggestionDocMerged, + screen, + seed, + enableSuggestions, + sync, + } = await setupConcurrentSuggestionTest({ + userAAction: "fix typo", + userBAction: "delete word", + }); + + // Seed: A writes "hello wrold" (typo) directly to baseDoc since + // suggestion mode isn't on yet. Then `seed()` fans baseDoc into + // all three suggestion docs so everyone starts from the same state. + userA.editor.replaceBlocks(userA.editor.document, [ + { id: "block-hello", type: "paragraph", content: "hello wrold" }, + ]); + seed(); + + await expect + .element(screen.getByTestId(userA.testId).getByText("hello wrold")) + .toBeVisible(); + + // Switch all editors into suggestion mode (subsequent edits in A + // and B are recorded as suggestions, merged starts watching its + // suggestion doc for incoming updates). + enableSuggestions(); + + // A: fix typo "wrold" -> "world". + const [blockA] = userA.editor.document; + userA.editor.updateBlock(blockA, { + type: "paragraph", + content: "hello world", + }); + + // B: delete the misspelled word entirely. + const [blockB] = userB.editor.document; + userB.editor.updateBlock(blockB, { type: "paragraph", content: "hello " }); + + await waitForSuggestion(userA.editor); + await waitForSuggestion(userB.editor); + + // Merge A's and B's suggestions into the merged doc. + sync(); + await waitForSuggestion(merged.editor); + + await expect(screen.getByTestId("editor-root")).toMatchScreenshot( + "concurrent-typo-fix-vs-delete", + ); + + // TODO: the merged YDoc ends up at "hello o" – an `o` survives even + // though both A (who replaced "wrold" with "world") and B (who + // deleted "wrold" outright) effectively wanted "wrold" gone. The + // CRDT keeps A's inserted `o` because B's delete-range covered the + // original "wrold" letters but not A's freshly-inserted characters, + // so the union of "delete everything B saw" + "keep what A added" + // leaves a stray `o`. Worth deciding whether this is the desired + // merge semantic for the product or whether the suggestion layer + // should resolve overlapping edits differently. + expect(ydocXml(baseDoc)).toMatchInlineSnapshot(` + " + + hello wrold + + " + `); + expect(ydocXml(suggestionDocA)).toMatchInlineSnapshot(` + " + + hello world + + " + `); + expect(ydocXml(suggestionDocB)).toMatchInlineSnapshot(` + " + + hello + + " + `); + expect(ydocXml(suggestionDocMerged)).toMatchInlineSnapshot(` + " + + hello o + + " + `); + expect(editorHtml(merged.editor)).toMatchInlineSnapshot(` + " + + + + hello + w + o + rold + + + + " + `); +}); + +// Concurrent format edits on the same word: A adds bold, B adds +// italic. After CRDT merge, both marks should land on "world". +test("concurrent: A bolds the word, B italicises the word", async () => { + const { + userA, + userB, + merged, + baseDoc, + suggestionDocA, + suggestionDocB, + suggestionDocMerged, + screen, + seed, + enableSuggestions, + sync, + } = await setupConcurrentSuggestionTest({ + userAAction: "bold 'world'", + userBAction: "italicise 'world'", + }); + + // Seed: A writes plain "hello world" directly to baseDoc, then + // `seed()` fans it into all three suggestion docs. + userA.editor.replaceBlocks(userA.editor.document, [ + { id: "block-hello", type: "paragraph", content: "hello world" }, + ]); + seed(); + + await expect + .element(screen.getByTestId(userA.testId).getByText("hello world")) + .toBeVisible(); + + enableSuggestions(); + + // A: bold "world". + const [blockA] = userA.editor.document; + userA.editor.updateBlock(blockA, { + type: "paragraph", + content: [ + { type: "text", text: "hello ", styles: {} }, + { type: "text", text: "world", styles: { bold: true } }, + ], + }); + + // B: italic "world". + const [blockB] = userB.editor.document; + userB.editor.updateBlock(blockB, { + type: "paragraph", + content: [ + { type: "text", text: "hello ", styles: {} }, + { type: "text", text: "world", styles: { italic: true } }, + ], + }); + + await waitForSuggestion(userA.editor); + await waitForSuggestion(userB.editor); + + sync(); + await waitForSuggestion(merged.editor); + + await expect(screen.getByTestId("editor-root")).toMatchScreenshot( + "concurrent-bold-vs-italic", + ); + + expect(ydocXml(baseDoc)).toMatchInlineSnapshot(` + " + + hello world + + " + `); + expect(ydocXml(suggestionDocA)).toMatchInlineSnapshot(` + " + + hello world + + " + `); + expect(ydocXml(suggestionDocB)).toMatchInlineSnapshot(` + " + + hello world + + " + `); + expect(ydocXml(suggestionDocMerged)).toMatchInlineSnapshot(` + " + + hello world + + " + `); + expect(editorHtml(merged.editor)).toMatchInlineSnapshot(` + " + + + + hello + + + world + + + + + + " + `); +}); diff --git a/tests/src/browser/y-prosemirror/basicText.test.tsx b/tests/src/browser/y-prosemirror/basicText.test.tsx new file mode 100644 index 0000000000..7b7afcc7d2 --- /dev/null +++ b/tests/src/browser/y-prosemirror/basicText.test.tsx @@ -0,0 +1,324 @@ +/* eslint-disable testing-library/render-result-naming-convention */ +/** + * Vitest browser-mode tests for suggestion-mode editing. Each test + * sets up a fresh editor + base/suggestion Y.Doc pair via + * `setupSuggestionTest()`, applies an edit in suggestion mode, and + * captures a screenshot plus inline XML snapshots of both Y.Docs and + * the ProseMirror document. The PM doc is where the suggestion marks + * live – the Y.Docs only carry the content of the different branches. + */ +import { SuggestionsExtension } from "@blocknote/core/y"; +import { expect, test } from "vitest"; + +import { + editorHtml, + setupSuggestionTest, + waitForSuggestion, + ydocXml, +} from "./fixtures/suggestionFixture.js"; + +// Pure text edit: replace one word with another and confirm the diff +// is rendered as inline / spans around the changed letters. +test("suggestion mode: 'hello world' -> 'hello universe'", async () => { + const { editor, screen, baseDoc, suggestionDoc, sync } = + await setupSuggestionTest({ userAction: "rename last word" }); + + // 1. Set the base doc to "hello world". The block id is pinned so the + // snapshots stay deterministic. + editor.replaceBlocks(editor.document, [ + { id: "block-hello", type: "paragraph", content: "hello world" }, + ]); + + // 2. Replay base updates into the suggestion doc so both docs start + // from the same state. + sync(); + + await expect + .element(screen.getByTestId("editor-A").getByText("hello world")) + .toBeVisible(); + + // 3. Subsequent edits are recorded as suggestions instead of mutating + // the doc directly. + editor.getExtension(SuggestionsExtension)!.enableSuggestions(); + + // 4. Replace "world" with "universe" via updateBlock. + const [block] = editor.document; + editor.updateBlock(block, { type: "paragraph", content: "hello universe" }); + + // Wait for the suggestion edit to land in the DOM (React commits the + // re-render on the next frame; without this the screenshot can race + // the update). "unive" only exists once "world" -> "universe" has + // been split into / spans, so this is a precise sentinel. + await expect + .element(screen.getByTestId("editor-A").getByText("unive")) + .toBeVisible(); + + // 5a. Visual snapshot of the rendered editor. + await expect(screen.getByTestId("editor-root")).toMatchScreenshot( + "suggestion-mode-universe", + ); + + // 5b. Y.Doc XML – just the merged textual state; suggestion marks + // don't live here. + expect(ydocXml(baseDoc)).toMatchInlineSnapshot(` + " + + hello world + + " + `); + expect(ydocXml(suggestionDoc)).toMatchInlineSnapshot(` + " + + hello universe + + " + `); + + // 5c. ProseMirror XML – this is where the suggestion marks + // (`y-attributed-insert` / `y-attributed-delete`) live. + expect(editorHtml(editor)).toMatchInlineSnapshot(` + " + + + + hello + wo + unive + r + ld + se + + + + " + `); +}); + +// TODO: format-only suggestions have no visual marker in the rendered +// editor – the screenshot for this test shows "hello **world**" with +// no indication that the bold is a *pending* suggestion. Only +// `y-attributed-insert` / `y-attributed-delete` have a `toDOM` in +// SuggestionMarks.ts; `y-attributed-format` does not. Decide whether +// this is intentional (formats considered "trivial") or a UX gap. +// +// Format-only addition: text content stays the same but a style mark +// (bold) is added on top. Surfaces how suggestions track pure format +// changes (currently `y-attributed-format`, with no visual marker). +test("suggestion mode: add bold to 'world'", async () => { + const { editor, screen, baseDoc, suggestionDoc, sync } = + await setupSuggestionTest({ userAction: "bold 'world'" }); + + // Base: plain "hello world". + editor.replaceBlocks(editor.document, [ + { id: "block-hello", type: "paragraph", content: "hello world" }, + ]); + sync(); + await expect + .element(screen.getByTestId("editor-A").getByText("hello world")) + .toBeVisible(); + + editor.getExtension(SuggestionsExtension)!.enableSuggestions(); + + // Suggestion edit: bold the word "world" (content text is unchanged, + // only the style differs). + const [block] = editor.document; + editor.updateBlock(block, { + type: "paragraph", + content: [ + { type: "text", text: "hello ", styles: {} }, + { type: "text", text: "world", styles: { bold: true } }, + ], + }); + + await waitForSuggestion(editor); + + await expect(screen.getByTestId("editor-root")).toMatchScreenshot( + "suggestion-mode-add-bold", + ); + + // TODO: the base and suggestion YDoc XML snapshots below are + // identical even though one represents the "before bold" state and + // the other the "after bold" state. `Y.XmlFragment.toString()` only + // serialises element/text structure – marks and attribution data + // live elsewhere and don't surface here. Consider a richer YDoc + // serializer that includes formatting + attribution metadata so + // these snapshots actually differ. + expect(ydocXml(baseDoc)).toMatchInlineSnapshot(` + " + + hello world + + " + `); + expect(ydocXml(suggestionDoc)).toMatchInlineSnapshot(` + " + + hello world + + " + `); + expect(editorHtml(editor)).toMatchInlineSnapshot(` + " + + + + hello + + world + + + + + " + `); +}); + +// Format-only removal: bold mark is stripped from an already-styled +// word, text content unchanged. Mirror of the add-bold case to check +// removal is handled symmetrically. +test("suggestion mode: remove bold from 'world'", async () => { + const { editor, screen, baseDoc, suggestionDoc, sync } = + await setupSuggestionTest({ userAction: "unbold 'world'" }); + + // Base: "hello " + bold "world". + editor.replaceBlocks(editor.document, [ + { + id: "block-hello", + type: "paragraph", + content: [ + { type: "text", text: "hello ", styles: {} }, + { type: "text", text: "world", styles: { bold: true } }, + ], + }, + ]); + sync(); + // Use the full paragraph text – the User A column heading also + // contains the word "world", which would clash with getByText. + await expect + .element(screen.getByTestId("editor-A").getByText("hello world")) + .toBeVisible(); + + editor.getExtension(SuggestionsExtension)!.enableSuggestions(); + + // Suggestion edit: strip bold from "world". + const [block] = editor.document; + editor.updateBlock(block, { + type: "paragraph", + content: "hello world", + }); + + await waitForSuggestion(editor); + + await expect(screen.getByTestId("editor-root")).toMatchScreenshot( + "suggestion-mode-remove-bold", + ); + + expect(ydocXml(baseDoc)).toMatchInlineSnapshot(` + " + + hello world + + " + `); + expect(ydocXml(suggestionDoc)).toMatchInlineSnapshot(` + " + + hello world + + " + `); + expect(editorHtml(editor)).toMatchInlineSnapshot(` + " + + + + hello + world + + + + " + `); +}); + +// TODO: the snapshot below reveals that `y-attributed-format` wraps +// *all* marks on the affected range, not just the newly added one. +// The PM XML shows +// world +// so from the attribution data alone we can't tell which mark is new +// (italic) and which is pre-existing (bold). If accept/reject logic +// needs to revert only the new mark, this granularity is insufficient. +// +// Format added on top of an existing format: bold "world" gets italic +// layered on (bold is preserved). Checks that suggestion attribution +// is recorded only for the new mark, not the pre-existing one. +test("suggestion mode: add italic to already-bold 'world'", async () => { + const { editor, screen, baseDoc, suggestionDoc, sync } = + await setupSuggestionTest({ userAction: "italic on top of bold" }); + + // Base: "hello " + bold "world". + editor.replaceBlocks(editor.document, [ + { + id: "block-hello", + type: "paragraph", + content: [ + { type: "text", text: "hello ", styles: {} }, + { type: "text", text: "world", styles: { bold: true } }, + ], + }, + ]); + sync(); + await expect + .element(screen.getByTestId("editor-A").getByText("hello world")) + .toBeVisible(); + + editor.getExtension(SuggestionsExtension)!.enableSuggestions(); + + // Suggestion edit: add italic to "world" while keeping it bold. + const [block] = editor.document; + editor.updateBlock(block, { + type: "paragraph", + content: [ + { type: "text", text: "hello ", styles: {} }, + { type: "text", text: "world", styles: { bold: true, italic: true } }, + ], + }); + + await waitForSuggestion(editor); + + await expect(screen.getByTestId("editor-root")).toMatchScreenshot( + "suggestion-mode-add-italic-to-bold", + ); + + expect(ydocXml(baseDoc)).toMatchInlineSnapshot(` + " + + hello world + + " + `); + expect(ydocXml(suggestionDoc)).toMatchInlineSnapshot(` + " + + hello world + + " + `); + expect(editorHtml(editor)).toMatchInlineSnapshot(` + " + + + + hello + + + world + + + + + + " + `); +}); diff --git a/tests/src/browser/y-prosemirror/fixtures/concurrentSuggestionFixture.tsx b/tests/src/browser/y-prosemirror/fixtures/concurrentSuggestionFixture.tsx new file mode 100644 index 0000000000..0b81bc775e --- /dev/null +++ b/tests/src/browser/y-prosemirror/fixtures/concurrentSuggestionFixture.tsx @@ -0,0 +1,242 @@ +/* eslint-disable testing-library/render-result-naming-convention */ +/** + * Fixture for two-user concurrent suggestion tests. + * + * Layout: + * ┌──────┬─────────────────────┬─────────────────────┬────────┐ + * │ Base │ User A: │ User B: >; + /** + * Replay `baseDoc` into all three suggestion docs. Call after + * seeding the initial content via A's editor (with suggestion mode + * off – writes go straight to `baseDoc`) so all four docs start aligned. + */ + seed: () => void; + /** + * Switch all three editors into suggestion mode. Call after `seed()` + * – subsequent edits in A and B are recorded as suggestions, and the + * merged editor starts observing `suggestionDocMerged` for updates. + */ + enableSuggestions: () => void; + /** Fan A's and B's suggestion updates into `suggestionDocMerged`. */ + sync: () => void; +} + +const USER_A = { name: "User A", color: "#30bced" }; +const USER_B = { name: "User B", color: "#ee6352" }; +const USER_MERGED = { name: "Merged", color: "#888888" }; +const USER_BASE = { name: "Base", color: "#888888" }; + +export interface ConcurrentSuggestionFixtureOptions { + /** 1-5 word description of what User A does (rendered as column heading). */ + userAAction: string; + /** 1-5 word description of what User B does (rendered as column heading). */ + userBAction: string; +} + +export async function setupConcurrentSuggestionTest({ + userAAction, + userBAction, +}: ConcurrentSuggestionFixtureOptions): Promise { + const baseDoc = new Y.Doc(); + const suggestionDocA = new Y.Doc({ isSuggestionDoc: true }); + const suggestionDocB = new Y.Doc({ isSuggestionDoc: true }); + const suggestionDocMerged = new Y.Doc({ isSuggestionDoc: true }); + + const managerA = Y.createAttributionManagerFromDiff( + baseDoc, + suggestionDocA, + { attrs: new Y.Attributions() }, + ); + managerA.suggestionMode = true; + + const managerB = Y.createAttributionManagerFromDiff( + baseDoc, + suggestionDocB, + { attrs: new Y.Attributions() }, + ); + managerB.suggestionMode = true; + + // Merged is a viewer – it shows both users' suggestions but doesn't + // record new ones, so `suggestionMode = false`. + const managerMerged = Y.createAttributionManagerFromDiff( + baseDoc, + suggestionDocMerged, + { attrs: new Y.Attributions() }, + ); + managerMerged.suggestionMode = false; + + const awarenessA = makeAwareness(baseDoc, USER_A); + const awarenessB = makeAwareness(baseDoc, USER_B); + const awarenessMerged = makeAwareness(baseDoc, USER_MERGED); + + let editorBase!: BlockNoteEditor; + let editorA!: BlockNoteEditor; + let editorB!: BlockNoteEditor; + let editorMerged!: BlockNoteEditor; + + function Editors() { + editorBase = useCreateBlockNote( + withCollaboration({ + collaboration: { + fragment: baseDoc.get("doc"), + provider: { awareness: new Awareness(baseDoc) }, + user: USER_BASE, + }, + }), + ); + editorA = useCreateBlockNote( + withCollaboration({ + collaboration: { + fragment: baseDoc.get("doc"), + provider: { awareness: awarenessA }, + suggestionDoc: suggestionDocA, + attributionManager: managerA, + user: USER_A, + }, + }), + ); + editorB = useCreateBlockNote( + withCollaboration({ + collaboration: { + fragment: baseDoc.get("doc"), + provider: { awareness: awarenessB }, + suggestionDoc: suggestionDocB, + attributionManager: managerB, + user: USER_B, + }, + }), + ); + editorMerged = useCreateBlockNote( + withCollaboration({ + collaboration: { + fragment: baseDoc.get("doc"), + provider: { awareness: awarenessMerged }, + suggestionDoc: suggestionDocMerged, + attributionManager: managerMerged, + user: USER_MERGED, + }, + }), + ); + + return ( +
+
+ Base + +
+
+ User A: {userAAction} + +
+
+ User B: {userBAction} + +
+
+ Merged + +
+
+ ); + } + + // Four columns at 1fr each need a wider viewport so the rightmost + // column doesn't clip BlockNote content. + await page.viewport(1800, 800); + + const screen = await render(); + + return { + userA: { editor: editorA, testId: "editor-A" }, + userB: { editor: editorB, testId: "editor-B" }, + merged: { editor: editorMerged, testId: "editor-merged" }, + baseDoc, + suggestionDocA, + suggestionDocB, + suggestionDocMerged, + screen, + seed: () => { + const update = Y.encodeStateAsUpdate(baseDoc); + Y.applyUpdate(suggestionDocA, update); + Y.applyUpdate(suggestionDocB, update); + Y.applyUpdate(suggestionDocMerged, update); + }, + enableSuggestions: () => { + editorA.getExtension(SuggestionsExtension)!.enableSuggestions(); + editorB.getExtension(SuggestionsExtension)!.enableSuggestions(); + editorMerged.getExtension(SuggestionsExtension)!.enableSuggestions(); + }, + sync: () => { + Y.applyUpdate(suggestionDocMerged, Y.encodeStateAsUpdate(suggestionDocA)); + Y.applyUpdate(suggestionDocMerged, Y.encodeStateAsUpdate(suggestionDocB)); + }, + }; +} + +function makeAwareness( + doc: Y.Doc, + user: { name: string; color: string }, +): Awareness { + const a = new Awareness(doc); + a.setLocalStateField("user", user); + return a; +} diff --git a/tests/src/browser/y-prosemirror/fixtures/suggestionFixture.tsx b/tests/src/browser/y-prosemirror/fixtures/suggestionFixture.tsx new file mode 100644 index 0000000000..e49bba8a1b --- /dev/null +++ b/tests/src/browser/y-prosemirror/fixtures/suggestionFixture.tsx @@ -0,0 +1,207 @@ +/* eslint-disable testing-library/render-result-naming-convention */ +/** + * Shared fixture for browser-mode suggestion tests. + * + * Layout: + * ┌──────────┬──────────────────────┐ + * │ Base │ User A: │ + * └──────────┴──────────────────────┘ + * + * - `Base` is a read-only editor bound to `baseDoc` – it shows the + * pre-suggestion state and is visible in the screenshot so the + * reviewer can see the "before" without leaving the file. + * - `User A` is the suggesting editor. Its column heading includes a + * short caller-supplied action description so the screenshot is + * self-explanatory. + * + * The provider/yhub round-trip is replaced by a manual `sync()`. + */ +import "@blocknote/core/fonts/inter.css"; +import "@blocknote/mantine/style.css"; + +import { BlockNoteEditor } from "@blocknote/core"; +import { withCollaboration } from "@blocknote/core/y"; +import { BlockNoteView } from "@blocknote/mantine"; +import { useCreateBlockNote } from "@blocknote/react"; +import { Node as PMNode } from "@tiptap/pm/model"; +import { Awareness } from "@y/protocols/awareness"; +import * as Y from "@y/y"; +import { prettify } from "htmlfy"; +import { expect } from "vitest"; +import { page } from "vitest/browser"; +import { render } from "vitest-browser-react"; + +export interface SuggestionFixture { + /** User A's editor – this is the one the test makes suggestions through. */ + editor: BlockNoteEditor; + screen: Awaited>; + baseDoc: Y.Doc; + suggestionDoc: Y.Doc; + /** Replay updates from `baseDoc` into `suggestionDoc`. */ + sync: () => void; +} + +export interface SuggestionFixtureOptions { + /** + * 1-5 word description of what User A does (e.g. "fix typo", + * "bold world"). Rendered in the User A column heading so the + * screenshot is self-explanatory. + */ + userAction: string; +} + +export async function setupSuggestionTest({ + userAction, +}: SuggestionFixtureOptions): Promise { + const baseDoc = new Y.Doc(); + const baseAwareness = new Awareness(baseDoc); + baseAwareness.setLocalStateField("user", { + name: "User A", + color: "#30bced", + }); + + const suggestionDoc = new Y.Doc({ isSuggestionDoc: true }); + const attributionManager = Y.createAttributionManagerFromDiff( + baseDoc, + suggestionDoc, + { attrs: new Y.Attributions() }, + ); + attributionManager.suggestionMode = true; + + let editorA!: BlockNoteEditor; + let editorBase!: BlockNoteEditor; + + function Editors() { + editorA = useCreateBlockNote( + withCollaboration({ + collaboration: { + fragment: baseDoc.get("doc"), + provider: { awareness: baseAwareness }, + suggestionDoc, + attributionManager, + user: { name: "User A", color: "#30bced" }, + }, + }), + ); + editorBase = useCreateBlockNote( + withCollaboration({ + collaboration: { + fragment: baseDoc.get("doc"), + provider: { awareness: new Awareness(baseDoc) }, + user: { name: "Base", color: "#888888" }, + }, + }), + ); + return ( +
+
+ Base + +
+
+ User A: {userAction} + +
+
+ ); + } + + await page.viewport(1200, 800); + + const screen = await render(); + + return { + editor: editorA, + screen, + baseDoc, + suggestionDoc, + sync: () => Y.applyUpdate(suggestionDoc, Y.encodeStateAsUpdate(baseDoc)), + }; +} + +/** + * Wait until any suggestion mark (`y-attributed-insert` / + * `y-attributed-delete`) is present in the editor's PM doc. Use this + * after a suggestion-mode edit before snapshotting/screenshotting – + * the PM transaction is sync but the React/DOM commit is not. + * + * For tests whose edit changes visible text, prefer waiting on the + * inserted text via `expect.element(getByText(...))` – it's more + * meaningful. + */ +export async function waitForSuggestion( + editor: BlockNoteEditor, +): Promise { + await expect + .poll(() => editor.prosemirrorState.doc.toString().includes("y-attributed")) + .toBe(true); +} + +/** Pretty-print a Y.Doc's `doc` XmlFragment for an inline snapshot. */ +export function ydocXml(doc: Y.Doc): string { + // `Y.XmlFragment.toString()` emits HTML5-style unquoted attributes + // for non-string values (e.g. `level=1`, `isToggleable=false`). + // htmlfy mangles those, so we wrap each unquoted value in quotes + // before pretty-printing. + const raw = doc + .get("doc") + .toString() + .replace( + /(\w[\w-]*)=([^"'\s>]+)/g, + (_, name, value) => `${name}="${value}"`, + ); + return prettify(raw, { tag_wrap: true }); +} + +/** + * Pretty-print the editor's ProseMirror doc for an inline snapshot. + * + * We walk the node tree directly rather than going through + * `DOMSerializer` (BlockNote's `renderHTML` adds CSS scaffolding that + * we don't want in snapshots) or `Node.toString()` (drops attrs, so + * block ids and suggestion-mark colors would disappear). + */ +export function editorHtml(editor: BlockNoteEditor): string { + return prettify(pmNodeToXml(editor.prosemirrorState.doc), { + tag_wrap: true, + }); +} + +function pmNodeToXml(node: PMNode): string { + if (node.isText) { + let out = escapeXml(node.text ?? ""); + // PM stores marks outermost-first; wrap innermost-first to preserve order. + for (const mark of node.marks) { + out = `<${mark.type.name}${formatAttrs(mark.attrs)}>${out}`; + } + return out; + } + let inner = ""; + node.content.forEach((child) => { + inner += pmNodeToXml(child); + }); + return `<${node.type.name}${formatAttrs(node.attrs)}>${inner}`; +} + +function formatAttrs(attrs: Record): string { + return Object.entries(attrs) + .filter(([, v]) => v !== null && v !== undefined) + .map(([k, v]) => ` ${k}="${escapeXml(String(v))}"`) + .join(""); +} + +function escapeXml(text: string): string { + return text + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} diff --git a/tests/src/browser/y-prosemirror/propChanges.concurrent.test.tsx b/tests/src/browser/y-prosemirror/propChanges.concurrent.test.tsx new file mode 100644 index 0000000000..2a7d2fb9b0 --- /dev/null +++ b/tests/src/browser/y-prosemirror/propChanges.concurrent.test.tsx @@ -0,0 +1,123 @@ +/* eslint-disable testing-library/render-result-naming-convention */ +/** + * Vitest browser-mode tests for two-user concurrent prop-change + * suggestions. Same shape as `basicText.concurrent.test.tsx` but the + * edits are block-level prop changes rather than content edits. + * + * See `propChanges.test.tsx` for the TODO on prop changes producing no + * `y-attributed-*` mark – the same applies here. + */ +import { expect, test } from "vitest"; + +import { setupConcurrentSuggestionTest } from "./fixtures/concurrentSuggestionFixture.js"; +import { + editorHtml, + ydocXml, +} from "./fixtures/suggestionFixture.js"; + +// Two users edit independent props on the same block: A changes +// `textColor`, B changes `backgroundColor`. Neither edit touches the +// other's prop, so the CRDT merge should preserve both. +test("concurrent: A changes textColor, B changes backgroundColor", async () => { + const { + userA, + userB, + merged, + baseDoc, + suggestionDocA, + suggestionDocB, + suggestionDocMerged, + screen, + seed, + enableSuggestions, + sync, + } = await setupConcurrentSuggestionTest({ + userAAction: "red text", + userBAction: "yellow background", + }); + + // Seed: plain "hello world" with default colors. + userA.editor.replaceBlocks(userA.editor.document, [ + { id: "block-hello", type: "paragraph", content: "hello world" }, + ]); + seed(); + await expect + .element(screen.getByTestId(userA.testId).getByText("hello world")) + .toBeVisible(); + + enableSuggestions(); + + // A: change textColor to red. + const [blockA] = userA.editor.document; + userA.editor.updateBlock(blockA, { + type: "paragraph", + props: { textColor: "red" }, + }); + + // B: change backgroundColor to yellow. + const [blockB] = userB.editor.document; + userB.editor.updateBlock(blockB, { + type: "paragraph", + props: { backgroundColor: "yellow" }, + }); + + // Prop changes don't generate y-attributed marks, so we poll on the + // individual editor doc states instead. + await expect + .poll(() => userA.editor.document[0]?.props?.textColor) + .toBe("red"); + await expect + .poll(() => userB.editor.document[0]?.props?.backgroundColor) + .toBe("yellow"); + + sync(); + + await expect + .poll(() => merged.editor.document[0]?.props?.textColor) + .toBe("red"); + await expect + .poll(() => merged.editor.document[0]?.props?.backgroundColor) + .toBe("yellow"); + + await expect(screen.getByTestId("editor-root")).toMatchScreenshot( + "concurrent-textColor-vs-backgroundColor", + ); + + expect(ydocXml(baseDoc)).toMatchInlineSnapshot(` + " + + hello world + + " + `); + expect(ydocXml(suggestionDocA)).toMatchInlineSnapshot(` + " + + hello world + + " + `); + expect(ydocXml(suggestionDocB)).toMatchInlineSnapshot(` + " + + hello world + + " + `); + expect(ydocXml(suggestionDocMerged)).toMatchInlineSnapshot(` + " + + hello world + + " + `); + expect(editorHtml(merged.editor)).toMatchInlineSnapshot(` + " + + + hello world + + + " + `); +}); diff --git a/tests/src/browser/y-prosemirror/propChanges.test.tsx b/tests/src/browser/y-prosemirror/propChanges.test.tsx new file mode 100644 index 0000000000..5d29b4a20f --- /dev/null +++ b/tests/src/browser/y-prosemirror/propChanges.test.tsx @@ -0,0 +1,390 @@ +/* eslint-disable testing-library/render-result-naming-convention */ +/** + * Vitest browser-mode tests for prop-change suggestions: block-level + * attribute edits (text alignment, heading level, image width / source, + * etc.) rather than content/text edits. Each test follows the same + * shape as `basicText.test.tsx`: seed, enable suggestions, edit, then + * screenshot + inline snapshots of base/suggestion docs + PM doc. + */ +import { SuggestionsExtension } from "@blocknote/core/y"; +import { expect, test } from "vitest"; + +import { + editorHtml, + setupSuggestionTest, + ydocXml, +} from "./fixtures/suggestionFixture.js"; + +// Tiny inline SVG data URLs – avoids a network fetch (placehold.co +// occasionally returns after the screenshot is taken). +const IMG_SRC_BASE = + "data:image/svg+xml;utf8,"; +const IMG_SRC_NEW = + "data:image/svg+xml;utf8,"; + +// TODO: block-level prop changes generate NO `y-attributed-*` mark in +// the editor's PM doc – the suggestion doc carries the new value but +// the editor shows it as if it were already accepted. Compare with the +// inline-format case in `basicText.test.tsx` which at least produces a +// `y-attributed-format` mark (still no visual style, but at least +// detectable from the data). Decide whether block-prop suggestions +// should also be wrapped in a `y-attributed-format` (or similar) so +// reviewers / accept-reject UI can target them. +// +// Block-level prop change: paragraph's `textAlignment` flips from +// "left" to "center". Text content is unchanged. +test("suggestion mode: change text alignment to center", async () => { + const { editor, screen, baseDoc, suggestionDoc, sync } = + await setupSuggestionTest({ userAction: "center align" }); + + editor.replaceBlocks(editor.document, [ + { id: "block-hello", type: "paragraph", content: "hello world" }, + ]); + sync(); + await expect + .element(screen.getByTestId("editor-A").getByText("hello world")) + .toBeVisible(); + + editor.getExtension(SuggestionsExtension)!.enableSuggestions(); + + const [block] = editor.document; + editor.updateBlock(block, { + type: "paragraph", + props: { textAlignment: "center" }, + }); + + // Prop changes don't generate `y-attributed-*` marks, so the + // `waitForSuggestion` helper used elsewhere is too narrow here. + // Poll on the editor's view of the prop instead. + await expect + .poll(() => editor.document[0]?.props?.textAlignment) + .toBe("center"); + + await expect(screen.getByTestId("editor-root")).toMatchScreenshot( + "prop-change-text-alignment", + ); + + expect(ydocXml(baseDoc)).toMatchInlineSnapshot(` + " + + hello world + + " + `); + expect(ydocXml(suggestionDoc)).toMatchInlineSnapshot(` + " + + hello world + + " + `); + expect(editorHtml(editor)).toMatchInlineSnapshot(` + " + + + hello world + + + " + `); +}); + +// Block-level prop change on a heading: bump `level` from 1 to 2. +// Same lack of attribution as the alignment case. +test("suggestion mode: change heading level from 1 to 2", async () => { + const { editor, screen, baseDoc, suggestionDoc, sync } = + await setupSuggestionTest({ userAction: "demote heading" }); + + editor.replaceBlocks(editor.document, [ + { + id: "block-hello", + type: "heading", + props: { level: 1 }, + content: "hello world", + }, + ]); + sync(); + await expect + .element(screen.getByTestId("editor-A").getByText("hello world")) + .toBeVisible(); + + editor.getExtension(SuggestionsExtension)!.enableSuggestions(); + + const [block] = editor.document; + editor.updateBlock(block, { + type: "heading", + props: { level: 2 }, + }); + + await expect.poll(() => editor.document[0]?.props?.level).toBe(2); + + await expect(screen.getByTestId("editor-root")).toMatchScreenshot( + "prop-change-heading-level", + ); + + expect(ydocXml(baseDoc)).toMatchInlineSnapshot(` + " + + hello world + + + + + " + `); + expect(ydocXml(suggestionDoc)).toMatchInlineSnapshot(` + " + + hello world + + + + + " + `); + expect(editorHtml(editor)).toMatchInlineSnapshot(` + " + + + hello world + + + + + + " + `); +}); + +// Image block prop change: `previewWidth`. Resizes the image, no +// content/text change. +test("suggestion mode: resize image (previewWidth)", async () => { + const { editor, screen, baseDoc, suggestionDoc, sync } = + await setupSuggestionTest({ userAction: "resize image" }); + + editor.replaceBlocks(editor.document, [ + { + id: "block-image", + type: "image", + props: { + url: IMG_SRC_BASE, + previewWidth: 200, + }, + }, + ]); + sync(); + // Default `alt=""` on the image makes it decorative, so + // `getByRole("img")` doesn't see it. Poll on the prop having + // landed in the editor instead. + await expect + .poll(() => editor.document[0]?.props?.url) + .toBe(IMG_SRC_BASE); + + editor.getExtension(SuggestionsExtension)!.enableSuggestions(); + + const [block] = editor.document; + editor.updateBlock(block, { + type: "image", + props: { previewWidth: 400 }, + }); + + await expect + .poll(() => editor.document[0]?.props?.previewWidth) + .toBe(400); + + await expect(screen.getByTestId("editor-root")).toMatchScreenshot( + "prop-change-image-width", + ); + + expect(ydocXml(baseDoc)).toMatchInlineSnapshot(` + " + + + + + + + " + `); + expect(ydocXml(suggestionDoc)).toMatchInlineSnapshot(` + " + + + + + + + " + `); + expect(editorHtml(editor)).toMatchInlineSnapshot(` + " + + + + + + + + + " + `); +}); + +// Image block prop change: `url`. Swaps the image source. +test("suggestion mode: change image source", async () => { + const { editor, screen, baseDoc, suggestionDoc, sync } = + await setupSuggestionTest({ userAction: "swap image src" }); + + editor.replaceBlocks(editor.document, [ + { + id: "block-image", + type: "image", + props: { + url: IMG_SRC_BASE, + previewWidth: 200, + }, + }, + ]); + sync(); + // Default `alt=""` on the image makes it decorative, so + // `getByRole("img")` doesn't see it. Poll on the prop having + // landed in the editor instead. + await expect.poll(() => editor.document[0]?.props?.url).toBe(IMG_SRC_BASE); + + editor.getExtension(SuggestionsExtension)!.enableSuggestions(); + + const [block] = editor.document; + editor.updateBlock(block, { + type: "image", + props: { url: IMG_SRC_NEW }, + }); + + await expect.poll(() => editor.document[0]?.props?.url).toBe(IMG_SRC_NEW); + + await expect(screen.getByTestId("editor-root")).toMatchScreenshot( + "prop-change-image-source", + ); + + expect(ydocXml(baseDoc)).toMatchInlineSnapshot(` + " + + + + + + + " + `); + expect(ydocXml(suggestionDoc)).toMatchInlineSnapshot(` + " + + + + + + + " + `); + expect(editorHtml(editor)).toMatchInlineSnapshot(` + " + + + + + + + + + " + `); +}); diff --git a/tests/src/browser/y-prosemirror/typeChanges.concurrent.test.tsx b/tests/src/browser/y-prosemirror/typeChanges.concurrent.test.tsx new file mode 100644 index 0000000000..54d228fb14 --- /dev/null +++ b/tests/src/browser/y-prosemirror/typeChanges.concurrent.test.tsx @@ -0,0 +1,143 @@ +/* eslint-disable testing-library/render-result-naming-convention */ +/** + * Vitest browser-mode tests for two-user concurrent type-change + * suggestions. Same shape as `propChanges.concurrent.test.tsx`. + * + * KNOWN BUG: see `typeChanges.test.tsx` – block-type changes in + * suggestion mode currently throw in y-prosemirror's `deltaToPSteps`. + * Both tests below are marked `test.fails`; when the upstream bug is + * fixed they will flip red and we can capture proper snapshots. + */ +import { expect, test } from "vitest"; + +import { setupConcurrentSuggestionTest } from "./fixtures/concurrentSuggestionFixture.js"; +import { + editorHtml, + ydocXml, +} from "./fixtures/suggestionFixture.js"; + +// Two competing type changes on the same block: A wants a heading, B +// wants a list item. +test.fails( + "concurrent: A → heading, B → list item", + async () => { + const { + userA, + userB, + merged, + baseDoc, + suggestionDocA, + suggestionDocB, + suggestionDocMerged, + screen, + seed, + enableSuggestions, + sync, + } = await setupConcurrentSuggestionTest({ + userAAction: "→ heading", + userBAction: "→ list item", + }); + + userA.editor.replaceBlocks(userA.editor.document, [ + { id: "block-hello", type: "paragraph", content: "hello world" }, + ]); + seed(); + await expect + .element(screen.getByTestId(userA.testId).getByText("hello world")) + .toBeVisible(); + + enableSuggestions(); + + const [blockA] = userA.editor.document; + userA.editor.updateBlock(blockA, { + type: "heading", + props: { level: 1 }, + }); + + const [blockB] = userB.editor.document; + userB.editor.updateBlock(blockB, { type: "bulletListItem" }); + + await expect.poll(() => userA.editor.document[0]?.type).toBe("heading"); + await expect + .poll(() => userB.editor.document[0]?.type) + .toBe("bulletListItem"); + + sync(); + + await expect(screen.getByTestId("editor-root")).toMatchScreenshot( + "concurrent-heading-vs-list", + ); + + expect(ydocXml(baseDoc)).toMatchInlineSnapshot(); + expect(ydocXml(suggestionDocA)).toMatchInlineSnapshot(); + expect(ydocXml(suggestionDocB)).toMatchInlineSnapshot(); + expect(ydocXml(suggestionDocMerged)).toMatchInlineSnapshot(); + expect(editorHtml(merged.editor)).toMatchInlineSnapshot(); + }, +); + +// Mixed: A does a text edit (no type change), B changes the type. +// Exercises the path where one user's suggestion is a regular text +// diff and the other's is a block-type swap. +test.fails( + "concurrent: A edits text, B → heading", + async () => { + const { + userA, + userB, + merged, + baseDoc, + suggestionDocA, + suggestionDocB, + suggestionDocMerged, + screen, + seed, + enableSuggestions, + sync, + } = await setupConcurrentSuggestionTest({ + userAAction: "world → universe", + userBAction: "→ heading", + }); + + userA.editor.replaceBlocks(userA.editor.document, [ + { id: "block-hello", type: "paragraph", content: "hello world" }, + ]); + seed(); + await expect + .element(screen.getByTestId(userA.testId).getByText("hello world")) + .toBeVisible(); + + enableSuggestions(); + + const [blockA] = userA.editor.document; + userA.editor.updateBlock(blockA, { + type: "paragraph", + content: "hello universe", + }); + + const [blockB] = userB.editor.document; + userB.editor.updateBlock(blockB, { + type: "heading", + props: { level: 1 }, + }); + + await expect + .poll(() => + userA.editor.prosemirrorState.doc.toString().includes("y-attributed"), + ) + .toBe(true); + await expect.poll(() => userB.editor.document[0]?.type).toBe("heading"); + + sync(); + + await expect(screen.getByTestId("editor-root")).toMatchScreenshot( + "concurrent-text-edit-vs-heading", + ); + + expect(ydocXml(baseDoc)).toMatchInlineSnapshot(); + expect(ydocXml(suggestionDocA)).toMatchInlineSnapshot(); + expect(ydocXml(suggestionDocB)).toMatchInlineSnapshot(); + expect(ydocXml(suggestionDocMerged)).toMatchInlineSnapshot(); + expect(editorHtml(merged.editor)).toMatchInlineSnapshot(); + }, +); diff --git a/tests/src/browser/y-prosemirror/typeChanges.test.tsx b/tests/src/browser/y-prosemirror/typeChanges.test.tsx new file mode 100644 index 0000000000..db9ddbe1cb --- /dev/null +++ b/tests/src/browser/y-prosemirror/typeChanges.test.tsx @@ -0,0 +1,85 @@ +/* eslint-disable testing-library/render-result-naming-convention */ +/** + * Vitest browser-mode tests for type-change suggestions: swapping the + * block type (paragraph ↔ heading ↔ list item) while preserving its + * inline content. Same shape as `propChanges.test.tsx`. + * + * KNOWN BUG: `editor.updateBlock(block, { type: ... })` in suggestion + * mode currently throws `TransformError: No node at mark step's + * position` from y-prosemirror's `deltaToPSteps`. Tests are marked + * `test.fails` so they pass while the bug exists – when the + * underlying issue is fixed, the tests will start passing for real + * and `test.fails` will flip them red, signalling that snapshots need + * to be captured. + */ +import { SuggestionsExtension } from "@blocknote/core/y"; +import { expect, test } from "vitest"; + +import { + editorHtml, + setupSuggestionTest, + ydocXml, +} from "./fixtures/suggestionFixture.js"; + +// Demote a bullet-list item to a plain paragraph. Inline content +// "hello world" stays the same; only the wrapping node type changes. +test.fails("suggestion mode: change list item to paragraph", async () => { + const { editor, screen, baseDoc, suggestionDoc, sync } = + await setupSuggestionTest({ userAction: "list → paragraph" }); + + editor.replaceBlocks(editor.document, [ + { + id: "block-hello", + type: "bulletListItem", + content: "hello world", + }, + ]); + sync(); + await expect + .element(screen.getByTestId("editor-A").getByText("hello world")) + .toBeVisible(); + + editor.getExtension(SuggestionsExtension)!.enableSuggestions(); + + const [block] = editor.document; + editor.updateBlock(block, { type: "paragraph" }); + + await expect.poll(() => editor.document[0]?.type).toBe("paragraph"); + + await expect(screen.getByTestId("editor-root")).toMatchScreenshot( + "type-change-list-to-paragraph", + ); + + expect(ydocXml(baseDoc)).toMatchInlineSnapshot(); + expect(ydocXml(suggestionDoc)).toMatchInlineSnapshot(); + expect(editorHtml(editor)).toMatchInlineSnapshot(); +}); + +// Promote a paragraph to a level-1 heading. Same inline content. +test.fails("suggestion mode: change paragraph to heading", async () => { + const { editor, screen, baseDoc, suggestionDoc, sync } = + await setupSuggestionTest({ userAction: "paragraph → heading" }); + + editor.replaceBlocks(editor.document, [ + { id: "block-hello", type: "paragraph", content: "hello world" }, + ]); + sync(); + await expect + .element(screen.getByTestId("editor-A").getByText("hello world")) + .toBeVisible(); + + editor.getExtension(SuggestionsExtension)!.enableSuggestions(); + + const [block] = editor.document; + editor.updateBlock(block, { type: "heading", props: { level: 1 } }); + + await expect.poll(() => editor.document[0]?.type).toBe("heading"); + + await expect(screen.getByTestId("editor-root")).toMatchScreenshot( + "type-change-paragraph-to-heading", + ); + + expect(ydocXml(baseDoc)).toMatchInlineSnapshot(); + expect(ydocXml(suggestionDoc)).toMatchInlineSnapshot(); + expect(editorHtml(editor)).toMatchInlineSnapshot(); +}); diff --git a/tests/vite.config.browser.ts b/tests/vite.config.browser.ts new file mode 100644 index 0000000000..5a3f5507b9 --- /dev/null +++ b/tests/vite.config.browser.ts @@ -0,0 +1,47 @@ +import react from "@vitejs/plugin-react"; +import * as path from "path"; +import { defineConfig } from "vite"; +import { playwright } from "@vitest/browser-playwright"; + +// Vitest browser mode config – runs tests in real Chromium via Playwright. +// Run with: pnpm test:browser (from /tests) +export default defineConfig((conf) => ({ + plugins: [react()], + test: { + include: ["./src/browser/**/*.test.ts", "./src/browser/**/*.test.tsx"], + setupFiles: ["./src/browser/vitestSetup.ts"], + browser: { + enabled: true, + provider: playwright(), + headless: true, + instances: [{ browser: "chromium" }], + // toMatchScreenshot defaults – be lenient since BlockNote renders + // include things like blinking cursors / awareness markers. + expect: { + toMatchScreenshot: { + comparatorName: "pixelmatch", + comparatorOptions: { + threshold: 0.2, + allowedMismatchedPixelRatio: 0.02, + }, + }, + }, + }, + }, + resolve: { + alias: + conf.command === "build" + ? ({ + "@shared": path.resolve(__dirname, "../shared/"), + } as Record) + : ({ + "@shared": path.resolve(__dirname, "../shared/"), + // load live from sources with live reload working + "@blocknote/core": path.resolve(__dirname, "../packages/core/src/"), + "@blocknote/react": path.resolve( + __dirname, + "../packages/react/src/", + ), + } as Record), + }, +}));