From 13fc14869bcf9d00d84b6397a0b9e3e9e2eacfaf Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Fri, 29 May 2026 07:34:10 -0700 Subject: [PATCH 1/3] [lexical-playground] Chore: Audit and de-flake the e2e suite (remove all @flaky tags) (#8585) Co-authored-by: Claude Opus 4.8 (1M context) --- .github/workflows/call-e2e-test.yml | 4 +- .../__tests__/e2e/Autocomplete.spec.mjs | 80 +- .../__tests__/e2e/ClearFormatting.spec.mjs | 149 +- .../lexical/CopyAndPaste.spec.mjs | 102 +- .../lexical/ListsCopyAndPaste.spec.mjs | 219 +- .../__tests__/e2e/History.spec.mjs | 692 ++-- .../__tests__/e2e/HorizontalRule.spec.mjs | 197 +- .../__tests__/e2e/Links.spec.mjs | 435 ++- .../__tests__/e2e/List.spec.mjs | 314 +- .../__tests__/e2e/Navigation.spec.mjs | 70 +- .../__tests__/e2e/Selection.spec.mjs | 2 - .../__tests__/e2e/Tab.spec.mjs | 136 +- .../__tests__/e2e/Tables.spec.mjs | 2782 ++++++++--------- .../__tests__/e2e/TextFormatting.spec.mjs | 90 +- .../__tests__/e2e/Toolbar.spec.mjs | 444 +-- .../regression/429-swapping-emoji.spec.mjs | 127 +- .../4697-repeated-table-selection.spec.mjs | 62 +- .../4872-full-row-span-cell-merge.spec.mjs | 60 +- .../__tests__/utils/index.mjs | 41 +- 19 files changed, 2959 insertions(+), 3047 deletions(-) diff --git a/.github/workflows/call-e2e-test.yml b/.github/workflows/call-e2e-test.yml index 2b5db8bbdf2..156f4ba382d 100644 --- a/.github/workflows/call-e2e-test.yml +++ b/.github/workflows/call-e2e-test.yml @@ -40,14 +40,14 @@ jobs: fail-on-cache-miss: ${{ inputs.fail-on-cache-miss }} - name: Run tests if: inputs.editor-mode != 'rich-text-with-collab' - run: pnpm run test-e2e-${{ inputs.prod && 'prod-' || '' }}${{ inputs.browser }} ${{ inputs.flaky && '--grep' || '--grep-invert' }} "@flaky" + run: pnpm run test-e2e-${{ inputs.prod && 'prod-' || '' }}${{ inputs.browser }} ${{ inputs.flaky && '--grep' || '--grep-invert' }} "@flaky"${{ inputs.flaky && ' --pass-with-no-tests' || '' }} - name: Run collab tests if: inputs.editor-mode == 'rich-text-with-collab' shell: bash run: | pnpm exec concurrently -k -s first \ "pnpm run collab" \ - "pnpm run test-e2e-collab-${{ inputs.prod && 'prod-' || '' }}${{ inputs.browser }} ${{ inputs.flaky && '--grep' || '--grep-invert' }} @flaky" + "pnpm run test-e2e-collab-${{ inputs.prod && 'prod-' || '' }}${{ inputs.browser }} ${{ inputs.flaky && '--grep' || '--grep-invert' }} @flaky${{ inputs.flaky && ' --pass-with-no-tests' || '' }}" - name: Upload Artifacts if: failure() uses: actions/upload-artifact@v7 diff --git a/packages/lexical-playground/__tests__/e2e/Autocomplete.spec.mjs b/packages/lexical-playground/__tests__/e2e/Autocomplete.spec.mjs index 0c15aa8f7fd..419df095f13 100644 --- a/packages/lexical-playground/__tests__/e2e/Autocomplete.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Autocomplete.spec.mjs @@ -28,49 +28,45 @@ test.describe('Autocomplete', () => { test.beforeEach(({isCollab, page}) => initialize({isAutocomplete: true, isCollab, page}), ); - test( - 'Can autocomplete a word', - {tag: '@flaky'}, - async ({page, isPlainText}) => { - await focusEditor(page); - await page.keyboard.type('Sort by alpha'); - await sleep(500); - // The ghost is a DOM-only decoration on the active TextNode's span and - // is intentionally not part of EditorState, so it doesn't sync through - // Yjs to the right collab frame. - await assertHTML( - page, - html` -

- - Sort by alpha - - betical (TAB) - + test('Can autocomplete a word', async ({page, isPlainText}) => { + await focusEditor(page); + await page.keyboard.type('Sort by alpha'); + await sleep(500); + // The ghost is a DOM-only decoration on the active TextNode's span and + // is intentionally not part of EditorState, so it doesn't sync through + // Yjs to the right collab frame. + await assertHTML( + page, + html` +

+ + Sort by alpha + + betical (TAB) -

- `, - html` -

- Sort by alpha -

- `, - ); - await page.keyboard.press('Tab'); - await page.keyboard.type(' order:'); - await assertHTML( - page, - html` -

- Sort by alphabetical order: -

- `, - ); - }, - ); + +

+ `, + html` +

+ Sort by alpha +

+ `, + ); + await page.keyboard.press('Tab'); + await page.keyboard.type(' order:'); + await assertHTML( + page, + html` +

+ Sort by alphabetical order: +

+ `, + ); + }); test('Can autocomplete in the same format as the original text', async ({ page, diff --git a/packages/lexical-playground/__tests__/e2e/ClearFormatting.spec.mjs b/packages/lexical-playground/__tests__/e2e/ClearFormatting.spec.mjs index e1f04d08967..89815224874 100644 --- a/packages/lexical-playground/__tests__/e2e/ClearFormatting.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/ClearFormatting.spec.mjs @@ -104,86 +104,87 @@ test.describe('Clear All Formatting', () => { ); }); - test( - `Should preserve the default styling of hashtags and mentions`, - { - tag: '@flaky', - }, - async ({page}) => { - await focusEditor(page); + test(`Should preserve the default styling of hashtags and mentions`, async ({ + page, + }) => { + await focusEditor(page); - await page.keyboard.type('#facebook testing'); - await selectAll(page); - await toggleItalic(page); - await selectFromBackgroundColorPicker(page); - await selectFromColorPicker(page); - await selectFromAdditionalStylesDropdown(page, '.clear'); - await assertHTML( - page, - html` -

- - #facebook - - testing -

- `, - ); + await page.keyboard.type('#facebook testing'); + await selectAll(page); + await toggleItalic(page); + await selectFromBackgroundColorPicker(page); + await selectFromColorPicker(page); + await selectFromAdditionalStylesDropdown(page, '.clear'); + await assertHTML( + page, + html` +

+ + #facebook + + testing +

+ `, + ); - await clearEditor(page); + await clearEditor(page); - await page.keyboard.type('@Luke'); + await page.keyboard.type('@Luke'); - await waitForSelector(page, '#typeahead-menu ul li'); - await assertHTML( - page, - html` -

- @Luke -

- `, - ); + // Wait until "Luke Skywalker" is the *highlighted* option, not merely + // present: while "@Luke" is still being typed, the partial query "@Lu" + // also matches "Agent Kallus" (kal**lu**s), which sorts earlier in the + // list and is highlighted first, so pressing Enter too early selects it. + await waitForSelector( + page, + '#typeahead-menu ul li[aria-selected="true"]:has-text("Luke Skywalker")', + ); + await assertHTML( + page, + html` +

+ @Luke +

+ `, + ); - await page.keyboard.press('Enter'); - await assertHTML( - page, - html` -

- - Luke Skywalker - -

- `, - ); + await page.keyboard.press('Enter'); + await assertHTML( + page, + html` +

+ + Luke Skywalker + +

+ `, + ); - await page.keyboard.type(' is testing'); - await selectAll(page); - await toggleBold(page); - await selectFromColorPicker(page); - await selectFromAdditionalStylesDropdown(page, '.clear'); - await assertHTML( - page, - html` -

- - Luke Skywalker - - is testing -

- `, - ); - }, - ); + await page.keyboard.type(' is testing'); + await selectAll(page); + await toggleBold(page); + await selectFromColorPicker(page); + await selectFromAdditionalStylesDropdown(page, '.clear'); + await assertHTML( + page, + html` +

+ + Luke Skywalker + + is testing +

+ `, + ); + }); test(`Can clear left/center/right alignment when BIU formatting already applied`, async ({ page, diff --git a/packages/lexical-playground/__tests__/e2e/CopyAndPaste/lexical/CopyAndPaste.spec.mjs b/packages/lexical-playground/__tests__/e2e/CopyAndPaste/lexical/CopyAndPaste.spec.mjs index ecd039e5058..b2ea77eb236 100644 --- a/packages/lexical-playground/__tests__/e2e/CopyAndPaste/lexical/CopyAndPaste.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/CopyAndPaste/lexical/CopyAndPaste.spec.mjs @@ -816,61 +816,59 @@ test.describe('CopyAndPaste', () => { ); }); - test( - 'Pasting a decorator node on a blank line inserts before the line', - { - tag: '@flaky', - }, - async ({page, isCollab, isPlainText}) => { - // TODO: This is skipped on collab because the right frame won't have the block cursor HTML - test.skip(isPlainText || isCollab); - - // copying and pasting the node is easier than creating the clipboard data - await focusEditor(page); - await insertYouTubeEmbed(page, YOUTUBE_SAMPLE_URL); - await page.keyboard.press('ArrowLeft'); // this selects the node - await withExclusiveClipboardAccess(async () => { - const clipboard = await copyToClipboard(page); - await page.keyboard.press('ArrowRight'); // this moves to a new line (empty paragraph node) - await pasteFromClipboard(page, clipboard); + test('Pasting a decorator node on a blank line inserts before the line', async ({ + page, + isCollab, + isPlainText, + }) => { + // TODO: This is skipped on collab because the right frame won't have the block cursor HTML + test.skip(isPlainText || isCollab); - await assertHTML( - page, - html` -


-
-
- -
+ // copying and pasting the node is easier than creating the clipboard data + await focusEditor(page); + await insertYouTubeEmbed(page, YOUTUBE_SAMPLE_URL); + await page.keyboard.press('ArrowLeft'); // this selects the node + await withExclusiveClipboardAccess(async () => { + const clipboard = await copyToClipboard(page); + await page.keyboard.press('ArrowRight'); // this moves to a new line (empty paragraph node) + await pasteFromClipboard(page, clipboard); + + await assertHTML( + page, + html` +


+
+
+
-
-
- -
+
+
+
+
-
- `, - ); - }); - }, - ); +
+
+ `, + ); + }); + }); test('Copy and paste paragraph into quote', async ({page, isPlainText}) => { test.skip(isPlainText); diff --git a/packages/lexical-playground/__tests__/e2e/CopyAndPaste/lexical/ListsCopyAndPaste.spec.mjs b/packages/lexical-playground/__tests__/e2e/CopyAndPaste/lexical/ListsCopyAndPaste.spec.mjs index a39089ebe25..dd53fc7af61 100644 --- a/packages/lexical-playground/__tests__/e2e/CopyAndPaste/lexical/ListsCopyAndPaste.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/CopyAndPaste/lexical/ListsCopyAndPaste.spec.mjs @@ -136,26 +136,80 @@ test.describe('Lists CopyAndPaste', () => { }); }); - test( - 'Copy and paste of partial list items into the list', - {tag: '@flaky'}, - async ({page, isPlainText, isCollab, browserName}) => { - test.skip(isPlainText); + test('Copy and paste of partial list items into the list', async ({ + page, + isPlainText, + isCollab, + browserName, + }) => { + test.skip(isPlainText); - await focusEditor(page); + await focusEditor(page); - // Add three list items - await page.keyboard.type('- one'); - await page.keyboard.press('Enter'); - await page.keyboard.type('two'); - await page.keyboard.press('Enter'); - await page.keyboard.type('three'); + // Add three list items + await page.keyboard.type('- one'); + await page.keyboard.press('Enter'); + await page.keyboard.type('two'); + await page.keyboard.press('Enter'); + await page.keyboard.type('three'); - await page.keyboard.press('Enter'); - await page.keyboard.press('Enter'); + await page.keyboard.press('Enter'); + await page.keyboard.press('Enter'); + + // Add a paragraph + await page.keyboard.type('Some text.'); + + await assertHTML( + page, + html` +
    +
  • + one +
  • +
  • + two +
  • +
  • + three +
  • +
+

+ Some text. +

+ `, + ); + await assertSelection(page, { + anchorOffset: 10, + anchorPath: [1, 0, 0], + focusOffset: 10, + focusPath: [1, 0, 0], + }); + + await page.keyboard.down('Shift'); + await moveToLineBeginning(page); + await moveLeft(page, 3); + await page.keyboard.up('Shift'); + + await assertSelection(page, { + anchorOffset: 10, + anchorPath: [1, 0, 0], + focusOffset: 3, + focusPath: [0, 2, 0, 0], + }); - // Add a paragraph - await page.keyboard.type('Some text.'); + await withExclusiveClipboardAccess(async () => { + // Copy the partial list item and paragraph + const clipboard = await copyToClipboard(page); + + // Select all and remove content + await page.keyboard.press('ArrowUp'); + await page.keyboard.press('ArrowUp'); + if (!IS_WINDOWS && browserName === 'firefox') { + await page.keyboard.press('ArrowUp'); + } + await moveToLineEnd(page); + + await page.keyboard.down('Enter'); await assertHTML( page, @@ -165,9 +219,12 @@ test.describe('Lists CopyAndPaste', () => { one
  • - two +
  • + two +
  • +
  • three
  • @@ -177,105 +234,49 @@ test.describe('Lists CopyAndPaste', () => { `, ); await assertSelection(page, { - anchorOffset: 10, - anchorPath: [1, 0, 0], - focusOffset: 10, - focusPath: [1, 0, 0], + anchorOffset: 0, + anchorPath: [0, 1], + focusOffset: 0, + focusPath: [0, 1], }); - await page.keyboard.down('Shift'); - await moveToLineBeginning(page); - await moveLeft(page, 3); - await page.keyboard.up('Shift'); + await pasteFromClipboard(page, clipboard); + await assertHTML( + page, + html` +
      +
    • + one +
    • +
    • + ee +
    • +
    +

    + Some text. +

    +
      +
    • + two +
    • +
    • + three +
    • +
    +

    + Some text. +

    + `, + ); await assertSelection(page, { anchorOffset: 10, anchorPath: [1, 0, 0], - focusOffset: 3, - focusPath: [0, 2, 0, 0], - }); - - await withExclusiveClipboardAccess(async () => { - // Copy the partial list item and paragraph - const clipboard = await copyToClipboard(page); - - // Select all and remove content - await page.keyboard.press('ArrowUp'); - await page.keyboard.press('ArrowUp'); - if (!IS_WINDOWS && browserName === 'firefox') { - await page.keyboard.press('ArrowUp'); - } - await moveToLineEnd(page); - - await page.keyboard.down('Enter'); - - await assertHTML( - page, - html` -
      -
    • - one -
    • -
    • -
      -
    • -
    • - two -
    • -
    • - three -
    • -
    -

    - Some text. -

    - `, - ); - await assertSelection(page, { - anchorOffset: 0, - anchorPath: [0, 1], - focusOffset: 0, - focusPath: [0, 1], - }); - - await pasteFromClipboard(page, clipboard); - - await assertHTML( - page, - html` -
      -
    • - one -
    • -
    • - ee -
    • -
    -

    - Some text. -

    -
      -
    • - two -
    • -
    • - three -
    • -
    -

    - Some text. -

    - `, - ); - await assertSelection(page, { - anchorOffset: 10, - anchorPath: [1, 0, 0], - focusOffset: 10, - focusPath: [1, 0, 0], - }); + focusOffset: 10, + focusPath: [1, 0, 0], }); - }, - ); + }); + }); test('Copy list items and paste back into list', async ({ page, diff --git a/packages/lexical-playground/__tests__/e2e/History.spec.mjs b/packages/lexical-playground/__tests__/e2e/History.spec.mjs index dce4d2219b6..3242893b94b 100644 --- a/packages/lexical-playground/__tests__/e2e/History.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/History.spec.mjs @@ -26,414 +26,412 @@ import { test.describe('History', () => { test.beforeEach(({isCollab, page}) => initialize({isCollab, page})); - test( - `Can type two paragraphs of text and correctly undo and redo`, - { - tag: '@flaky', - }, - async ({isRichText, page, isCollab}) => { - test.skip(isCollab); - await page.focus('div[contenteditable="true"]'); - await page.keyboard.type('hello'); - await sleep(1050); // default merge interval is 1000, add 50ms as overhead due to CI latency. - await page.keyboard.type(' world'); - await page.keyboard.press('Enter'); - await page.keyboard.type('hello world again'); - await moveLeft(page, 6); - await page.keyboard.type(', again and'); - - if (isRichText) { - await assertHTML( - page, - html` -

    - hello world -

    -

    - hello world, again and again -

    - `, - ); - await assertSelection(page, { - anchorOffset: 22, - anchorPath: [1, 0, 0], - focusOffset: 22, - focusPath: [1, 0, 0], - }); - } else { - await assertHTML( - page, - html` -

    - hello world -
    - hello world, again and again -

    - `, - ); - await assertSelection(page, { - anchorOffset: 22, - anchorPath: [0, 2, 0], - focusOffset: 22, - focusPath: [0, 2, 0], - }); - } - - await undo(page); - - if (isRichText) { - await assertHTML( - page, - html` -

    - hello world -

    -

    - hello world again -

    - `, - ); - await assertSelection(page, { - anchorOffset: 11, - anchorPath: [1, 0, 0], - focusOffset: 11, - focusPath: [1, 0, 0], - }); - } else { - await assertHTML( - page, - html` -

    - hello world -
    - hello world again -

    - `, - ); - await assertSelection(page, { - anchorOffset: 11, - anchorPath: [0, 2, 0], - focusOffset: 11, - focusPath: [0, 2, 0], - }); - } - - await undo(page); - - if (isRichText) { - await assertHTML( - page, - html` -

    - hello world -

    -

    -
    -

    - `, - ); - await assertSelection(page, { - anchorOffset: 0, - anchorPath: [1], - focusOffset: 0, - focusPath: [1], - }); - } else { - assertHTML( - page, - html` -

    - hello world -
    -
    -

    - `, - ); - await assertSelection(page, { - anchorOffset: 2, - anchorPath: [0], - focusOffset: 2, - focusPath: [0], - }); - } - - await undo(page); + test(`Can type two paragraphs of text and correctly undo and redo`, async ({ + isRichText, + page, + isCollab, + }) => { + test.skip(isCollab); + await page.focus('div[contenteditable="true"]'); + await page.keyboard.type('hello'); + await sleep(1050); // default merge interval is 1000, add 50ms as overhead due to CI latency. + await page.keyboard.type(' world'); + await page.keyboard.press('Enter'); + await page.keyboard.type('hello world again'); + await moveLeft(page, 6); + await page.keyboard.type(', again and'); + if (isRichText) { + await assertHTML( + page, + html` +

    + hello world +

    +

    + hello world, again and again +

    + `, + ); + await assertSelection(page, { + anchorOffset: 22, + anchorPath: [1, 0, 0], + focusOffset: 22, + focusPath: [1, 0, 0], + }); + } else { await assertHTML( page, html`

    hello world +
    + hello world, again and again +

    + `, + ); + await assertSelection(page, { + anchorOffset: 22, + anchorPath: [0, 2, 0], + focusOffset: 22, + focusPath: [0, 2, 0], + }); + } + + await undo(page); + + if (isRichText) { + await assertHTML( + page, + html` +

    + hello world +

    +

    + hello world again

    `, ); await assertSelection(page, { anchorOffset: 11, - anchorPath: [0, 0, 0], + anchorPath: [1, 0, 0], focusOffset: 11, - focusPath: [0, 0, 0], + focusPath: [1, 0, 0], + }); + } else { + await assertHTML( + page, + html` +

    + hello world +
    + hello world again +

    + `, + ); + await assertSelection(page, { + anchorOffset: 11, + anchorPath: [0, 2, 0], + focusOffset: 11, + focusPath: [0, 2, 0], }); + } - await undo(page); + await undo(page); + if (isRichText) { await assertHTML( page, html`

    - hello + hello world +

    +

    +

    `, ); await assertSelection(page, { - anchorOffset: 5, - anchorPath: [0, 0, 0], - focusOffset: 5, - focusPath: [0, 0, 0], + anchorOffset: 0, + anchorPath: [1], + focusOffset: 0, + focusPath: [1], }); + } else { + assertHTML( + page, + html` +

    + hello world +
    +
    +

    + `, + ); + await assertSelection(page, { + anchorOffset: 2, + anchorPath: [0], + focusOffset: 2, + focusPath: [0], + }); + } + + await undo(page); + + await assertHTML( + page, + html` +

    + hello world +

    + `, + ); + await assertSelection(page, { + anchorOffset: 11, + anchorPath: [0, 0, 0], + focusOffset: 11, + focusPath: [0, 0, 0], + }); + + await undo(page); + + await assertHTML( + page, + html` +

    + hello +

    + `, + ); + await assertSelection(page, { + anchorOffset: 5, + anchorPath: [0, 0, 0], + focusOffset: 5, + focusPath: [0, 0, 0], + }); + + await undo(page); + + await assertHTML( + page, + html` +


    + `, + ); + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [0], + focusOffset: 0, + focusPath: [0], + }); + + await redo(page); + + await assertHTML( + page, + html` +

    + hello +

    + `, + ); + await assertSelection(page, { + anchorOffset: 5, + anchorPath: [0, 0, 0], + focusOffset: 5, + focusPath: [0, 0, 0], + }); - await undo(page); + await redo(page); + + await assertHTML( + page, + html` +

    + hello world +

    + `, + ); + await assertSelection(page, { + anchorOffset: 11, + anchorPath: [0, 0, 0], + focusOffset: 11, + focusPath: [0, 0, 0], + }); + + await redo(page); + if (isRichText) { await assertHTML( page, html` +

    + hello world +


    `, ); await assertSelection(page, { anchorOffset: 0, - anchorPath: [0], + anchorPath: [1], focusOffset: 0, + focusPath: [1], + }); + } else { + await assertHTML( + page, + html` +

    + hello world +
    +
    +

    + `, + ); + await assertSelection(page, { + anchorOffset: 2, + anchorPath: [0], + focusOffset: 2, focusPath: [0], }); + } - await redo(page); + await redo(page); + if (isRichText) { await assertHTML( page, html`

    - hello + hello world +

    +

    + hello world again

    `, ); await assertSelection(page, { - anchorOffset: 5, - anchorPath: [0, 0, 0], - focusOffset: 5, - focusPath: [0, 0, 0], + anchorOffset: 11, + anchorPath: [1, 0, 0], + focusOffset: 11, + focusPath: [1, 0, 0], }); + } else { + assertHTML( + page, + html` +

    + hello world +
    + hello world again +

    + `, + ); + await assertSelection(page, { + anchorOffset: 11, + anchorPath: [0, 2, 0], + focusOffset: 11, + focusPath: [0, 2, 0], + }); + } - await redo(page); + await redo(page); + if (isRichText) { + assertHTML( + page, + html` +

    + hello world +

    +

    + hello world, again and again +

    + `, + ); + await assertSelection(page, { + anchorOffset: 22, + anchorPath: [1, 0, 0], + focusOffset: 22, + focusPath: [1, 0, 0], + }); + } else { + assertHTML( + page, + html` +

    + hello world +
    + hello world, again and again +

    + `, + ); + await assertSelection(page, { + anchorOffset: 22, + anchorPath: [0, 2, 0], + focusOffset: 22, + focusPath: [0, 2, 0], + }); + } + + await pressBackspace(page, 4); + + if (isRichText) { await assertHTML( page, html`

    hello world

    +

    + hello world, again again +

    `, ); await assertSelection(page, { - anchorOffset: 11, - anchorPath: [0, 0, 0], - focusOffset: 11, - focusPath: [0, 0, 0], + anchorOffset: 18, + anchorPath: [1, 0, 0], + focusOffset: 18, + focusPath: [1, 0, 0], + }); + } else { + await assertHTML( + page, + html` +

    + hello world +
    + hello world, again again +

    + `, + ); + await assertSelection(page, { + anchorOffset: 18, + anchorPath: [0, 2, 0], + focusOffset: 18, + focusPath: [0, 2, 0], }); + } - await redo(page); - - if (isRichText) { - await assertHTML( - page, - html` -

    - hello world -

    -


    - `, - ); - await assertSelection(page, { - anchorOffset: 0, - anchorPath: [1], - focusOffset: 0, - focusPath: [1], - }); - } else { - await assertHTML( - page, - html` -

    - hello world -
    -
    -

    - `, - ); - await assertSelection(page, { - anchorOffset: 2, - anchorPath: [0], - focusOffset: 2, - focusPath: [0], - }); - } - - await redo(page); - - if (isRichText) { - await assertHTML( - page, - html` -

    - hello world -

    -

    - hello world again -

    - `, - ); - await assertSelection(page, { - anchorOffset: 11, - anchorPath: [1, 0, 0], - focusOffset: 11, - focusPath: [1, 0, 0], - }); - } else { - assertHTML( - page, - html` -

    - hello world -
    - hello world again -

    - `, - ); - await assertSelection(page, { - anchorOffset: 11, - anchorPath: [0, 2, 0], - focusOffset: 11, - focusPath: [0, 2, 0], - }); - } - - await redo(page); - - if (isRichText) { - assertHTML( - page, - html` -

    - hello world -

    -

    - hello world, again and again -

    - `, - ); - await assertSelection(page, { - anchorOffset: 22, - anchorPath: [1, 0, 0], - focusOffset: 22, - focusPath: [1, 0, 0], - }); - } else { - assertHTML( - page, - html` -

    - hello world -
    - hello world, again and again -

    - `, - ); - await assertSelection(page, { - anchorOffset: 22, - anchorPath: [0, 2, 0], - focusOffset: 22, - focusPath: [0, 2, 0], - }); - } - - await pressBackspace(page, 4); - - if (isRichText) { - await assertHTML( - page, - html` -

    - hello world -

    -

    - hello world, again again -

    - `, - ); - await assertSelection(page, { - anchorOffset: 18, - anchorPath: [1, 0, 0], - focusOffset: 18, - focusPath: [1, 0, 0], - }); - } else { - await assertHTML( - page, - html` -

    - hello world -
    - hello world, again again -

    - `, - ); - await assertSelection(page, { - anchorOffset: 18, - anchorPath: [0, 2, 0], - focusOffset: 18, - focusPath: [0, 2, 0], - }); - } - - await undo(page); - - if (isRichText) { - await assertHTML( - page, - html` -

    - hello world -

    -

    - hello world, again and again -

    - `, - ); - await assertSelection(page, { - anchorOffset: 22, - anchorPath: [1, 0, 0], - focusOffset: 22, - focusPath: [1, 0, 0], - }); - } else { - await assertHTML( - page, - html` -

    - hello world -
    - hello world, again and again -

    - `, - ); - await assertSelection(page, { - anchorOffset: 22, - anchorPath: [0, 2, 0], - focusOffset: 22, - focusPath: [0, 2, 0], - }); - } - }, - ); + await undo(page); + + if (isRichText) { + await assertHTML( + page, + html` +

    + hello world +

    +

    + hello world, again and again +

    + `, + ); + await assertSelection(page, { + anchorOffset: 22, + anchorPath: [1, 0, 0], + focusOffset: 22, + focusPath: [1, 0, 0], + }); + } else { + await assertHTML( + page, + html` +

    + hello world +
    + hello world, again and again +

    + `, + ); + await assertSelection(page, { + anchorOffset: 22, + anchorPath: [0, 2, 0], + focusOffset: 22, + focusPath: [0, 2, 0], + }); + } + }); test('Can coalesce when switching inline styles (#1151)', async ({ page, diff --git a/packages/lexical-playground/__tests__/e2e/HorizontalRule.spec.mjs b/packages/lexical-playground/__tests__/e2e/HorizontalRule.spec.mjs index 02f446b9f06..b77c4ea034a 100644 --- a/packages/lexical-playground/__tests__/e2e/HorizontalRule.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/HorizontalRule.spec.mjs @@ -34,126 +34,127 @@ async function toggleBulletList(page) { test.describe('HorizontalRule', () => { test.beforeEach(({isCollab, page}) => initialize({isCollab, page})); - test( - 'Can create a horizontal rule and move selection around it', - {tag: '@flaky'}, - async ({page, isCollab, isPlainText, browserName}) => { - test.skip(isPlainText); - await focusEditor(page); + test('Can create a horizontal rule and move selection around it', async ({ + page, + isCollab, + isPlainText, + browserName, + }) => { + test.skip(isPlainText); + await focusEditor(page); - await selectFromInsertDropdown(page, '.horizontal-rule'); + await selectFromInsertDropdown(page, '.horizontal-rule'); - await waitForSelector(page, 'hr'); + await waitForSelector(page, 'hr'); - await assertHTML( - page, - html` -


    -
    -


    - `, - ); + await assertHTML( + page, + html` +


    +
    +


    + `, + ); - await assertSelection(page, { - anchorOffset: 0, - anchorPath: [2], - focusOffset: 0, - focusPath: [2], - }); + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [2], + focusOffset: 0, + focusPath: [2], + }); - await page.keyboard.press('ArrowUp'); + await page.keyboard.press('ArrowUp'); - await assertSelection(page, { - anchorOffset: 0, - anchorPath: [0], - focusOffset: 0, - focusPath: [0], - }); + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [0], + focusOffset: 0, + focusPath: [0], + }); - await page.keyboard.press('ArrowRight'); - await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowRight'); - await assertSelection(page, { - anchorOffset: 0, - anchorPath: [2], - focusOffset: 0, - focusPath: [2], - }); + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [2], + focusOffset: 0, + focusPath: [2], + }); - await page.keyboard.press('ArrowLeft'); - await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); - await assertSelection(page, { - anchorOffset: 0, - anchorPath: [0], - focusOffset: 0, - focusPath: [0], - }); + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [0], + focusOffset: 0, + focusPath: [0], + }); - await page.keyboard.type('Some text'); + await page.keyboard.type('Some text'); - await page.keyboard.press('ArrowRight'); - await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowRight'); - await assertSelection(page, { - anchorOffset: 0, - anchorPath: [2], - focusOffset: 0, - focusPath: [2], - }); + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [2], + focusOffset: 0, + focusPath: [2], + }); - await page.keyboard.type('Some more text'); + await page.keyboard.type('Some more text'); - await assertHTML( - page, - html` -

    - Some text -

    -
    -

    - Some more text -

    - `, - ); + await assertHTML( + page, + html` +

    + Some text +

    +
    +

    + Some more text +

    + `, + ); - await moveToLineBeginning(page); + await moveToLineBeginning(page); - await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); - await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); - await assertSelection(page, { - anchorOffset: 1, - anchorPath: [0], - focusOffset: 1, - focusPath: [0], - }); + await assertSelection(page, { + anchorOffset: 1, + anchorPath: [0], + focusOffset: 1, + focusPath: [0], + }); - await pressBackspace(page, 10); + await pressBackspace(page, 10); - // Collab doesn't process the cursor correctly - if (!isCollab) { - await assertHTML( - page, - '

    Some more text

    ', - ); - } + // Collab doesn't process the cursor correctly + if (!isCollab) { + await assertHTML( + page, + '

    Some more text

    ', + ); + } - await assertSelection(page, { - anchorOffset: 0, - anchorPath: [], - focusOffset: 0, - focusPath: [], - }); - }, - ); + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [], + focusOffset: 0, + focusPath: [], + }); + }); test('Will add a horizontal rule at the end of a current TextNode and move selection to the new ParagraphNode.', async ({ page, diff --git a/packages/lexical-playground/__tests__/e2e/Links.spec.mjs b/packages/lexical-playground/__tests__/e2e/Links.spec.mjs index b2d2f9de02a..57895fe24cb 100644 --- a/packages/lexical-playground/__tests__/e2e/Links.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Links.spec.mjs @@ -407,140 +407,132 @@ test.describe.parallel('Links', () => { ); }); - test( - `Can create a link with some text after, insert paragraph, then backspace, it should merge correctly`, - { - tag: '@flaky', - }, - async ({page}) => { - await focusEditor(page); - await page.keyboard.type(' abc def '); - await moveLeft(page, 5); - await selectCharacters(page, 'left', 3); + test(`Can create a link with some text after, insert paragraph, then backspace, it should merge correctly`, async ({ + page, + }) => { + await focusEditor(page); + await page.keyboard.type(' abc def '); + await moveLeft(page, 5); + await selectCharacters(page, 'left', 3); - // link - await click(page, '.link'); - await click(page, '.link-confirm'); + // link + await click(page, '.link'); + await click(page, '.link-confirm'); - await assertHTML( - page, - html` -

    - - - abc - - def -

    - `, - ); + await assertHTML( + page, + html` +

    + + + abc + + def +

    + `, + ); - await moveLeft(page, 1); - await moveRight(page, 2); - await page.keyboard.press('Enter'); + await moveLeft(page, 1); + await moveRight(page, 2); + await page.keyboard.press('Enter'); - await assertHTML( - page, - html` -

    - - - ab - -

    -

    - - c - - def -

    - `, - ); + await assertHTML( + page, + html` +

    + + + ab + +

    +

    + + c + + def +

    + `, + ); - await page.keyboard.press('Backspace'); + await page.keyboard.press('Backspace'); - await assertHTML( - page, - html` -

    - - - abc - - def -

    - `, - ); - }, - ); + await assertHTML( + page, + html` +

    + + + abc + + def +

    + `, + ); + }); - test( - `Can backspace across a link and it deletes text, not the whole link`, - { - tag: '@flaky', - }, - async ({page}) => { - await focusEditor(page); - await page.keyboard.type(' abc def '); - await moveLeft(page, 5); - await selectCharacters(page, 'left', 3); + test(`Can backspace across a link and it deletes text, not the whole link`, async ({ + page, + }) => { + await focusEditor(page); + await page.keyboard.type(' abc def '); + await moveLeft(page, 5); + await selectCharacters(page, 'left', 3); - // link - await click(page, '.link'); - await click(page, '.link-confirm'); + // link + await click(page, '.link'); + await click(page, '.link-confirm'); - await assertHTML( - page, - html` -

    - - - abc - - def -

    - `, - ); + await assertHTML( + page, + html` +

    + + + abc + + def +

    + `, + ); - await moveRight(page, 4); + await moveRight(page, 4); - await page.keyboard.press('Backspace'); - await page.keyboard.press('Backspace'); - await page.keyboard.press('Backspace'); - await page.keyboard.press('Backspace'); + await page.keyboard.press('Backspace'); + await page.keyboard.press('Backspace'); + await page.keyboard.press('Backspace'); + await page.keyboard.press('Backspace'); - await assertHTML( - page, - html` -

    - - - ab - - f -

    - `, - ); - }, - ); + await assertHTML( + page, + html` +

    + + + ab + + f +

    + `, + ); + }); test(`Can create a link then replace it with plain text`, async ({page}) => { await focusEditor(page); @@ -1841,120 +1833,111 @@ test.describe.parallel('Links', () => { ); }); - test( - 'Can handle pressing Enter inside a Link', - {tag: '@flaky'}, - async ({page}) => { - await focusEditor(page); - await page.keyboard.type('Hello awesome'); - await selectAll(page); - await click(page, '.link'); - await click(page, '.link-confirm'); - await page.keyboard.press('ArrowRight'); - await page.keyboard.type('world'); + test('Can handle pressing Enter inside a Link', async ({page}) => { + await focusEditor(page); + await page.keyboard.type('Hello awesome'); + await selectAll(page); + await click(page, '.link'); + await click(page, '.link-confirm'); + await page.keyboard.press('ArrowRight'); + await page.keyboard.type('world'); - await moveToLineBeginning(page); - await moveRight(page, 6); + await moveToLineBeginning(page); + await moveRight(page, 6); - await page.keyboard.press('Enter'); + await page.keyboard.press('Enter'); - await assertHTML( - page, - html` -

    - - Hello - -

    -

    - - awesome - - world -

    - `, - undefined, - {ignoreClasses: true}, - ); - }, - ); + await assertHTML( + page, + html` +

    + + Hello + +

    +

    + + awesome + + world +

    + `, + undefined, + {ignoreClasses: true}, + ); + }); - test( - 'Can handle pressing Enter inside a Link containing multiple TextNodes', - {tag: '@flaky'}, - async ({page, isCollab}) => { - await focusEditor(page); - await page.keyboard.type('Hello '); - await toggleBold(page); - await page.keyboard.type('awe'); - await toggleBold(page); - await page.keyboard.type('some'); - await selectAll(page); - await click(page, '.link'); - await click(page, '.link-confirm'); - await page.keyboard.press('ArrowRight'); - await page.keyboard.type(' world'); + test('Can handle pressing Enter inside a Link containing multiple TextNodes', async ({ + page, + isCollab, + }) => { + await focusEditor(page); + await page.keyboard.type('Hello '); + await toggleBold(page); + await page.keyboard.type('awe'); + await toggleBold(page); + await page.keyboard.type('some'); + await selectAll(page); + await click(page, '.link'); + await click(page, '.link-confirm'); + await page.keyboard.press('ArrowRight'); + await page.keyboard.type(' world'); - await moveToLineBeginning(page); - await moveRight(page, 6); + await moveToLineBeginning(page); + await moveRight(page, 6); - await page.keyboard.press('Enter'); + await page.keyboard.press('Enter'); - await assertHTML( - page, - html` -

    - - Hello - -

    -

    - - awe - some - - world -

    - `, - undefined, - {ignoreClasses: true}, - ); - }, - ); + await assertHTML( + page, + html` +

    + + Hello + +

    +

    + + awe + some + + world +

    + `, + undefined, + {ignoreClasses: true}, + ); + }); - test( - 'Can handle pressing Enter at the beginning of a Link', - { - tag: '@flaky', - }, - async ({page}) => { - await focusEditor(page); - await page.keyboard.type('Hello awesome'); - await selectAll(page); - await click(page, '.link'); - await click(page, '.link-confirm'); - await page.keyboard.press('ArrowRight'); - await page.keyboard.type(' world'); + test('Can handle pressing Enter at the beginning of a Link', async ({ + page, + }) => { + await focusEditor(page); + await page.keyboard.type('Hello awesome'); + await selectAll(page); + await click(page, '.link'); + await click(page, '.link-confirm'); + await page.keyboard.press('ArrowRight'); + await page.keyboard.type(' world'); - await moveToLineBeginning(page); - await page.keyboard.press('Enter'); + await moveToLineBeginning(page); + await page.keyboard.press('Enter'); - await assertHTML( - page, - html` -


    -

    - - Hello awesome - - world -

    - `, - undefined, - {ignoreClasses: true}, - ); - }, - ); + await assertHTML( + page, + html` +


    +

    + + Hello awesome + + world +

    + `, + undefined, + {ignoreClasses: true}, + ); + }); test('Can handle pressing Enter at the end of a Link', async ({ isCollab, diff --git a/packages/lexical-playground/__tests__/e2e/List.spec.mjs b/packages/lexical-playground/__tests__/e2e/List.spec.mjs index 5c3718a2393..7489a29f2ab 100644 --- a/packages/lexical-playground/__tests__/e2e/List.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/List.spec.mjs @@ -169,57 +169,54 @@ test.describe('Checklist focus option', () => { test.describe.parallel('Nested List', () => { test.beforeEach(({isCollab, page}) => initialize({isCollab, page})); - test( - `Can create a list and partially copy some content out of it`, - { - tag: '@flaky', - }, - async ({page, isCollab}) => { - await focusEditor(page); - await page.keyboard.type( - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam venenatis risus ac cursus efficitur. Cras efficitur magna odio, lacinia posuere mauris placerat in. Etiam eu congue nisl. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nulla vulputate justo id eros convallis, vel pellentesque orci hendrerit. Pellentesque accumsan molestie eros, vitae tempor nisl semper sit amet. Sed vulputate leo dolor, et bibendum quam feugiat eget. Praesent vestibulum libero sed enim ornare, in consequat dui posuere. Maecenas ornare vestibulum felis, non elementum urna imperdiet sit amet.', - ); - await toggleBulletList(page); - await moveToEditorBeginning(page); - await moveRight(page, 6); - await selectCharacters(page, 'right', 11); - - await withExclusiveClipboardAccess(async () => { - const clipboard = await copyToClipboard(page); - - await moveToEditorEnd(page); - await page.keyboard.press('Enter'); - await page.keyboard.press('Enter'); - - await pasteFromClipboard(page, clipboard); - }); - - await assertHTML( - page, - html` -
      -
    • - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam - venenatis risus ac cursus efficitur. Cras efficitur magna odio, - lacinia posuere mauris placerat in. Etiam eu congue nisl. - Vestibulum ante ipsum primis in faucibus orci luctus et ultrices - posuere cubilia curae; Nulla vulputate justo id eros convallis, - vel pellentesque orci hendrerit. Pellentesque accumsan molestie - eros, vitae tempor nisl semper sit amet. Sed vulputate leo - dolor, et bibendum quam feugiat eget. Praesent vestibulum libero - sed enim ornare, in consequat dui posuere. Maecenas ornare - vestibulum felis, non elementum urna imperdiet sit amet. - -
    • -
    -

    - ipsum dolor -

    - `, - ); - }, - ); + test(`Can create a list and partially copy some content out of it`, async ({ + page, + isCollab, + }) => { + await focusEditor(page); + await page.keyboard.type( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam venenatis risus ac cursus efficitur. Cras efficitur magna odio, lacinia posuere mauris placerat in. Etiam eu congue nisl. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nulla vulputate justo id eros convallis, vel pellentesque orci hendrerit. Pellentesque accumsan molestie eros, vitae tempor nisl semper sit amet. Sed vulputate leo dolor, et bibendum quam feugiat eget. Praesent vestibulum libero sed enim ornare, in consequat dui posuere. Maecenas ornare vestibulum felis, non elementum urna imperdiet sit amet.', + ); + await toggleBulletList(page); + await moveToEditorBeginning(page); + await moveRight(page, 6); + await selectCharacters(page, 'right', 11); + + await withExclusiveClipboardAccess(async () => { + const clipboard = await copyToClipboard(page); + + await moveToEditorEnd(page); + await page.keyboard.press('Enter'); + await page.keyboard.press('Enter'); + + await pasteFromClipboard(page, clipboard); + }); + + await assertHTML( + page, + html` +
      +
    • + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam + venenatis risus ac cursus efficitur. Cras efficitur magna odio, + lacinia posuere mauris placerat in. Etiam eu congue nisl. + Vestibulum ante ipsum primis in faucibus orci luctus et ultrices + posuere cubilia curae; Nulla vulputate justo id eros convallis, + vel pellentesque orci hendrerit. Pellentesque accumsan molestie + eros, vitae tempor nisl semper sit amet. Sed vulputate leo dolor, + et bibendum quam feugiat eget. Praesent vestibulum libero sed enim + ornare, in consequat dui posuere. Maecenas ornare vestibulum + felis, non elementum urna imperdiet sit amet. + +
    • +
    +

    + ipsum dolor +

    + `, + ); + }); test('Should outdent if indented when the backspace key is pressed', async ({ page, @@ -2614,71 +2611,68 @@ test.describe.parallel('Nested List', () => { ); }); - test( - 'can navigate and check/uncheck with keyboard', - { - tag: '@flaky', - }, - async ({page, isCollab}) => { - await focusEditor(page); - await toggleCheckList(page); - // - // [ ] a - // [ ] b - // [ ] c - // [ ] d - // [ ] e - // [ ] f - await page.keyboard.type('a'); - await page.keyboard.press('Enter'); - await page.keyboard.type('b'); - await page.keyboard.press('Enter'); - await click(page, '.toolbar-item.alignment'); - await click(page, 'button:has-text("Indent")'); - await page.keyboard.type('c'); - await page.keyboard.press('Enter'); - await click(page, '.toolbar-item.alignment'); - await click(page, 'button:has-text("Indent")'); - await page.keyboard.type('d'); - await page.keyboard.press('Enter'); - await page.keyboard.type('e'); - await page.keyboard.press('Enter'); - await page.keyboard.press('Backspace'); - await page.keyboard.press('Backspace'); - await page.keyboard.type('f'); - - const assertCheckCount = async (checkCount, uncheckCount) => { - const pageOrFrame = await (isCollab ? page.frame('left') : page); - await expect( - pageOrFrame.locator('li[role="checkbox"][aria-checked="true"]'), - ).toHaveCount(checkCount); - await expect( - pageOrFrame.locator('li[role="checkbox"][aria-checked="false"]'), - ).toHaveCount(uncheckCount); - }; - - await assertCheckCount(0, 6); - - // Go back to select checkbox - await page.keyboard.press('ArrowLeft'); - await page.keyboard.press('ArrowLeft'); + test('can navigate and check/uncheck with keyboard', async ({ + page, + isCollab, + }) => { + await focusEditor(page); + await toggleCheckList(page); + // + // [ ] a + // [ ] b + // [ ] c + // [ ] d + // [ ] e + // [ ] f + await page.keyboard.type('a'); + await page.keyboard.press('Enter'); + await page.keyboard.type('b'); + await page.keyboard.press('Enter'); + await click(page, '.toolbar-item.alignment'); + await click(page, 'button:has-text("Indent")'); + await page.keyboard.type('c'); + await page.keyboard.press('Enter'); + await click(page, '.toolbar-item.alignment'); + await click(page, 'button:has-text("Indent")'); + await page.keyboard.type('d'); + await page.keyboard.press('Enter'); + await page.keyboard.type('e'); + await page.keyboard.press('Enter'); + await page.keyboard.press('Backspace'); + await page.keyboard.press('Backspace'); + await page.keyboard.type('f'); + + const assertCheckCount = async (checkCount, uncheckCount) => { + const pageOrFrame = await (isCollab ? page.frame('left') : page); + await expect( + pageOrFrame.locator('li[role="checkbox"][aria-checked="true"]'), + ).toHaveCount(checkCount); + await expect( + pageOrFrame.locator('li[role="checkbox"][aria-checked="false"]'), + ).toHaveCount(uncheckCount); + }; + + await assertCheckCount(0, 6); + + // Go back to select checkbox + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('Space'); + + await repeat(5, async () => { + await page.keyboard.press('ArrowUp', {delay: 50}); await page.keyboard.press('Space'); + }); - await repeat(5, async () => { - await page.keyboard.press('ArrowUp', {delay: 50}); - await page.keyboard.press('Space'); - }); - - await assertCheckCount(6, 0); + await assertCheckCount(6, 0); - await repeat(3, async () => { - await page.keyboard.press('ArrowDown', {delay: 50}); - await page.keyboard.press('Space'); - }); + await repeat(3, async () => { + await page.keyboard.press('ArrowDown', {delay: 50}); + await page.keyboard.press('Space'); + }); - await assertCheckCount(3, 3); - }, - ); + await assertCheckCount(3, 3); + }); test('replaces existing element node', async ({page}) => { // Create two quote blocks, select it and format to a list @@ -2736,56 +2730,52 @@ test.describe.parallel('Nested List', () => { }); }); - test( - 'remove list breaks when selection in empty nested list item 2', - { - tag: '@flaky', - }, - async ({page}) => { - await focusEditor(page); - await page.keyboard.type('Hello World'); - await page.keyboard.press('Enter'); - await page.keyboard.type('a'); - await toggleBulletList(page); - await page.keyboard.press('Enter'); - await page.keyboard.type('b'); - await page.keyboard.press('ArrowUp'); - await page.keyboard.press('Enter'); - await click(page, '.toolbar-item.alignment'); - await click(page, 'button:has-text("Indent")'); - await toggleBulletList(page); - await assertHTML( - page, - html` -

    - Hello World -

    -
      -
    • - a -
    • -
    -

    -
    -

    -
      -
    • - b -
    • -
    - `, - ); - await assertSelection(page, { - anchorOffset: 0, - anchorPath: [2], - focusOffset: 0, - focusPath: [2], - }); - }, - ); + test('remove list breaks when selection in empty nested list item 2', async ({ + page, + }) => { + await focusEditor(page); + await page.keyboard.type('Hello World'); + await page.keyboard.press('Enter'); + await page.keyboard.type('a'); + await toggleBulletList(page); + await page.keyboard.press('Enter'); + await page.keyboard.type('b'); + await page.keyboard.press('ArrowUp'); + await page.keyboard.press('Enter'); + await click(page, '.toolbar-item.alignment'); + await click(page, 'button:has-text("Indent")'); + await toggleBulletList(page); + await assertHTML( + page, + html` +

    + Hello World +

    +
      +
    • + a +
    • +
    +

    +
    +

    +
      +
    • + b +
    • +
    + `, + ); + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [2], + focusOffset: 0, + focusPath: [2], + }); + }); test('new list item should preserve format from previous list item even after new list item is indented', async ({ page, }) => { diff --git a/packages/lexical-playground/__tests__/e2e/Navigation.spec.mjs b/packages/lexical-playground/__tests__/e2e/Navigation.spec.mjs index d6fbe935310..5f9cf77d962 100644 --- a/packages/lexical-playground/__tests__/e2e/Navigation.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Navigation.spec.mjs @@ -508,62 +508,28 @@ test.describe('Keyboard Navigation', () => { // navigate through the text // 1 left await moveToPrevWord(page); - if (browserName === 'webkit') { - await assertSelection(page, { - anchorOffset: 7, - anchorPath: [0, 2, 0], - focusOffset: 7, - focusPath: [0, 2, 0], - }); - } else if (browserName === 'firefox') { - await assertSelection(page, { - anchorOffset: 7, - anchorPath: [0, 2, 0], - focusOffset: 7, - focusPath: [0, 2, 0], - }); - } else { - await assertSelection(page, { - anchorOffset: 7, - anchorPath: [0, 2, 0], - focusOffset: 7, - focusPath: [0, 2, 0], - }); - } + await assertSelection(page, { + anchorOffset: 7, + anchorPath: [0, 2, 0], + focusOffset: 7, + focusPath: [0, 2, 0], + }); // 2 left await moveToPrevWord(page); - if (browserName === 'firefox') { - await assertSelection(page, { - anchorOffset: 2, - anchorPath: [0, 2, 0], - focusOffset: 2, - focusPath: [0, 2, 0], - }); - } else { - await assertSelection(page, { - anchorOffset: 2, - anchorPath: [0, 2, 0], - focusOffset: 2, - focusPath: [0, 2, 0], - }); - } + await assertSelection(page, { + anchorOffset: 2, + anchorPath: [0, 2, 0], + focusOffset: 2, + focusPath: [0, 2, 0], + }); // 3 left await moveToPrevWord(page); - if (browserName === 'firefox') { - await assertSelection(page, { - anchorOffset: 6, - anchorPath: [0, 0, 0], - focusOffset: 6, - focusPath: [0, 0, 0], - }); - } else { - await assertSelection(page, { - anchorOffset: 6, - anchorPath: [0, 0, 0], - focusOffset: 6, - focusPath: [0, 0, 0], - }); - } + await assertSelection(page, { + anchorOffset: 6, + anchorPath: [0, 0, 0], + focusOffset: 6, + focusPath: [0, 0, 0], + }); // 4 left await moveToPrevWord(page); await assertSelection(page, { diff --git a/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs b/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs index 013e2792088..c33a8a963f7 100644 --- a/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs @@ -1305,10 +1305,8 @@ test.describe.parallel('Selection', () => { page, isPlainText, isCollab, - browserName, }) => { test.skip(isPlainText || isCollab); - test.skip(browserName === 'firefox'); await page.keyboard.type('קצת'); await insertDateTime(page); await moveLeft(page); diff --git a/packages/lexical-playground/__tests__/e2e/Tab.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tab.spec.mjs index d4c877a3b33..45dd4d7d584 100644 --- a/packages/lexical-playground/__tests__/e2e/Tab.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tab.spec.mjs @@ -20,78 +20,74 @@ import { /* eslint-disable sort-keys-fix/sort-keys-fix */ test.describe('Tab', () => { test.beforeEach(({isCollab, page}) => initialize({isCollab, page})); - test( - `can tab + IME`, - {tag: '@flaky'}, - async ({page, isPlainText, browserName}) => { - // CDP session is only available in Chromium - test.skip( - isPlainText || browserName === 'firefox' || browserName === 'webkit', - ); + test(`can tab + IME`, async ({page, isPlainText, browserName}) => { + // CDP session is only available in Chromium + test.skip( + isPlainText || browserName === 'firefox' || browserName === 'webkit', + ); - const client = await page.context().newCDPSession(page); - async function imeType() { - // await page.keyboard.imeSetComposition('s', 1, 1); - await client.send('Input.imeSetComposition', { - selectionStart: 1, - selectionEnd: 1, - text: 's', - }); - // await page.keyboard.imeSetComposition('す', 1, 1); - await client.send('Input.imeSetComposition', { - selectionStart: 1, - selectionEnd: 1, - text: 'す', - }); - // await page.keyboard.imeSetComposition('すs', 2, 2); - await client.send('Input.imeSetComposition', { - selectionStart: 2, - selectionEnd: 2, - text: 'すs', - }); - // await page.keyboard.imeSetComposition('すsh', 3, 3); - await client.send('Input.imeSetComposition', { - selectionStart: 3, - selectionEnd: 3, - text: 'すsh', - }); - // await page.keyboard.imeSetComposition('すし', 2, 2); - await client.send('Input.imeSetComposition', { - selectionStart: 2, - selectionEnd: 2, - text: 'すし', - }); - // await page.keyboard.insertText('すし'); - await client.send('Input.insertText', { - text: 'すし', - }); - await page.keyboard.type(' '); - } - await focusEditor(page); - await enableCompositionKeyEvents(page); - // Indent - await page.keyboard.press('Tab'); - await imeType(); - await page.keyboard.press('Tab'); - await imeType(); + const client = await page.context().newCDPSession(page); + async function imeType() { + // await page.keyboard.imeSetComposition('s', 1, 1); + await client.send('Input.imeSetComposition', { + selectionStart: 1, + selectionEnd: 1, + text: 's', + }); + // await page.keyboard.imeSetComposition('す', 1, 1); + await client.send('Input.imeSetComposition', { + selectionStart: 1, + selectionEnd: 1, + text: 'す', + }); + // await page.keyboard.imeSetComposition('すs', 2, 2); + await client.send('Input.imeSetComposition', { + selectionStart: 2, + selectionEnd: 2, + text: 'すs', + }); + // await page.keyboard.imeSetComposition('すsh', 3, 3); + await client.send('Input.imeSetComposition', { + selectionStart: 3, + selectionEnd: 3, + text: 'すsh', + }); + // await page.keyboard.imeSetComposition('すし', 2, 2); + await client.send('Input.imeSetComposition', { + selectionStart: 2, + selectionEnd: 2, + text: 'すし', + }); + // await page.keyboard.insertText('すし'); + await client.send('Input.insertText', { + text: 'すし', + }); + await page.keyboard.type(' '); + } + await focusEditor(page); + await enableCompositionKeyEvents(page); + // Indent + await page.keyboard.press('Tab'); + await imeType(); + await page.keyboard.press('Tab'); + await imeType(); - await assertHTML( - page, - html` -

    - すし - - すし -

    - `, - ); - }, - ); + await assertHTML( + page, + html` +

    + すし + + すし +

    + `, + ); + }); test('can tab inside code block #4399', async ({page, isPlainText}) => { test.skip(isPlainText); diff --git a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs index 06bd677c9c8..ee117c50c76 100644 --- a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs @@ -847,67 +847,65 @@ test.describe.parallel('Tables', () => { }); }); - test( - `Can select cells using Table selection`, - { - tag: '@flaky', - }, - async ({page, isPlainText, isCollab}) => { - test.skip(isPlainText); - await initialize({isCollab, page}); + test(`Can select cells using Table selection`, async ({ + page, + isPlainText, + isCollab, + }) => { + test.skip(isPlainText); + await initialize({isCollab, page}); - await focusEditor(page); - await insertTable(page, 2, 3); + await focusEditor(page); + await insertTable(page, 2, 3); - await fillTablePartiallyWithText(page); - await selectCellsFromTableCords( - page, - {x: 0, y: 0}, - {x: 1, y: 1}, - true, - false, - ); + await fillTablePartiallyWithText(page); + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 1, y: 1}, + true, + false, + ); - await assertHTML( - page, - html` -


    - - - - - - - - - - - - - - - - -
    -

    a

    -
    -

    bb

    -
    -

    cc

    -
    -

    d

    -
    -

    e

    -
    -

    f

    -
    -


    - `, - undefined, - {ignoreClasses: true}, - ); - }, - ); + await assertHTML( + page, + html` +


    + + + + + + + + + + + + + + + + +
    +

    a

    +
    +

    bb

    +
    +

    cc

    +
    +

    d

    +
    +

    e

    +
    +

    f

    +
    +


    + `, + undefined, + {ignoreClasses: true}, + ); + }); test(`Can select cells using Table selection via keyboard`, async ({ page, @@ -993,72 +991,70 @@ test.describe.parallel('Tables', () => { ); }); - test( - `Can style text using Table selection`, - { - tag: '@flaky', - }, - async ({page, isPlainText, isCollab}) => { - test.skip(isPlainText); - await initialize({isCollab, page}); + test(`Can style text using Table selection`, async ({ + page, + isPlainText, + isCollab, + }) => { + test.skip(isPlainText); + await initialize({isCollab, page}); - await focusEditor(page); - await insertTable(page, 2, 3); + await focusEditor(page); + await insertTable(page, 2, 3); - await fillTablePartiallyWithText(page); - await selectCellsFromTableCords( - page, - {x: 0, y: 0}, - {x: 1, y: 1}, - true, - false, - ); + await fillTablePartiallyWithText(page); + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 1, y: 1}, + true, + false, + ); - await clickSelectors(page, ['.bold', '.italic', '.underline']); + await clickSelectors(page, ['.bold', '.italic', '.underline']); - await selectFromAdditionalStylesDropdown(page, '.strikethrough'); + await selectFromAdditionalStylesDropdown(page, '.strikethrough'); - // Check that the character styles are applied. - await assertHTML( - page, - html` -


    - - - - - - - - - - - - - - - - -
    -

    a

    -
    -

    bb

    -
    -

    cc

    -
    -

    d

    -
    -

    e

    -
    -

    f

    -
    -


    - `, - undefined, - {ignoreClasses: true}, - ); - }, - ); + // Check that the character styles are applied. + await assertHTML( + page, + html` +


    + + + + + + + + + + + + + + + + +
    +

    a

    +
    +

    bb

    +
    +

    cc

    +
    +

    d

    +
    +

    e

    +
    +

    f

    +
    +


    + `, + undefined, + {ignoreClasses: true}, + ); + }); test(`Can style on empty table cells and paragraphs with no text`, async ({ page, @@ -1195,165 +1191,161 @@ test.describe.parallel('Tables', () => { ); }); - test( - `Can copy + paste (internal) using Table selection`, - { - tag: '@flaky', - }, - async ({page, isPlainText, isCollab}) => { - test.skip(isPlainText); - await initialize({isCollab, page}); + test(`Can copy + paste (internal) using Table selection`, async ({ + page, + isPlainText, + isCollab, + }) => { + test.skip(isPlainText); + await initialize({isCollab, page}); - await focusEditor(page); - await insertTable(page, 2, 3); + await focusEditor(page); + await insertTable(page, 2, 3); - await fillTablePartiallyWithText(page); - await selectCellsFromTableCords( - page, - {x: 0, y: 0}, - {x: 1, y: 1}, - true, - false, - ); + await fillTablePartiallyWithText(page); + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 1, y: 1}, + true, + false, + ); - await withExclusiveClipboardAccess(async () => { - const clipboard = await copyToClipboard(page); + await withExclusiveClipboardAccess(async () => { + const clipboard = await copyToClipboard(page); - // For some reason you need to click the paragraph twice for this to pass - // on Collab Firefox. - await click(page, 'div.ContentEditable__root > p:first-of-type'); - await click(page, 'div.ContentEditable__root > p:first-of-type'); + // For some reason you need to click the paragraph twice for this to pass + // on Collab Firefox. + await click(page, 'div.ContentEditable__root > p:first-of-type'); + await click(page, 'div.ContentEditable__root > p:first-of-type'); - await pasteFromClipboard(page, clipboard); - }); + await pasteFromClipboard(page, clipboard); + }); - // Check that the character styles are applied. - await assertHTML( - page, - html` - - - - - - - - - - - - - -
    -

    a

    -
    -

    bb

    -
    -

    d

    -
    -

    e

    -
    - - - - - - - - - - - - - - - - -
    -

    a

    -
    -

    bb

    -
    -

    cc

    -
    -

    d

    -
    -

    e

    -
    -

    f

    -
    -


    - `, - undefined, - {ignoreClasses: true}, - ); - }, - ); - - test( - `Can clear text using Table selection`, - { - tag: '@flaky', - }, - async ({page, isPlainText, isCollab}) => { - test.skip(isPlainText); - await initialize({isCollab, page}); + // Check that the character styles are applied. + await assertHTML( + page, + html` + + + + + + + + + + + + + +
    +

    a

    +
    +

    bb

    +
    +

    d

    +
    +

    e

    +
    + + + + + + + + + + + + + + + + +
    +

    a

    +
    +

    bb

    +
    +

    cc

    +
    +

    d

    +
    +

    e

    +
    +

    f

    +
    +


    + `, + undefined, + {ignoreClasses: true}, + ); + }); - await focusEditor(page); - await insertTable(page, 2, 3); + test(`Can clear text using Table selection`, async ({ + page, + isPlainText, + isCollab, + }) => { + test.skip(isPlainText); + await initialize({isCollab, page}); - await fillTablePartiallyWithText(page); - await selectCellsFromTableCords( - page, - {x: 0, y: 0}, - {x: 1, y: 1}, - true, - false, - ); + await focusEditor(page); + await insertTable(page, 2, 3); - await page.keyboard.press('Backspace'); + await fillTablePartiallyWithText(page); + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 1, y: 1}, + true, + false, + ); - // Check that the text was cleared. - await assertHTML( - page, - html` -


    - - - - - - - - - - - - - - - - -
    -


    -
    -


    -
    -

    cc

    -
    -


    -
    -


    -
    -

    f

    -
    -


    - `, - undefined, - {ignoreClasses: true}, - ); - }, - ); + await page.keyboard.press('Backspace'); + + // Check that the text was cleared. + await assertHTML( + page, + html` +


    + + + + + + + + + + + + + + + + +
    +


    +
    +


    +
    +

    cc

    +
    +


    +
    +


    +
    +

    f

    +
    +


    + `, + undefined, + {ignoreClasses: true}, + ); + }); test(`Range Selection is corrected when it contains a partial Table.`, async ({ page, @@ -1553,76 +1545,75 @@ test.describe.parallel('Tables', () => { ); }); - test( - 'Table selection: can select multiple cells and insert a decorator', - { - tag: '@flaky', - }, - async ({page, isPlainText, isCollab, browserName}) => { - test.skip(isPlainText); - await initialize({isCollab, page}); + test('Table selection: can select multiple cells and insert a decorator', async ({ + page, + isPlainText, + isCollab, + browserName, + }) => { + test.skip(isPlainText); + await initialize({isCollab, page}); - await focusEditor(page); + await focusEditor(page); - await insertTable(page, 2, 2); + await insertTable(page, 2, 2); - await click(page, '.PlaygroundEditorTheme__tableCell:first-child'); - await page.keyboard.type('Hello'); + await click(page, '.PlaygroundEditorTheme__tableCell:first-child'); + await page.keyboard.type('Hello'); - await page.keyboard.down('Shift'); - await page.keyboard.press('ArrowRight'); - await page.keyboard.press('ArrowDown'); - await page.keyboard.up('Shift'); + await page.keyboard.down('Shift'); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowDown'); + await page.keyboard.up('Shift'); - await insertDateTime(page); - await page.keyboard.type(' <- it works!'); + await insertDateTime(page); + await page.keyboard.type(' <- it works!'); - await assertHTML( - page, - html` -


    - - - - - - - - - - - - - -
    -

    - Hello -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    - ${getExpectedDateTimeHtml()} - <- it works! -

    -
    -


    - `, - ); - }, - ); + await assertHTML( + page, + html` +


    + + + + + + + + + + + + + +
    +

    + Hello +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    + ${getExpectedDateTimeHtml()} + <- it works! +

    +
    +


    + `, + ); + }); test('Table selection: can backspace lines, backspacing empty cell does not destroy it #3278', async ({ page, @@ -1910,99 +1901,97 @@ test.describe.parallel('Tables', () => { ); }); - test( - 'Resize merged cells width (2)', - { - tag: '@flaky', - }, - async ({page, isPlainText, isCollab}) => { - test.skip(isPlainText); - await initialize({isCollab, page}); + test('Resize merged cells width (2)', async ({ + page, + isPlainText, + isCollab, + }) => { + test.skip(isPlainText); + await initialize({isCollab, page}); - await focusEditor(page); + await focusEditor(page); - await insertTable(page, 3, 3); - await click(page, '.PlaygroundEditorTheme__tableCell'); - await selectCellsFromTableCords( - page, - {x: 0, y: 0}, - {x: 1, y: 1}, - true, - false, - ); - await mergeTableCells(page); - await click(page, 'th'); - const resizerBoundingBox = await selectorBoundingBox( - page, - '.TableCellResizer__resizer:first-child', - ); - const x = resizerBoundingBox.x + resizerBoundingBox.width / 2; - const y = resizerBoundingBox.y + resizerBoundingBox.height / 2; - await page.mouse.move(x, y); - await page.mouse.down(); - await page.mouse.move(x + 50, y); - await page.mouse.up(); + await insertTable(page, 3, 3); + await click(page, '.PlaygroundEditorTheme__tableCell'); + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 1, y: 1}, + true, + false, + ); + await mergeTableCells(page); + await click(page, 'th'); + const resizerBoundingBox = await selectorBoundingBox( + page, + '.TableCellResizer__resizer:first-child', + ); + const x = resizerBoundingBox.x + resizerBoundingBox.width / 2; + const y = resizerBoundingBox.y + resizerBoundingBox.height / 2; + await page.mouse.move(x, y); + await page.mouse.down(); + await page.mouse.move(x + 50, y); + await page.mouse.up(); - await assertHTML( - page, - html` -


    - - - - - - - - - - - - - - - - - - -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -


    - `, - ); - }, - ); + await assertHTML( + page, + html` +


    + + + + + + + + + + + + + + + + + + +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +


    + `, + ); + }); test('Resize merged cells height', async ({ browserName, @@ -2331,414 +2320,412 @@ test.describe.parallel('Tables', () => { ); }); - test( - 'Merge/unmerge with already merged cells', - { - tag: '@flaky', - }, - async ({page, isPlainText, isCollab}) => { - test.skip(isPlainText); - await initialize({isCollab, page}); + test('Merge/unmerge with already merged cells', async ({ + page, + isPlainText, + isCollab, + }) => { + test.skip(isPlainText); + await initialize({isCollab, page}); - await focusEditor(page); + await focusEditor(page); - // 1. Create a 5x5 table - await insertTable(page, 5, 5); + // 1. Create a 5x5 table + await insertTable(page, 5, 5); - // 2. Type the letters in specific cells - await click(page, '.PlaygroundEditorTheme__tableCell'); + // 2. Type the letters in specific cells + await click(page, '.PlaygroundEditorTheme__tableCell'); - // Move to cell for 'a' and type - await moveRight(page, 1); - await moveDown(page, 2); - await page.keyboard.type('a'); + // Move to cell for 'a' and type + await moveRight(page, 1); + await moveDown(page, 2); + await page.keyboard.type('a'); - // Move to cell for 'b' and type - await moveRight(page, 1); - await page.keyboard.type('b'); + // Move to cell for 'b' and type + await moveRight(page, 1); + await page.keyboard.type('b'); - // Move to cell for 'c' and type - await moveLeft(page, 2); - await moveDown(page, 1); - await page.keyboard.type('c'); + // Move to cell for 'c' and type + await moveLeft(page, 2); + await moveDown(page, 1); + await page.keyboard.type('c'); - // Move to cell for 'd' and type - await moveRight(page, 1); - await page.keyboard.type('d'); + // Move to cell for 'd' and type + await moveRight(page, 1); + await page.keyboard.type('d'); - // Move to cell for 'e' and type - await moveRight(page, 1); - await page.keyboard.type('e'); + // Move to cell for 'e' and type + await moveRight(page, 1); + await page.keyboard.type('e'); - // Move to cell for 'f' and type - await moveDown(page, 1); - await page.keyboard.type('f'); + // Move to cell for 'f' and type + await moveDown(page, 1); + await page.keyboard.type('f'); - // 3. First merge: Select and merge cells a,b,c,d - await selectCellsFromTableCords( - page, - {x: 1, y: 2}, - {x: 2, y: 3}, - false, - false, - ); - await mergeTableCells(page); - // 4. Second merge: Select and merge cells e,f - await selectCellsFromTableCords( - page, - {x: 1, y: 3}, - {x: 3, y: 4}, - false, - false, - ); - await mergeTableCells(page); + // 3. First merge: Select and merge cells a,b,c,d + await selectCellsFromTableCords( + page, + {x: 1, y: 2}, + {x: 2, y: 3}, + false, + false, + ); + await mergeTableCells(page); + // 4. Second merge: Select and merge cells e,f + await selectCellsFromTableCords( + page, + {x: 1, y: 3}, + {x: 3, y: 4}, + false, + false, + ); + await mergeTableCells(page); - // 5. Final merge: Non-rectangular selection attempt - await selectCellsFromTableCords( - page, - {x: 2, y: 4}, - {x: 1, y: 3}, - false, - false, - ); - await mergeTableCells(page); - // 6. Assert the final state - await assertHTML( - page, - html` -


    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    - a -

    -

    - b -

    -

    - c -

    -

    - d -

    -

    - e -

    -

    - f -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -


    - `, - ); + // 5. Final merge: Non-rectangular selection attempt + await selectCellsFromTableCords( + page, + {x: 2, y: 4}, + {x: 1, y: 3}, + false, + false, + ); + await mergeTableCells(page); + // 6. Assert the final state + await assertHTML( + page, + html` +


    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    + a +

    +

    + b +

    +

    + c +

    +

    + d +

    +

    + e +

    +

    + f +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +


    + `, + ); - await unmergeTableCell(page); - await assertHTML( - page, - html` -


    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    - a -

    -

    - b -

    -

    - c -

    -

    - d -

    -

    - e -

    -

    - f -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -


    - `, - ); - }, - ); + await unmergeTableCell(page); + await assertHTML( + page, + html` +


    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    + a +

    +

    + b +

    +

    + c +

    +

    + d +

    +

    + e +

    +

    + f +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +


    + `, + ); + }); test('Merged cell tab navigation forward', async ({ page, @@ -3084,42 +3071,202 @@ test.describe.parallel('Tables', () => { -


    - - -


    + dir="auto"> +


    + + +


    + + +


    + + + + +


    + + +

    + A +

    +

    + B +

    +

    + C +

    +

    + D +

    + + + + +


    + + + +


    + `, + ); + }); + + test('Select multiple merged cells (selection expands to a rectangle)', async ({ + page, + isPlainText, + isCollab, + }) => { + test.skip(isPlainText); + await initialize({isCollab, page}); + + await focusEditor(page); + + await insertTable(page, 3, 3); + + await click(page, '.PlaygroundEditorTheme__tableCell'); + await moveDown(page, 1); + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 0, y: 1}, + true, + true, + ); + await mergeTableCells(page); + + await moveRight(page, 1); + await selectCellsFromTableCords( + page, + {x: 1, y: 0}, + {x: 2, y: 0}, + true, + true, + ); + await mergeTableCells(page); + + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 1, y: 0}, + true, + true, + ); + + await assertHTML( + page, + html` +


    + + + + + + + + + + + + + + + + + + + +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +


    + `, + html` +


    + + + + + + + + - - + @@ -3127,8 +3274,20 @@ test.describe.parallel('Tables', () => { + +
    +

    +
    +

    -


    +

    +
    +

    -


    -
    -

    - A -

    -

    - B -

    +

    - C +

    +

    - D +

    -


    +

    +
    +

    +

    +
    +

    +
    +

    +
    +

    +


    @@ -3136,359 +3295,183 @@ test.describe.parallel('Tables', () => { ); }); - test( - 'Select multiple merged cells (selection expands to a rectangle)', - { - tag: '@flaky', - }, - async ({page, isPlainText, isCollab}) => { - test.skip(isPlainText); - await initialize({isCollab, page}); - - await focusEditor(page); - - await insertTable(page, 3, 3); - - await click(page, '.PlaygroundEditorTheme__tableCell'); - await moveDown(page, 1); - await selectCellsFromTableCords( - page, - {x: 0, y: 0}, - {x: 0, y: 1}, - true, - true, - ); - await mergeTableCells(page); - - await moveRight(page, 1); - await selectCellsFromTableCords( - page, - {x: 1, y: 0}, - {x: 2, y: 0}, - true, - true, - ); - await mergeTableCells(page); - - await selectCellsFromTableCords( - page, - {x: 0, y: 0}, - {x: 1, y: 0}, - true, - true, - ); - - await assertHTML( - page, - html` -


    - - - - - - - - - - - - - - - - - - - -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -


    - `, - html` -


    - - - - - - - - - - - - - - - - - - - -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -


    - `, - ); - }, - ); - - test( - 'Merge multiple merged cells and then unmerge', - { - tag: '@flaky', - }, - async ({page, isPlainText, isCollab}) => { - test.skip(isPlainText); - await initialize({isCollab, page}); - - await focusEditor(page); - - await insertTable(page, 3, 3); + test('Merge multiple merged cells and then unmerge', async ({ + page, + isPlainText, + isCollab, + }) => { + test.skip(isPlainText); + await initialize({isCollab, page}); - await click(page, '.PlaygroundEditorTheme__tableCell'); - await moveDown(page, 1); - await selectCellsFromTableCords( - page, - {x: 0, y: 0}, - {x: 0, y: 1}, - true, - true, - ); - await mergeTableCells(page); + await focusEditor(page); - await moveRight(page, 1); - await selectCellsFromTableCords( - page, - {x: 1, y: 0}, - {x: 2, y: 0}, - true, - true, - ); - await mergeTableCells(page); + await insertTable(page, 3, 3); - await selectCellsFromTableCords( - page, - {x: 0, y: 0}, - {x: 1, y: 0}, - true, - true, - ); - await mergeTableCells(page); + await click(page, '.PlaygroundEditorTheme__tableCell'); + await moveDown(page, 1); + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 0, y: 1}, + true, + true, + ); + await mergeTableCells(page); - await assertHTML( - page, - html` -


    - - - - - - - - - -
    - - - - - -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -


    - `, - ); + await moveRight(page, 1); + await selectCellsFromTableCords( + page, + {x: 1, y: 0}, + {x: 2, y: 0}, + true, + true, + ); + await mergeTableCells(page); - await selectCellsFromTableCords( - page, - {x: 0, y: 0}, - {x: 0, y: 0}, - true, - true, - ); - await unmergeTableCell(page); + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 1, y: 0}, + true, + true, + ); + await mergeTableCells(page); - await assertHTML( - page, - html` -


    - - - - - - - - - - - - - - - - - - - - - -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -


    - `, - ); - }, - ); + await assertHTML( + page, + html` +


    + + + + + + + + + +
    + + + + + +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +


    + `, + ); + + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 0, y: 0}, + true, + true, + ); + await unmergeTableCell(page); + + await assertHTML( + page, + html` +


    + + + + + + + + + + + + + + + + + + + + + +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +

    +
    +


    + `, + ); + }); test('Insert row above (with conflicting merged cell)', async ({ page, @@ -3730,24 +3713,93 @@ test.describe.parallel('Tables', () => { -


    +


    + + + + +


    + + +


    + + +


    + + +


    + + + +


    + `, + ); + }); + + test('Delete rows (with conflicting merged cell)', async ({ + page, + isPlainText, + isCollab, + }) => { + test.skip(isPlainText); + await initialize({isCollab, page}); + + await focusEditor(page); + + await insertTable(page, 4, 2); + + await selectCellsFromTableCords( + page, + {x: 1, y: 1}, + {x: 1, y: 3}, + false, + false, + ); + await mergeTableCells(page); + + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 0, y: 1}, + true, + true, + ); + + await deleteTableRows(page); + + await assertHTML( + page, + html` +


    + + + + + + + + - - -
    +

    +
    +

    +

    +
    +

    +
    -


    -
    -


    -
    -


    +

    +
    +

    -


    -


    @@ -3755,80 +3807,6 @@ test.describe.parallel('Tables', () => { ); }); - test( - 'Delete rows (with conflicting merged cell)', - { - tag: '@flaky', - }, - async ({page, isPlainText, isCollab}) => { - test.skip(isPlainText); - await initialize({isCollab, page}); - - await focusEditor(page); - - await insertTable(page, 4, 2); - - await selectCellsFromTableCords( - page, - {x: 1, y: 1}, - {x: 1, y: 3}, - false, - false, - ); - await mergeTableCells(page); - - await selectCellsFromTableCords( - page, - {x: 0, y: 0}, - {x: 0, y: 1}, - true, - true, - ); - - await deleteTableRows(page); - - await assertHTML( - page, - html` -


    - - - - - - - - - - - - -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -

    -
    -


    - `, - ); - }, - ); - test('Delete selected rows (with merged cell overflowing selection from top and bottom)', async ({ page, isPlainText, @@ -4487,61 +4465,55 @@ test.describe.parallel('Tables', () => { ); }); - test( - 'Delete columns backward', - { - tag: '@flaky', - }, - async ({page, isPlainText, isCollab}) => { - test.skip(isPlainText); - await initialize({isCollab, page}); + test('Delete columns backward', async ({page, isPlainText, isCollab}) => { + test.skip(isPlainText); + await initialize({isCollab, page}); - await focusEditor(page); + await focusEditor(page); - await insertTable(page, 2, 4); + await insertTable(page, 2, 4); - await selectCellsFromTableCords( - page, - {x: 3, y: 1}, - {x: 1, y: 1}, - false, - false, - ); + await selectCellsFromTableCords( + page, + {x: 3, y: 1}, + {x: 1, y: 1}, + false, + false, + ); - await deleteTableColumns(page); + await deleteTableColumns(page); - await assertHTML( - page, - html` -


    - - - - - - - - - - -
    -

    -
    -

    -
    -

    -
    -

    -
    -


    - `, - ); - }, - ); + await assertHTML( + page, + html` +


    + + + + + + + + + + +
    +

    +
    +

    +
    +

    +
    +

    +
    +


    + `, + ); + }); test('Delete columns forward at end of table', async ({ page, @@ -4894,89 +4866,87 @@ test.describe.parallel('Tables', () => { ); }); - test( - 'Can align text using Table selection', - { - tag: '@flaky', - }, - async ({page, isPlainText, isCollab}) => { - test.skip(isPlainText); - await initialize({isCollab, page}); + test('Can align text using Table selection', async ({ + page, + isPlainText, + isCollab, + }) => { + test.skip(isPlainText); + await initialize({isCollab, page}); - await focusEditor(page); - await insertTable(page, 2, 3); + await focusEditor(page); + await insertTable(page, 2, 3); - await fillTablePartiallyWithText(page); - await selectCellsFromTableCords( - page, - {x: 0, y: 0}, - {x: 1, y: 1}, - true, - false, - ); + await fillTablePartiallyWithText(page); + await selectCellsFromTableCords( + page, + {x: 0, y: 0}, + {x: 1, y: 1}, + true, + false, + ); - await selectFromAlignDropdown(page, '.center-align'); + await selectFromAlignDropdown(page, '.center-align'); - await assertHTML( - page, - html` -


    - - - - - - - - - - - - - - - - -
    -

    - a -

    -
    -

    - bb -

    -
    -

    cc

    -
    -

    - d -

    -
    -

    - e -

    -
    -

    f

    -
    -


    - `, - undefined, - {ignoreClasses: true}, - ); - }, - ); + await assertHTML( + page, + html` +


    + + + + + + + + + + + + + + + + +
    +

    + a +

    +
    +

    + bb +

    +
    +

    cc

    +
    +

    + d +

    +
    +

    + e +

    +
    +

    f

    +
    +


    + `, + undefined, + {ignoreClasses: true}, + ); + }); test('Paste and insert new lines after unmerging cells', async ({ page, diff --git a/packages/lexical-playground/__tests__/e2e/TextFormatting.spec.mjs b/packages/lexical-playground/__tests__/e2e/TextFormatting.spec.mjs index 961f2bfbfb4..eaa24fdd5a7 100644 --- a/packages/lexical-playground/__tests__/e2e/TextFormatting.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/TextFormatting.spec.mjs @@ -1179,63 +1179,63 @@ test.describe.parallel('TextFormatting', () => { expect(isButtonActiveStatusDisplayedCorrectly).toBe(true); }); - test( - 'Regression #2523: can toggle format when selecting a TextNode edge followed by a non TextNode; ', - {tag: '@flaky'}, - async ({page, isCollab, isPlainText}) => { - test.skip(isPlainText); - await focusEditor(page); + test('Regression #2523: can toggle format when selecting a TextNode edge followed by a non TextNode; ', async ({ + page, + isCollab, + isPlainText, + }) => { + test.skip(isPlainText); + await focusEditor(page); - await page.keyboard.type('A'); - await insertDateTime(page); - await page.keyboard.type('BC'); + await page.keyboard.type('A'); + await insertDateTime(page); + await page.keyboard.type('BC'); - await moveLeft(page, 1); - await selectCharacters(page, 'left', 2); + await moveLeft(page, 1); + await selectCharacters(page, 'left', 2); - if (!isCollab) { - await assertHTML( - page, - html` -

    - A - ${getExpectedDateTimeHtml({selected: true})} - BC -

    - `, - ); - } - await toggleBold(page); - await assertHTML( - page, - html` -

    - A - ${getExpectedDateTimeHtml({formats: ['bold']})} - - B - - C -

    - `, - ); - await toggleBold(page); + if (!isCollab) { await assertHTML( page, - // After formatting the text, the selection will be reset from the decorator node, - // so it will retain its previous format when toggleBold is triggered again html`

    A - ${getExpectedDateTimeHtml({formats: ['bold']})} + ${getExpectedDateTimeHtml({selected: true})} BC

    `, ); - }, - ); + } + await toggleBold(page); + await assertHTML( + page, + html` +

    + A + ${getExpectedDateTimeHtml({formats: ['bold']})} + + B + + C +

    + `, + ); + await toggleBold(page); + await assertHTML( + page, + // After formatting the text, the selection will be reset from the decorator node, + // so it will retain its previous format when toggleBold is triggered again + html` +

    + A + ${getExpectedDateTimeHtml({formats: ['bold']})} + BC +

    + `, + ); + }); test('Multiline selection format ignores new lines', async ({ page, diff --git a/packages/lexical-playground/__tests__/e2e/Toolbar.spec.mjs b/packages/lexical-playground/__tests__/e2e/Toolbar.spec.mjs index 8470dbc4711..2ce6302f597 100644 --- a/packages/lexical-playground/__tests__/e2e/Toolbar.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Toolbar.spec.mjs @@ -28,6 +28,7 @@ import { selectFromAlignDropdown, selectFromInsertDropdown, test, + waitForSelector, } from '../utils/index.mjs'; test.describe('Toolbar', () => { @@ -40,233 +41,232 @@ test.describe('Toolbar', () => { }), ); - test( - 'Insert image caption + table', - { - tag: '@flaky', - }, - async ({page, isPlainText}) => { - // TODO(collab-v2): nested editors are not supported yet - test.skip(isPlainText || IS_COLLAB_V2); - await focusEditor(page); + test('Insert image caption + table', async ({page, isPlainText}) => { + // TODO(collab-v2): nested editors are not supported yet + test.skip(isPlainText || IS_COLLAB_V2); + await focusEditor(page); - // Add caption - await insertSampleImage(page); - // Catch flakiness earlier - await assertHTML( - page, - html` -

    - -

    - Yellow flower in tilt shift lens -
    - -
    -

    - `, - undefined, - { - ignoreClasses: true, - ignoreInlineStyles: true, - }, - ); - await click(page, '.editor-image img'); - await click(page, '.image-caption-button'); - await focus(page, '.ImageNode__contentEditable'); - await page.keyboard.type('Yellow flower in tilt shift lens'); - await assertHTML( - page, - html` -

    - -

    - Yellow flower in tilt shift lens -
    -
    -
    -

    - - Yellow flower in tilt shift lens - -

    -
    + // Add caption + await insertSampleImage(page); + // The image is rendered behind React.Suspense (fallback={null}) and only + // appears once the sample asset finishes loading; under parallel load + // that can exceed assertHTML's 5s retry window, leaving an empty + // decorator. Wait for the explicitly before asserting. + await waitForSelector(page, '.editor-image img', {timeout: 30000}); + // Catch flakiness earlier + await assertHTML( + page, + html` +

    + +

    + Yellow flower in tilt shift lens +
    + +
    +

    + `, + undefined, + { + ignoreClasses: true, + ignoreInlineStyles: true, + }, + ); + await click(page, '.editor-image img'); + await click(page, '.image-caption-button'); + await focus(page, '.ImageNode__contentEditable'); + await page.keyboard.type('Yellow flower in tilt shift lens'); + await assertHTML( + page, + html` +

    + +

    + Yellow flower in tilt shift lens +
    +
    +
    +

    + + Yellow flower in tilt shift lens + +

    - -
    -

    - `, - undefined, - { - ignoreClasses: true, - ignoreInlineStyles: true, - }, - actualHtml => - // flaky fix: remove the extra


    that appears occasionally in CI runs - actualHtml.replace( - html` -

    - - Yellow flower in tilt shift lens - -

    -


    - `, - html` -

    - - Yellow flower in tilt shift lens - -

    - `, - ), - ); +
    + +
    +

    + `, + undefined, + { + ignoreClasses: true, + ignoreInlineStyles: true, + }, + actualHtml => + // flaky fix: remove the extra


    that appears occasionally in CI runs + actualHtml.replace( + html` +

    + + Yellow flower in tilt shift lens + +

    +


    + `, + html` +

    + + Yellow flower in tilt shift lens + +

    + `, + ), + ); - // Delete image - // TODO Revisit the a11y side of NestedEditors - await evaluate(page, () => { - const p = document.querySelector('[contenteditable="true"] p'); - document.getSelection().setBaseAndExtent(p, 0, p, 0); - }); - await selectAll(page); - await page.keyboard.press('Delete'); - await assertHTML( - page, - html` -


    - `, - undefined, - { - ignoreClasses: true, - ignoreInlineStyles: true, - }, - ); + // Delete image + // TODO Revisit the a11y side of NestedEditors + await evaluate(page, () => { + const p = document.querySelector('[contenteditable="true"] p'); + document.getSelection().setBaseAndExtent(p, 0, p, 0); + }); + await selectAll(page); + await page.keyboard.press('Delete'); + await assertHTML( + page, + html` +


    + `, + undefined, + { + ignoreClasses: true, + ignoreInlineStyles: true, + }, + ); - // Add table - await selectFromInsertDropdown(page, '.table'); - await click(page, '[data-test-id="table-model-confirm-insert"] button'); + // Add table + await selectFromInsertDropdown(page, '.table'); + await click(page, '[data-test-id="table-model-confirm-insert"] button'); - await assertHTML( - page, - html` -

    -
    -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -


    -
    -


    -
    -


    -
    -


    -
    -


    -
    -


    -
    -


    -
    -


    -
    -


    -
    -


    -
    -


    -
    -


    -
    -


    -
    -


    -
    -


    -
    -


    -
    -


    -
    -


    -
    -


    -
    -


    -
    -


    -
    -


    -
    -


    -
    -


    -
    -


    -
    -


    - `, - undefined, - { - ignoreClasses: true, - ignoreInlineStyles: true, - }, - ); - }, - ); + await assertHTML( + page, + html` +

    +
    +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +


    +
    +


    +
    +


    +
    +


    +
    +


    +
    +


    +
    +


    +
    +


    +
    +


    +
    +


    +
    +


    +
    +


    +
    +


    +
    +


    +
    +


    +
    +


    +
    +


    +
    +


    +
    +


    +
    +


    +
    +


    +
    +


    +
    +


    +
    +


    +
    +


    +
    +


    + `, + undefined, + { + ignoreClasses: true, + ignoreInlineStyles: true, + }, + ); + }); test('Center align image', async ({page, isPlainText, isCollab}) => { // Image selection can't be synced in collab diff --git a/packages/lexical-playground/__tests__/regression/429-swapping-emoji.spec.mjs b/packages/lexical-playground/__tests__/regression/429-swapping-emoji.spec.mjs index 1efb6b4c916..063cea3efb9 100644 --- a/packages/lexical-playground/__tests__/regression/429-swapping-emoji.spec.mjs +++ b/packages/lexical-playground/__tests__/regression/429-swapping-emoji.spec.mjs @@ -18,15 +18,40 @@ import { test.describe('Regression test #429', () => { test.beforeEach(({isCollab, page}) => initialize({isCollab, page})); - test( - `Can add new lines before the line with emoji`, - {tag: '@flaky'}, - async ({isRichText, page}) => { - await focusEditor(page); - await page.keyboard.type(':) or :('); + test(`Can add new lines before the line with emoji`, async ({ + isRichText, + page, + }) => { + await focusEditor(page); + await page.keyboard.type(':) or :('); + await assertHTML( + page, + html` +

    + + 🙂 + + or + + 🙁 + +

    + `, + ); + await assertSelection(page, { + anchorOffset: 2, + anchorPath: [0, 2, 0, 0], + focusOffset: 2, + focusPath: [0, 2, 0, 0], + }); + + await moveLeft(page, 6); + await page.keyboard.press('Enter'); + if (isRichText) { await assertHTML( page, html` +


    🙂 @@ -39,65 +64,17 @@ test.describe('Regression test #429', () => { `, ); await assertSelection(page, { - anchorOffset: 2, - anchorPath: [0, 2, 0, 0], - focusOffset: 2, - focusPath: [0, 2, 0, 0], + anchorOffset: 0, + anchorPath: [1, 0, 0, 0], + focusOffset: 0, + focusPath: [1, 0, 0, 0], }); - - await moveLeft(page, 6); - await page.keyboard.press('Enter'); - if (isRichText) { - await assertHTML( - page, - html` -


    -

    - - 🙂 - - or - - 🙁 - -

    - `, - ); - await assertSelection(page, { - anchorOffset: 0, - anchorPath: [1, 0, 0, 0], - focusOffset: 0, - focusPath: [1, 0, 0, 0], - }); - } else { - await assertHTML( - page, - html` -

    -
    - - 🙂 - - or - - 🙁 - -

    - `, - ); - await assertSelection(page, { - anchorOffset: 0, - anchorPath: [0, 1, 0, 0], - focusOffset: 0, - focusPath: [0, 1, 0, 0], - }); - } - - await page.keyboard.press('Backspace'); + } else { await assertHTML( page, html`

    +
    🙂 @@ -110,10 +87,32 @@ test.describe('Regression test #429', () => { ); await assertSelection(page, { anchorOffset: 0, - anchorPath: [0, 0, 0, 0], + anchorPath: [0, 1, 0, 0], focusOffset: 0, - focusPath: [0, 0, 0, 0], + focusPath: [0, 1, 0, 0], }); - }, - ); + } + + await page.keyboard.press('Backspace'); + await assertHTML( + page, + html` +

    + + 🙂 + + or + + 🙁 + +

    + `, + ); + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [0, 0, 0, 0], + focusOffset: 0, + focusPath: [0, 0, 0, 0], + }); + }); }); diff --git a/packages/lexical-playground/__tests__/regression/4697-repeated-table-selection.spec.mjs b/packages/lexical-playground/__tests__/regression/4697-repeated-table-selection.spec.mjs index 00c34ff7c27..3f2db3e13d5 100644 --- a/packages/lexical-playground/__tests__/regression/4697-repeated-table-selection.spec.mjs +++ b/packages/lexical-playground/__tests__/regression/4697-repeated-table-selection.spec.mjs @@ -18,41 +18,39 @@ import { test.describe('Regression test #4697', () => { test.beforeEach(({isCollab, page}) => initialize({isCollab, page})); - test( - 'repeated table selection results in table selection', - { - tag: '@flaky', - }, - async ({page, isPlainText, isCollab}) => { - test.skip(isPlainText); + test('repeated table selection results in table selection', async ({ + page, + isPlainText, + isCollab, + }) => { + test.skip(isPlainText); - await focusEditor(page); + await focusEditor(page); - await insertTable(page, 4, 4); + await insertTable(page, 4, 4); - await click(page, '.PlaygroundEditorTheme__tableCell'); - await selectCellsFromTableCords( - page, - {x: 1, y: 1}, - {x: 2, y: 2}, - false, - false, - ); - await page.pause(); + await click(page, '.PlaygroundEditorTheme__tableCell'); + await selectCellsFromTableCords( + page, + {x: 1, y: 1}, + {x: 2, y: 2}, + false, + false, + ); + await page.pause(); - await selectCellsFromTableCords( - page, - {x: 2, y: 1}, - {x: 2, y: 2}, - false, - false, - ); - await page.pause(); + await selectCellsFromTableCords( + page, + {x: 2, y: 1}, + {x: 2, y: 2}, + false, + false, + ); + await page.pause(); - await assertTableSelectionCoordinates(page, { - anchor: {x: 2, y: 1}, - focus: {x: 2, y: 2}, - }); - }, - ); + await assertTableSelectionCoordinates(page, { + anchor: {x: 2, y: 1}, + focus: {x: 2, y: 2}, + }); + }); }); diff --git a/packages/lexical-playground/__tests__/regression/4872-full-row-span-cell-merge.spec.mjs b/packages/lexical-playground/__tests__/regression/4872-full-row-span-cell-merge.spec.mjs index 7463fbe4ae6..b02d4b59659 100644 --- a/packages/lexical-playground/__tests__/regression/4872-full-row-span-cell-merge.spec.mjs +++ b/packages/lexical-playground/__tests__/regression/4872-full-row-span-cell-merge.spec.mjs @@ -18,41 +18,39 @@ import { test.describe('Regression test #4872', () => { test.beforeEach(({isCollab, page}) => initialize({isCollab, page})); - test( - 'merging two full rows does not break table selection', - { - tag: '@flaky', - }, - async ({page, isPlainText, isCollab}) => { - test.skip(isPlainText); + test('merging two full rows does not break table selection', async ({ + page, + isPlainText, + isCollab, + }) => { + test.skip(isPlainText); - await focusEditor(page); + await focusEditor(page); - await insertTable(page, 5, 5); + await insertTable(page, 5, 5); - await click(page, '.PlaygroundEditorTheme__tableCell'); - await selectCellsFromTableCords( - page, - {x: 0, y: 1}, - {x: 4, y: 2}, - true, - false, - ); + await click(page, '.PlaygroundEditorTheme__tableCell'); + await selectCellsFromTableCords( + page, + {x: 0, y: 1}, + {x: 4, y: 2}, + true, + false, + ); - await mergeTableCells(page); + await mergeTableCells(page); - await selectCellsFromTableCords( - page, - {x: 1, y: 4}, - {x: 2, y: 4}, - false, - false, - ); + await selectCellsFromTableCords( + page, + {x: 1, y: 4}, + {x: 2, y: 4}, + false, + false, + ); - await assertTableSelectionCoordinates(page, { - anchor: {x: 1, y: 4}, - focus: {x: 2, y: 4}, - }); - }, - ); + await assertTableSelectionCoordinates(page, { + anchor: {x: 1, y: 4}, + focus: {x: 2, y: 4}, + }); + }); }); diff --git a/packages/lexical-playground/__tests__/utils/index.mjs b/packages/lexical-playground/__tests__/utils/index.mjs index 8f63d20b5f8..4e877b27e2f 100644 --- a/packages/lexical-playground/__tests__/utils/index.mjs +++ b/packages/lexical-playground/__tests__/utils/index.mjs @@ -148,17 +148,36 @@ export async function initialize({ */ async function exposeLexicalEditor(page) { if (IS_COLLAB) { - await Promise.all( - ['left', 'right'].map(async name => { - const frameLocator = page.frameLocator(`[name="${name}"]`); - await expect( - frameLocator.locator('.action-button.connect'), - ).toHaveAttribute('title', /Disconnect/); - await expect( - frameLocator.locator('[data-lexical-editor="true"] p'), - ).toBeVisible(); - }), - ); + // The split view loads the playground in two iframes that connect to a + // single shared y-websocket server. Under parallel test load one frame + // occasionally fails to boot/activate collab within the timeout (its + // ".action-button.connect" toolbar button never appears, or the websocket + // connect/backoff runs long) -- the dominant residual source of @flaky + // collab failures. Reload and retry a few times so a transient + // boot/connect hiccup during setup doesn't fail the whole test. + const waitForCollabFramesReady = () => + Promise.all( + ['left', 'right'].map(async name => { + const frameLocator = page.frameLocator(`[name="${name}"]`); + await expect( + frameLocator.locator('.action-button.connect'), + ).toHaveAttribute('title', /Disconnect/, {timeout: 15000}); + await expect( + frameLocator.locator('[data-lexical-editor="true"] p'), + ).toBeVisible({timeout: 15000}); + }), + ); + for (let attempt = 0; ; attempt++) { + try { + await waitForCollabFramesReady(); + break; + } catch (err) { + if (attempt >= 2) { + throw err; + } + await page.reload(); + } + } // Ensure that they started up with the correct empty state await assertHTML( page, From 316ac77fcd258d7433f09b08f8eef07728fc6f5d Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Fri, 29 May 2026 07:34:30 -0700 Subject: [PATCH 2/3] [lexical][lexical-code-core][lexical-code-prism][lexical-code-shiki] Chore: Consolidate text tokenization through tokenizeRawText / $generateNodesFromRawText (#8579) Co-authored-by: Claude --- .../src/FlatStructureUtils.ts | 19 ++++---------- .../lexical-code-prism/src/FacadePrism.ts | 19 +++++--------- .../lexical-code-shiki/src/FacadeShiki.ts | 25 +++++++++++-------- packages/lexical/src/nodes/LexicalTextNode.ts | 18 ++----------- 4 files changed, 27 insertions(+), 54 deletions(-) diff --git a/packages/lexical-code-core/src/FlatStructureUtils.ts b/packages/lexical-code-core/src/FlatStructureUtils.ts index 447373fa51a..30543567433 100644 --- a/packages/lexical-code-core/src/FlatStructureUtils.ts +++ b/packages/lexical-code-core/src/FlatStructureUtils.ts @@ -25,6 +25,7 @@ import { $isLineBreakNode, $isTabNode, getTextDirection, + tokenizeRawText, } from 'lexical'; import { @@ -237,20 +238,10 @@ export function $getEndOfCodeInLine( */ export function $plainifyCodeContent(text: string): LexicalNode[] { const out: LexicalNode[] = []; - const lines = text.split('\n'); - lines.forEach((line, lineIdx) => { - if (lineIdx > 0) { - out.push($createLineBreakNode()); - } - const tabParts = line.split('\t'); - tabParts.forEach((part, partIdx) => { - if (partIdx > 0) { - out.push($createTabNode()); - } - if (part.length > 0) { - out.push($createCodeHighlightNode(part)); - } - }); + tokenizeRawText(text, { + linebreak: () => out.push($createLineBreakNode()), + tab: () => out.push($createTabNode()), + text: part => out.push($createCodeHighlightNode(part)), }); return out; } diff --git a/packages/lexical-code-prism/src/FacadePrism.ts b/packages/lexical-code-prism/src/FacadePrism.ts index 291d14b53b1..a8a6e858bb9 100644 --- a/packages/lexical-code-prism/src/FacadePrism.ts +++ b/packages/lexical-code-prism/src/FacadePrism.ts @@ -29,7 +29,7 @@ import 'prismjs/components/prism-java'; import 'prismjs/components/prism-cpp'; import {$createCodeHighlightNode} from '@lexical/code-core'; -import {$createLineBreakNode, $createTabNode} from 'lexical'; +import {$createLineBreakNode, $createTabNode, tokenizeRawText} from 'lexical'; declare global { interface Window { @@ -272,18 +272,11 @@ function $mapTokensToLexicalStructure( for (const token of tokens) { if (typeof token === 'string') { - const partials = token.split(/(\n|\t)/); - const partialsLength = partials.length; - for (let i = 0; i < partialsLength; i++) { - const part = partials[i]; - if (part === '\n' || part === '\r\n') { - nodes.push($createLineBreakNode()); - } else if (part === '\t') { - nodes.push($createTabNode()); - } else if (part.length > 0) { - nodes.push($createCodeHighlightNode(part, type)); - } - } + tokenizeRawText(token, { + linebreak: () => nodes.push($createLineBreakNode()), + tab: () => nodes.push($createTabNode()), + text: part => nodes.push($createCodeHighlightNode(part, type)), + }); } else { const {content, alias} = token; if (typeof content === 'string') { diff --git a/packages/lexical-code-shiki/src/FacadeShiki.ts b/packages/lexical-code-shiki/src/FacadeShiki.ts index 37e9ac3a040..da96ebdbfd3 100644 --- a/packages/lexical-code-shiki/src/FacadeShiki.ts +++ b/packages/lexical-code-shiki/src/FacadeShiki.ts @@ -19,7 +19,12 @@ import { stringifyTokenStyle, } from '@shikijs/core'; import {createJavaScriptRegexEngine} from '@shikijs/engine-javascript'; -import {$createLineBreakNode, $createTabNode, $getNodeByKey} from 'lexical'; +import { + $createLineBreakNode, + $createTabNode, + $getNodeByKey, + tokenizeRawText, +} from 'lexical'; import {bundledLanguagesInfo} from 'shiki/langs'; import {bundledThemesInfo} from 'shiki/themes'; @@ -195,19 +200,17 @@ function mapTokensToLexicalStructure( } } - const parts = text.split('\t'); - parts.forEach((part: string, pidx: number) => { - if (pidx) { - nodes.push($createTabNode()); - } - if (part !== '') { + const style = stringifyTokenStyle( + token.htmlStyle || getTokenStyleObject(token), + ); + tokenizeRawText(text, { + linebreak: () => nodes.push($createLineBreakNode()), + tab: () => nodes.push($createTabNode()), + text: (part: string) => { const node = $createCodeHighlightNode(part); - const style = stringifyTokenStyle( - token.htmlStyle || getTokenStyleObject(token), - ); node.setStyle(style); nodes.push(node); - } + }, }); }); }); diff --git a/packages/lexical/src/nodes/LexicalTextNode.ts b/packages/lexical/src/nodes/LexicalTextNode.ts index 2ee0c44e5e8..0561bc1f39b 100644 --- a/packages/lexical/src/nodes/LexicalTextNode.ts +++ b/packages/lexical/src/nodes/LexicalTextNode.ts @@ -53,6 +53,7 @@ import { import {LexicalNode} from '../LexicalNode'; import {$cloneNodeState} from '../LexicalNodeState'; import { + $generateNodesFromRawText, $getSelection, $internalMakeRangeSelection, $isRangeSelection, @@ -74,8 +75,6 @@ import { toggleTextFormatType, } from '../LexicalUtils'; import {setDOMStyleFromCSS} from '../utils/setDOMStyle'; -import {$createLineBreakNode} from './LexicalLineBreakNode'; -import {$createTabNode} from './LexicalTabNode'; export type SerializedTextNode = Spread< { @@ -1262,20 +1261,7 @@ function $convertTextDOMNode(domNode: Node): DOMConversionOutput { let textContent = domNode_.textContent || ''; // No collapse and preserve segment break for pre, pre-wrap and pre-line if (findParentPreDOMNode(domNode_) !== null) { - const parts = textContent.split(/(\r?\n|\t)/); - const nodes: Array = []; - const length = parts.length; - for (let i = 0; i < length; i++) { - const part = parts[i]; - if (part === '\n' || part === '\r\n') { - nodes.push($createLineBreakNode()); - } else if (part === '\t') { - nodes.push($createTabNode()); - } else if (part !== '') { - nodes.push($createTextNode(part)); - } - } - return {node: nodes}; + return {node: $generateNodesFromRawText(textContent)}; } textContent = textContent.replace(/\r/g, '').replace(/[ \t\n]+/g, ' '); if (textContent === '') { From 111a0d8f501fbe570606d9aa5e39540f6c52f84e Mon Sep 17 00:00:00 2001 From: Sherry Date: Fri, 29 May 2026 22:52:41 +0800 Subject: [PATCH 3/3] fix(lexical-html): $generateHtmlFromNodes should self-establish active-editor scope (back-compat for #8519) (#8589) Co-authored-by: Bob Ippolito --- .../unit/LexicalHtmlBackwardCompat.test.ts | 60 +++++++++++++++++++ packages/lexical-html/src/index.ts | 5 ++ packages/lexical/src/LexicalUpdates.ts | 12 ++++ packages/lexical/src/index.ts | 1 + 4 files changed, 78 insertions(+) create mode 100644 packages/lexical-html/src/__tests__/unit/LexicalHtmlBackwardCompat.test.ts diff --git a/packages/lexical-html/src/__tests__/unit/LexicalHtmlBackwardCompat.test.ts b/packages/lexical-html/src/__tests__/unit/LexicalHtmlBackwardCompat.test.ts new file mode 100644 index 00000000000..b93e528b166 --- /dev/null +++ b/packages/lexical-html/src/__tests__/unit/LexicalHtmlBackwardCompat.test.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import {createHeadlessEditor} from '@lexical/headless'; +import {$generateHtmlFromNodes} from '@lexical/html'; +import {$createParagraphNode, $createTextNode, $getRoot} from 'lexical'; +import {describe, expect, test} from 'vitest'; + +describe('$generateHtmlFromNodes backward compatibility', () => { + test('works inside legacy editor.getEditorState().read(cb) scope (no active editor)', () => { + const editor = createHeadlessEditor({ + nodes: [], + }); + + // Populate editor state + editor.update( + () => { + const paragraph = $createParagraphNode(); + paragraph.append($createTextNode('Hello world')); + $getRoot().append(paragraph); + }, + {discrete: true}, + ); + + // Legacy pattern: editor.getEditorState().read(cb) WITHOUT {editor} option. + // Before this fix, this would throw because $setTextContent calls $getEditor() + // which requires an active editor scope. + const html = editor.getEditorState().read(() => { + return $generateHtmlFromNodes(editor); + }); + + expect(html).toContain('Hello world'); + }); + + test('still works inside editor.read() scope (active editor present)', () => { + const editor = createHeadlessEditor({ + nodes: [], + }); + + editor.update( + () => { + const paragraph = $createParagraphNode(); + paragraph.append($createTextNode('Test content')); + $getRoot().append(paragraph); + }, + {discrete: true}, + ); + + const html = editor.read(() => { + return $generateHtmlFromNodes(editor); + }); + + expect(html).toContain('Test content'); + }); +}); diff --git a/packages/lexical-html/src/index.ts b/packages/lexical-html/src/index.ts index 084f9a35016..6ebb29fcc70 100644 --- a/packages/lexical-html/src/index.ts +++ b/packages/lexical-html/src/index.ts @@ -20,6 +20,7 @@ import type { import invariant from '@lexical/internal/invariant'; import {$sliceSelectedTextNodeContent} from '@lexical/selection'; import { + $assumeActiveEditor, $createLineBreakNode, $createParagraphNode, $getEditor, @@ -251,6 +252,10 @@ export function $generateHtmlFromNodes( 'To use $generateHtmlFromNodes in headless mode please initialize a headless browser implementation such as JSDom or use withDOM from @lexical/headless/dom before calling this function.', ); } + // BC: $setTextContent now requires an active-editor scope (added in #8519). + // If the caller is in a legacy `editorState.read(cb)` scope (no active editor), + // establish one via internal API. + $assumeActiveEditor(editor); return $generateDOMFromNodes(document.createElement('div'), selection, editor) .innerHTML; } diff --git a/packages/lexical/src/LexicalUpdates.ts b/packages/lexical/src/LexicalUpdates.ts index aa95268b6cd..09f2862e6c0 100644 --- a/packages/lexical/src/LexicalUpdates.ts +++ b/packages/lexical/src/LexicalUpdates.ts @@ -115,6 +115,18 @@ export function getActiveEditorState(): EditorState { return activeEditorState; } +/** @internal */ +export function $assumeActiveEditor(editor: LexicalEditor): void { + // Throw if called outside of an update + if (getActiveEditorState() !== null && activeEditor === null) { + activeEditor = editor; + } + invariant( + activeEditor === editor, + 'The given editor argument does not match $getEditor() in this context. Use editor.getEditorState().read(..., {editor}) if this cross-editor call is intentional.', + ); +} + export function getActiveEditor(): LexicalEditor { if (activeEditor === null) { invariant( diff --git a/packages/lexical/src/index.ts b/packages/lexical/src/index.ts index fd9dbb62d19..aaf0322790d 100644 --- a/packages/lexical/src/index.ts +++ b/packages/lexical/src/index.ts @@ -255,6 +255,7 @@ export { tokenizeRawText, } from './LexicalSelection'; export { + $assumeActiveEditor, $fullReconcile, $parseSerializedNode, isCurrentlyReadOnlyMode,