Repository navigation
[lexical-y] Chore: Cover the schema version gate's remaining paths, and update stale test comments #9537
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[lexical-y] Chore: Cover the schema version gate's remaining paths, and update stale test comments #9537
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,7 @@ import {afterEach, describe, expect, test, vi} from 'vitest'; | |
|
|
||
| import { | ||
| createYDoc, | ||
| RECOVERED_READ_WARNING, | ||
| UNSUPPORTED_EDIT_WARNING, | ||
| UNSUPPORTED_SCHEMA_WARNING, | ||
| YBinding, | ||
|
|
@@ -49,6 +50,7 @@ import { | |
| seedYState, | ||
| YHeadlessError, | ||
| } from '../../headless'; | ||
| import {Reader} from '../../internal/Read'; | ||
| import {$p, NODES} from '../utils/harness'; | ||
|
|
||
| const SCHEMA = 'lexical-y-schema:'; | ||
|
|
@@ -444,6 +446,45 @@ describe('schema version gate: timing', () => { | |
| expect(Y.encodeStateVector(older.doc)).toEqual(before); | ||
| }); | ||
|
|
||
| test('local updates still pending past the commit limit: the read does not run, and the recovery gates', async () => { | ||
| const caught = catchMicrotasks(); | ||
| const older = seeded(1, 'ab'); | ||
| const newer = client(2); | ||
| sync(older, newer); | ||
| newer.binding.initialize(); | ||
| type(newer, 'd'); | ||
| await new Promise(resolve => setTimeout(resolve, 0)); | ||
| // More pending commits than the read waits for: it gives up before it | ||
| // runs (its `before` never gates), and only the recovery can. | ||
| let left = 40; | ||
| older.editor.registerUpdateListener(({tags}) => { | ||
| if (left > 0 && !tags.has('collaboration')) { | ||
| const n = left--; | ||
| older.editor.update(() => void $getRoot().append($p(`q${n}`))); | ||
| throw new Error(`app failure ${n}`); | ||
| } | ||
| }); | ||
| older.editor.update(() => void $getRoot().append($p('first'))); | ||
| Y.applyUpdate( | ||
| older.doc, | ||
| Y.encodeStateAsUpdate(newer.doc, Y.encodeStateVector(older.doc)), | ||
| ); | ||
| await Promise.resolve(); | ||
| expect(caught.length).toBeGreaterThan(0); | ||
| expect(older.binding.status).toBe('readonly'); | ||
| // The recovery read gated, not a fail-closed path (which warns about | ||
| // what it cannot read instead of a recovered read). | ||
| expect(codes(older)).toEqual([ | ||
| RECOVERED_READ_WARNING, | ||
| UNSUPPORTED_SCHEMA_WARNING, | ||
| ]); | ||
| const before = Y.encodeStateVector(older.doc); | ||
| older.editor.update(() => void $getRoot().append($p('late')), { | ||
| discrete: true, | ||
| }); | ||
| expect(Y.encodeStateVector(older.doc)).toEqual(before); | ||
| }); | ||
|
|
||
| test('a record that arrives alone while an update runs: the update is written, then read only', () => { | ||
| const older = seeded(1, 'ab'); | ||
| const update = recordOnly(older.doc, SCHEMA + '2'); | ||
|
|
@@ -566,6 +607,85 @@ describe('schema version gate: timing', () => { | |
| }); | ||
| expect(records(v2.doc)).toEqual([SCHEMA + '1', SCHEMA + '2']); | ||
| }); | ||
|
|
||
| test('a record that arrives while an update runs that then throws: later updates are refused', () => { | ||
| const older = seeded(1, 'ab'); | ||
| const update = recordOnly(older.doc, SCHEMA + '2'); | ||
| expect(() => | ||
| older.editor.update( | ||
| () => { | ||
| const t = $getRoot().getFirstDescendant() as TextNode; | ||
| t.setTextContent('X' + t.getTextContent()); | ||
| Y.applyUpdate(older.doc, update); | ||
| throw new Error('app failure'); | ||
| }, | ||
| {discrete: true}, | ||
| ), | ||
| ).toThrow('app failure'); | ||
| // The update the gate waited for never committed: the next one is not | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI: this test reaches |
||
| // written, and the binding is read only. | ||
| const before = Y.encodeStateVector(older.doc); | ||
| type(older, 'c'); | ||
| expect(Y.encodeStateVector(older.doc)).toEqual(before); | ||
| expect(older.binding.status).toBe('readonly'); | ||
| expect(codes(older)).toContain(UNSUPPORTED_SCHEMA_WARNING); | ||
| expect(text(older)).toBe('ab'); | ||
| }); | ||
|
|
||
| test('a binding the schema gate holds stays read only when the layout record changes', () => { | ||
| const older = seeded(1, 'ab'); | ||
| Y.applyUpdate(older.doc, recordOnly(older.doc, SCHEMA + '2')); | ||
| expect(older.binding.status).toBe('readonly'); | ||
| const editable: boolean[] = []; | ||
| older.editor.registerEditableListener(value => { | ||
| editable.push(value); | ||
| }); | ||
| // A newer client records layout 2 (its first set-valued member does). | ||
| Y.applyUpdate(older.doc, recordOnly(older.doc, 'lexical-y:2')); | ||
| expect(older.binding.status).toBe('readonly'); | ||
| expect(editable).toEqual([]); | ||
| expect(older.editor.isEditable()).toBe(false); | ||
| // Warned once, when the gate closed. | ||
| expect(codes(older)).toEqual([UNSUPPORTED_SCHEMA_WARNING]); | ||
| const before = Y.encodeStateVector(older.doc); | ||
| type(older, 'c'); | ||
| expect(Y.encodeStateVector(older.doc)).toEqual(before); | ||
| }); | ||
|
|
||
| test('a newer record in a change whose read throws: the recovery gates', () => { | ||
| const older = seeded(1, 'ab'); | ||
| const newer = client(2); | ||
| sync(older, newer); | ||
| newer.binding.initialize(); | ||
| type(newer, 'd'); | ||
| const proto = Reader.prototype as unknown as { | ||
| $apply: (...args: unknown[]) => void; | ||
| }; | ||
| const apply = proto.$apply; | ||
| proto.$apply = function () { | ||
| throw new Error('simulated reader failure'); | ||
| }; | ||
| try { | ||
| Y.applyUpdate( | ||
| older.doc, | ||
| Y.encodeStateAsUpdate(newer.doc, Y.encodeStateVector(older.doc)), | ||
| ); | ||
| } finally { | ||
| proto.$apply = apply; | ||
| } | ||
| // Gated before the read; the recovery gates again (a no-op here). | ||
| expect(codes(older)).toEqual([ | ||
| UNSUPPORTED_SCHEMA_WARNING, | ||
| RECOVERED_READ_WARNING, | ||
| ]); | ||
| expect(older.binding.status).toBe('readonly'); | ||
| // Rebuilt from the document, with the newer client's edit. | ||
| expect(text(older)).toBe('abd'); | ||
| const before = Y.encodeStateVector(older.doc); | ||
| type(older, 'e'); | ||
| expect(Y.encodeStateVector(older.doc)).toEqual(before); | ||
| expect(text(older)).toBe('abd'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('schema version: headless', () => { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional: the test just above ("a recovery that first writes many pending local updates still gates", lines 408-447) uses 20 listener updates, which its read commits in full before gating in
before. It never reaches the recovery its name and its comment at lines 417-418 describe, and it keeps passing without the gate atBinding.ts:2530; with this scenario the read only gives up from 32 updates. Renaming it (for example "a read that first commits many pending local updates still gates") and fixing that comment would match what this comment says.