Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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()])),
);
Comment thread
etrepum marked this conversation as resolved.
});
});
13 changes: 9 additions & 4 deletions packages/lexical-history/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading