diff --git a/.github/workflows/after-approval.yml b/.github/workflows/after-approval.yml deleted file mode 100644 index a3751ecc12a..00000000000 --- a/.github/workflows/after-approval.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: After Approval - -on: - pull_request_review: - types: [submitted] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - label-pr: - if: github.event.review.state == 'approved' && !contains(github.event.pull_request.labels.*.name, 'extended-tests') - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - steps: - - name: Add label for extended tests - env: - GH_TOKEN: ${{ secrets.LEXICAL_BOT_TOKEN }} - GH_REPO: ${{ github.repository }} - NUMBER: ${{ github.event.pull_request.number }} - run: | - echo "Adding label 'extended-tests' to PR $NUMBER" - gh pr edit "$NUMBER" --add-label "extended-tests" || (echo "Failed to add label" && exit 1) diff --git a/.github/workflows/tests-extended.yml b/.github/workflows/tests-extended.yml index fd1fad554ea..7db2fe055bb 100644 --- a/.github/workflows/tests-extended.yml +++ b/.github/workflows/tests-extended.yml @@ -7,6 +7,8 @@ on: - 'packages/lexical-website/**' - 'packages/*/README.md' - '.size-limit.js' + pull_request_review: + types: [submitted] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -14,9 +16,9 @@ concurrency: jobs: e2e-tests: - if: github.repository_owner == 'facebook' && contains(github.event.pull_request.labels.*.name, 'extended-tests') + if: (github.repository_owner == 'facebook' && contains(github.event.pull_request.labels.*.name, 'extended-tests') && github.event.review.state != 'approved') || (github.event.review.state == 'approved' && !contains(github.event.pull_request.labels.*.name, 'extended-tests')) uses: ./.github/workflows/call-e2e-all-tests.yml integration-tests: - if: github.repository_owner == 'facebook' && contains(github.event.pull_request.labels.*.name, 'extended-tests') + if: (github.repository_owner == 'facebook' && contains(github.event.pull_request.labels.*.name, 'extended-tests') && github.event.review.state != 'approved') || (github.event.review.state == 'approved' && !contains(github.event.pull_request.labels.*.name, 'extended-tests')) uses: ./.github/workflows/call-integration-tests.yml diff --git a/.size-limit.js b/.size-limit.js index 6e709a0be8a..2061cdfaf00 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -50,6 +50,7 @@ function sizeLimitConfig(pkg) { return ['require', 'import'].map((type) => { const aliasType = getAliasType(type); return { + import: '*', path: aliasType[pkg] != null ? aliasType[pkg] diff --git a/packages/lexical-playground/__tests__/e2e/CopyAndPaste/lexical/ContextMenuCopyAndPaste.spec.mjs b/packages/lexical-playground/__tests__/e2e/CopyAndPaste/lexical/ContextMenuCopyAndPaste.spec.mjs new file mode 100644 index 00000000000..6c67fbbab66 --- /dev/null +++ b/packages/lexical-playground/__tests__/e2e/CopyAndPaste/lexical/ContextMenuCopyAndPaste.spec.mjs @@ -0,0 +1,55 @@ +/** + * 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 { + assertHTML, + click, + doubleClick, + focusEditor, + html, + initialize, + pasteFromClipboard, + test, +} from '../../../utils/index.mjs'; + +test.describe('ContextMenuCopyAndPaste', () => { + test.use({shouldUseLexicalContextMenu: true}); + test.beforeEach(({isCollab, page, shouldUseLexicalContextMenu}) => + initialize({isCollab, page, shouldUseLexicalContextMenu}), + ); + + test('Basic copy-paste #6231', async ({page, isPlainText}) => { + test.skip(isPlainText); + await focusEditor(page); + + await page.keyboard.type('hello'); + await click(page, '.lock'); + + await page.pause(); + await doubleClick(page, 'div[contenteditable="false"] span'); + await page.pause(); + await click(page, 'div[contenteditable="false"] span', {button: 'right'}); + await click(page, '#typeahead-menu [role="option"] :text("Copy")'); + + await click(page, '.unlock'); + await focusEditor(page); + + await pasteFromClipboard(page); + + await assertHTML( + page, + html` +
+ hellohello +
+ `, + ); + }); +}); diff --git a/packages/lexical-playground/__tests__/e2e/Links.spec.mjs b/packages/lexical-playground/__tests__/e2e/Links.spec.mjs index 65c24da850c..994a0cf775e 100644 --- a/packages/lexical-playground/__tests__/e2e/Links.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Links.spec.mjs @@ -35,7 +35,7 @@ test.beforeEach(({isPlainText}) => { test.skip(isPlainText); }); -test.describe('Links', () => { +test.describe.parallel('Links', () => { test.beforeEach(({isCollab, page}) => initialize({isCollab, page})); test(`Can convert a text node into a link`, async ({page}) => { await focusEditor(page); diff --git a/packages/lexical-playground/__tests__/e2e/List.spec.mjs b/packages/lexical-playground/__tests__/e2e/List.spec.mjs index 7978fb67d77..77fa0cd66f1 100644 --- a/packages/lexical-playground/__tests__/e2e/List.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/List.spec.mjs @@ -68,7 +68,7 @@ test.beforeEach(({isPlainText}) => { test.skip(isPlainText); }); -test.describe('Nested List', () => { +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`, async ({ diff --git a/packages/lexical-playground/__tests__/e2e/Markdown.spec.mjs b/packages/lexical-playground/__tests__/e2e/Markdown.spec.mjs index 962e8933af1..22c2942ae3a 100644 --- a/packages/lexical-playground/__tests__/e2e/Markdown.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Markdown.spec.mjs @@ -49,7 +49,7 @@ async function checkHTMLExpectationsIncludingUndoRedo( await assertHTML(page, forwardHTML); } -test.describe('Markdown', () => { +test.describe.parallel('Markdown', () => { test.beforeEach(({isCollab, page}) => initialize({isCollab, page})); const triggersAndExpectations = [ { @@ -374,7 +374,7 @@ async function assertMarkdownImportExport( await assertHTML(page, expectedHTML); } -test.describe('Markdown', () => { +test.describe.parallel('Markdown', () => { test.beforeEach(({isCollab, isPlainText, page}) => { test.skip(isPlainText); return initialize({isCollab, page}); diff --git a/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs b/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs index 20ebff8f39c..00a16f8fefd 100644 --- a/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs @@ -46,7 +46,7 @@ import { YOUTUBE_SAMPLE_URL, } from '../utils/index.mjs'; -test.describe('Selection', () => { +test.describe.parallel('Selection', () => { test.beforeEach(({isCollab, page}) => initialize({isCollab, page})); test('does not focus the editor on load', async ({page}) => { const editorHasFocus = async () => diff --git a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs index a8df457e4b7..105ed967e1e 100644 --- a/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tables.spec.mjs @@ -69,7 +69,7 @@ async function fillTablePartiallyWithText(page) { await page.keyboard.press('c'); } -test.describe('Tables', () => { +test.describe.parallel('Tables', () => { test(`Can a table be inserted from the toolbar`, async ({ page, isPlainText, @@ -152,7 +152,8 @@ test.describe('Tables', () => { ); }); - test.describe(`Can exit tables with the horizontal arrow keys`, () => { + test.describe + .parallel(`Can exit tables with the horizontal arrow keys`, () => { test(`Can exit the first cell of a non-nested table`, async ({ page, isPlainText, @@ -488,7 +489,7 @@ test.describe('Tables', () => { }); }); - test.describe(`Can navigate table with keyboard`, () => { + test.describe.parallel(`Can navigate table with keyboard`, () => { test(`Can navigate cells horizontally`, async ({ page, isPlainText, diff --git a/packages/lexical-playground/__tests__/e2e/TextFormatting.spec.mjs b/packages/lexical-playground/__tests__/e2e/TextFormatting.spec.mjs index 281a84444aa..da2187c764a 100644 --- a/packages/lexical-playground/__tests__/e2e/TextFormatting.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/TextFormatting.spec.mjs @@ -32,7 +32,7 @@ import { waitForSelector, } from '../utils/index.mjs'; -test.describe('TextFormatting', () => { +test.describe.parallel('TextFormatting', () => { test.beforeEach(({isCollab, page}) => initialize({isCollab, page})); test(`Can create bold text using the shortcut`, async ({ page, diff --git a/packages/lexical-playground/__tests__/e2e/Toolbar.spec.mjs b/packages/lexical-playground/__tests__/e2e/Toolbar.spec.mjs index 134672ff3c8..0eab008b6c0 100644 --- a/packages/lexical-playground/__tests__/e2e/Toolbar.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Toolbar.spec.mjs @@ -99,6 +99,25 @@ test.describe('Toolbar', () => { ignoreClasses: true, ignoreInlineStyles: true, }, + (actualHtml) => + // flaky fix: remove the extra+ + Yellow flower in tilt shift lens + +
++ + Yellow flower in tilt shift lens + +
+ `, + ), ); // Delete image diff --git a/packages/lexical-playground/__tests__/utils/index.mjs b/packages/lexical-playground/__tests__/utils/index.mjs index 01a8606166c..c9097052429 100644 --- a/packages/lexical-playground/__tests__/utils/index.mjs +++ b/packages/lexical-playground/__tests__/utils/index.mjs @@ -61,6 +61,7 @@ export async function initialize({ showNestedEditorTreeView, tableCellMerge, tableCellBackgroundColor, + shouldUseLexicalContextMenu, }) { // Tests with legacy events often fail to register keypress, so // slowing it down to reduce flakiness @@ -90,6 +91,7 @@ export async function initialize({ if (tableCellBackgroundColor !== undefined) { appSettings.tableCellBackgroundColor = tableCellBackgroundColor; } + appSettings.shouldUseLexicalContextMenu = !!shouldUseLexicalContextMenu; const urlParams = appSettingsToURLParams(appSettings); const url = `http://localhost:${E2E_PORT}/${ @@ -145,6 +147,7 @@ export const test = base.extend({ isPlainText: IS_PLAIN_TEXT, isRichText: IS_RICH_TEXT, legacyEvents: LEGACY_EVENTS, + shouldUseLexicalContextMenu: false, }); export {expect} from '@playwright/test'; @@ -177,6 +180,7 @@ async function assertHTMLOnPageOrFrame( ignoreClasses, ignoreInlineStyles, frameName, + actualHtmlModificationsCallback = (actualHtml) => actualHtml, ) { const expected = prettifyHTML(expectedHtml.replace(/\n/gm, ''), { ignoreClasses, @@ -187,10 +191,13 @@ async function assertHTMLOnPageOrFrame( .locator('div[contenteditable="true"]') .first() .innerHTML(); - const actual = prettifyHTML(actualHtml.replace(/\n/gm, ''), { + let actual = prettifyHTML(actualHtml.replace(/\n/gm, ''), { ignoreClasses, ignoreInlineStyles, }); + + actual = actualHtmlModificationsCallback(actual); + expect( actual, `innerHTML of contenteditable in ${frameName} did not match`, @@ -206,6 +213,7 @@ export async function assertHTML( expectedHtml, expectedHtmlFrameRight = expectedHtml, {ignoreClasses = false, ignoreInlineStyles = false} = {}, + actualHtmlModificationsCallback, ) { if (IS_COLLAB) { await Promise.all([ @@ -215,6 +223,7 @@ export async function assertHTML( ignoreClasses, ignoreInlineStyles, 'left frame', + actualHtmlModificationsCallback, ), assertHTMLOnPageOrFrame( page.frame('right'), @@ -222,6 +231,7 @@ export async function assertHTML( ignoreClasses, ignoreInlineStyles, 'right frame', + actualHtmlModificationsCallback, ), ]); } else { @@ -407,7 +417,10 @@ export async function copyToClipboard(page) { return await copyToClipboardPageOrFrame(getPageOrFrame(page)); } -async function pasteFromClipboardPageOrFrame(pageOrFrame, clipboardData) { +async function pasteWithClipboardDataFromPageOrFrame( + pageOrFrame, + clipboardData, +) { const canUseBeforeInput = await supportsBeforeInput(pageOrFrame); await pageOrFrame.evaluate( async ({ @@ -478,7 +491,16 @@ async function pasteFromClipboardPageOrFrame(pageOrFrame, clipboardData) { * @param {import('@playwright/test').Page} page */ export async function pasteFromClipboard(page, clipboardData) { - await pasteFromClipboardPageOrFrame(getPageOrFrame(page), clipboardData); + if (clipboardData === undefined) { + await keyDownCtrlOrMeta(page); + await page.keyboard.press('v'); + await keyUpCtrlOrMeta(page); + return; + } + await pasteWithClipboardDataFromPageOrFrame( + getPageOrFrame(page), + clipboardData, + ); } export async function sleep(delay) { @@ -525,6 +547,12 @@ export async function click(page, selector, options) { await frame.click(selector, options); } +export async function doubleClick(page, selector, options) { + const frame = getPageOrFrame(page); + await frame.waitForSelector(selector, options); + await frame.dblclick(selector, options); +} + export async function focus(page, selector, options) { await locate(page, selector).focus(options); } diff --git a/packages/lexical-playground/src/nodes/InlineImageComponent.tsx b/packages/lexical-playground/src/nodes/InlineImageNode/InlineImageComponent.tsx similarity index 95% rename from packages/lexical-playground/src/nodes/InlineImageComponent.tsx rename to packages/lexical-playground/src/nodes/InlineImageNode/InlineImageComponent.tsx index 085e5ce330e..7cc9428f465 100644 --- a/packages/lexical-playground/src/nodes/InlineImageComponent.tsx +++ b/packages/lexical-playground/src/nodes/InlineImageNode/InlineImageComponent.tsx @@ -34,14 +34,14 @@ import { import * as React from 'react'; import {Suspense, useCallback, useEffect, useRef, useState} from 'react'; -import useModal from '../hooks/useModal'; -import LinkPlugin from '../plugins/LinkPlugin'; -import Button from '../ui/Button'; -import ContentEditable from '../ui/ContentEditable'; -import {DialogActions} from '../ui/Dialog'; -import Placeholder from '../ui/Placeholder'; -import Select from '../ui/Select'; -import TextInput from '../ui/TextInput'; +import useModal from '../../hooks/useModal'; +import LinkPlugin from '../../plugins/LinkPlugin'; +import Button from '../../ui/Button'; +import ContentEditable from '../../ui/ContentEditable'; +import {DialogActions} from '../../ui/Dialog'; +import Placeholder from '../../ui/Placeholder'; +import Select from '../../ui/Select'; +import TextInput from '../../ui/TextInput'; import {$isInlineImageNode, InlineImageNode} from './InlineImageNode'; const imageCache = new Set(); @@ -352,7 +352,7 @@ export default function InlineImageComponent({ return (