From 3e7db302ee0c2da875e09968581294ea19d247d3 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 15 May 2025 08:57:23 -0700 Subject: [PATCH] fix(richtext-lexical): newTab not being able to be checked to true by default (#12389) Previously the value of new tab checkbox in the link feature was not able to be set to true by default because we were passing `false` as a default value. This fixes that and adds test coverage for customising that link drawer. --- .../src/features/link/client/index.tsx | 1 - test/lexical/baseConfig.ts | 2 + .../LexicalLinkFeature/e2e.spec.ts | 79 +++++++++++++++++++ .../collections/LexicalLinkFeature/index.ts | 44 +++++++++++ .../collections/LexicalLinkFeature/utils.ts | 49 ++++++++++++ test/lexical/payload-types.ts | 39 +++++++++ test/lexical/slugs.ts | 2 + 7 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 test/lexical/collections/LexicalLinkFeature/e2e.spec.ts create mode 100644 test/lexical/collections/LexicalLinkFeature/index.ts create mode 100644 test/lexical/collections/LexicalLinkFeature/utils.ts diff --git a/packages/richtext-lexical/src/features/link/client/index.tsx b/packages/richtext-lexical/src/features/link/client/index.tsx index c8600e309..2200c2d20 100644 --- a/packages/richtext-lexical/src/features/link/client/index.tsx +++ b/packages/richtext-lexical/src/features/link/client/index.tsx @@ -64,7 +64,6 @@ const toolbarGroups: ToolbarGroup[] = [ const linkFields: Partial = { doc: null, - newTab: false, } editor.dispatchCommand(TOGGLE_LINK_WITH_MODAL_COMMAND, { diff --git a/test/lexical/baseConfig.ts b/test/lexical/baseConfig.ts index f163abbd3..1d86a2b7f 100644 --- a/test/lexical/baseConfig.ts +++ b/test/lexical/baseConfig.ts @@ -11,6 +11,7 @@ import { } from './collections/Lexical/index.js' import { LexicalAccessControl } from './collections/LexicalAccessControl/index.js' import { LexicalInBlock } from './collections/LexicalInBlock/index.js' +import { LexicalLinkFeature } from './collections/LexicalLinkFeature/index.js' import { LexicalLocalizedFields } from './collections/LexicalLocalized/index.js' import { LexicalMigrateFields } from './collections/LexicalMigrate/index.js' import { LexicalObjectReferenceBugCollection } from './collections/LexicalObjectReferenceBug/index.js' @@ -28,6 +29,7 @@ export const baseConfig: Partial = { // ...extend config here collections: [ LexicalFullyFeatured, + LexicalLinkFeature, getLexicalFieldsCollection({ blocks: lexicalBlocks, inlineBlocks: lexicalInlineBlocks, diff --git a/test/lexical/collections/LexicalLinkFeature/e2e.spec.ts b/test/lexical/collections/LexicalLinkFeature/e2e.spec.ts new file mode 100644 index 000000000..90e6e040d --- /dev/null +++ b/test/lexical/collections/LexicalLinkFeature/e2e.spec.ts @@ -0,0 +1,79 @@ +import { expect, test } from '@playwright/test' +import { AdminUrlUtil } from 'helpers/adminUrlUtil.js' +import { reInitializeDB } from 'helpers/reInitializeDB.js' +import { lexicalLinkFeatureSlug } from 'lexical/slugs.js' +import path from 'path' +import { fileURLToPath } from 'url' + +import { ensureCompilationIsDone } from '../../../helpers.js' +import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js' +import { TEST_TIMEOUT_LONG } from '../../../playwright.config.js' +import { LexicalHelpers } from './utils.js' +const filename = fileURLToPath(import.meta.url) +const currentFolder = path.dirname(filename) +const dirname = path.resolve(currentFolder, '../../') + +const { beforeAll, beforeEach, describe } = test + +// Unlike the other suites, this one runs in parallel, as they run on the `lexical-fully-featured/create` URL and are "pure" tests +test.describe.configure({ mode: 'parallel' }) + +const { serverURL } = await initPayloadE2ENoConfig({ + dirname, +}) + +describe('Lexical Link Feature', () => { + beforeAll(async ({ browser }, testInfo) => { + testInfo.setTimeout(TEST_TIMEOUT_LONG) + process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit + const page = await browser.newPage() + await ensureCompilationIsDone({ page, serverURL }) + await page.close() + }) + beforeEach(async ({ page }) => { + await reInitializeDB({ + serverURL, + snapshotKey: 'fieldsTest', + uploadsDir: [ + path.resolve(dirname, './collections/Upload/uploads'), + path.resolve(dirname, './collections/Upload2/uploads2'), + ], + }) + const url = new AdminUrlUtil(serverURL, lexicalLinkFeatureSlug) + const lexical = new LexicalHelpers(page) + await page.goto(url.create) + await lexical.editor.first().focus() + }) + + test('can add new custom fields in link feature modal', async ({ page }) => { + const lexical = new LexicalHelpers(page) + + await lexical.editor.fill('link') + await lexical.editor.selectText() + + const linkButtonClass = `.rich-text-lexical__wrap .fixed-toolbar .toolbar-popup__button-link` + const linkButton = page.locator(linkButtonClass).first() + + await linkButton.click() + + const customField = lexical.drawer.locator('#field-someText') + + await expect(customField).toBeVisible() + }) + + test('can set default value of newTab checkbox to checked', async ({ page }) => { + const lexical = new LexicalHelpers(page) + + await lexical.editor.fill('link') + await lexical.editor.selectText() + + const linkButtonClass = `.rich-text-lexical__wrap .fixed-toolbar .toolbar-popup__button-link` + const linkButton = page.locator(linkButtonClass).first() + + await linkButton.click() + + const checkboxField = lexical.drawer.locator(`[id^="field-newTab"]`) + + await expect(checkboxField).toBeChecked() + }) +}) diff --git a/test/lexical/collections/LexicalLinkFeature/index.ts b/test/lexical/collections/LexicalLinkFeature/index.ts new file mode 100644 index 000000000..57f84a7be --- /dev/null +++ b/test/lexical/collections/LexicalLinkFeature/index.ts @@ -0,0 +1,44 @@ +import type { CheckboxField, CollectionConfig } from 'payload' + +import { + FixedToolbarFeature, + lexicalEditor, + LinkFeature, + TreeViewFeature, +} from '@payloadcms/richtext-lexical' + +import { lexicalLinkFeatureSlug } from '../../slugs.js' + +export const LexicalLinkFeature: CollectionConfig = { + slug: lexicalLinkFeatureSlug, + labels: { + singular: 'Lexical Link Feature', + plural: 'Lexical Link Feature', + }, + fields: [ + { + name: 'richText', + type: 'richText', + editor: lexicalEditor({ + features: ({ defaultFeatures }) => [ + ...defaultFeatures, + TreeViewFeature(), + LinkFeature({ + fields: ({ defaultFields }) => { + const modifiedFields = defaultFields.map((field) => { + if (field.name === 'newTab') { + return { ...field, defaultValue: true } as CheckboxField + } + + return field + }) + + return [...modifiedFields, { type: 'text', name: 'someText' }] + }, + }), + FixedToolbarFeature(), + ], + }), + }, + ], +} diff --git a/test/lexical/collections/LexicalLinkFeature/utils.ts b/test/lexical/collections/LexicalLinkFeature/utils.ts new file mode 100644 index 000000000..95030f7fe --- /dev/null +++ b/test/lexical/collections/LexicalLinkFeature/utils.ts @@ -0,0 +1,49 @@ +import type { Page } from 'playwright' + +import { expect } from '@playwright/test' + +export class LexicalHelpers { + page: Page + constructor(page: Page) { + this.page = page + } + + async save(container: 'document' | 'drawer') { + if (container === 'drawer') { + await this.drawer.getByText('Save').click() + } else { + throw new Error('Not implemented') + } + await this.page.waitForTimeout(1000) + } + + async slashCommand( + // prettier-ignore + command: 'block' | 'check' | 'code' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' |'h6' | 'inline' + | 'link' | 'ordered' | 'paragraph' | 'quote' | 'relationship' | 'unordered' | 'upload', + ) { + await this.page.keyboard.press(`/`) + + const slashMenuPopover = this.page.locator('#slash-menu .slash-menu-popup') + await expect(slashMenuPopover).toBeVisible() + await this.page.keyboard.type(command) + await this.page.keyboard.press(`Enter`) + await expect(slashMenuPopover).toBeHidden() + } + + get decorator() { + return this.editor.locator('[data-lexical-decorator="true"]') + } + + get drawer() { + return this.page.locator('.drawer__content') + } + + get editor() { + return this.page.locator('[data-lexical-editor="true"]') + } + + get paragraph() { + return this.editor.locator('p') + } +} diff --git a/test/lexical/payload-types.ts b/test/lexical/payload-types.ts index 1964b49ff..1f99ec265 100644 --- a/test/lexical/payload-types.ts +++ b/test/lexical/payload-types.ts @@ -84,6 +84,7 @@ export interface Config { blocks: {}; collections: { 'lexical-fully-featured': LexicalFullyFeatured; + 'lexical-link-feature': LexicalLinkFeature; 'lexical-fields': LexicalField; 'lexical-migrate-fields': LexicalMigrateField; 'lexical-localized-fields': LexicalLocalizedField; @@ -103,6 +104,7 @@ export interface Config { collectionsJoins: {}; collectionsSelect: { 'lexical-fully-featured': LexicalFullyFeaturedSelect | LexicalFullyFeaturedSelect; + 'lexical-link-feature': LexicalLinkFeatureSelect | LexicalLinkFeatureSelect; 'lexical-fields': LexicalFieldsSelect | LexicalFieldsSelect; 'lexical-migrate-fields': LexicalMigrateFieldsSelect | LexicalMigrateFieldsSelect; 'lexical-localized-fields': LexicalLocalizedFieldsSelect | LexicalLocalizedFieldsSelect; @@ -179,6 +181,30 @@ export interface LexicalFullyFeatured { updatedAt: string; createdAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "lexical-link-feature". + */ +export interface LexicalLinkFeature { + id: string; + richText?: { + root: { + type: string; + children: { + type: string; + version: number; + [k: string]: unknown; + }[]; + direction: ('ltr' | 'rtl') | null; + format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''; + indent: number; + version: number; + }; + [k: string]: unknown; + } | null; + updatedAt: string; + createdAt: string; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "lexical-fields". @@ -804,6 +830,10 @@ export interface PayloadLockedDocument { relationTo: 'lexical-fully-featured'; value: string | LexicalFullyFeatured; } | null) + | ({ + relationTo: 'lexical-link-feature'; + value: string | LexicalLinkFeature; + } | null) | ({ relationTo: 'lexical-fields'; value: string | LexicalField; @@ -903,6 +933,15 @@ export interface LexicalFullyFeaturedSelect { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "lexical-link-feature_select". + */ +export interface LexicalLinkFeatureSelect { + richText?: T; + updatedAt?: T; + createdAt?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "lexical-fields_select". diff --git a/test/lexical/slugs.ts b/test/lexical/slugs.ts index 73cf16710..502cec89c 100644 --- a/test/lexical/slugs.ts +++ b/test/lexical/slugs.ts @@ -2,6 +2,8 @@ export const usersSlug = 'users' export const lexicalFullyFeaturedSlug = 'lexical-fully-featured' export const lexicalFieldsSlug = 'lexical-fields' + +export const lexicalLinkFeatureSlug = 'lexical-link-feature' export const lexicalLocalizedFieldsSlug = 'lexical-localized-fields' export const lexicalMigrateFieldsSlug = 'lexical-migrate-fields' export const lexicalRelationshipFieldsSlug = 'lexical-relationship-fields'