From ad1ab754cd940d70a1430c2f00213bf14a85b8e5 Mon Sep 17 00:00:00 2001 From: Bartosz Tomczyk Date: Mon, 5 Oct 2026 10:49:51 +0200 Subject: [PATCH 1/5] test(e2e): report the CKEditor state when the mid-word preview times out Co-Authored-By: Claude Opus 5.5 --- .github/workflows/probe-ckeditor.yml | 45 ++++++++++++++++++++++++++++ tests/e2e/full.e2e.test.ts | 22 +++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/probe-ckeditor.yml diff --git a/.github/workflows/probe-ckeditor.yml b/.github/workflows/probe-ckeditor.yml new file mode 100644 index 000000000..a43cdb315 --- /dev/null +++ b/.github/workflows/probe-ckeditor.yml @@ -0,0 +1,45 @@ +# Temporary: reproduce the Firefox CKEditor mid-word flake. Removed before merge. +name: Probe CKEditor mid-word + +on: + pull_request: + branches: [master] + +permissions: + contents: read + +jobs: + shard: + name: Firefox shard 2/2 (copy ${{ matrix.copy }}) + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + copy: [1, 2, 3, 4, 5, 6] + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version-file: package.json + - run: bun install --frozen-lockfile + - run: bunx puppeteer browsers install firefox + - run: bun run test:e2e:full --platform=firefox --shard=2/2 + + alone: + name: Firefox CKEditor mid-word alone x10 + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version-file: package.json + - run: bun install --frozen-lockfile + - run: bunx puppeteer browsers install firefox + - run: | + failed=0 + for i in $(seq 10); do + bun run test:e2e:full --platform=firefox --test-name-pattern="caret is mid-word" || { echo "RUN $i FAILED"; failed=1; } + done + exit $failed diff --git a/tests/e2e/full.e2e.test.ts b/tests/e2e/full.e2e.test.ts index 945279d2f..ab320c688 100644 --- a/tests/e2e/full.e2e.test.ts +++ b/tests/e2e/full.e2e.test.ts @@ -3984,7 +3984,27 @@ describeE2E(`Extension E2E Test [${BROWSER_TYPE}]`, () => { return text.length > 0 ? text : false; }, { timeoutMs: suiteTimeout(3000, 6000) }, - ); + ).catch(async (cause) => { + const state = await page.evaluate(() => { + const editable = document.querySelector(".ck-editor__editable")!; + const selection = window.getSelection(); + return { + dom: editable.textContent, + model: ( + window as typeof window & { __testCkEditor?: { getData(): string } } + ).__testCkEditor?.getData(), + caret: + selection?.anchorNode === editable.querySelector("p")?.firstChild + ? selection?.anchorOffset + : `${selection?.anchorNode?.nodeName}:${selection?.anchorOffset}`, + focused: document.activeElement === editable, + ft: Array.from(editable.attributes, (attribute) => attribute.name) + .filter((name) => name.startsWith("data-ft")) + .map((name) => `${name}=${editable.getAttribute(name)}`), + }; + }); + throw new Error(`CKEditor state: ${JSON.stringify(state)}`, { cause }); + }); // The preview must read as the post-acceptance text: a single word // starting with "Thr" followed by " dog walked the street" with no From c6882f083d58323f04eb0996be20ab1862b9ea9b Mon Sep 17 00:00:00 2001 From: Bartosz Tomczyk Date: Mon, 5 Oct 2026 10:54:23 +0200 Subject: [PATCH 2/5] ci: probe the mid-word flake under CPU load (temporary) --- .github/workflows/probe-ckeditor.yml | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/.github/workflows/probe-ckeditor.yml b/.github/workflows/probe-ckeditor.yml index a43cdb315..3e8c2e920 100644 --- a/.github/workflows/probe-ckeditor.yml +++ b/.github/workflows/probe-ckeditor.yml @@ -9,27 +9,14 @@ permissions: contents: read jobs: - shard: - name: Firefox shard 2/2 (copy ${{ matrix.copy }}) + stressed: + name: Firefox mid-word under CPU load (copy ${{ matrix.copy }}) runs-on: ubuntu-latest timeout-minutes: 30 strategy: fail-fast: false matrix: - copy: [1, 2, 3, 4, 5, 6] - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - with: - bun-version-file: package.json - - run: bun install --frozen-lockfile - - run: bunx puppeteer browsers install firefox - - run: bun run test:e2e:full --platform=firefox --shard=2/2 - - alone: - name: Firefox CKEditor mid-word alone x10 - runs-on: ubuntu-latest - timeout-minutes: 30 + copy: [1, 2, 3, 4, 5, 6, 7, 8] steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 @@ -38,8 +25,9 @@ jobs: - run: bun install --frozen-lockfile - run: bunx puppeteer browsers install firefox - run: | + for c in $(seq $(nproc)); do (while :; do :; done) & done failed=0 - for i in $(seq 10); do - bun run test:e2e:full --platform=firefox --test-name-pattern="caret is mid-word" || { echo "RUN $i FAILED"; failed=1; } + for i in $(seq 15); do + bun run test:e2e:full --platform=firefox --test-name-pattern="caret is mid-word" > run.log 2>&1 || { echo "RUN $i FAILED"; grep -E "CKEditor state|error:" run.log; failed=1; } done exit $failed From 7610af76b230cb8860b5d05233b4305ee2899477 Mon Sep 17 00:00:00 2001 From: Bartosz Tomczyk Date: Mon, 5 Oct 2026 11:03:00 +0200 Subject: [PATCH 3/5] fix(tests): set the CKEditor caret through its model, not the DOM Three CKEditor tests put the caret with a DOM selection and then typed at once. CKEditor 5 copies a DOM selection into its model only on the next "selectionchange" event. When the key came first (a slow Firefox runner), CKEditor typed at the old model caret: "rThe dog walked the street" in place of "Thre dog walked the street". The mid-word test then saw no preview and failed on master. setCkEditorCaret now sets the selection with model.change and focuses the editing view, so the model and the DOM agree before the key. Co-Authored-By: Claude Opus 5.5 --- tests/e2e/full.e2e.test.ts | 93 ++++++++++++++------------------------ 1 file changed, 34 insertions(+), 59 deletions(-) diff --git a/tests/e2e/full.e2e.test.ts b/tests/e2e/full.e2e.test.ts index ab320c688..7b65694f2 100644 --- a/tests/e2e/full.e2e.test.ts +++ b/tests/e2e/full.e2e.test.ts @@ -573,6 +573,35 @@ async function gotoTestPage( await page.bringToFront(); } +/** + * Puts the CKEditor caret in paragraph `index` at `offset` through the model. + * A DOM selection reaches the model only on the next "selectionchange" event, + * so a key typed before it lands at the old model caret. + */ +async function setCkEditorCaret(page: Page, index: number, offset: number | "end") { + await page.evaluate( + (index, offset) => { + type Editor = { + model: { + document: { getRoot(): { getChild(index: number): unknown } }; + change( + callback: (writer: { setSelection(node: unknown, offset: unknown): void }) => void, + ): void; + }; + editing: { view: { focus(): void } }; + }; + const editor = (window as typeof window & { __testCkEditor?: Editor }).__testCkEditor; + if (!editor) throw new Error("CKEditor test instance not found"); + editor.editing.view.focus(); + editor.model.change((writer) => + writer.setSelection(editor.model.document.getRoot().getChild(index), offset), + ); + }, + index, + offset, + ); +} + async function waitForInputReady(page: Page | Frame, selector: string) { if (selector === CKEDITOR_SELECTOR) { await waitUntil( @@ -2301,29 +2330,7 @@ describeE2E(`Extension E2E Test [${BROWSER_TYPE}]`, () => { ckEditor.setData("

