I am trying to write some unit tests to simulate or document lifecycle. I have these states I am trying:
1. Initial State
2. Sync with Server: Snapshot 1
3. Send to Client with Offline Edit
4. Out of Server Band Edit: Snapshot 2
5. Sync with Server: Snapshot 3
6. Revert to Snapshot 1
7. Revert to Snapshot 2
8. Revert to Snapshot 3
Here is my test code:
import { expect } from 'chai';
import * as Y from 'yjs';
describe('Full Stack Interaction', () => {
describe.only('Snapshots', () => {
const mapKey = 'Data';
const creatorUserId = '123';
const serviceUserId = '456';
const clientUserId = '789';
let initialState: Uint8Array;
let serverState: Uint8Array;
let clientState: Uint8Array;
let neverGarbageCollectedState: Uint8Array;
let snapshot1: Y.Snapshot;
let snapshot2: Y.Snapshot;
let snapshot3: Y.Snapshot;
it('1. Initial State', () => {
// Initialize
const result = new Y.Doc();
const permanentUserDataCreator = new Y.PermanentUserData(result);
permanentUserDataCreator.setUserMapping(result, result.clientID, creatorUserId);
// Edit
const taskGroups = result.getMap(mapKey);
const task1 = new Y.Map<string>([
['id', 'task_1'],
['label', 'I am Task 1'],
['sortWeight', 'M']
]);
const task2 = new Y.Map<string>([
['id', 'task_2'],
['label', 'I am Task 2'],
['sortWeight', 'N']
]);
taskGroups.set('task_1', task1);
taskGroups.set('task_2', task2);
// Save
initialState = Y.encodeStateAsUpdate(result);
// Assert
expect(result.getMap(mapKey).toJSON()).to.deep.equal({
task_1: { id: 'task_1', label: 'I am Task 1', sortWeight: 'M' },
task_2: { id: 'task_2', label: 'I am Task 2', sortWeight: 'N' }
});
});
it('2. Sync with Server: Snapshot 1', () => {
// Initialize
const serviceYDoc = new Y.Doc();
Y.applyUpdate(serviceYDoc, initialState);
// Save
serverState = Y.encodeStateAsUpdate(serviceYDoc);
// Save Snapshot
const neverCollectedYDoc = new Y.Doc({ gc: false});
Y.applyUpdate(neverCollectedYDoc, initialState);
Y.applyUpdate(neverCollectedYDoc, serverState);
snapshot1 = Y.snapshot(neverCollectedYDoc);
neverGarbageCollectedState = Y.encodeStateAsUpdate(neverCollectedYDoc);
// Assert
expect(serviceYDoc.getMap(mapKey).toJSON()).to.deep.equal({
task_1: { id: 'task_1', label: 'I am Task 1', sortWeight: 'M' },
task_2: { id: 'task_2', label: 'I am Task 2', sortWeight: 'N' }
});
expect(serverState).to.deep.equal(initialState);
});
it('3. Send to Client with Offline Edit', () => {
// Initialize
const clientYDoc = new Y.Doc();
const permanentUserDataConduit = new Y.PermanentUserData(clientYDoc);
permanentUserDataConduit.setUserMapping(clientYDoc, clientYDoc.clientID, clientUserId);
Y.applyUpdate(clientYDoc, serverState);
// Edit
(clientYDoc.getMap(mapKey).get('task_1')! as Y.Map<string>).set('label', 'Task 1 Label Updated');
(clientYDoc.getMap(mapKey).get('task_2')! as Y.Map<string>).set('label', 'Task 2 Label Updated');
// Save
clientState = Y.encodeStateAsUpdate(clientYDoc);
// Assert
expect(clientYDoc.getMap(mapKey).toJSON()).to.deep.equal({
task_1: { id: 'task_1', label: 'Task 1 Label Updated', sortWeight: 'M' },
task_2: { id: 'task_2', label: 'Task 2 Label Updated', sortWeight: 'N' }
});
expect(serverState).to.deep.equal(initialState);
expect(clientState).to.not.deep.equal(initialState);
expect(clientState).to.not.deep.equal(serverState);
});
it('4. Out of Server Band Edit: Snapshot 2', () => {
// Initialize
const serviceYDoc = new Y.Doc();
const permanentUserDataConduit = new Y.PermanentUserData(serviceYDoc);
permanentUserDataConduit.setUserMapping(serviceYDoc, serviceYDoc.clientID, serviceUserId);
Y.applyUpdate(serviceYDoc, serverState);
// Edit
(serviceYDoc.getMap(mapKey).get('task_2')! as Y.Map<string>).set('sortWeight', 'B');
// Save
serverState = Y.encodeStateAsUpdate(serviceYDoc);
// Snapshot
const neverCollectedYDoc = new Y.Doc({ gc: false});
Y.applyUpdate(neverCollectedYDoc, neverGarbageCollectedState);
Y.applyUpdate(neverCollectedYDoc, serverState);
snapshot2 = Y.snapshot(neverCollectedYDoc);
neverGarbageCollectedState = Y.encodeStateAsUpdate(neverCollectedYDoc);
// Assert
expect(serviceYDoc.getMap(mapKey).toJSON()).to.deep.equal({
task_1: { id: 'task_1', label: 'I am Task 1', sortWeight: 'M' },
task_2: { id: 'task_2', label: 'I am Task 2', sortWeight: 'B' }
});
expect(serverState).to.not.deep.equal(initialState);
expect(clientState).to.not.deep.equal(initialState);
expect(clientState).to.not.deep.equal(serverState);
expect(snapshot1).to.not.deep.equal(snapshot2);
});
it('5. Sync with Server: Snapshot 3', () => {
// Sync
const serviceYDoc = new Y.Doc();
Y.applyUpdate(serviceYDoc, serverState);
Y.applyUpdate(serviceYDoc, clientState);
const clientYDoc = new Y.Doc();
Y.applyUpdate(clientYDoc, clientState);
Y.applyUpdate(clientYDoc, serverState);
// Save
clientState = Y.encodeStateAsUpdate(clientYDoc);
serverState = Y.encodeStateAsUpdate(serviceYDoc);
// Save Snapshot
const neverCollectedYDoc = new Y.Doc({ gc: false});
Y.applyUpdate(neverCollectedYDoc, neverGarbageCollectedState);
Y.applyUpdate(neverCollectedYDoc, serverState);
snapshot3 = Y.snapshot(neverCollectedYDoc);
neverGarbageCollectedState = Y.encodeStateAsUpdate(neverCollectedYDoc);
// Assert
expect(clientYDoc.getMap(mapKey).toJSON()).to.deep.equal({
task_1: { id: 'task_1', label: 'Task 1 Label Updated', sortWeight: 'M' },
task_2: { id: 'task_2', label: 'Task 2 Label Updated', sortWeight: 'B' }
});
expect(serverState).to.not.deep.equal(initialState);
expect(clientState).to.not.deep.equal(initialState);
expect(clientState).to.deep.equal(serverState);
expect(snapshot1).to.not.deep.equal(snapshot2);
expect(snapshot1).to.not.deep.equal(snapshot3);
expect(snapshot2).to.not.deep.equal(snapshot3);
});
it('6. Revert to Snapshot 1', () => {
// Initialize
const clientYDoc = new Y.Doc();
Y.applyUpdate(clientYDoc, clientState);
// Snapshot and diff
const neverCollectedYDoc = new Y.Doc({ gc: false });
Y.applyUpdate(neverCollectedYDoc, neverGarbageCollectedState);
const docAtSnapshot = Y.createDocFromSnapshot(neverCollectedYDoc, snapshot1);
const diffForRevert = Y.diffUpdate(Y.encodeStateAsUpdate(docAtSnapshot), Y.encodeStateVector(clientYDoc));
Y.applyUpdate(clientYDoc, diffForRevert);
// Sync
const serviceYDoc = new Y.Doc();
Y.applyUpdate(serviceYDoc, serverState);
Y.applyUpdate(serviceYDoc, Y.encodeStateAsUpdate(clientYDoc));
Y.applyUpdate(serviceYDoc, serverState);
// Save
// Snapshot :-)
clientState = Y.encodeStateAsUpdate(clientYDoc);
serverState = Y.encodeStateAsUpdate(serviceYDoc);
// Assert
expect(docAtSnapshot.getMap(mapKey).toJSON()).to.deep.equal({
task_1: { id: 'task_1', label: 'I am Task 1', sortWeight: 'M' },
task_2: { id: 'task_2', label: 'I am Task 2', sortWeight: 'N' }
});
expect(clientYDoc.getMap(mapKey).toJSON()).to.deep.equal({
task_1: { id: 'task_1', label: 'I am Task 1', sortWeight: 'M' },
task_2: { id: 'task_2', label: 'I am Task 2', sortWeight: 'N' }
});
expect(serverState).to.not.deep.equal(initialState);
expect(clientState).to.not.deep.equal(initialState);
expect(clientState).to.deep.equal(serverState);
});
it('7. Revert to Snapshot 2', () => {
// Initialize
const clientYDoc = new Y.Doc();
Y.applyUpdate(clientYDoc, clientState);
// Snapshot and diff
const neverCollectedYDoc = new Y.Doc({ gc: false });
Y.applyUpdate(neverCollectedYDoc, neverGarbageCollectedState);
const docAtSnapshot = Y.createDocFromSnapshot(neverCollectedYDoc, snapshot2);
const diffForRevert = Y.diffUpdate(Y.encodeStateAsUpdate(docAtSnapshot), Y.encodeStateVector(clientYDoc));
Y.applyUpdate(clientYDoc, diffForRevert);
// Sync
const serviceYDoc = new Y.Doc();
Y.applyUpdate(serviceYDoc, serverState);
Y.applyUpdate(serviceYDoc, Y.encodeStateAsUpdate(clientYDoc));
Y.applyUpdate(serviceYDoc, serverState);
// Save
// Snapshot :-)
clientState = Y.encodeStateAsUpdate(clientYDoc);
serverState = Y.encodeStateAsUpdate(serviceYDoc);
// Assert
expect(docAtSnapshot.getMap(mapKey).toJSON()).to.deep.equal({
task_1: { id: 'task_1', label: 'I am Task 1', sortWeight: 'M' },
task_2: { id: 'task_2', label: 'I am Task 2', sortWeight: 'B' }
});
expect(serviceYDoc.getMap(mapKey).toJSON()).to.deep.equal({
task_1: { id: 'task_1', label: 'I am Task 1', sortWeight: 'M' },
task_2: { id: 'task_2', label: 'I am Task 2', sortWeight: 'B' }
});
expect(serverState).to.not.deep.equal(initialState);
expect(clientState).to.not.deep.equal(initialState);
expect(clientState).to.deep.equal(serverState);
});
it('8. Revert to Snapshot 3', () => {
// Initialize
const clientYDoc = new Y.Doc();
Y.applyUpdate(clientYDoc, clientState);
// Snapshot and diff
const neverCollectedYDoc = new Y.Doc({ gc: false });
Y.applyUpdate(neverCollectedYDoc, neverGarbageCollectedState);
const docAtSnapshot = Y.createDocFromSnapshot(neverCollectedYDoc, snapshot3);
const diffForRevert = Y.diffUpdate(Y.encodeStateAsUpdate(docAtSnapshot), Y.encodeStateVector(clientYDoc));
Y.applyUpdate(clientYDoc, diffForRevert);
// Sync
const serviceYDoc = new Y.Doc();
Y.applyUpdate(serviceYDoc, serverState);
Y.applyUpdate(serviceYDoc, Y.encodeStateAsUpdate(clientYDoc));
Y.applyUpdate(serviceYDoc, serverState);
// Save
// Snapshot :-)
clientState = Y.encodeStateAsUpdate(clientYDoc);
serverState = Y.encodeStateAsUpdate(serviceYDoc);
// Assert
expect(docAtSnapshot.getMap(mapKey).toJSON()).to.deep.equal({
task_1: { id: 'task_1', label: 'Task 1 Label Updated', sortWeight: 'M' },
task_2: { id: 'task_2', label: 'Task 2 Label Updated', sortWeight: 'B' }
});
expect(clientYDoc.getMap(mapKey).toJSON()).to.deep.equal({
task_1: { id: 'task_1', label: 'Task 1 Label Updated', sortWeight: 'M' },
task_2: { id: 'task_2', label: 'Task 2 Label Updated', sortWeight: 'B' }
});
expect(serverState).to.not.deep.equal(initialState);
expect(clientState).to.not.deep.equal(initialState);
expect(clientState).to.deep.equal(serverState);
});
});
});
Here are my results:
Full Stack Interaction
Snapshots
âś” 1. Initial State
âś” 2. Sync with Server: Snapshot 1
âś” 3. Send to Client with Offline Edit
âś” 4. Out of Server Band Edit: Snapshot 2
âś” 5. Sync with Server: Snapshot 3
1) 6. Revert to Snapshot 1
2) 7. Revert to Snapshot 2
âś” 8. Revert to Snapshot 3
6 passing (57ms)
2 failing
1) Full Stack Interaction
Snapshots
6. Revert to Snapshot 1:
AssertionError: expected { Object (task_1, task_2) } to deeply equal { Object (task_1, task_2) }
+ expected - actual
{
"task_1": {
"id": "task_1"
- "label": "Task 1 Label Updated"
+ "label": "I am Task 1"
"sortWeight": "M"
}
"task_2": {
"id": "task_2"
- "label": "Task 2 Label Updated"
- "sortWeight": "B"
+ "label": "I am Task 2"
+ "sortWeight": "N"
}
}
at Context.<anonymous> (test/fullstack-yparts.spec.ts:196:58)
at processImmediate (internal/timers.js:464:21)
2) Full Stack Interaction
Snapshots
7. Revert to Snapshot 2:
AssertionError: expected { Object (task_1, task_2) } to deeply equal { Object (task_1, task_2) }
+ expected - actual
{
"task_1": {
"id": "task_1"
- "label": "Task 1 Label Updated"
+ "label": "I am Task 1"
"sortWeight": "M"
}
"task_2": {
"id": "task_2"
- "label": "Task 2 Label Updated"
+ "label": "I am Task 2"
"sortWeight": "B"
}
}
at Context.<anonymous> (test/fullstack-yparts.spec.ts:233:59)
at processImmediate (internal/timers.js:464:21)
It looks like we cannot “revert forward” like a git revert? (using diff ops off of snapshots and the fully reconstructed doc from a snapshot?)
Refer to this issue for additional context: restoring document to a specific state · Issue #159 · yjs/yjs · GitHub