From b89283f59d65fc2b57895e87f57a96853ffb88be Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Thu, 2 Apr 2026 15:10:58 +0200 Subject: [PATCH 1/5] Made inserting links not call `insertText` when text is the same --- packages/core/src/editor/managers/StyleManager.ts | 7 ++++++- packages/core/src/extensions/LinkToolbar/LinkToolbar.ts | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/core/src/editor/managers/StyleManager.ts b/packages/core/src/editor/managers/StyleManager.ts index e03c46a6d1..e8ffa99881 100644 --- a/packages/core/src/editor/managers/StyleManager.ts +++ b/packages/core/src/editor/managers/StyleManager.ts @@ -169,7 +169,12 @@ export class StyleManager< const { from, to } = tr.selection; if (text) { - tr.insertText(text, from, to).addMark(from, from + text.length, mark); + const existingText = tr.doc.textBetween(from, to); + if (text !== existingText) { + tr.insertText(text, from, to); + } + + tr.addMark(from, from + text.length, mark); } else { tr.setSelection(TextSelection.create(tr.doc, to)).addMark( from, diff --git a/packages/core/src/extensions/LinkToolbar/LinkToolbar.ts b/packages/core/src/extensions/LinkToolbar/LinkToolbar.ts index 1a61d67d44..f190bc97e6 100644 --- a/packages/core/src/extensions/LinkToolbar/LinkToolbar.ts +++ b/packages/core/src/extensions/LinkToolbar/LinkToolbar.ts @@ -88,7 +88,12 @@ export const LinkToolbarExtension = createExtension(({ editor }) => { if (!range) { return; } - tr.insertText(text, range.from, range.to); + + const existingText = tr.doc.textBetween(range.from, range.to); + if (text !== existingText) { + tr.insertText(text, range.from, range.to); + } + tr.addMark( range.from, range.from + text.length, From 5a7a186ad733577aad6776a2e7ded294a26aeb65 Mon Sep 17 00:00:00 2001 From: mianguyen Date: Sun, 5 Apr 2026 17:41:50 -0500 Subject: [PATCH 2/5] test: add e2e test for preserving marks when editing a link (#2573) --- .../linktoolbar/linktoolbar.test.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/src/end-to-end/linktoolbar/linktoolbar.test.ts diff --git a/tests/src/end-to-end/linktoolbar/linktoolbar.test.ts b/tests/src/end-to-end/linktoolbar/linktoolbar.test.ts new file mode 100644 index 0000000000..9a047f8f53 --- /dev/null +++ b/tests/src/end-to-end/linktoolbar/linktoolbar.test.ts @@ -0,0 +1,50 @@ +import { expect } from "@playwright/test"; +import { test } from "../../setup/setupScript.js"; +import { BASE_URL, LINK_BUTTON_SELECTOR } from "../../utils/const.js"; +import { focusOnEditor } from "../../utils/editor.js"; + +test.beforeEach(async ({ page }) => { + await page.goto(BASE_URL); +}); + +test.describe("Check Link Toolbar functionality", () => { + test("Should preserve existing marks when editing a link", async ({ + page, + }) => { + await focusOnEditor(page); + + // Type bold text + await page.keyboard.type("hello"); + await page.keyboard.press("Shift+Home"); + + // Make it bold via formatting toolbar + await page.waitForSelector(`[data-test="bold"]`); + await page.click(`[data-test="bold"]`); + + // Add link + await page.keyboard.press("Shift+Home"); + await page.waitForSelector(LINK_BUTTON_SELECTOR); + await page.click(LINK_BUTTON_SELECTOR); + await page.keyboard.type("https://example.com"); + await page.keyboard.press("Enter"); + + // Move cursor back onto the linked text to trigger link toolbar + await page.keyboard.press("ArrowLeft"); + await page.waitForTimeout(500); + + // Click Edit link button + const editButton = page.getByText("Edit link"); + await editButton.waitFor({ state: "visible" }); + await editButton.click(); + + await page.keyboard.press("Control+A"); + await page.keyboard.type("https://example2.com"); + await page.keyboard.press("Enter"); + + await page.waitForTimeout(300); + + // Verify bold mark is still present on the text + const boldText = page.locator("strong a, a strong"); + await expect(boldText).toBeVisible(); + }); +}); \ No newline at end of file From 17386a6b9a390456bc0d80feea14d1caf7be118e Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Tue, 14 Apr 2026 17:54:30 +0200 Subject: [PATCH 3/5] Fixed some comments UI issues, updated test and added comments testing example --- .../10-comments-testing/.bnexample.json | 9 +++ .../10-comments-testing/README.md | 3 + .../10-comments-testing/index.html | 14 ++++ .../10-comments-testing/main.tsx | 11 ++++ .../10-comments-testing/package.json | 32 +++++++++ .../10-comments-testing/src/App.tsx | 44 +++++++++++++ .../10-comments-testing/tsconfig.json | 36 ++++++++++ .../10-comments-testing/vite.config.ts | 32 +++++++++ packages/core/src/comments/extension.ts | 1 + .../components/Comments/FloatingComposer.tsx | 26 +++++++- playground/src/examples.gen.tsx | 24 +++++++ pnpm-lock.yaml | 65 ++++++++++++++++--- .../src/end-to-end/comments/comments.test.ts | 43 ++++++++++++ .../linktoolbar/linktoolbar.test.ts | 50 -------------- tests/src/utils/const.ts | 4 ++ 15 files changed, 334 insertions(+), 60 deletions(-) create mode 100644 examples/07-collaboration/10-comments-testing/.bnexample.json create mode 100644 examples/07-collaboration/10-comments-testing/README.md create mode 100644 examples/07-collaboration/10-comments-testing/index.html create mode 100644 examples/07-collaboration/10-comments-testing/main.tsx create mode 100644 examples/07-collaboration/10-comments-testing/package.json create mode 100644 examples/07-collaboration/10-comments-testing/src/App.tsx create mode 100644 examples/07-collaboration/10-comments-testing/tsconfig.json create mode 100644 examples/07-collaboration/10-comments-testing/vite.config.ts create mode 100644 tests/src/end-to-end/comments/comments.test.ts delete mode 100644 tests/src/end-to-end/linktoolbar/linktoolbar.test.ts diff --git a/examples/07-collaboration/10-comments-testing/.bnexample.json b/examples/07-collaboration/10-comments-testing/.bnexample.json new file mode 100644 index 0000000000..5d7d986420 --- /dev/null +++ b/examples/07-collaboration/10-comments-testing/.bnexample.json @@ -0,0 +1,9 @@ +{ + "playground": true, + "docs": false, + "author": "matthewlipski", + "tags": ["Advanced", "Comments", "Testing"], + "dependencies": { + "yjs": "^13.6.27" + } +} diff --git a/examples/07-collaboration/10-comments-testing/README.md b/examples/07-collaboration/10-comments-testing/README.md new file mode 100644 index 0000000000..b59f2ecd1b --- /dev/null +++ b/examples/07-collaboration/10-comments-testing/README.md @@ -0,0 +1,3 @@ +# Comments Testing + +A minimal comments example used for end-to-end testing. Uses a local Y.Doc (no collaboration provider) with a single hardcoded editor user. diff --git a/examples/07-collaboration/10-comments-testing/index.html b/examples/07-collaboration/10-comments-testing/index.html new file mode 100644 index 0000000000..f50976be79 --- /dev/null +++ b/examples/07-collaboration/10-comments-testing/index.html @@ -0,0 +1,14 @@ + + + + + Comments Testing + + + +
+ + + diff --git a/examples/07-collaboration/10-comments-testing/main.tsx b/examples/07-collaboration/10-comments-testing/main.tsx new file mode 100644 index 0000000000..677c7f7eed --- /dev/null +++ b/examples/07-collaboration/10-comments-testing/main.tsx @@ -0,0 +1,11 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./src/App.jsx"; + +const root = createRoot(document.getElementById("root")!); +root.render( + + + +); diff --git a/examples/07-collaboration/10-comments-testing/package.json b/examples/07-collaboration/10-comments-testing/package.json new file mode 100644 index 0000000000..6c4e09d1ae --- /dev/null +++ b/examples/07-collaboration/10-comments-testing/package.json @@ -0,0 +1,32 @@ +{ + "name": "@blocknote/example-collaboration-comments-testing", + "description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "type": "module", + "private": true, + "version": "0.12.4", + "scripts": { + "start": "vite", + "dev": "vite", + "build:prod": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@blocknote/ariakit": "latest", + "@blocknote/core": "latest", + "@blocknote/mantine": "latest", + "@blocknote/react": "latest", + "@blocknote/shadcn": "latest", + "@mantine/core": "^8.3.11", + "@mantine/hooks": "^8.3.11", + "@mantine/utils": "^6.0.22", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "yjs": "^13.6.27" + }, + "devDependencies": { + "@types/react": "^19.2.3", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^4.7.0", + "vite": "^5.4.20" + } +} \ No newline at end of file diff --git a/examples/07-collaboration/10-comments-testing/src/App.tsx b/examples/07-collaboration/10-comments-testing/src/App.tsx new file mode 100644 index 0000000000..3bada358c1 --- /dev/null +++ b/examples/07-collaboration/10-comments-testing/src/App.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { + CommentsExtension, + DefaultThreadStoreAuth, + YjsThreadStore, +} from "@blocknote/core/comments"; +import { BlockNoteView } from "@blocknote/mantine"; +import "@blocknote/mantine/style.css"; +import { useCreateBlockNote } from "@blocknote/react"; +import { useMemo } from "react"; +import * as Y from "yjs"; + +const USER = { + id: "1", + username: "John Doe", + avatarUrl: "https://placehold.co/100x100?text=John", + role: "editor" as const, +}; + +async function resolveUsers(userIds: string[]) { + return [USER].filter((user) => userIds.includes(user.id)); +} + +export default function App() { + const doc = useMemo(() => new Y.Doc(), []); + + const threadStore = useMemo(() => { + return new YjsThreadStore( + USER.id, + doc.getMap("threads"), + new DefaultThreadStoreAuth(USER.id, USER.role), + ); + }, [doc]); + + const editor = useCreateBlockNote( + { + extensions: [CommentsExtension({ threadStore, resolveUsers })], + }, + [threadStore], + ); + + return ; +} diff --git a/examples/07-collaboration/10-comments-testing/tsconfig.json b/examples/07-collaboration/10-comments-testing/tsconfig.json new file mode 100644 index 0000000000..dbe3e6f62d --- /dev/null +++ b/examples/07-collaboration/10-comments-testing/tsconfig.json @@ -0,0 +1,36 @@ +{ + "__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "composite": true + }, + "include": [ + "." + ], + "__ADD_FOR_LOCAL_DEV_references": [ + { + "path": "../../../packages/core/" + }, + { + "path": "../../../packages/react/" + } + ] +} \ No newline at end of file diff --git a/examples/07-collaboration/10-comments-testing/vite.config.ts b/examples/07-collaboration/10-comments-testing/vite.config.ts new file mode 100644 index 0000000000..f62ab20bc2 --- /dev/null +++ b/examples/07-collaboration/10-comments-testing/vite.config.ts @@ -0,0 +1,32 @@ +// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY +import react from "@vitejs/plugin-react"; +import * as fs from "fs"; +import * as path from "path"; +import { defineConfig } from "vite"; +// import eslintPlugin from "vite-plugin-eslint"; +// https://vitejs.dev/config/ +export default defineConfig((conf) => ({ + plugins: [react()], + optimizeDeps: {}, + build: { + sourcemap: true, + }, + resolve: { + alias: + conf.command === "build" || + !fs.existsSync(path.resolve(__dirname, "../../packages/core/src")) + ? {} + : ({ + // Comment out the lines below to load a built version of blocknote + // or, keep as is to 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 any), + }, +})); diff --git a/packages/core/src/comments/extension.ts b/packages/core/src/comments/extension.ts index 4e8e566cef..2e8632296d 100644 --- a/packages/core/src/comments/extension.ts +++ b/packages/core/src/comments/extension.ts @@ -306,6 +306,7 @@ export const CommentsExtension = createExtension( selectedThreadId: undefined, pendingComment: true, })); + editor.focus(); editor .getExtension(ShowSelectionExtension) ?.showSelection(true, "comments"); diff --git a/packages/react/src/components/Comments/FloatingComposer.tsx b/packages/react/src/components/Comments/FloatingComposer.tsx index 1cc72d634d..aa3e786dd9 100644 --- a/packages/react/src/components/Comments/FloatingComposer.tsx +++ b/packages/react/src/components/Comments/FloatingComposer.tsx @@ -1,4 +1,12 @@ -import { mergeCSSClasses } from "@blocknote/core"; +import { + BlockSchema, + DefaultBlockSchema, + DefaultInlineContentSchema, + DefaultStyleSchema, + InlineContentSchema, + mergeCSSClasses, + StyleSchema, +} from "@blocknote/core"; import { CommentsExtension } from "@blocknote/core/comments"; import { useComponentsContext } from "../../editor/ComponentsContext.js"; @@ -7,13 +15,21 @@ import { useExtension } from "../../hooks/useExtension.js"; import { useDictionary } from "../../i18n/dictionary.js"; import { CommentEditor } from "./CommentEditor.js"; import { defaultCommentEditorSchema } from "./defaultCommentEditorSchema.js"; +import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js"; +import { TextSelection } from "@tiptap/pm/state"; /** * The FloatingComposer component displays a comment editor "floating" card. * * It's used when the user highlights a parts of the document to create a new comment / thread. */ -export function FloatingComposer() { +export function FloatingComposer< + B extends BlockSchema = DefaultBlockSchema, + I extends InlineContentSchema = DefaultInlineContentSchema, + S extends StyleSchema = DefaultStyleSchema, +>() { + const editor = useBlockNoteEditor(); + const comments = useExtension(CommentsExtension); const Components = useComponentsContext()!; @@ -57,6 +73,12 @@ export function FloatingComposer() { }, }); comments.stopPendingComment(); + editor.transact((tr) => { + tr.setSelection( + TextSelection.create(tr.doc, tr.selection.to), + ); + }); + editor.focus(); }} > Save diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx index 00a2e5afe5..ebe940400d 100644 --- a/playground/src/examples.gen.tsx +++ b/playground/src/examples.gen.tsx @@ -1687,6 +1687,30 @@ "slug": "collaboration" }, "readme": "In this example, we can fork a document and edit it independently of other collaborators. Then, we can choose to merge the changes back into the original document, or discard the changes.\n\n**Try it out:** Open this page in a new browser tab or window to see it in action!\n\n**Relevant Docs:**\n\n- [Editor Setup](/docs/getting-started/editor-setup)" + }, + { + "projectSlug": "comments-testing", + "fullSlug": "collaboration/comments-testing", + "pathFromRoot": "examples/07-collaboration/10-comments-testing", + "config": { + "playground": true, + "docs": false, + "author": "matthewlipski", + "tags": [ + "Advanced", + "Comments", + "Testing" + ], + "dependencies": { + "yjs": "^13.6.27" + } as any + }, + "title": "Comments Testing", + "group": { + "pathFromRoot": "examples/07-collaboration", + "slug": "collaboration" + }, + "readme": "A minimal comments example used for end-to-end testing. Uses a local Y.Doc (no collaboration provider) with a single hardcoded editor user." } ] }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d3078f28a..70315f0ee6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4004,6 +4004,55 @@ importers: specifier: ^5.4.20 version: 5.4.21(@types/node@25.6.0)(lightningcss@1.32.0)(terser@5.46.1) + examples/07-collaboration/10-comments-testing: + dependencies: + '@blocknote/ariakit': + specifier: latest + version: link:../../../packages/ariakit + '@blocknote/core': + specifier: latest + version: link:../../../packages/core + '@blocknote/mantine': + specifier: latest + version: link:../../../packages/mantine + '@blocknote/react': + specifier: latest + version: link:../../../packages/react + '@blocknote/shadcn': + specifier: latest + version: link:../../../packages/shadcn + '@mantine/core': + specifier: ^8.3.11 + version: 8.3.18(@mantine/hooks@8.3.18(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@mantine/hooks': + specifier: ^8.3.11 + version: 8.3.18(react@19.2.4) + '@mantine/utils': + specifier: ^6.0.22 + version: 6.0.22(react@19.2.4) + react: + specifier: ^19.2.3 + version: 19.2.4 + react-dom: + specifier: ^19.2.3 + version: 19.2.4(react@19.2.4) + yjs: + specifier: ^13.6.27 + version: 13.6.30 + devDependencies: + '@types/react': + specifier: ^19.2.3 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^4.7.0 + version: 4.7.0(vite@5.4.21(@types/node@25.6.0)(lightningcss@1.32.0)(terser@5.46.1)) + vite: + specifier: ^5.4.20 + version: 5.4.21(@types/node@25.6.0)(lightningcss@1.32.0)(terser@5.46.1) + examples/08-extensions/01-tiptap-arrow-conversion: dependencies: '@blocknote/ariakit': @@ -23889,8 +23938,8 @@ snapshots: '@next/eslint-plugin-next': 16.2.2 eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.39.4(jiti@2.6.1)) @@ -23939,7 +23988,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -23950,7 +23999,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -23964,14 +24013,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -24012,7 +24061,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -24023,7 +24072,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 diff --git a/tests/src/end-to-end/comments/comments.test.ts b/tests/src/end-to-end/comments/comments.test.ts new file mode 100644 index 0000000000..fd68d462ea --- /dev/null +++ b/tests/src/end-to-end/comments/comments.test.ts @@ -0,0 +1,43 @@ +import { expect } from "@playwright/test"; +import { test } from "../../setup/setupScript.js"; +import { COMMENTS_URL, LINK_BUTTON_SELECTOR } from "../../utils/const.js"; +import { focusOnEditor } from "../../utils/editor.js"; + +test.beforeEach(async ({ page }) => { + await page.goto(COMMENTS_URL); +}); + +test.describe("Check Comments functionality", () => { + test("Should preserve existing comments when adding a link", async ({ + page, + }) => { + await focusOnEditor(page); + + // Type text and select it + await page.keyboard.type("hello"); + await page.keyboard.press("Shift+Home"); + + await page.click('[data-test="addcomment"]'); + await page.waitForSelector(".bn-thread"); + + await page.keyboard.type("test comment"); + await page.click('button[data-test="save"]'); + + // Wait for the comment mark to appear on the text + await page.click("span.bn-thread-mark", { position: { x: 1, y: 1 } }); + + // Re-select the commented text by clicking at the start and shift-clicking + // at the end, staying within the editor + await page.keyboard.press("Shift+End"); + + // Add a link via the formatting toolbar + await page.waitForSelector(LINK_BUTTON_SELECTOR); + await page.click(LINK_BUTTON_SELECTOR); + await page.waitForSelector("input"); + + await page.keyboard.type("https://example.com"); + await page.keyboard.press("Enter"); + + await expect(await page.locator("span.bn-thread-mark")).toBeVisible(); + }); +}); diff --git a/tests/src/end-to-end/linktoolbar/linktoolbar.test.ts b/tests/src/end-to-end/linktoolbar/linktoolbar.test.ts deleted file mode 100644 index 9a047f8f53..0000000000 --- a/tests/src/end-to-end/linktoolbar/linktoolbar.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { expect } from "@playwright/test"; -import { test } from "../../setup/setupScript.js"; -import { BASE_URL, LINK_BUTTON_SELECTOR } from "../../utils/const.js"; -import { focusOnEditor } from "../../utils/editor.js"; - -test.beforeEach(async ({ page }) => { - await page.goto(BASE_URL); -}); - -test.describe("Check Link Toolbar functionality", () => { - test("Should preserve existing marks when editing a link", async ({ - page, - }) => { - await focusOnEditor(page); - - // Type bold text - await page.keyboard.type("hello"); - await page.keyboard.press("Shift+Home"); - - // Make it bold via formatting toolbar - await page.waitForSelector(`[data-test="bold"]`); - await page.click(`[data-test="bold"]`); - - // Add link - await page.keyboard.press("Shift+Home"); - await page.waitForSelector(LINK_BUTTON_SELECTOR); - await page.click(LINK_BUTTON_SELECTOR); - await page.keyboard.type("https://example.com"); - await page.keyboard.press("Enter"); - - // Move cursor back onto the linked text to trigger link toolbar - await page.keyboard.press("ArrowLeft"); - await page.waitForTimeout(500); - - // Click Edit link button - const editButton = page.getByText("Edit link"); - await editButton.waitFor({ state: "visible" }); - await editButton.click(); - - await page.keyboard.press("Control+A"); - await page.keyboard.type("https://example2.com"); - await page.keyboard.press("Enter"); - - await page.waitForTimeout(300); - - // Verify bold mark is still present on the text - const boldText = page.locator("strong a, a strong"); - await expect(boldText).toBeVisible(); - }); -}); \ No newline at end of file diff --git a/tests/src/utils/const.ts b/tests/src/utils/const.ts index 61cedc194d..b04b77d6a2 100644 --- a/tests/src/utils/const.ts +++ b/tests/src/utils/const.ts @@ -43,6 +43,10 @@ export const ALERT_BLOCK_URL = !process.env.RUN_IN_DOCKER ? `http://localhost:${PORT}/custom-schema/alert-block?hideMenu` : `http://host.docker.internal:${PORT}/custom-schema/alert-block?hideMenu`; +export const COMMENTS_URL = !process.env.RUN_IN_DOCKER + ? `http://localhost:${PORT}/collaboration/comments-testing?hideMenu` + : `http://host.docker.internal:${PORT}/collaboration/comments-testing?hideMenu`; + export const PASTE_ZONE_SELECTOR = "#pasteZone"; export const EDITOR_SELECTOR = `.bn-editor`; From 50fdda42afc1ff3cb85b723854d0f654e521d761 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Tue, 14 Apr 2026 18:09:49 +0200 Subject: [PATCH 4/5] Updated test --- tests/src/end-to-end/comments/comments.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/src/end-to-end/comments/comments.test.ts b/tests/src/end-to-end/comments/comments.test.ts index fd68d462ea..b7354cf616 100644 --- a/tests/src/end-to-end/comments/comments.test.ts +++ b/tests/src/end-to-end/comments/comments.test.ts @@ -33,7 +33,6 @@ test.describe("Check Comments functionality", () => { // Add a link via the formatting toolbar await page.waitForSelector(LINK_BUTTON_SELECTOR); await page.click(LINK_BUTTON_SELECTOR); - await page.waitForSelector("input"); await page.keyboard.type("https://example.com"); await page.keyboard.press("Enter"); From 437a7d366c9f18bcba78398dfecb1c98cb600c5b Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Tue, 14 Apr 2026 19:53:12 +0200 Subject: [PATCH 5/5] Fixed failing link toolbar tests --- tests/src/end-to-end/ariakit/ariakit.test.ts | 2 ++ tests/src/end-to-end/shadcn/shadcn.test.ts | 2 ++ tests/src/end-to-end/theming/theming.test.ts | 2 ++ 3 files changed, 6 insertions(+) diff --git a/tests/src/end-to-end/ariakit/ariakit.test.ts b/tests/src/end-to-end/ariakit/ariakit.test.ts index aa8c05c7d6..dd5fa0eccf 100644 --- a/tests/src/end-to-end/ariakit/ariakit.test.ts +++ b/tests/src/end-to-end/ariakit/ariakit.test.ts @@ -35,7 +35,9 @@ test.describe("Check Ariakit UI", () => { await page.keyboard.type("link"); await page.keyboard.press("Enter"); + await page.waitForTimeout(500); await page.keyboard.press("ArrowLeft"); + await page.keyboard.press("ArrowRight"); await page.waitForTimeout(500); expect(await page.screenshot()).toMatchSnapshot("ariakit-link-toolbar.png"); diff --git a/tests/src/end-to-end/shadcn/shadcn.test.ts b/tests/src/end-to-end/shadcn/shadcn.test.ts index c4a46ed8cc..fe1a2e385f 100644 --- a/tests/src/end-to-end/shadcn/shadcn.test.ts +++ b/tests/src/end-to-end/shadcn/shadcn.test.ts @@ -35,7 +35,9 @@ test.describe("Check ShadCN UI", () => { await page.keyboard.type("link"); await page.keyboard.press("Enter"); + await page.waitForTimeout(500); await page.keyboard.press("ArrowLeft"); + await page.keyboard.press("ArrowRight"); await page.waitForTimeout(700); expect(await page.screenshot()).toMatchSnapshot("shadcn-link-toolbar.png"); diff --git a/tests/src/end-to-end/theming/theming.test.ts b/tests/src/end-to-end/theming/theming.test.ts index 8bc75a6fab..1647cda1a9 100644 --- a/tests/src/end-to-end/theming/theming.test.ts +++ b/tests/src/end-to-end/theming/theming.test.ts @@ -46,7 +46,9 @@ test.describe("Check Dark Theme is Automatically Applied", () => { await page.waitForTimeout(500); await page.keyboard.type("link"); await page.keyboard.press("Enter"); + await page.waitForTimeout(500); await page.keyboard.press("ArrowLeft"); + await page.keyboard.press("ArrowRight"); await page.waitForTimeout(500); expect(await page.screenshot()).toMatchSnapshot("dark-link-toolbar.png");