From 0e762abddfe4f6d179b9c9af7df058dbd0d8ebf7 Mon Sep 17 00:00:00 2001 From: Bijin A B Date: Tue, 12 May 2026 13:03:03 +0530 Subject: [PATCH] chore: fixes --- .../codeeditor-state/fold-persistence.spec.ts | 54 ++++++++++++++----- tests/utils/page/actions.ts | 10 ++++ 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/tests/codeeditor-state/fold-persistence.spec.ts b/tests/codeeditor-state/fold-persistence.spec.ts index 1630dcf89..8fa6b0c91 100644 --- a/tests/codeeditor-state/fold-persistence.spec.ts +++ b/tests/codeeditor-state/fold-persistence.spec.ts @@ -533,24 +533,50 @@ test.describe('CodeEditor — undo (Cmd-Z) survives a tab switch', () => { await selectBodyMode(page, 'JSON'); await setBodyContent(page, SAMPLE_BODY); - // Insert all three sentinels in one `evaluate` (no `await` between calls): + // Insert all three sentinels with three distinct CM history entries + // (preserved by the `*`-prefixed origins) while ensuring the React + // wrapper sees only ONE onChange. The wrapper's `_onEdit` listener + // dispatches `updateRequestBody` on every `change` event; on slow + // runners three rapid dispatches don't always batch, and an + // intermediate re-render with a stale `props.value` can trigger + // `componentDidUpdate`'s `setValue(props.value)` path, wiping a + // just-inserted sentinel. We detach `change` listeners for the + // duration of the three `replaceRange`s, restore them after, then + // fire ONE synthetic change so the wrapper dispatches once with the + // final value — leaving editor content and redux state in sync before + // any downstream tab-switch reads from `props.value`. await cmFor(page, page.locator('.request-pane')).evaluate((el) => { const editor = (el as any).CodeMirror; editor.focus(); const doc = editor.getDoc(); - const append = (sentinel: string, originSuffix: string) => { - const lastLine = doc.lastLine(); - const lastLineLen = doc.getLine(lastLine).length; - doc.replaceRange( - `\n${sentinel}`, - { line: lastLine, ch: lastLineLen }, - undefined, - `*${originSuffix}` - ); - }; - append('// SENTINEL_ONE', 'sentinel-1'); - append('// SENTINEL_TWO', 'sentinel-2'); - append('// SENTINEL_THREE', 'sentinel-3'); + // CM5 stores listeners in an internal `_handlers` map on the editor. + // Save and clear the `change` slot, do the inserts, restore, then + // fire one synthetic change to flush the final value through onEdit. + const handlersSlot = editor._handlers || (editor._handlers = {}); + const savedChange = (handlersSlot.change || []).slice(); + handlersSlot.change = []; + try { + const append = (sentinel: string, originSuffix: string) => { + const lastLine = doc.lastLine(); + const lastLineLen = doc.getLine(lastLine).length; + doc.replaceRange( + `\n${sentinel}`, + { line: lastLine, ch: lastLineLen }, + undefined, + `*${originSuffix}` + ); + }; + append('// SENTINEL_ONE', 'sentinel-1'); + append('// SENTINEL_TWO', 'sentinel-2'); + append('// SENTINEL_THREE', 'sentinel-3'); + } finally { + handlersSlot.change = savedChange; + } + // `_onEdit` only reads `editor.getValue()`; the change descriptor + // arg is unused, so passing null is safe. + savedChange.forEach((handler: (cm: unknown, change: unknown) => void) => { + handler(editor, null); + }); }); const cm = cmFor(page, page.locator('.request-pane')); diff --git a/tests/utils/page/actions.ts b/tests/utils/page/actions.ts index 78528433a..873bd09b3 100644 --- a/tests/utils/page/actions.ts +++ b/tests/utils/page/actions.ts @@ -839,6 +839,12 @@ const setResponsePreviewMode = async (page: Page, mode: 'editor' | 'preview') => const dropdown = page.getByTestId('format-response-tab-dropdown'); await dropdown.waitFor({ state: 'visible', timeout: 5000 }); const toggle = page.getByTestId('preview-response-tab'); + // The toggle's `title` reflects current state (`Turn off|on Preview Mode`). + // Wait until it's actually one of those values — `getAttribute` returns + // `null` if read before React flushes props to DOM, which would mislead + // the state check below into thinking we're already in editor mode and + // skip the toggle click, leaving us stuck in preview. + await expect(toggle).toHaveAttribute('title', /^Turn (off|on) Preview Mode$/); const isPreview = (await toggle.getAttribute('title')) === 'Turn off Preview Mode'; const wantPreview = mode === 'preview'; if (isPreview !== wantPreview) { @@ -848,6 +854,10 @@ const setResponsePreviewMode = async (page: Page, mode: 'editor' | 'preview') => // interactions (format selection, asserts) aren't shadowed by it. await responseFormatTab.click(); } + // Confirm the dropdown actually closed before returning. Otherwise a + // subsequent format-selector click can land in a half-open state and + // miss the next interaction. + await dropdown.waitFor({ state: 'hidden', timeout: 5000 }); }; /**