Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 13 additions & 16 deletions .github/workflows/call-e2e-all-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,19 @@ jobs:
browser: ${{ matrix.browser }}
editor-mode: 'rich-text-with-collab'

collab-v2-linux:
needs: [build-ubuntu]
strategy:
matrix:
node-version: [24.x]
browser: ['chromium', 'firefox']
uses: ./.github/workflows/call-e2e-test.yml
with:
os: 'ubuntu-latest'
node-version: ${{ matrix.node-version }}
browser: ${{ matrix.browser }}
editor-mode: 'rich-text-with-collab-v2'

collab-windows:
needs: [build-windows]
strategy:
Expand Down Expand Up @@ -198,19 +211,3 @@ jobs:
node-version: ${{ matrix.node-version }}
browser: ${{ matrix.browser }}
editor-mode: ${{ matrix.editor-mode }}
flaky:
# There aren't currently any flaky tests
if: false
needs: [build-ubuntu]
strategy:
matrix:
node-version: [24.x]
browser: ['chromium', 'firefox']
editor-mode: ['rich-text', 'plain-text', 'rich-text-with-collab']
uses: ./.github/workflows/call-e2e-test.yml
with:
os: 'ubuntu-latest'
flaky: true
node-version: ${{ matrix.node-version }}
browser: ${{ matrix.browser }}
editor-mode: ${{ matrix.editor-mode }}
12 changes: 5 additions & 7 deletions .github/workflows/call-e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,13 @@ on:
browser: {required: true, type: string}
editor-mode: {required: true, type: string}
prod: {required: false, type: boolean}
flaky: {required: false, type: boolean} # Defaults to false: dont run flaky
fail-on-cache-miss: {required: false, type: boolean, default: true}

permissions: {}

