From ce63602582ae90cb49a1da52beecfcbba97b67d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Hamburger=20Gr=C3=B8ngaard?= Date: Fri, 19 Jun 2026 11:46:46 +0200 Subject: [PATCH] test: give Firefox focus tests headroom to settle the focus->blur->focus transient Focusing an empty editor emits `focused` and then programmatically sets the selection (`handleOnFocus` calls `editorEngine.select`). On Firefox that selection-set on a freshly-focused empty contenteditable transiently blurs and re-focuses the element, so the DOM event stream is `focused -> blurred -> focused`. The settled last event is `focused`, but under CI load it can arrive after the default 1s `vi.waitFor` window, so the wait exhausts while the last event is still the transient `blurred` and the test fails with `expected 'blurred' to deeply equal 'focused'`. This is a timing flake, not a wrong final state and not specific to any source change: it reproduces on the changeset release PR (#2826), which touches no source. Give the focus waits a 10s timeout (and the tests a 40s budget so a single slow wait is not capped by the default test timeout) so Firefox can reach the settled `focused` state under load. The existing `retry: 3` stays as a backstop. Behavior asserted is unchanged; only the time allowed to observe it. --- packages/editor/tests/focus.test.tsx | 330 ++++++++++++++------------- 1 file changed, 176 insertions(+), 154 deletions(-) diff --git a/packages/editor/tests/focus.test.tsx b/packages/editor/tests/focus.test.tsx index c7381ef6d1..fb55562745 100644 --- a/packages/editor/tests/focus.test.tsx +++ b/packages/editor/tests/focus.test.tsx @@ -5,167 +5,189 @@ import type {EditorEmittedEvent} from '../src' import {EventListenerPlugin} from '../src/plugins/plugin.event-listener' import {createTestEditor} from '../src/test/vitest' +// Focusing an empty editor emits `focused` and then programmatically sets the +// selection (`handleOnFocus` -> `editorEngine.select`). On Firefox that +// selection-set on a freshly-focused empty contenteditable transiently blurs +// and re-focuses the element, so the DOM event stream is +// `focused -> blurred -> focused`. The settled last event is `focused`, but +// under CI load it can arrive well after the default 1s `vi.waitFor` window, +// so the wait exhausts on the transient `blurred`. Give the focus waits (and +// the tests themselves) enough headroom to reach the settled state. +const FOCUS_SETTLE = {timeout: 10_000, interval: 100} as const +const TEST_TIMEOUT = 40_000 + describe('focus', () => { - test('Scenario: Focusing on an empty editor', async () => { - const keyGenerator = createTestKeyGenerator() - const focusEvents: Array = [] - - const {editor, locator} = await createTestEditor({ - keyGenerator, - children: ( - <> - - { - if (event.type === 'focused' || event.type === 'blurred') { - focusEvents.push(event.type) - } - }} - /> - - ), - }) - - const expectedSelection = { - anchor: {path: [{_key: 'k0'}, 'children', {_key: 'k1'}], offset: 0}, - focus: {path: [{_key: 'k0'}, 'children', {_key: 'k1'}], offset: 0}, - backward: false, - } - - const toolbarLocator = page.getByTestId('toolbar') - await vi.waitFor(() => expect.element(toolbarLocator).toBeInTheDocument()) - - await userEvent.click(locator) - - // Wait for both focus and selection to settle. - // Firefox can be slow to process focus on empty contenteditable elements. - await vi.waitFor(() => { - expect(focusEvents.at(-1)).toEqual('focused') - expect(editor.getSnapshot().context.selection).toEqual(expectedSelection) - }) - - await userEvent.click(toolbarLocator) - - await vi.waitFor(() => { - expect(focusEvents.at(-1)).toEqual('blurred') - }) - - await userEvent.click(locator) - - await vi.waitFor(() => { - expect(focusEvents.at(-1)).toEqual('focused') - expect(editor.getSnapshot().context.selection).toEqual(expectedSelection) - }) - }) - - test('Scenario: Focusing on a non-empty editor', async () => { - const keyGenerator = createTestKeyGenerator() - const focusEvents: Array = [] - const fooBlockKey = keyGenerator() - const fooSpanKey = keyGenerator() - const barBlockKey = keyGenerator() - const barSpanKey = keyGenerator() - const initialValue = [ - { - _type: 'block', - _key: fooBlockKey, - children: [ - { - _type: 'span', - _key: fooSpanKey, - text: 'foo', - marks: [], - }, - ], - markDefs: [], - style: 'normal', - }, - { - _type: 'block', - _key: barBlockKey, - children: [ - { - _type: 'span', - _key: barSpanKey, - text: 'b', - marks: [], - }, - ], - markDefs: [], - style: 'normal', - }, - ] - - const {editor, locator} = await createTestEditor({ - keyGenerator, - initialValue, - children: ( - <> - - { - if (event.type === 'focused' || event.type === 'blurred') { - focusEvents.push(event.type) - } - }} - /> - - ), - }) - - const barSpanLocator = locator.getByText('b') - const toolbarLocator = page.getByTestId('toolbar') - await vi.waitFor(() => expect.element(barSpanLocator).toBeInTheDocument()) - await vi.waitFor(() => expect.element(toolbarLocator).toBeInTheDocument()) - - await userEvent.click(barSpanLocator) - - await vi.waitFor(() => { - expect(focusEvents.at(-1)).toEqual('focused') - }) - - await vi.waitFor(() => { - expect(editor.getSnapshot().context.selection).toEqual({ - anchor: { - path: [{_key: barBlockKey}, 'children', {_key: barSpanKey}], - offset: 0, + test( + 'Scenario: Focusing on an empty editor', + {timeout: TEST_TIMEOUT}, + async () => { + const keyGenerator = createTestKeyGenerator() + const focusEvents: Array = [] + + const {editor, locator} = await createTestEditor({ + keyGenerator, + children: ( + <> + + { + if (event.type === 'focused' || event.type === 'blurred') { + focusEvents.push(event.type) + } + }} + /> + + ), + }) + + const expectedSelection = { + anchor: {path: [{_key: 'k0'}, 'children', {_key: 'k1'}], offset: 0}, + focus: {path: [{_key: 'k0'}, 'children', {_key: 'k1'}], offset: 0}, + backward: false, + } + + const toolbarLocator = page.getByTestId('toolbar') + await vi.waitFor(() => expect.element(toolbarLocator).toBeInTheDocument()) + + await userEvent.click(locator) + + // Wait for both focus and selection to settle. + await vi.waitFor(() => { + expect(focusEvents.at(-1)).toEqual('focused') + expect(editor.getSnapshot().context.selection).toEqual( + expectedSelection, + ) + }, FOCUS_SETTLE) + + await userEvent.click(toolbarLocator) + + await vi.waitFor(() => { + expect(focusEvents.at(-1)).toEqual('blurred') + }, FOCUS_SETTLE) + + await userEvent.click(locator) + + await vi.waitFor(() => { + expect(focusEvents.at(-1)).toEqual('focused') + expect(editor.getSnapshot().context.selection).toEqual( + expectedSelection, + ) + }, FOCUS_SETTLE) + }, + ) + + test( + 'Scenario: Focusing on a non-empty editor', + {timeout: TEST_TIMEOUT}, + async () => { + const keyGenerator = createTestKeyGenerator() + const focusEvents: Array = [] + const fooBlockKey = keyGenerator() + const fooSpanKey = keyGenerator() + const barBlockKey = keyGenerator() + const barSpanKey = keyGenerator() + const initialValue = [ + { + _type: 'block', + _key: fooBlockKey, + children: [ + { + _type: 'span', + _key: fooSpanKey, + text: 'foo', + marks: [], + }, + ], + markDefs: [], + style: 'normal', }, - focus: { - path: [{_key: barBlockKey}, 'children', {_key: barSpanKey}], - offset: 0, + { + _type: 'block', + _key: barBlockKey, + children: [ + { + _type: 'span', + _key: barSpanKey, + text: 'b', + marks: [], + }, + ], + markDefs: [], + style: 'normal', }, - backward: false, + ] + + const {editor, locator} = await createTestEditor({ + keyGenerator, + initialValue, + children: ( + <> + + { + if (event.type === 'focused' || event.type === 'blurred') { + focusEvents.push(event.type) + } + }} + /> + + ), }) - }) - await userEvent.click(toolbarLocator) + const barSpanLocator = locator.getByText('b') + const toolbarLocator = page.getByTestId('toolbar') + await vi.waitFor(() => expect.element(barSpanLocator).toBeInTheDocument()) + await vi.waitFor(() => expect.element(toolbarLocator).toBeInTheDocument()) - await vi.waitFor(() => { - expect(focusEvents.at(-1)).toEqual('blurred') - }) + await userEvent.click(barSpanLocator) - await userEvent.click(barSpanLocator) + await vi.waitFor(() => { + expect(focusEvents.at(-1)).toEqual('focused') + }, FOCUS_SETTLE) - await vi.waitFor(() => { - expect(focusEvents.at(-1)).toEqual('focused') - }) + await vi.waitFor(() => { + expect(editor.getSnapshot().context.selection).toEqual({ + anchor: { + path: [{_key: barBlockKey}, 'children', {_key: barSpanKey}], + offset: 0, + }, + focus: { + path: [{_key: barBlockKey}, 'children', {_key: barSpanKey}], + offset: 0, + }, + backward: false, + }) + }, FOCUS_SETTLE) - await vi.waitFor(() => { - expect(editor.getSnapshot().context.selection).toEqual({ - anchor: { - path: [{_key: barBlockKey}, 'children', {_key: barSpanKey}], - offset: 0, - }, - focus: { - path: [{_key: barBlockKey}, 'children', {_key: barSpanKey}], - offset: 0, - }, - backward: false, - }) - }) - }) + await userEvent.click(toolbarLocator) + + await vi.waitFor(() => { + expect(focusEvents.at(-1)).toEqual('blurred') + }, FOCUS_SETTLE) + + await userEvent.click(barSpanLocator) + + await vi.waitFor(() => { + expect(focusEvents.at(-1)).toEqual('focused') + }, FOCUS_SETTLE) + + await vi.waitFor(() => { + expect(editor.getSnapshot().context.selection).toEqual({ + anchor: { + path: [{_key: barBlockKey}, 'children', {_key: barSpanKey}], + offset: 0, + }, + focus: { + path: [{_key: barBlockKey}, 'children', {_key: barSpanKey}], + offset: 0, + }, + backward: false, + }) + }, FOCUS_SETTLE) + }, + ) })