Skip to content

Commit 9ca5cda

Browse files
authored
test: fix flaky lexical e2e navigation timeouts and drawer clicks (#15694)
### What Fixed flaky lexical e2e tests that were timing out on navigation in CI: **test/lexical/collections/Lexical/e2e/main/e2e.spec.ts:** - `ensure lexical fields in blocks have correct value when moving blocks` - `select decoratorNodes` - `ensure newly created upload node has fields, saves them, and loads them correctly` - `Preserve indent and text-align when converting Lexical <-> HTML` **test/lexical/collections/RichText/e2e.spec.ts:** - `should only list RTE enabled collections in link drawer` - `should only list non-upload collections in relationship drawer` - `should populate relationship link` - `should open upload document drawer from read-only field` - `should populate new links` **test/lexical/collections/Lexical/e2e/blocks/e2e.spec.ts:** - `ensure individual inline blocks in lexical editor within a block have initial state on initial load` - `nested lexical fields display label and description` - `ensure inline blocks restore their state after undoing a removal` <img width="1444" height="582" alt="Screenshot 2026-02-19 at 2 02 07 PM" src="https://github.com/user-attachments/assets/0009c55a-1dd9-4e54-82ca-2f989ef78e70" /> <img width="1316" height="310" alt="Screenshot 2026-02-19 at 2 37 35 PM" src="https://github.com/user-attachments/assets/3828afce-3828-4894-bc6e-9899e7caaa3e" /> <img width="1807" height="586" alt="Screenshot 2026-02-19 at 3 42 29 PM" src="https://github.com/user-attachments/assets/fd30ee9d-d484-4190-ad6a-553e3cb103b8" /> ### Why List view link clicks were timing out in CI after 30 seconds. Clicks weren't reliably triggering navigation in slow environments. The upload node test also tried clicking drawer togglers before they were visible, causing page crashes. ### How - Replaced link clicks with `goToFirstCell` helper in `navigateToLexicalFields` and affected tests - Added `waitForFormReady` to ensure forms are hydrated before interaction - Added explicit `waitFor({ state: 'visible' })` for drawer toggler buttons before clicking
1 parent 4af5a85 commit 9ca5cda

File tree

4 files changed

+110
-30
lines changed

4 files changed

+110
-30
lines changed

test/lexical/collections/Lexical/e2e/blocks/e2e.spec.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,18 @@ import { fileURLToPath } from 'url'
1818
import type { PayloadTestSDK } from '../../../../../__helpers/shared/sdk/index.js'
1919
import type { Config, LexicalField, Upload } from '../../../../payload-types.js'
2020

21+
import { assertNetworkRequests } from '../../../../../__helpers/e2e/assertNetworkRequests.js'
2122
import {
2223
ensureCompilationIsDone,
2324
initPageConsoleErrorCatch,
2425
saveDocAndAssert,
2526
waitForFormReady,
2627
} from '../../../../../__helpers/e2e/helpers.js'
28+
import { goToFirstCell } from '../../../../../__helpers/e2e/navigateToDoc.js'
2729
import { AdminUrlUtil } from '../../../../../__helpers/shared/adminUrlUtil.js'
2830
import { assertToastErrors } from '../../../../../__helpers/shared/assertToastErrors.js'
29-
import { assertNetworkRequests } from '../../../../../__helpers/e2e/assertNetworkRequests.js'
30-
import { initPayloadE2ENoConfig } from '../../../../../__helpers/shared/initPayloadE2ENoConfig.js'
3131
import { reInitializeDB } from '../../../../../__helpers/shared/clearAndSeed/reInitializeDB.js'
32+
import { initPayloadE2ENoConfig } from '../../../../../__helpers/shared/initPayloadE2ENoConfig.js'
3233
import { RESTClient } from '../../../../../__helpers/shared/rest.js'
3334
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../../../../../playwright.config.js'
3435
import { lexicalFieldsSlug } from '../../../../slugs.js'
@@ -1274,8 +1275,12 @@ describe('lexicalBlocks', () => {
12741275
// Previously, we had the issue that nested lexical fields did not display the field label and description, as
12751276
// their client field configs were generated incorrectly on the server.
12761277
await page.goto('http://localhost:3000/admin/collections/LexicalInBlock?limit=10')
1277-
await page.locator('.cell-id a').first().click()
1278-
await page.waitForURL(`**/collections/LexicalInBlock/**`)
1278+
1279+
// Wait for table to be fully loaded
1280+
await expect(page.locator('tbody tr')).not.toHaveCount(0)
1281+
1282+
await goToFirstCell(page, serverURL)
1283+
await waitForFormReady(page)
12791284

12801285
await expect(
12811286
page.locator('.LexicalEditorTheme__block-blockInLexical .render-fields label.field-label'),
@@ -1293,12 +1298,15 @@ describe('lexicalBlocks', () => {
12931298
test('ensure individual inline blocks in lexical editor within a block have initial state on initial load', async () => {
12941299
await page.goto('http://localhost:3000/admin/collections/LexicalInBlock?limit=10')
12951300

1301+
// Wait for table to be fully loaded
1302+
await expect(page.locator('tbody tr')).not.toHaveCount(0)
1303+
12961304
await assertNetworkRequests(
12971305
page,
12981306
'/collections/LexicalInBlock/',
12991307
async () => {
1300-
await page.locator('.cell-id a').first().click()
1301-
await page.waitForURL(`**/collections/LexicalInBlock/**`)
1308+
await goToFirstCell(page, serverURL)
1309+
await waitForFormReady(page)
13021310

13031311
await expect(
13041312
page.locator('.LexicalEditorTheme__inlineBlock:has-text("Inline Block In Lexical")'),
@@ -1452,8 +1460,11 @@ describe('lexicalBlocks', () => {
14521460
test('ensure inline blocks restore their state after undoing a removal', async () => {
14531461
await page.goto('http://localhost:3000/admin/collections/LexicalInBlock?limit=10')
14541462

1455-
await page.locator('.cell-id a').first().click()
1456-
await page.waitForURL(`**/collections/LexicalInBlock/**`)
1463+
// Wait for table to be fully loaded
1464+
await expect(page.locator('tbody tr')).not.toHaveCount(0)
1465+
1466+
await goToFirstCell(page, serverURL)
1467+
await waitForFormReady(page)
14571468

14581469
// Wait for the page to be fully loaded and elements to be stable
14591470
await page.waitForLoadState('domcontentloaded')

test/lexical/collections/Lexical/e2e/main/e2e.spec.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ import {
2020
initPageConsoleErrorCatch,
2121
saveDocAndAssert,
2222
saveDocHotkeyAndAssert,
23-
throttleTest,
23+
waitForFormReady,
2424
} from '../../../../../__helpers/e2e/helpers.js'
25+
import { goToFirstCell } from '../../../../../__helpers/e2e/navigateToDoc.js'
2526
import { AdminUrlUtil } from '../../../../../__helpers/shared/adminUrlUtil.js'
26-
import { initPayloadE2ENoConfig } from '../../../../../__helpers/shared/initPayloadE2ENoConfig.js'
2727
import { reInitializeDB } from '../../../../../__helpers/shared/clearAndSeed/reInitializeDB.js'
28+
import { initPayloadE2ENoConfig } from '../../../../../__helpers/shared/initPayloadE2ENoConfig.js'
2829
import { RESTClient } from '../../../../../__helpers/shared/rest.js'
2930
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../../../../../playwright.config.js'
3031
import { lexicalFieldsSlug } from '../../../../slugs.js'
@@ -54,13 +55,12 @@ async function navigateToLexicalFields(
5455
await page.goto(url.list)
5556
}
5657

57-
const linkToDoc = page.locator('tbody tr:first-child a').first()
58-
await expect(() => expect(linkToDoc).toBeTruthy()).toPass({ timeout: POLL_TOPASS_TIMEOUT })
59-
const linkDocHref = await linkToDoc.getAttribute('href')
58+
// Wait for table to be fully loaded
59+
await expect(page.locator('tbody tr')).not.toHaveCount(0)
6060

61-
await linkToDoc.click()
62-
63-
await page.waitForURL(`**${linkDocHref}`)
61+
// Navigate to first document
62+
await goToFirstCell(page, serverURL)
63+
await waitForFormReady(page)
6464

6565
if (collectionSlug === 'lexical-fields') {
6666
const richTextField = page.locator('.rich-text-lexical').nth(2) // second
@@ -607,10 +607,12 @@ describe('lexicalMain', () => {
607607
)
608608

609609
// Click on button with class lexical-upload__upload-drawer-toggler
610-
await newUploadNode
610+
const drawerToggler = newUploadNode
611611
.locator('.LexicalEditorTheme__upload__upload-drawer-toggler')
612612
.first()
613-
.click()
613+
await drawerToggler.waitFor({ state: 'visible' })
614+
await drawerToggler.scrollIntoViewIfNeeded()
615+
await drawerToggler.click()
614616

615617
const uploadExtraFieldsDrawer = page
616618
.locator('dialog[id^=drawer_1_lexical-upload-drawer-]')
@@ -649,10 +651,12 @@ describe('lexicalMain', () => {
649651
await reloadedUploadNode.click() // Focus the upload node
650652
await reloadedUploadNode.hover()
651653

652-
await reloadedUploadNode
654+
const reloadedDrawerToggler = reloadedUploadNode
653655
.locator('.LexicalEditorTheme__upload__upload-drawer-toggler')
654656
.first()
655-
.click()
657+
await reloadedDrawerToggler.waitFor({ state: 'visible' })
658+
await reloadedDrawerToggler.scrollIntoViewIfNeeded()
659+
await reloadedDrawerToggler.click()
656660
const reloadedUploadExtraFieldsDrawer = page
657661
.locator('dialog[id^=drawer_1_lexical-upload-drawer-]')
658662
.first()
@@ -1409,7 +1413,14 @@ describe('lexicalMain', () => {
14091413
// https://github.com/payloadcms/payload/issues/5146
14101414
test('Preserve indent and text-align when converting Lexical <-> HTML', async () => {
14111415
await page.goto('http://localhost:3000/admin/collections/rich-text-fields?limit=10')
1412-
await page.getByLabel('Create new Rich Text Field').click()
1416+
1417+
await expect(page.locator('tbody tr').first()).toBeVisible()
1418+
1419+
const createButton = page.getByLabel('Create new Rich Text Field')
1420+
await expect(createButton).toBeEnabled()
1421+
const href = await createButton.getAttribute('href')
1422+
await page.goto(`${serverURL}${href}`)
1423+
await waitForFormReady(page)
14131424
await page.getByLabel('Title*').click()
14141425
await page.getByLabel('Title*').fill('Indent and Text-align')
14151426
await page.getByRole('paragraph').nth(1).click()
@@ -1438,8 +1449,12 @@ describe('lexicalMain', () => {
14381449
// Previously, we had the issue that the lexical field values did not update when moving blocks, as the MOVE_ROW form action did not request
14391450
// re-rendering of server components
14401451
await page.goto('http://localhost:3000/admin/collections/LexicalInBlock?limit=10')
1441-
await page.locator('.cell-id a').first().click()
1442-
await page.waitForURL(`**/collections/LexicalInBlock/**`)
1452+
1453+
// Wait for table to be fully loaded
1454+
await expect(page.locator('tbody tr')).not.toHaveCount(0)
1455+
1456+
await goToFirstCell(page, serverURL)
1457+
await waitForFormReady(page)
14431458

14441459
await expect(page.locator('#blocks-row-0 .LexicalEditorTheme__paragraph')).toContainText('1')
14451460
await expect(page.locator('#blocks-row-0 .section-title__input')).toHaveValue('1') // block name

test/lexical/collections/RichText/e2e.spec.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import {
99
ensureCompilationIsDone,
1010
initPageConsoleErrorCatch,
1111
saveDocAndAssert,
12+
waitForFormReady,
1213
} from '../../../__helpers/e2e/helpers.js'
14+
import { goToFirstCell } from '../../../__helpers/e2e/navigateToDoc.js'
1315
import { AdminUrlUtil } from '../../../__helpers/shared/adminUrlUtil.js'
14-
import { initPayloadE2ENoConfig } from '../../../__helpers/shared/initPayloadE2ENoConfig.js'
1516
import { reInitializeDB } from '../../../__helpers/shared/clearAndSeed/reInitializeDB.js'
17+
import { initPayloadE2ENoConfig } from '../../../__helpers/shared/initPayloadE2ENoConfig.js'
1618
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../../../playwright.config.js'
1719

1820
const filename = fileURLToPath(import.meta.url)
@@ -53,13 +55,12 @@ describe('Rich Text', () => {
5355
const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'rich-text-fields')
5456
await page.goto(url.list)
5557

56-
const linkToDoc = page.locator('.row-1 .cell-title a').first()
57-
await expect(() => expect(linkToDoc).toBeTruthy()).toPass({ timeout: POLL_TOPASS_TIMEOUT })
58-
const linkDocHref = await linkToDoc.getAttribute('href')
59-
60-
await linkToDoc.click()
58+
// Wait for table to be fully loaded
59+
await expect(page.locator('tbody tr')).not.toHaveCount(0)
6160

62-
await page.waitForURL(`**${linkDocHref}`)
61+
// Navigate to first document
62+
await goToFirstCell(page, serverURL)
63+
await waitForFormReady(page)
6364
}
6465

6566
describe('cell', () => {

test/lexical/payload-types.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export interface Config {
8484
blocks: {};
8585
collections: {
8686
'lexical-fully-featured': LexicalFullyFeatured;
87+
'lexical-autosave': LexicalAutosave;
8788
'lexical-link-feature': LexicalLinkFeature;
8889
'lexical-lists-features': LexicalListsFeature;
8990
'lexical-heading-feature': LexicalHeadingFeature;
@@ -111,6 +112,7 @@ export interface Config {
111112
collectionsJoins: {};
112113
collectionsSelect: {
113114
'lexical-fully-featured': LexicalFullyFeaturedSelect<false> | LexicalFullyFeaturedSelect<true>;
115+
'lexical-autosave': LexicalAutosaveSelect<false> | LexicalAutosaveSelect<true>;
114116
'lexical-link-feature': LexicalLinkFeatureSelect<false> | LexicalLinkFeatureSelect<true>;
115117
'lexical-lists-features': LexicalListsFeaturesSelect<false> | LexicalListsFeaturesSelect<true>;
116118
'lexical-heading-feature': LexicalHeadingFeatureSelect<false> | LexicalHeadingFeatureSelect<true>;
@@ -194,6 +196,37 @@ export interface LexicalFullyFeatured {
194196
updatedAt: string;
195197
createdAt: string;
196198
}
199+
/**
200+
* This interface was referenced by `Config`'s JSON-Schema
201+
* via the `definition` "lexical-autosave".
202+
*/
203+
export interface LexicalAutosave {
204+
id: string;
205+
title?: string | null;
206+
cta?:
207+
| {
208+
richText?: {
209+
root: {
210+
type: string;
211+
children: {
212+
type: any;
213+
version: number;
214+
[k: string]: unknown;
215+
}[];
216+
direction: ('ltr' | 'rtl') | null;
217+
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
218+
indent: number;
219+
version: number;
220+
};
221+
[k: string]: unknown;
222+
} | null;
223+
id?: string | null;
224+
}[]
225+
| null;
226+
updatedAt: string;
227+
createdAt: string;
228+
_status?: ('draft' | 'published') | null;
229+
}
197230
/**
198231
* This interface was referenced by `Config`'s JSON-Schema
199232
* via the `definition` "lexical-link-feature".
@@ -1049,6 +1082,10 @@ export interface PayloadLockedDocument {
10491082
relationTo: 'lexical-fully-featured';
10501083
value: string | LexicalFullyFeatured;
10511084
} | null)
1085+
| ({
1086+
relationTo: 'lexical-autosave';
1087+
value: string | LexicalAutosave;
1088+
} | null)
10521089
| ({
10531090
relationTo: 'lexical-link-feature';
10541091
value: string | LexicalLinkFeature;
@@ -1176,6 +1213,22 @@ export interface LexicalFullyFeaturedSelect<T extends boolean = true> {
11761213
updatedAt?: T;
11771214
createdAt?: T;
11781215
}
1216+
/**
1217+
* This interface was referenced by `Config`'s JSON-Schema
1218+
* via the `definition` "lexical-autosave_select".
1219+
*/
1220+
export interface LexicalAutosaveSelect<T extends boolean = true> {
1221+
title?: T;
1222+
cta?:
1223+
| T
1224+
| {
1225+
richText?: T;
1226+
id?: T;
1227+
};
1228+
updatedAt?: T;
1229+
createdAt?: T;
1230+
_status?: T;
1231+
}
11791232
/**
11801233
* This interface was referenced by `Config`'s JSON-Schema
11811234
* via the `definition` "lexical-link-feature_select".

0 commit comments

Comments
 (0)