jobs:
e2e-test:
runs-on: ${{ inputs.os }}
continue-on-error: ${{ inputs.flaky }}
if: (inputs.browser != 'webkit' || inputs.os == 'macos-latest')
env:
CI: true
Expand All @@ -39,19 +37,19 @@ jobs:
node-version: ${{ inputs.node-version }}
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"${{ inputs.flaky && ' --pass-with-no-tests' || '' }}
if: ${{ !startsWith(inputs.editor-mode, 'rich-text-with-collab') }}
run: pnpm run test-e2e-${{ inputs.prod && 'prod-' || '' }}${{ inputs.browser }}
- name: Run collab tests
if: inputs.editor-mode == 'rich-text-with-collab'
if: ${{ startsWith(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${{ inputs.flaky && ' --pass-with-no-tests' || '' }}"
"pnpm run test-e2e-collab-${{ inputs.editor-mode == 'rich-text-with-collab-v2' && 'v2-' || '' }}${{ inputs.prod && 'prod-' || '' }}${{ inputs.browser }}"
- name: Upload Artifacts
if: failure()
uses: actions/upload-artifact@v7
with:
name: Test Results ${{ inputs.os }}-${{ inputs.browser }}-${{ inputs.editor-mode }}-${{ inputs.prod && 'prod' || 'dev' }}-${{ inputs.node-version }}-${{ inputs.flaky && 'flaky' || ''}}-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
name: Test Results ${{ inputs.os }}-${{ inputs.browser }}-${{ inputs.editor-mode }}-${{ inputs.prod && 'prod' || 'dev' }}-${{ inputs.node-version }}-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ env.test_results_path }}
retention-days: 7
16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,14 @@
"test-e2e-collab-v2-webkit": "cross-env E2E_BROWSER=webkit E2E_EDITOR_MODE=rich-text-with-collab-v2 playwright test --project=\"webkit\"",
"test-e2e-prod-chromium": "cross-env E2E_BROWSER=chromium E2E_PORT=4000 playwright test --project=\"chromium\"",
"test-e2e-collab-prod-chromium": "cross-env E2E_BROWSER=chromium E2E_PORT=4000 E2E_EDITOR_MODE=rich-text-with-collab playwright test --project=\"chromium\"",
"test-e2e-ci-chromium": "pnpm run prepare-ci && cross-env E2E_PORT=4000 pnpm run test-e2e-chromium --grep-invert \"@flaky\"",
"test-e2e-ci-firefox": "pnpm run prepare-ci && cross-env E2E_PORT=4000 pnpm run test-e2e-firefox --grep-invert \"@flaky\"",
"test-e2e-ci-webkit": "pnpm run prepare-ci && cross-env E2E_PORT=4000 pnpm run test-e2e-webkit --grep-invert \"@flaky\"",
"test-e2e-collab-ci-chromium": "pnpm run prepare-ci && cross-env E2E_PORT=4000 concurrently -k -s \"first\" \"pnpm run collab\" -P \"pnpm run test-e2e-collab-chromium --grep-invert '@flaky' {@}\"",
"test-e2e-collab-ci-firefox": "pnpm run prepare-ci && cross-env E2E_PORT=4000 concurrently -k -s \"first\" \"pnpm run collab\" -P \"pnpm run test-e2e-collab-firefox --grep-invert '@flaky' {@}\"",
"test-e2e-collab-ci-webkit": "pnpm run prepare-ci && cross-env E2E_PORT=4000 concurrently -k -s \"first\" \"pnpm run collab\" -P \"pnpm run test-e2e-collab-webkit --grep-invert '@flaky' {@}\"",
"test-e2e-prod-ci-chromium": "pnpm run prepare-ci-prod && cross-env E2E_PORT=4000 pnpm run test-e2e-prod-chromium --grep-invert \"@flaky\"",
"test-e2e-collab-prod-ci-chromium": "pnpm run prepare-ci-prod && cross-env E2E_PORT=4000 concurrently -k -s \"first\" \"pnpm run collab\" -P \"pnpm run test-e2e-collab-prod-chromium --grep-invert '@flaky' {@}\"",
"test-e2e-ci-chromium": "pnpm run prepare-ci && cross-env E2E_PORT=4000 pnpm run test-e2e-chromium",
"test-e2e-ci-firefox": "pnpm run prepare-ci && cross-env E2E_PORT=4000 pnpm run test-e2e-firefox",
"test-e2e-ci-webkit": "pnpm run prepare-ci && cross-env E2E_PORT=4000 pnpm run test-e2e-webkit",
"test-e2e-collab-ci-chromium": "pnpm run prepare-ci && cross-env E2E_PORT=4000 concurrently -k -s \"first\" \"pnpm run collab\" -P \"pnpm run test-e2e-collab-chromium {@}\"",
"test-e2e-collab-ci-firefox": "pnpm run prepare-ci && cross-env E2E_PORT=4000 concurrently -k -s \"first\" \"pnpm run collab\" -P \"pnpm run test-e2e-collab-firefox {@}\"",
"test-e2e-collab-ci-webkit": "pnpm run prepare-ci && cross-env E2E_PORT=4000 concurrently -k -s \"first\" \"pnpm run collab\" -P \"pnpm run test-e2e-collab-webkit {@}\"",
"test-e2e-prod-ci-chromium": "pnpm run prepare-ci-prod && cross-env E2E_PORT=4000 pnpm run test-e2e-prod-chromium",
"test-e2e-collab-prod-ci-chromium": "pnpm run prepare-ci-prod && cross-env E2E_PORT=4000 concurrently -k -s \"first\" \"pnpm run collab\" -P \"pnpm run test-e2e-collab-prod-chromium {@}\"",
"debug-run-all": "npm-run-all 'debug-test-e2e-* -- {1}' --",
"run-all": "npm-run-all 'test-e2e-* -- {1}' --",
"debug-test-e2e": "cross-env playwright test --debug",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
selectFromBackgroundColorPicker,
selectFromColorPicker,
test,
waitForSelector,
waitForTypeaheadMenuOption,
} from '../utils/index.mjs';

test.describe('Clear All Formatting', () => {
Expand Down Expand Up @@ -131,14 +131,9 @@ test.describe('Clear All Formatting', () => {

await page.keyboard.type('@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")',
);
// Wait until "Luke Skywalker" is the *highlighted* option before pressing
// Enter; see waitForTypeaheadMenuOption for why merely-present is racy.
await waitForTypeaheadMenuOption(page, 'Luke Skywalker');
await assertHTML(
page,
html`
Expand Down
12 changes: 10 additions & 2 deletions packages/lexical-playground/__tests__/e2e/CodeActionMenu.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,13 @@ test.describe('CodeActionMenu', () => {
await mouseMoveToSelector(page, 'code.PlaygroundEditorTheme__code');
await click(page, 'button[aria-label=prettier]');

await page.waitForTimeout(3000);
// Prettier loads and formats asynchronously; wait for the reformatted
// result instead of a fixed timeout. Formatting turns the single input
// line into multiple lines, so a <br> inside the code block is a reliable
// "prettier finished" signal (attached, not visible: <br> has no box).
await waitForSelector(page, 'code.PlaygroundEditorTheme__code br', {
state: 'attached',
});

await assertHTML(
page,
Expand Down Expand Up @@ -254,7 +260,9 @@ test.describe('CodeActionMenu', () => {
await mouseMoveToSelector(page, 'code.PlaygroundEditorTheme__code');
await click(page, 'button[aria-label=prettier]');

await page.waitForTimeout(3000);
// Prettier reports invalid syntax asynchronously; wait for the error badge
// to appear instead of a fixed timeout (the assertions below do not retry).
await waitForSelector(page, 'i.format.prettier-error');

expect(await page.$('i.format.prettier-error')).toBeTruthy();

Expand Down
117 changes: 62 additions & 55 deletions packages/lexical-playground/__tests__/e2e/Collaboration.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,20 @@ import {expect} from '@playwright/test';

import {
moveLeft,
moveToLineBeginning,
moveToLineEnd,
selectCharacters,
toggleBold,
undo,
} from '../keyboardShortcuts/index.mjs';
import {
advanceHistoryClock,
assertHTML,
assertSelection,
click,
focusEditor,
html,
initialize,
sleep,
test,
} from '../utils/index.mjs';

Expand All @@ -46,7 +48,7 @@ test.describe('Collaboration', () => {
await page.keyboard.press('Enter');
await page.keyboard.press('Enter');
await page.keyboard.type('world');
await sleep(1050); // default merge interval is 1000, add 50ms as overhead due to CI latency.
await advanceHistoryClock(page);
await page.keyboard.press('ArrowUp');
await page.keyboard.type('hello world again');

Expand Down Expand Up @@ -229,17 +231,20 @@ test.describe('Collaboration', () => {
await focusEditor(page);
await page.keyboard.type('Line 1');
await page.keyboard.press('Enter');
await sleep(1050); // default merge interval is 1000, add 50ms as overhead due to CI latency.
await advanceHistoryClock(page);
await page.keyboard.type('This is a test. ');

// Right collaborator types at the end of paragraph 2
await sleep(1050);
await page
.frameLocator('iframe[name="right"]')
.locator('[data-lexical-editor="true"]')
.focus();
await page.keyboard.press('ArrowDown'); // Move caret to end of paragraph 2
await page.keyboard.press('ArrowDown');
// Right collaborator types at the end of paragraph 2. Click into that
// paragraph to place the caret — a real remote user positions the cursor
// with a click — then move to the line end. Relying on a default caret
// position + ArrowDown is fragile: once content has synced in, the idle
// frame's selection is not guaranteed to start at the top of the document.
const rightFrame = page.frameLocator('iframe[name="right"]');
await expect(
rightFrame.locator('[data-lexical-editor="true"]'),
).toContainText('This is a test.');
await rightFrame.locator('p').nth(1).click();
await moveToLineEnd(page);
await page.keyboard.type('Word');

await assertHTML(
Expand Down Expand Up @@ -311,7 +316,7 @@ test.describe('Collaboration', () => {
</p>
`,
);
await sleep(1050);
await advanceHistoryClock(page);
await toggleBold(page);
await page.keyboard.type('bold');

Expand All @@ -329,12 +334,11 @@ test.describe('Collaboration', () => {
`,
);

// Right collaborator types at the end of the paragraph.
await page
.frameLocator('iframe[name="right"]')
.locator('[data-lexical-editor="true"]')
.focus();
await page.keyboard.press('ArrowDown', {delay: 50}); // Move caret to end of paragraph
// Right collaborator types at the end of the paragraph. Click to place the
// caret (real remote user positioning) then move to the line end, rather
// than relying on a default caret position + ArrowDown.
await page.frameLocator('iframe[name="right"]').locator('p').click();
await moveToLineEnd(page);
await page.keyboard.type('BOLD');

await assertHTML(
Expand All @@ -351,9 +355,14 @@ test.describe('Collaboration', () => {
`,
);

// Left collaborator undoes their bold text.
await sleep(1050);
await page.frameLocator('iframe[name="left"]').getByLabel('Undo').click();
// Left collaborator undoes their bold text. The assertHTML above already
// confirmed both frames converged, so wait for the undo control to be
// actionable instead of sleeping.
const undoButton = page
.frameLocator('iframe[name="left"]')
.getByLabel('Undo');
await expect(undoButton).toBeEnabled();
await undoButton.click();

// The undo also removed bold the text node from YJS.
// Check that the dangling text from right user was merged into the preceding text node.
Expand Down Expand Up @@ -396,7 +405,7 @@ test.describe('Collaboration', () => {
await focusEditor(page);
await page.keyboard.type('normal bold');

await sleep(1050);
await advanceHistoryClock(page);
await selectCharacters(page, 'left', 'bold'.length);
await toggleBold(page);

Expand All @@ -414,12 +423,12 @@ test.describe('Collaboration', () => {
`,
);

// Right collaborator types in the middle of the bold word.
await page
.frameLocator('iframe[name="right"]')
.locator('[data-lexical-editor="true"]')
.focus();
await page.keyboard.press('ArrowDown', {delay: 50});
// Right collaborator types in the middle of the bold word. Click to place
// the caret (real remote-user positioning), then step left from the line
// end — relying on a default caret position + ArrowDown is fragile because
// the idle frame's synced selection isn't guaranteed to start at the top.
await page.frameLocator('iframe[name="right"]').locator('p').click();
await moveToLineEnd(page);
await page.keyboard.press('ArrowLeft');
await page.keyboard.press('ArrowLeft');
await page.keyboard.type('BOLD');
Expand Down Expand Up @@ -481,17 +490,16 @@ test.describe('Collaboration', () => {
`,
);

// Collaboration should still work.
await page
.frameLocator('iframe[name="right"]')
.locator('[data-lexical-editor="true"]')
.focus();
// TODO this is a workaround for Firefox so that the
// selection picks up the text format
// Collaboration should still work. Click into the paragraph and move to the
// line end, rather than relying on a default caret position + ArrowDown.
await page.frameLocator('iframe[name="right"]').locator('p').click();
await moveToLineEnd(page);
// Firefox doesn't carry the bold format to a caret at the very end of the
// bold run, so re-enter the run and return so the appended text inherits it.
if (browserName === 'firefox') {
await page.keyboard.press('ArrowLeft', {delay: 50});
await page.keyboard.press('ArrowLeft');
await page.keyboard.press('ArrowRight');
}
await page.keyboard.press('ArrowDown', {delay: 50});
await page.keyboard.type(' text');

await assertHTML(
Expand Down Expand Up @@ -521,7 +529,7 @@ test.describe('Collaboration', () => {
await focusEditor(page);
await page.keyboard.type('Check out the website!');

await sleep(1050);
await advanceHistoryClock(page);
await page.keyboard.press('ArrowLeft');
await selectCharacters(page, 'left', 'website'.length);
await page
Expand All @@ -546,12 +554,11 @@ test.describe('Collaboration', () => {
`,
);

// Right collaborator adds some text.
await page
.frameLocator('iframe[name="right"]')
.locator('[data-lexical-editor="true"]')
.focus();
await page.keyboard.press('ArrowDown', {delay: 50});
// Right collaborator adds some text just before the trailing "!". Click to
// place the caret (real remote user positioning), move to the line end,
// then step left over "!", rather than relying on a default caret position.
await page.frameLocator('iframe[name="right"]').locator('p').click();
await moveToLineEnd(page);
await page.keyboard.press('ArrowLeft');
await page.keyboard.type(' now');

Expand Down Expand Up @@ -616,12 +623,11 @@ test.describe('Collaboration', () => {
`,
);

// Collaboration should still work.
await page
.frameLocator('iframe[name="right"]')
.locator('[data-lexical-editor="true"]')
.focus();
await page.keyboard.press('ArrowDown', {delay: 50});
// Collaboration should still work. Click into the paragraph and move to the
// line end to append after "now!", rather than relying on a default caret
// position + ArrowDown.
await page.frameLocator('iframe[name="right"]').locator('p').click();
await moveToLineEnd(page);
await page.keyboard.type(' Check it out.');

await assertHTML(
Expand Down Expand Up @@ -674,7 +680,7 @@ test.describe('Collaboration', () => {
);

// Left collaborator deletes A, right deletes B.
await sleep(1050);
await advanceHistoryClock(page);
await page.keyboard.press('Delete');
await assertHTML(
page,
Expand All @@ -689,10 +695,11 @@ test.describe('Collaboration', () => {
</p>
`,
);
await page
.frameLocator('iframe[name="right"]')
.locator('[data-lexical-editor="true"]')
.focus();
// Right collaborator deletes "B". Click into the paragraph and move to the
// line start so forward-Delete removes the leading bold "B", rather than
// relying on a default caret position.
await page.frameLocator('iframe[name="right"]').locator('p').click();
await moveToLineBeginning(page);
await page.keyboard.press('Delete');

await assertHTML(
Expand Down
Loading