diff --git a/packages/lexical-history/src/__tests__/unit/SharedHistoryMaxDepth.test.ts b/packages/lexical-history/src/__tests__/unit/SharedHistoryMaxDepth.test.ts new file mode 100644 index 00000000000..0f1b316c711 --- /dev/null +++ b/packages/lexical-history/src/__tests__/unit/SharedHistoryMaxDepth.test.ts @@ -0,0 +1,99 @@ +/** + * 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 { + buildEditorFromExtensions, + getExtensionDependencyFromEditor, +} from '@lexical/extension'; +import {HistoryExtension, SharedHistoryExtension} from '@lexical/history'; +import {configExtension} from 'lexical'; +import {describe, expect, test} from 'vitest'; + +const PARENT_MAX_DEPTH = 7; +const PARENT_DELAY = 123; + +function buildParent() { + return buildEditorFromExtensions({ + dependencies: [ + configExtension(HistoryExtension, { + delay: PARENT_DELAY, + maxDepth: PARENT_MAX_DEPTH, + }), + ], + name: 'shared-max-depth-parent', + }); +} + +function buildChild() { + return buildEditorFromExtensions({ + dependencies: [SharedHistoryExtension], + name: 'shared-max-depth-child', + }); +} + +describe('SharedHistoryExtension forwards the parent history signals', () => { + test('the child adopts the parent maxDepth', () => { + using parent = buildParent(); + using child = buildChild(); + + const shared = getExtensionDependencyFromEditor( + child, + SharedHistoryExtension, + ); + shared.output.parentEditor.value = parent; + + const parentHistory = getExtensionDependencyFromEditor( + parent, + HistoryExtension, + ); + const childHistory = getExtensionDependencyFromEditor( + child, + HistoryExtension, + ); + + // Premise: the parent really carries the configured cap, and the child's + // own default differs from it. + expect(parentHistory.output.maxDepth.peek()).toBe(PARENT_MAX_DEPTH); + + // The child pushes onto the parent's shared undoStack, so it has to apply + // the parent's cap rather than its own default. + expect(childHistory.output.maxDepth.peek()).toBe(PARENT_MAX_DEPTH); + }); + + test('the already-forwarded signals still arrive', () => { + using parent = buildParent(); + using child = buildChild(); + + const shared = getExtensionDependencyFromEditor( + child, + SharedHistoryExtension, + ); + shared.output.parentEditor.value = parent; + + const parentHistory = getExtensionDependencyFromEditor( + parent, + HistoryExtension, + ); + const childHistory = getExtensionDependencyFromEditor( + child, + HistoryExtension, + ); + const keys = [ + 'delay', + 'historyState', + 'now', + 'maxDepth', + 'disabled', + ] as const; + expect( + Object.fromEntries(keys.map(k => [k, childHistory.output[k].peek()])), + ).toEqual( + Object.fromEntries(keys.map(k => [k, parentHistory.output[k].peek()])), + ); + }); +}); diff --git a/packages/lexical-history/src/index.ts b/packages/lexical-history/src/index.ts index 52bbd6fe875..e51e0dea7ff 100644 --- a/packages/lexical-history/src/index.ts +++ b/packages/lexical-history/src/index.ts @@ -660,10 +660,11 @@ interface HistoryExtensionInit { /** * The output signals exposed by {@link HistoryExtension}. * - * Config-derived signals (`delay`, `disabled`, `historyState`, `now`) are - * writable so that peer extensions such as {@link SharedHistoryExtension} can - * redirect them at runtime. The `canUndo` / `canRedo` signals are - * **readonly** for consumers — they are derived from the current + * Config-derived signals (`delay`, `disabled`, `historyState`, `maxDepth`, + * `now`) are writable so that peer extensions such as + * {@link SharedHistoryExtension} can redirect them at runtime. + * The `canUndo` / `canRedo` signals are **readonly** for + * consumers — they are derived from the current * {@link HistoryState} and kept in sync automatically. */ export interface HistoryExtensionOutput { @@ -819,6 +820,10 @@ export const SharedHistoryExtension = /* @__PURE__ */ defineExtension({ output.delay.value = parentOutput.delay.value; output.historyState.value = parentOutput.historyState.value; output.now.value = parentOutput.now.value; + // The cap must come from the parent too: the child pushes onto the + // parent's shared undoStack, so applying the child's own (default + // null) maxDepth would silently void the limit the app configured. + output.maxDepth.value = parentOutput.maxDepth.value; // Note that toggling the parent history will force this to be changed output.disabled.value = parentOutput.disabled.value; }); diff --git a/packages/lexical-playground/__tests__/e2e/ShadowDOM.spec.mjs b/packages/lexical-playground/__tests__/e2e/ShadowDOM.spec.mjs index 7e50b06c1b4..2d4230be45e 100644 --- a/packages/lexical-playground/__tests__/e2e/ShadowDOM.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/ShadowDOM.spec.mjs @@ -503,10 +503,10 @@ test.describe('Shadow DOM', () => { const t0 = Date.now(); await page.keyboard.type('a'.repeat(1000), {delay: 0}); const elapsed = Date.now() - t0; - // 60s is loose enough for CI Firefox (which has hit ~40s on + // Loose enough for CI macOS webkit (which has hit ~68s on // shared runners) but tight enough to still flag a real regression // — locally this run is around 1–2s. - expect(elapsed).toBeLessThan(60_000); + expect(elapsed).toBeLessThan(100_000); const text = await page .locator('div[contenteditable="true"]') .first()