Skip to content

[lexical-y][lexical-website] Bug Fix: Keep $initialEditorState out of documents that have content - #9508

Merged
zurfyx merged 1 commit into
mainfrom
zurfyx/y-initial-state-binding
Oct 29, 2026
Merged

zurfyx merged 1 commit into
mainfrom
zurfyx/y-initial-state-binding

Conversation

@zurfyx

@zurfyx zurfyx commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Note

Stack, part 5 of 10. Based on #9505 (zurfyx/y-undo-member-closer). Review only this PR's own commits.

The @lexical/y fixes touch the same writer, cursor, reader and binding internals, so they are one linear stack: each PR is based on the one below it, each conflict is resolved once, in the PR whose change causes it, and every CI run tests the real combination.

Merge order: bottom first, #9509 → #9523 → #9522 → #9505 → #9508 → #9502 → #9504 → #9527 → #9507 → #9510. With squash merges, rebase the next PR onto main before merging it (git rebase --onto main <old base branch>).

The stack
# PR Branch Base
1 #9509 zurfyx/update-error-rollback main
2 #9523 zurfyx/write-before-unread-change #9509
3 #9522 zurfyx/y-keep-chars-at-format-boundary #9523
4 #9505 zurfyx/y-undo-member-closer #9522
5 #9508 (this PR) zurfyx/y-initial-state-binding #9505
6 #9502 zurfyx/y-reorder-intent #9508
7 #9504 zurfyx/y-preserve-unknown-attrs #9502
8 #9527 zurfyx/y-move-keeps-unknown-fields #9504
9 #9507 zurfyx/y-composition-holdback #9527
10 #9510 zurfyx/y-incremental-read-marks #9507

Follow-ups for after the stack (not in it):

Description

An editor built with $initialEditorState (a template) and YCollabExtension
wrote the template into a Y.Doc that already had content: an editor built
again on the same doc (React StrictMode, or dispose and build again), a doc
loaded from a local cache before the editor, or synced: true. A template
that replaces the root wiped everyone's content; one that appends added a
copy on every mount.

YCollabExtension.register started the binding, which reads a loaded doc and
goes active at once. InitialStateExtension applies $initialEditorState
later, in afterRegistration, so the binding wrote it as an edit, and
initialize() then did nothing. synced: true initialized in register too:
on a new doc it seeded an empty root and then wrote the template as an edit,
so two clients opening an empty room got two copies, and an undo step.

sequenceDiagram
  participant B as buildEditorFromExtensions
  participant I as InitialStateExtension
  participant Y as YCollabExtension
  participant D as Y.Doc with content
  rect rgb(255, 235, 235)
  Note over B,D: Before
  B->>Y: register: binding.start() reads D, status active
  B->>I: afterRegistration: $initialEditorState
  I->>D: written as an edit (replace wipes, append duplicates)
  end
  rect rgb(232, 245, 233)
  Note over B,D: After
  B->>I: afterRegistration: $initialEditorState (editor only)
  B->>Y: afterRegistration: binding.start() reads D over it
  Note over D: nothing written
  end
Loading

The binding now starts in afterRegistration, after InitialStateExtension's,
which becomes an explicit dependency so the order holds by construction. The
extension API documents register as running before the editor's initial
state is set and afterRegistration as the place for code that must run
after it. What the editor holds when the binding starts is its initial
state: a doc with content replaces it and nothing is written, and only
initialize() writes it, into an empty doc, through the content-hashed seed,
so clients initializing the same content write one copy, now also with
synced: true. This covers every form of $initialEditorState (a function,
JSON, an EditorState), the default empty paragraph, and content any
extension writes in register.