next

"); }); - await page.focus(CKEDITOR_SELECTOR); - await page.evaluate(() => { - const editable = document.querySelector(".ck-editor__editable"); - const firstParagraph = editable?.querySelector("p"); - if (!editable || !firstParagraph) { - throw new Error("CKEditor editable or first paragraph missing"); - } - const textNode = - firstParagraph.firstChild && firstParagraph.firstChild.nodeType === Node.TEXT_NODE - ? firstParagraph.firstChild - : firstParagraph.appendChild(document.createTextNode("")); - - const selection = window.getSelection(); - if (!selection) { - throw new Error("Selection unavailable"); - } - - const range = document.createRange(); - range.setStart(textNode, textNode.textContent?.length ?? 0); - range.collapse(true); - selection.removeAllRanges(); - selection.addRange(range); - }); + await setCkEditorCaret(page, 0, "end"); await page.keyboard.type("h"); const liCount = await waitForVisibleSuggestions(page); @@ -2429,27 +2436,12 @@ describeE2E(`Extension E2E Test [${BROWSER_TYPE}]`, () => { ckEditor.setData("

Quill Rich Text Editor

fixed

"); }); - await page.focus(CKEDITOR_SELECTOR); + await setCkEditorCaret(page, 1, "end"); await page.evaluate(() => { const editable = document.querySelector(".ck-editor__editable"); - const secondParagraph = editable?.querySelectorAll("p")[1]; - if (!editable || !secondParagraph) { - throw new Error("CKEditor editable or second paragraph missing"); - } - const textNode = - secondParagraph.firstChild && secondParagraph.firstChild.nodeType === Node.TEXT_NODE - ? secondParagraph.firstChild - : secondParagraph.appendChild(document.createTextNode("")); - const selection = window.getSelection(); - if (!selection) { - throw new Error("Selection unavailable"); + if (!editable) { + throw new Error("CKEditor editable missing"); } - const range = document.createRange(); - range.setStart(textNode, textNode.textContent?.length ?? 0); - range.collapse(true); - selection.removeAllRanges(); - selection.addRange(range); - const debugWindow = window as typeof window & { __ftDebugInputs?: Array<{ inputType: string; data: string; text: string }>; __ftDebugKeys?: string[]; @@ -3949,25 +3941,8 @@ describeE2E(`Extension E2E Test [${BROWSER_TYPE}]`, () => { ckEditor.setData("

The dog walked the street

"); }); - await page.focus(CKEDITOR_SELECTOR); // Place the caret inside the first word, after "Th". - await page.evaluate(() => { - const editable = document.querySelector(".ck-editor__editable"); - const firstParagraph = editable?.querySelector("p"); - const textNode = firstParagraph?.firstChild; - if (!(textNode instanceof Text)) { - throw new Error("CKEditor first paragraph text node missing"); - } - const selection = window.getSelection(); - if (!selection) { - throw new Error("Selection unavailable"); - } - const range = document.createRange(); - range.setStart(textNode, 2); // "Th|e dog walked the street" - range.collapse(true); - selection.removeAllRanges(); - selection.addRange(range); - }); + await setCkEditorCaret(page, 0, 2); // Type "r" so the text becomes "Thr|e dog walked the street". await page.keyboard.type("r"); From 36d26cee70e1a94db8af6067bbe8d206ae658c9b Mon Sep 17 00:00:00 2001 From: Bartosz Tomczyk Date: Mon, 5 Oct 2026 11:03:00 +0200 Subject: [PATCH 4/5] ci: probe the fixed CKEditor tests under CPU load (temporary) --- .github/workflows/probe-ckeditor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/probe-ckeditor.yml b/.github/workflows/probe-ckeditor.yml index 3e8c2e920..f9a414277 100644 --- a/.github/workflows/probe-ckeditor.yml +++ b/.github/workflows/probe-ckeditor.yml @@ -28,6 +28,6 @@ jobs: for c in $(seq $(nproc)); do (while :; do :; done) & done failed=0 for i in $(seq 15); do - bun run test:e2e:full --platform=firefox --test-name-pattern="caret is mid-word" > run.log 2>&1 || { echo "RUN $i FAILED"; grep -E "CKEditor state|error:" run.log; failed=1; } + bun run test:e2e:full --platform=firefox --test-name-pattern="caret is mid-word|paragraph break when accepting|active second paragraph" > run.log 2>&1 || { echo "RUN $i FAILED"; grep -E "CKEditor state|error:" run.log; failed=1; } done exit $failed From ecd83fa9747e36033aeebc0155dbed6a0dfa01b2 Mon Sep 17 00:00:00 2001 From: Bartosz Tomczyk Date: Mon, 5 Oct 2026 11:11:20 +0200 Subject: [PATCH 5/5] ci: remove the temporary CKEditor flake probe Co-Authored-By: Claude Opus 5.5 --- .github/workflows/probe-ckeditor.yml | 33 ---------------------------- 1 file changed, 33 deletions(-) delete mode 100644 .github/workflows/probe-ckeditor.yml diff --git a/.github/workflows/probe-ckeditor.yml b/.github/workflows/probe-ckeditor.yml deleted file mode 100644 index f9a414277..000000000 --- a/.github/workflows/probe-ckeditor.yml +++ /dev/null @@ -1,33 +0,0 @@ -# Temporary: reproduce the Firefox CKEditor mid-word flake. Removed before merge. -name: Probe CKEditor mid-word - -on: - pull_request: - branches: [master] - -permissions: - contents: read - -jobs: - stressed: - name: Firefox mid-word under CPU load (copy ${{ matrix.copy }}) - runs-on: ubuntu-latest - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - copy: [1, 2, 3, 4, 5, 6, 7, 8] - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - with: - bun-version-file: package.json - - run: bun install --frozen-lockfile - - run: bunx puppeteer browsers install firefox - - run: | - for c in $(seq $(nproc)); do (while :; do :; done) & done - failed=0 - for i in $(seq 15); do - bun run test:e2e:full --platform=firefox --test-name-pattern="caret is mid-word|paragraph break when accepting|active second paragraph" > run.log 2>&1 || { echo "RUN $i FAILED"; grep -E "CKEditor state|error:" run.log; failed=1; } - done - exit $failed