Also:

  • initialize() called before the binding has started (from an extension's
    register, or a listener that runs while it starts, for example on a
    provider that has already synced) runs once it has, after the initial
    state is applied and the binding has checked the editor's configuration,
    instead of seeding an empty root at once.
  • initialize() commits a pending update before it looks at the document: a
    remote change that a listener of that commit applies is read (or, for an
    older layout's root, refused), not written over.
  • HMRExtension is declared as a peer dependency (ordering only). Its
    development reload restores the previous editor's state in
    afterRegistration; listed after YCollabExtension, that restore
    overwrote the shared document and deleted a peer's edit.

The other option was to let the binding take over $initialEditorState
(suppress InitialStateExtension's write and pass the initializer to
initialize()). That needs new API on InitialStateExtension, covers only
that one source of initial content, and leaves the editor blank until the
first sync. Starting the binding later applies its existing rule for the
content it finds when it starts, with no new API.

An extension that depends on YCollabExtension and changes content in its
own afterRegistration still makes an edit: it runs after the binding
starts, and such a change cannot be told apart from an intentional edit
made right after the build. The docs say so.

Unchanged: initialize() on an empty root that held items or attributes
before, or that sits inside another shared type, still writes under the
client's own id (the docs list it under Limits), so clients doing that at
once still write a copy each.

Behavior changes (the first three are in the changeset's Verify:):

  • Another extension's register sees the binding before it starts
    (status is 'pending', undoManager is null); on dispose, the binding
    stops before the other extensions' register cleanups run.
  • YBinding.initialize() called before start() runs when the binding
    starts, instead of writing at once.
  • An editor opened on an initialized document whose root is empty shows it
    empty instead of writing an empty paragraph as an edit; typing creates the
    paragraph.
  • The binding's own node transforms are registered after those that other
    extensions register in register, as they already were for extensions
    listed before it.

Restack (October 29)

Rebased onto #9505 with no conflicts. #9510, higher in the stack, brands bound documents (createYDoc) and migrates this PR's regressions/InitialState.test.tsx to it in the same commit; this PR keeps new Y.Doc(), which is what binds below #9510.

Changeset

.changeset/lexical-y-initial-state.md: @lexical/y patch, [fix], with a
Verify: paragraph.

Test plan

New packages/lexical-y/src/__tests__/unit/regressions/InitialState.test.tsx
(finding S7-23, 43 tests): every form of $initialEditorState on a remount,
a doc loaded from a cache, synced: true, and two editors bound at once; a
doc synced after the build; content written in register; a state
HMRExtension restores; five remounts with edits in between; React
StrictMode with LexicalExtensionComposer mounted five times; a doc whose
root is empty; initialize() called while extensions register or from a
listener while the binding starts (loaded doc, empty doc with two clients,
synced false and true, an option the binding cannot write with); a remote
change, or an older layout's root, applied during initialize()'s commit;
an undo step made during initialization reaching CAN_UNDO_COMMAND
listeners; two clients on an empty doc with synced false and true; a
seedYState doc opened by a client with a template; a seedYState seed of
the template merging with a client's initialization.

pnpm exec vitest run --project unit packages/lexical-y/src/__tests__/unit/regressions/InitialState.test.tsx

Before

On main (92d6af7):

AssertionError: expected 'paragraph("real content") / paragraph…' to be 'paragraph("real content")' // Object.is equality
Expected: "paragraph("real content")"
Received: "paragraph("real content") / paragraph("Untitled")"

AssertionError: expected 'paragraph("Untitled")' to be 'paragraph("real content")' // Object.is equality
Expected: "paragraph("real content")"
Received: "paragraph("Untitled")"

AssertionError: expected 'paragraph("Untitled") / paragraph("Un…' to be 'paragraph("Untitled")' // Object.is equality
Expected: "paragraph("Untitled")"
Received: "paragraph("Untitled") / paragraph("Untitled")"

      Tests  35 failed | 8 passed (43)
All 43 tests on main
     × a function that appends, remount on the same document: the editor shows the document and writes nothing
     × a function that appends, a document loaded from a cache before the editor: the editor shows the document and writes nothing
     × a function that appends, a loaded document with synced: true: the editor shows the document and writes nothing
     × a function that appends, two editors on the same document at once: the editor shows the document and writes nothing
     × a function that replaces, remount on the same document: the editor shows the document and writes nothing
     × a function that replaces, a document loaded from a cache before the editor: the editor shows the document and writes nothing
     × a function that replaces, a loaded document with synced: true: the editor shows the document and writes nothing
     × a function that replaces, two editors on the same document at once: the editor shows the document and writes nothing
     × JSON, remount on the same document: the editor shows the document and writes nothing
     × JSON, a document loaded from a cache before the editor: the editor shows the document and writes nothing
     × JSON, a loaded document with synced: true: the editor shows the document and writes nothing
     × JSON, two editors on the same document at once: the editor shows the document and writes nothing
     × an EditorState, remount on the same document: the editor shows the document and writes nothing
     × an EditorState, a document loaded from a cache before the editor: the editor shows the document and writes nothing
     × an EditorState, a loaded document with synced: true: the editor shows the document and writes nothing
     × an EditorState, two editors on the same document at once: the editor shows the document and writes nothing
     ✓ the document synced after the editor is built: the editor shows it and writes nothing
     × initialize() called while extensions register (a provider that has synced): the editor shows the document and writes nothing
     × a remote change applied while initialize() commits a pending update is read, not written
     × content of an older layout root that arrives while initialize() commits a pending update is refused, not written next to
     × content an extension writes while it registers is initial state too
     × a state HMRExtension restores is initial state too, wherever it is listed
     × remounted five times: nothing is duplicated or lost, and edits made in between are written
     × React StrictMode: LexicalExtensionComposer mounted five times on a loaded document
     × a document seeded with seedYState, synced before the editor is built: the editor shows the seed and writes nothing
     ✓ a document seeded with seedYState, synced after the editor is built: the editor shows the seed and writes nothing
     × a document whose root is empty: the editor shows it empty and writes nothing until an edit
     ✓ a function that appends, synced: false: two clients that open an empty document at once write it once
     × a function that appends, synced: true: two clients that open an empty document at once write it once
     ✓ a function that replaces, synced: false: two clients that open an empty document at once write it once
     × a function that replaces, synced: true: two clients that open an empty document at once write it once
     ✓ JSON, synced: false: two clients that open an empty document at once write it once
     × JSON, synced: true: two clients that open an empty document at once write it once
     ✓ an EditorState, synced: false: two clients that open an empty document at once write it once
     × an EditorState, synced: true: two clients that open an empty document at once write it once
     × initialize() called from an editable listener while the binding starts (a provider that syncs at once), synced: false: the initial state is written once, as a seed
     × initialize() called from an editable listener while the binding starts (a provider that syncs at once), synced: true: the initial state is written once, as a seed
     × initialize() called while extensions register (a provider that syncs at once), synced: false: the initial state is written once, as a seed
     × initialize() called while extensions register (a provider that syncs at once), synced: true: the initial state is written once, as a seed
     × an edit a listener makes in reply to the initialization is announced to CAN_UNDO_COMMAND listeners
     ✓ initialize() called while extensions register, with an option the binding cannot write with: read only, nothing written
     ✓ synced: false: a client that initializes an empty document with its template, and a seedYState seed of that template, merge into one copy
     × synced: true: a client that initializes an empty document with its template, and a seedYState seed of that template, merge into one copy

After

 ✓ |unit| packages/lexical-y/src/__tests__/unit/regressions/InitialState.test.tsx (43 tests) 1026ms
      Tests  43 passed (43)

The whole @lexical/y unit suite,
pnpm exec vitest run --project unit packages/lexical-y:

 Test Files  149 passed | 2 skipped (151)
      Tests  4956 passed | 16 expected fail | 2 skipped (4974)

pnpm run ci-check exits 0 (TypeScript, Flow Found 0 errors, Prettier,
ESLint, Changeset check passed: 6 pending changeset(s) valid.), and
scripts/__tests__/unit/treeShakingSource.test.ts passes for @lexical/y.

The collab Y e2e specs, pnpm run test-e2e-collab-y-chromium with
pnpm run start:y: 25 passed, 6 flaky. The flaky ones failed their first
attempt on a page or frame load timeout and passed on retry; main on the same
machine gives the same first-attempt timeouts (27 passed, 4 flaky).
CollabYSync.spec.mjs with --repeat-each=3 --retries=0: 33 passed.
Firefox and WebKit were not run.

@vercel

vercel Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
lexical Ready Ready Preview Oct 29, 2026 6:32pm UTC
lexical-playground Ready Ready Preview Oct 29, 2026 6:32pm UTC

Request Review

@zurfyx
zurfyx requested a review from etrepum as a code owner October 29, 2026 01:05
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 29, 2026
@pkg-pr-new

pkg-pr-new Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
lexical

npm i https://pkg.pr.new/lexical@9508

@lexical/a11y

npm i https://pkg.pr.new/@lexical/a11y@9508

@lexical/clipboard

npm i https://pkg.pr.new/@lexical/clipboard@9508

@lexical/code

npm i https://pkg.pr.new/@lexical/code@9508

@lexical/code-core

npm i https://pkg.pr.new/@lexical/code-core@9508

@lexical/code-prism

npm i https://pkg.pr.new/@lexical/code-prism@9508

@lexical/code-shiki

npm i https://pkg.pr.new/@lexical/code-shiki@9508

@lexical/compiler

npm i https://pkg.pr.new/@lexical/compiler@9508

@lexical/devtools-core

npm i https://pkg.pr.new/@lexical/devtools-core@9508

@lexical/dragon

npm i https://pkg.pr.new/@lexical/dragon@9508

@lexical/eslint-plugin

npm i https://pkg.pr.new/@lexical/eslint-plugin@9508

@lexical/extension

npm i https://pkg.pr.new/@lexical/extension@9508

@lexical/fast-check

npm i https://pkg.pr.new/@lexical/fast-check@9508

@lexical/file

npm i https://pkg.pr.new/@lexical/file@9508

@lexical/hashtag

npm i https://pkg.pr.new/@lexical/hashtag@9508

@lexical/headless

npm i https://pkg.pr.new/@lexical/headless@9508

@lexical/history

npm i https://pkg.pr.new/@lexical/history@9508

@lexical/html

npm i https://pkg.pr.new/@lexical/html@9508

@lexical/internal

npm i https://pkg.pr.new/@lexical/internal@9508

@lexical/link

npm i https://pkg.pr.new/@lexical/link@9508

@lexical/list

npm i https://pkg.pr.new/@lexical/list@9508

@lexical/mark

npm i https://pkg.pr.new/@lexical/mark@9508

@lexical/markdown

npm i https://pkg.pr.new/@lexical/markdown@9508

@lexical/mdast

npm i https://pkg.pr.new/@lexical/mdast@9508

@lexical/offset

npm i https://pkg.pr.new/@lexical/offset@9508

@lexical/overflow

npm i https://pkg.pr.new/@lexical/overflow@9508

@lexical/plain-text

npm i https://pkg.pr.new/@lexical/plain-text@9508

@lexical/react

npm i https://pkg.pr.new/@lexical/react@9508

@lexical/rich-text

npm i https://pkg.pr.new/@lexical/rich-text@9508

@lexical/selection

npm i https://pkg.pr.new/@lexical/selection@9508

@lexical/table

npm i https://pkg.pr.new/@lexical/table@9508

@lexical/tailwind

npm i https://pkg.pr.new/@lexical/tailwind@9508

@lexical/text

npm i https://pkg.pr.new/@lexical/text@9508

@lexical/utils

npm i https://pkg.pr.new/@lexical/utils@9508

@lexical/y

npm i https://pkg.pr.new/@lexical/y@9508

@lexical/yjs

npm i https://pkg.pr.new/@lexical/yjs@9508

commit: de27a67

@zurfyx
zurfyx force-pushed the zurfyx/y-initial-state-binding branch from 421e0ba to cda2d59 Compare October 29, 2026 05:32
@zurfyx
zurfyx changed the base branch from main to zurfyx/y-undo-member-closer October 29, 2026 05:33
@zurfyx
zurfyx force-pushed the zurfyx/y-initial-state-binding branch from cda2d59 to 70830bd Compare October 29, 2026 10:47
Comment on lines +112 to +115
const update = () => {
editor.dispatchCommand(CAN_UNDO_COMMAND, um.canUndo());
editor.dispatchCommand(CAN_REDO_COMMAND, um.canRedo());
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CAN_UNDO_COMMAND and CAN_REDO_COMMAND were deprecated in favor of HistoryExtension's canUndo/canRedo signals since following the commands easily leads to incoherent state. Probably the right course of action here is to move those signals up to a HistoryStateExtension that both YCollabExtension and HistoryExtension both write to.

@zurfyx
zurfyx force-pushed the zurfyx/y-undo-member-closer branch from e3bc5a3 to 603812b Compare October 29, 2026 18:12
… documents that have content

## Description

An editor built with `$initialEditorState` (a template) and `YCollabExtension`
wrote the template into a `Y.Doc` that already had content: an editor built
again on the same doc (React StrictMode, or dispose and build again), a doc
loaded from a local cache before the editor, or `synced: true`. A template
that replaces the root wiped everyone's content; one that appends added a
copy on every mount.

`YCollabExtension.register` started the binding, which reads a loaded doc and
goes active at once. `InitialStateExtension` applies `$initialEditorState`
later, in `afterRegistration`, so the binding wrote it as an edit, and
`initialize()` then did nothing. `synced: true` initialized in `register` too:
on a new doc it seeded an empty root and then wrote the template as an edit,
so two clients opening an empty room got two copies, and an undo step.

The binding now starts in `afterRegistration`, after `InitialStateExtension`'s,
which becomes an explicit dependency so the order holds by construction. The
extension API documents `register` as running before the editor's initial
state is set and `afterRegistration` as the place for code that must run
after it. What the editor holds when the binding starts is its initial
state: a doc with content replaces it and nothing is written, and only
`initialize()` writes it, into an empty doc, through the content-hashed seed,
so clients initializing the same content write one copy, now also with
`synced: true`. This covers every form of `$initialEditorState` (a function,
JSON, an `EditorState`), the default empty paragraph, and content any
extension writes in `register`.

Also:

- `initialize()` called before the binding has started (from an extension's
  `register`, or a listener that runs while it starts, for example on a
  provider that has already synced) runs once it has, after the initial
  state is applied and the binding has checked the editor's configuration,
  instead of seeding an empty root at once.
- `initialize()` commits a pending update before it looks at the document: a
  remote change that a listener of that commit applies is read (or, for an
  older layout's root, refused), not written over.
- `HMRExtension` is declared as a peer dependency (ordering only). Its
  development reload restores the previous editor's state in
  `afterRegistration`; listed after `YCollabExtension`, that restore
  overwrote the shared document and deleted a peer's edit.

The other option was to let the binding take over `$initialEditorState`
(suppress `InitialStateExtension`'s write and pass the initializer to
`initialize()`). That needs new API on `InitialStateExtension`, covers only
that one source of initial content, and leaves the editor blank until the
first sync. Starting the binding later applies its existing rule for the
content it finds when it starts, with no new API.

An extension that depends on `YCollabExtension` and changes content in its
own `afterRegistration` still makes an edit: it runs after the binding
starts, and such a change cannot be told apart from an intentional edit
made right after the build. The docs say so.

Unchanged: `initialize()` on an empty root that held items or attributes
before, or that sits inside another shared type, still writes under the
client's own id (the docs list it under Limits), so clients doing that at
once still write a copy each.

Behavior changes (the first three are in the changeset's `Verify:`):

- Another extension's `register` sees the binding before it starts
  (`status` is `'pending'`, `undoManager` is `null`); on dispose, the binding
  stops before the other extensions' `register` cleanups run.
- `YBinding.initialize()` called before `start()` runs when the binding
  starts, instead of writing at once.
- An editor opened on an initialized document whose root is empty shows it
  empty instead of writing an empty paragraph as an edit; typing creates the
  paragraph.
- The binding's own node transforms are registered after those that other
  extensions register in `register`, as they already were for extensions
  listed before it.

## Changeset

`.changeset/lexical-y-initial-state.md`: `@lexical/y` patch, `[fix]`, with a
`Verify:` paragraph.

## Test plan

New `packages/lexical-y/src/__tests__/unit/regressions/InitialState.test.tsx`
(finding S7-23, 43 tests): every form of `$initialEditorState` on a remount,
a doc loaded from a cache, `synced: true`, and two editors bound at once; a
doc synced after the build; content written in `register`; a state
`HMRExtension` restores; five remounts with edits in between; React
StrictMode with `LexicalExtensionComposer` mounted five times; a doc whose
root is empty; `initialize()` called while extensions register or from a
listener while the binding starts (loaded doc, empty doc with two clients,
`synced` false and true, an option the binding cannot write with); a remote
change, or an older layout's root, applied during `initialize()`'s commit;
an undo step made during initialization reaching `CAN_UNDO_COMMAND`
listeners; two clients on an empty doc with `synced` false and true; a
`seedYState` doc opened by a client with a template; a `seedYState` seed of
the template merging with a client's initialization.

`pnpm exec vitest run --project unit packages/lexical-y/src/__tests__/unit/regressions/InitialState.test.tsx`

### Before

On main (92d6af7):

```
     × a function that appends, remount on the same document: the editor shows the document and writes nothing
     × a function that appends, a document loaded from a cache before the editor: the editor shows the document and writes nothing
     × a function that appends, a loaded document with synced: true: the editor shows the document and writes nothing
     × a function that appends, two editors on the same document at once: the editor shows the document and writes nothing
     × a function that replaces, remount on the same document: the editor shows the document and writes nothing
     × a function that replaces, a document loaded from a cache before the editor: the editor shows the document and writes nothing
     × a function that replaces, a loaded document with synced: true: the editor shows the document and writes nothing
     × a function that replaces, two editors on the same document at once: the editor shows the document and writes nothing
     × JSON, remount on the same document: the editor shows the document and writes nothing
     × JSON, a document loaded from a cache before the editor: the editor shows the document and writes nothing
     × JSON, a loaded document with synced: true: the editor shows the document and writes nothing
     × JSON, two editors on the same document at once: the editor shows the document and writes nothing
     × an EditorState, remount on the same document: the editor shows the document and writes nothing
     × an EditorState, a document loaded from a cache before the editor: the editor shows the document and writes nothing
     × an EditorState, a loaded document with synced: true: the editor shows the document and writes nothing
     × an EditorState, two editors on the same document at once: the editor shows the document and writes nothing
     ✓ the document synced after the editor is built: the editor shows it and writes nothing
     × initialize() called while extensions register (a provider that has synced): the editor shows the document and writes nothing
     × a remote change applied while initialize() commits a pending update is read, not written
     × content of an older layout root that arrives while initialize() commits a pending update is refused, not written next to
     × content an extension writes while it registers is initial state too
     × a state HMRExtension restores is initial state too, wherever it is listed
     × remounted five times: nothing is duplicated or lost, and edits made in between are written
     × React StrictMode: LexicalExtensionComposer mounted five times on a loaded document
     × a document seeded with seedYState, synced before the editor is built: the editor shows the seed and writes nothing
     ✓ a document seeded with seedYState, synced after the editor is built: the editor shows the seed and writes nothing
     × a document whose root is empty: the editor shows it empty and writes nothing until an edit
     ✓ a function that appends, synced: false: two clients that open an empty document at once write it once
     × a function that appends, synced: true: two clients that open an empty document at once write it once
     ✓ a function that replaces, synced: false: two clients that open an empty document at once write it once
     × a function that replaces, synced: true: two clients that open an empty document at once write it once
     ✓ JSON, synced: false: two clients that open an empty document at once write it once
     × JSON, synced: true: two clients that open an empty document at once write it once
     ✓ an EditorState, synced: false: two clients that open an empty document at once write it once
     × an EditorState, synced: true: two clients that open an empty document at once write it once
     × initialize() called from an editable listener while the binding starts (a provider that syncs at once), synced: false: the initial state is written once, as a seed
     × initialize() called from an editable listener while the binding starts (a provider that syncs at once), synced: true: the initial state is written once, as a seed
     × initialize() called while extensions register (a provider that syncs at once), synced: false: the initial state is written once, as a seed
     × initialize() called while extensions register (a provider that syncs at once), synced: true: the initial state is written once, as a seed
     × an edit a listener makes in reply to the initialization is announced to CAN_UNDO_COMMAND listeners
     ✓ initialize() called while extensions register, with an option the binding cannot write with: read only, nothing written
     ✓ synced: false: a client that initializes an empty document with its template, and a seedYState seed of that template, merge into one copy
     × synced: true: a client that initializes an empty document with its template, and a seedYState seed of that template, merge into one copy

AssertionError: expected 'paragraph("real content") / paragraph…' to be 'paragraph("real content")' // Object.is equality
Expected: "paragraph("real content")"
Received: "paragraph("real content") / paragraph("Untitled")"

AssertionError: expected 'paragraph("Untitled")' to be 'paragraph("real content")' // Object.is equality
Expected: "paragraph("real content")"
Received: "paragraph("Untitled")"

AssertionError: expected 'paragraph("Untitled") / paragraph("Un…' to be 'paragraph("Untitled")' // Object.is equality
Expected: "paragraph("Untitled")"
Received: "paragraph("Untitled") / paragraph("Untitled")"

      Tests  35 failed | 8 passed (43)
```

### After

```
 ✓ |unit| packages/lexical-y/src/__tests__/unit/regressions/InitialState.test.tsx (43 tests) 1026ms
      Tests  43 passed (43)
```

The whole `@lexical/y` unit suite,
`pnpm exec vitest run --project unit packages/lexical-y`:

```
 Test Files  149 passed | 2 skipped (151)
      Tests  4956 passed | 16 expected fail | 2 skipped (4974)
```

`pnpm run ci-check` exits 0 (TypeScript, Flow `Found 0 errors`, Prettier,
ESLint, `Changeset check passed: 6 pending changeset(s) valid.`), and
`scripts/__tests__/unit/treeShakingSource.test.ts` passes for `@lexical/y`.

The collab Y e2e specs, `pnpm run test-e2e-collab-y-chromium` with
`pnpm run start:y`: `25 passed`, `6 flaky`. The flaky ones failed their first
attempt on a page or frame load timeout and passed on retry; main on the same
machine gives the same first-attempt timeouts (`27 passed`, `4 flaky`).
`CollabYSync.spec.mjs` with `--repeat-each=3 --retries=0`: `33 passed`.
Firefox and WebKit were not run.
@zurfyx
zurfyx force-pushed the zurfyx/y-initial-state-binding branch from 70830bd to de27a67 Compare October 29, 2026 18:30
@zurfyx
zurfyx changed the base branch from zurfyx/y-undo-member-closer to main October 29, 2026 18:31
@zurfyx
zurfyx added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit 82bef09 Oct 29, 2026
57 of 65 checks passed

This branch was successfully deployed

2 active deployments
Preview – lexical — de27a67c Deployed Oct 29, 2026 by vercel[bot]
Preview – lexical-playground — de27a67c Deployed Oct 29, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants