Files
59158a636d Add createTime to Document. (#6781)
* Add createTime to Document.

* More createTime tests

* All tests pass except for Bundles tests.

* Don't use createTime in isEqual.

* getBaseDocument should always get results from remoteDocumentCache.

* Bring back tests.

* Add test that checks createTime gets updated after remote event.

* Add test for Set->RemoteEvent->Ack.

* try higher timeout.

Co-authored-by: Wu-Hui <[email protected]>
2022-12-02 12:23:21 -08:00

1093 lines
33 KiB
TypeScript

/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect } from 'chai';
import {
arrayRemove,
arrayUnion,
increment,
Timestamp,
serverTimestamp,
deleteField
} from '../../../src';
import { MutableDocument } from '../../../src/model/document';
import { FieldMask } from '../../../src/model/field_mask';
import {
mutationApplyToLocalView,
mutationApplyToRemoteDocument,
mutationExtractBaseValue,
Mutation,
MutationResult,
Precondition,
calculateOverlayMutation
} from '../../../src/model/mutation';
import { serverTimestamp as serverTimestampInternal } from '../../../src/model/server_timestamps';
import {
ArrayRemoveTransformOperation,
ArrayUnionTransformOperation
} from '../../../src/model/transform_operation';
import { Dict } from '../../../src/util/obj';
import { addEqualityMatcher } from '../../util/equality_matcher';
import {
computeCombinations,
computePermutations,
deletedDoc,
deleteMutation,
doc,
field,
invalidDoc,
key,
mergeMutation,
mutationResult,
patchMutation,
setMutation,
unknownDoc,
version,
wrap,
wrapObject
} from '../../util/helpers';
describe('Mutation', () => {
addEqualityMatcher();
const timestamp = Timestamp.now();
/**
* For each document in `docs`, calculate the overlay mutations of each
* possible permutation, check whether this holds:
* document + overlay_mutation = document + mutation_list
* Returns how many cases it has run.
*/
function runPermutationTests(
docs: MutableDocument[],
mutations: Mutation[]
): number {
let testCases = 0;
const permutations = computePermutations(mutations);
docs.forEach(doc => {
permutations.forEach(permutation => {
verifyOverlayRoundTrips(doc, ...permutation);
testCases++;
});
});
return testCases;
}
function getDescription(
document: MutableDocument,
mutations: Mutation[],
overlay: Mutation | null
): string {
let result = 'Overlay Mutation failed with:\n';
result += 'document:\n';
result += document.toString();
result += '\n\n';
result += 'mutations:\n';
mutations.forEach(mutation => {
result += mutation.toString() + '\n';
});
result += '\n';
result += 'overlay:\n';
result += overlay === null ? 'null' : overlay.toString();
result += '\n\n';
return result;
}
function verifyOverlayRoundTrips(
doc: MutableDocument,
...mutations: Mutation[]
): void {
const docForMutations = doc.mutableCopy();
const docForOverlay = doc.mutableCopy();
let mask: FieldMask | null = null;
for (const mutation of mutations) {
mask = mutationApplyToLocalView(
mutation,
docForMutations,
mask,
timestamp
);
}
const overlay = calculateOverlayMutation(docForMutations, mask);
if (overlay !== null) {
mutationApplyToLocalView(
overlay,
docForOverlay,
/* previousMask= */ null,
timestamp
);
}
expect(docForOverlay).to.deep.equal(
docForMutations,
getDescription(doc, mutations, overlay)
);
}
it('can apply sets to documents', () => {
const docData = { foo: 'foo-value', baz: 'baz-value' };
const document = doc('collection/key', 0, docData);
const set = setMutation('collection/key', { bar: 'bar-value' });
mutationApplyToLocalView(
set,
document,
/* previousMask= */ null,
timestamp
);
expect(document).to.deep.equal(
doc('collection/key', 0, { bar: 'bar-value' }).setHasLocalMutations()
);
});
it('can apply patches to documents', () => {
const docData = { foo: { bar: 'bar-value' }, baz: 'baz-value' };
const document = doc('collection/key', 0, docData);
const patch = patchMutation('collection/key', {
'foo.bar': 'new-bar-value'
});
mutationApplyToLocalView(
patch,
document,
/* previousMask= */ null,
timestamp
);
expect(document).to.deep.equal(
doc('collection/key', 0, {
foo: { bar: 'new-bar-value' },
baz: 'baz-value'
}).setHasLocalMutations()
);
});
it('can apply patches with merges to missing documents', () => {
const timestamp = Timestamp.now();
const document = deletedDoc('collection/key', 0);
const patch = patchMutation(
'collection/key',
{ 'foo.bar': 'new-bar-value' },
Precondition.none()
);
mutationApplyToLocalView(
patch,
document,
/* previousMask= */ null,
timestamp
);
expect(document).to.deep.equal(
doc('collection/key', 0, {
foo: { bar: 'new-bar-value' }
}).setHasLocalMutations()
);
});
it('can apply patches with merges to null documents', () => {
const timestamp = Timestamp.now();
const document = MutableDocument.newInvalidDocument(key('collection/key'));
const patch = patchMutation(
'collection/key',
{ 'foo.bar': 'new-bar-value' },
Precondition.none()
);
mutationApplyToLocalView(
patch,
document,
/* previousMask= */ null,
timestamp
);
expect(document).to.deep.equal(
doc('collection/key', 0, {
foo: { bar: 'new-bar-value' }
}).setHasLocalMutations()
);
});
it('will delete values from the field-mask', () => {
const document = doc('collection/key', 0, {
foo: { bar: 'bar-value', baz: 'baz-value' }
});
const patch = patchMutation('collection/key', {
'foo.bar': deleteField()
});
mutationApplyToLocalView(
patch,
document,
/* previousMask= */ null,
timestamp
);
expect(document).to.deep.equal(
doc('collection/key', 0, {
foo: { baz: 'baz-value' }
}).setHasLocalMutations()
);
});
it('will patch a primitive value', () => {
const document = doc('collection/key', 0, {
foo: 'foo-value',
baz: 'baz-value'
});
const patch = patchMutation('collection/key', {
'foo.bar': 'new-bar-value'
});
mutationApplyToLocalView(
patch,
document,
/* previousMask= */ null,
timestamp
);
expect(document).to.deep.equal(
doc('collection/key', 0, {
foo: { bar: 'new-bar-value' },
baz: 'baz-value'
}).setHasLocalMutations()
);
});
it('patching a NoDocument yields a NoDocument', () => {
const document = deletedDoc('collection/key', 0);
const patch = patchMutation('collection/key', { foo: 'bar' });
mutationApplyToLocalView(
patch,
document,
/* previousMask= */ null,
timestamp
);
expect(document).to.deep.equal(deletedDoc('collection/key', 0));
});
it('can apply local serverTimestamp transforms to documents', () => {
const docData = { foo: { bar: 'bar-value' }, baz: 'baz-value' };
const document = doc('collection/key', 0, docData);
const transform = patchMutation('collection/key', {
'foo.bar': serverTimestamp()
});
mutationApplyToLocalView(
transform,
document,
/* previousMask= */ null,
timestamp
);
// Server timestamps aren't parsed, so we manually insert it.
const data = wrapObject({
foo: { bar: '<server-timestamp>' },
baz: 'baz-value'
});
data.set(field('foo.bar'), serverTimestampInternal(timestamp, null));
const expectedDoc = doc('collection/key', 0, data).setHasLocalMutations();
expect(document).to.deep.equal(expectedDoc);
});
// NOTE: This is more a test of UserDataReader code than Mutation code but
// we don't have unit tests for it currently. We could consider removing this
// test once we have integration tests.
it('can create arrayUnion() transform.', () => {
const transform = patchMutation('collection/key', {
a: arrayUnion('tag'),
'bar.baz': arrayUnion(true, { nested: { a: [1, 2] } })
});
expect(transform.fieldTransforms).to.have.lengthOf(2);
const first = transform.fieldTransforms[0];
expect(first.field).to.deep.equal(field('a'));
expect(first.transform).to.deep.equal(
new ArrayUnionTransformOperation([wrap('tag')])
);
const second = transform.fieldTransforms[1];
expect(second.field).to.deep.equal(field('bar.baz'));
expect(second.transform).to.deep.equal(
new ArrayUnionTransformOperation([
wrap(true),
wrap({ nested: { a: [1, 2] } })
])
);
});
// NOTE: This is more a test of UserDataReader code than Mutation code but
// we don't have unit tests for it currently. We could consider removing this
// test once we have integration tests.
it('can create arrayRemove() transform.', () => {
const transform = patchMutation('collection/key', {
foo: arrayRemove('tag')
});
expect(transform.fieldTransforms).to.have.lengthOf(1);
const first = transform.fieldTransforms[0];
expect(first.field).to.deep.equal(field('foo'));
expect(first.transform).to.deep.equal(
new ArrayRemoveTransformOperation([wrap('tag')])
);
});
it('can apply local arrayUnion transform to missing field', () => {
const baseDoc = {};
const transform = { missing: arrayUnion(1, 2) };
const expected = { missing: [1, 2] };
verifyTransform(baseDoc, transform, expected);
});
it('can apply local arrayUnion transform to non-array field', () => {
const baseDoc = { 'non-array': 42 };
const transform = { 'non-array': arrayUnion(1, 2) };
const expected = { 'non-array': [1, 2] };
verifyTransform(baseDoc, transform, expected);
});
it('can apply local arrayUnion transform with non-existing elements', () => {
const baseDoc = { array: [1, 3] };
const transform = { array: arrayUnion(2, 4) };
const expected = { array: [1, 3, 2, 4] };
verifyTransform(baseDoc, transform, expected);
});
it('can apply local arrayUnion transform with existing elements', () => {
const baseDoc = { array: [1, 3] };
const transform = { array: arrayUnion(1, 3) };
const expected = { array: [1, 3] };
verifyTransform(baseDoc, transform, expected);
});
it('can apply local arrayUnion transform with duplicate existing elements', () => {
// Duplicate entries in your existing array should be preserved.
const baseDoc = { array: [1, 2, 2, 3] };
const transform = { array: arrayUnion(2) };
const expected = { array: [1, 2, 2, 3] };
verifyTransform(baseDoc, transform, expected);
});
it('can apply local arrayUnion transform with duplicate union elements', () => {
// Duplicate entries in your union array should only be added once.
const baseDoc = { array: [1, 3] };
const transform = { array: arrayUnion(2, 2) };
const expected = { array: [1, 3, 2] };
verifyTransform(baseDoc, transform, expected);
});
it('can apply local arrayUnion transform with non-primitive elements', () => {
// Union nested object values (one existing, one not).
const baseDoc = { array: [1, { a: 'b' }] };
const transform = { array: arrayUnion({ a: 'b' }, { c: 'd' }) };
const expected = { array: [1, { a: 'b' }, { c: 'd' }] };
verifyTransform(baseDoc, transform, expected);
});
it('can apply local arrayUnion transform with partially-overlapping elements', () => {
// Union objects that partially overlap an existing object.
const baseDoc = { array: [1, { a: 'b', c: 'd' }] };
const transform = { array: arrayUnion({ a: 'b' }, { c: 'd' }) };
const expected = { array: [1, { a: 'b', c: 'd' }, { a: 'b' }, { c: 'd' }] };
verifyTransform(baseDoc, transform, expected);
});
it('can apply local arrayRemove transform to missing field', () => {
const baseDoc = {};
const transform = { missing: arrayRemove(1, 2) };
const expected = { missing: [] };
verifyTransform(baseDoc, transform, expected);
});
it('can apply local arrayRemove transform to non-array field', () => {
const baseDoc = { 'non-array': 42 };
const transform = { 'non-array': arrayRemove(1, 2) };
const expected = { 'non-array': [] };
verifyTransform(baseDoc, transform, expected);
});
it('can apply local arrayRemove transform with non-existing elements', () => {
const baseDoc = { array: [1, 3] };
const transform = { array: arrayRemove(2, 4) };
const expected = { array: [1, 3] };
verifyTransform(baseDoc, transform, expected);
});
it('can apply local arrayRemove transform with existing elements', () => {
const baseDoc = { array: [1, 2, 3, 4] };
const transform = { array: arrayRemove(1, 3) };
const expected = { array: [2, 4] };
verifyTransform(baseDoc, transform, expected);
});
it('can apply local arrayRemove transform with non-primitive elements', () => {
// Remove nested object values (one existing, one not).
const baseDoc = { array: [1, { a: 'b' }] };
const transform = {
array: arrayRemove({ a: 'b' }, { c: 'd' })
};
const expected = { array: [1] };
verifyTransform(baseDoc, transform, expected);
});
function verifyTransform(
baseData: Dict<unknown>,
transformData: Dict<unknown> | Array<Dict<unknown>>,
expectedData: Dict<unknown>
): void {
const document = doc('collection/key', 0, baseData);
const transforms = Array.isArray(transformData)
? transformData
: [transformData];
for (const transformData of transforms) {
const transform = patchMutation('collection/key', transformData);
mutationApplyToLocalView(
transform,
document,
/* previousMask= */ null,
timestamp
);
}
const expectedDoc = doc(
'collection/key',
0,
expectedData
).setHasLocalMutations();
expect(document).to.deep.equal(expectedDoc);
}
it('can apply server-acked serverTimestamp transform to documents', () => {
const docData = { foo: { bar: 'bar-value' }, baz: 'baz-value' };
const document = doc('collection/key', 0, docData);
const transform = patchMutation('collection/key', {
'foo.bar': serverTimestamp()
});
const mutationResult = new MutationResult(version(1), [
{
timestampValue: {
seconds: timestamp.seconds,
nanos: timestamp.nanoseconds
}
}
]);
mutationApplyToRemoteDocument(transform, document, mutationResult);
expect(document).to.deep.equal(
doc('collection/key', 1, {
foo: { bar: timestamp.toDate() },
baz: 'baz-value'
}).setHasCommittedMutations()
);
});
it('can apply server-acked array transforms to document', () => {
const docData = { array1: [1, 2], array2: ['a', 'b'] };
const document = doc('collection/key', 0, docData);
const transform = setMutation('collection/key', {
array1: arrayUnion(2, 3),
array2: arrayRemove('a', 'c')
});
// Server just sends null transform results for array operations.
const mutationResult = new MutationResult(version(1), [null, null]);
mutationApplyToRemoteDocument(transform, document, mutationResult);
expect(document).to.deep.equal(
doc('collection/key', 1, {
array1: [1, 2, 3],
array2: ['b']
}).setHasCommittedMutations()
);
});
it('can apply numeric add transform to document', () => {
const baseDoc = {
longPlusLong: 1,
longPlusDouble: 2,
doublePlusLong: 3.3,
doublePlusDouble: 4.0,
longPlusNan: 5,
doublePlusNan: 6.6,
longPlusInfinity: 7,
doublePlusInfinity: 8.8
};
const transform = {
longPlusLong: increment(1),
longPlusDouble: increment(2.2),
doublePlusLong: increment(3),
doublePlusDouble: increment(4.4),
longPlusNan: increment(Number.NaN),
doublePlusNan: increment(Number.NaN),
longPlusInfinity: increment(Number.POSITIVE_INFINITY),
doublePlusInfinity: increment(Number.POSITIVE_INFINITY)
};
const expected = {
longPlusLong: 2,
longPlusDouble: 4.2,
doublePlusLong: 6.3,
doublePlusDouble: 8.4,
longPlusNan: Number.NaN,
doublePlusNan: Number.NaN,
longPlusInfinity: Number.POSITIVE_INFINITY,
doublePlusInfinity: Number.POSITIVE_INFINITY
};
verifyTransform(baseDoc, transform, expected);
});
it('can apply numeric add transform to unexpected type', () => {
const baseDoc = { stringVal: 'zero' };
const transform = { stringVal: increment(1) };
const expected = { stringVal: 1 };
verifyTransform(baseDoc, transform, expected);
});
it('can apply numeric add transform to missing field', () => {
const baseDoc = {};
const transform = { missing: increment(1) };
const expected = { missing: 1 };
verifyTransform(baseDoc, transform, expected);
});
it('can apply numeric add transforms consecutively', () => {
const baseDoc = { numberVal: 1 };
const transform1 = { numberVal: increment(2) };
const transform2 = { numberVal: increment(3) };
const transform3 = { numberVal: increment(4) };
const expected = { numberVal: 10 };
verifyTransform(baseDoc, [transform1, transform2, transform3], expected);
});
// PORTING NOTE: The `increment()` overflow/underflow tests from Android/iOS
// are not applicable to Web since we expose JavaScript's number arithmetic
// directly.
it('can apply server-acked numeric add transform to document', () => {
const docData = { sum: 1 };
const document = doc('collection/key', 0, docData);
const transform = setMutation('collection/key', {
sum: increment(2)
});
const mutationResult = new MutationResult(version(1), [
{ integerValue: 3 }
]);
mutationApplyToRemoteDocument(transform, document, mutationResult);
expect(document).to.deep.equal(
doc('collection/key', 1, { sum: 3 }).setHasCommittedMutations()
);
});
it('can apply deletes to documents', () => {
const document = doc('collection/key', 0, { foo: 'bar' });
const mutation = deleteMutation('collection/key');
mutationApplyToLocalView(
mutation,
document,
/* previousMask= */ null,
Timestamp.now()
);
expect(document).to.deep.equal(
deletedDoc('collection/key', 0).setHasLocalMutations()
);
});
it('can apply sets with mutation results', () => {
const document = doc('collection/key', 0, { foo: 'bar' });
const docSet = setMutation('collection/key', { foo: 'new-bar' });
const setResult = mutationResult(4);
mutationApplyToRemoteDocument(docSet, document, setResult);
expect(document).to.deep.equal(
doc('collection/key', 4, { foo: 'new-bar' }).setHasCommittedMutations()
);
});
it('will apply patches with mutation results', () => {
const document = doc('collection/key', 0, { foo: 'bar' });
const mutation = patchMutation('collection/key', { foo: 'new-bar' });
const result = mutationResult(5);
mutationApplyToRemoteDocument(mutation, document, result);
expect(document).to.deep.equal(
doc('collection/key', 5, { foo: 'new-bar' }).setHasCommittedMutations()
);
});
function assertVersionTransitions(
mutation: Mutation,
base: MutableDocument,
mutationResult: MutationResult,
expected: MutableDocument
): void {
const documentCopy = base.mutableCopy();
mutationApplyToRemoteDocument(mutation, documentCopy, mutationResult);
expect(documentCopy).to.deep.equal(expected);
}
it('transitions versions correctly', () => {
const docV3 = doc('collection/key', 3, {});
const deletedV3 = deletedDoc('collection/key', 3);
const invalidV3 = invalidDoc('collection/key');
const set = setMutation('collection/key', {});
const patch = patchMutation('collection/key', {});
const deleter = deleteMutation('collection/key');
const mutationResult = new MutationResult(
version(7),
/*transformResults=*/ []
);
const docV7Unknown = unknownDoc('collection/key', 7);
const docV7Deleted = deletedDoc(
'collection/key',
7
).setHasCommittedMutations();
const docV7Committed = doc(
'collection/key',
7,
{}
).setHasCommittedMutations();
assertVersionTransitions(set, docV3, mutationResult, docV7Committed);
assertVersionTransitions(set, deletedV3, mutationResult, docV7Committed);
assertVersionTransitions(set, invalidV3, mutationResult, docV7Committed);
assertVersionTransitions(patch, docV3, mutationResult, docV7Committed);
assertVersionTransitions(patch, deletedV3, mutationResult, docV7Unknown);
assertVersionTransitions(patch, invalidV3, mutationResult, docV7Unknown);
assertVersionTransitions(deleter, docV3, mutationResult, docV7Deleted);
assertVersionTransitions(deleter, deletedV3, mutationResult, docV7Deleted);
assertVersionTransitions(deleter, invalidV3, mutationResult, docV7Deleted);
});
it('extracts null base value for non-transform mutation', () => {
const data = { foo: 'foo' };
const baseDoc = doc('collection/key', 0, data);
const set = setMutation('collection/key', { foo: 'bar' });
expect(mutationExtractBaseValue(set, baseDoc)).to.be.null;
const patch = patchMutation('collection/key', { foo: 'bar' });
expect(mutationExtractBaseValue(patch, baseDoc)).to.be.null;
const deleter = deleteMutation('collection/key');
expect(mutationExtractBaseValue(deleter, baseDoc)).to.be.null;
});
it('extracts null base value for ServerTimestamp', () => {
const allValues = { time: 'foo', nested: { time: 'foo' } };
const baseDoc = doc('collection/key', 0, allValues);
const allTransforms = {
time: serverTimestamp(),
nested: { time: serverTimestamp() }
};
// Server timestamps are idempotent and don't have base values.
const transform = patchMutation('collection/key', allTransforms);
expect(mutationExtractBaseValue(transform, baseDoc)).to.be.null;
});
it('extracts base value for increment', () => {
const allValues = {
ignore: 'foo',
double: 42.0,
long: 42,
text: 'foo',
map: {},
nested: { ignore: 'foo', double: 42.0, long: 42, text: 'foo', map: {} }
};
const baseDoc = doc('collection/key', 0, allValues);
const allTransforms = {
double: increment(1),
long: increment(1),
text: increment(1),
map: increment(1),
missing: increment(1),
nested: {
double: increment(1),
long: increment(1),
text: increment(1),
map: increment(1),
missing: increment(1)
}
};
const transform = patchMutation('collection/key', allTransforms);
const expectedBaseValue = wrapObject({
double: 42.0,
long: 42,
text: 0,
map: 0,
missing: 0,
nested: { double: 42.0, long: 42, text: 0, map: 0, missing: 0 }
});
const actualBaseValue = mutationExtractBaseValue(transform, baseDoc);
expect(expectedBaseValue.isEqual(actualBaseValue!)).to.be.true;
});
it('increment twice', () => {
const document = doc('collection/key', 0, { sum: 0 });
const inc = { sum: increment(1) };
const transform = setMutation('collection/key', inc);
mutationApplyToLocalView(
transform,
document,
/* previousMask= */ null,
Timestamp.now()
);
mutationApplyToLocalView(
transform,
document,
/* previousMask= */ null,
Timestamp.now()
);
expect(document.isFoundDocument()).to.be.true;
expect(document.data.field(field('sum'))).to.deep.equal(wrap(2));
});
// Mutation Overlay tests
it('overlay with no mutation', () => {
const doc1 = doc('collection/key', 1, {
'foo': 'foo-value',
'baz': 'baz-value'
});
verifyOverlayRoundTrips(doc1);
});
it('overlay with mutations fail by preconditions', () => {
verifyOverlayRoundTrips(
deletedDoc('collection/key', 1),
patchMutation('collection/key', { 'foo': 'bar' }),
patchMutation('collection/key', { 'a': 1 })
);
});
it('overlay with patch on invalid document', () => {
verifyOverlayRoundTrips(
MutableDocument.newInvalidDocument(key('collection/key')),
patchMutation('collection/key', { 'a': 1 })
);
});
it('overlay with one set mutation', () => {
const doc1 = doc('collection/key', 1, {
'foo': 'foo-value',
'baz': 'baz-value'
});
verifyOverlayRoundTrips(
doc1,
setMutation('collection/key', { 'bar': 'bar-value' })
);
});
it('overlay with one patch mutation', () => {
const doc1 = doc('collection/key', 1, {
'foo': { 'bar': 'bar-value' },
'baz': 'baz-value'
});
verifyOverlayRoundTrips(
doc1,
patchMutation('collection/key', { 'foo.bar': 'new-bar-value' })
);
});
it('overlay with patch then merge', () => {
const upsert = mergeMutation(
'collection/key',
{ 'foo.bar': 'new-bar-value' },
[field('foo.bar')]
);
verifyOverlayRoundTrips(deletedDoc('collection/key', 1), upsert);
});
it('overlay with delete then patch', () => {
const doc1 = doc('collection/key', 1, { 'foo': 1 });
const deleteMutation1 = deleteMutation('collection/key');
const patchMutation1 = patchMutation('collection/key', {
'foo.bar': 'new-bar-value'
});
verifyOverlayRoundTrips(doc1, deleteMutation1, patchMutation1);
});
it('overlay with delete then merge', () => {
const doc1 = doc('collection/key', 1, { 'foo': 1 });
const deleteMutation1 = deleteMutation('collection/key');
const mergeMutation1 = mergeMutation(
'collection/key',
{ 'foo.bar': 'new-bar-value' },
[field('foo.bar')]
);
verifyOverlayRoundTrips(doc1, deleteMutation1, mergeMutation1);
});
it('overlay with patch then patch to delete field', () => {
const doc1 = doc('collection/key', 1, { 'foo': 1 });
const patch = patchMutation('collection/key', {
'foo': 'foo-patched-value',
'bar.baz': increment(1)
});
const patchToDeleteField = patchMutation('collection/key', {
'foo': 'foo-patched-value',
'bar.baz': deleteField()
});
verifyOverlayRoundTrips(doc1, patch, patchToDeleteField);
});
it('overlay with patch then merge with array union', () => {
const doc1 = doc('collection/key', 1, { 'foo': 1 });
const patch = patchMutation('collection/key', {
'foo': 'foo-patched-value',
'bar.baz': increment(1)
});
const merge = mergeMutation(
'collection/key',
{ 'arrays': arrayUnion(1, 2, 3) },
[field('arrays')]
);
verifyOverlayRoundTrips(doc1, patch, merge);
});
it('overlay with array union then remove', () => {
const doc1 = doc('collection/key', 1, { 'foo': 1 });
const union = mergeMutation(
'collection/key',
{ 'arrays': arrayUnion(1, 2, 3) },
[]
);
const remove = mergeMutation(
'collection/key',
{ 'foo': 'xxx', 'arrays': arrayRemove(2) },
[field('foo')]
);
verifyOverlayRoundTrips(doc1, union, remove);
});
it('overlay with set then increment', () => {
const doc1 = doc('collection/key', 1, { 'foo': 1 });
const set = setMutation('collection/key', { 'foo': 2 });
const update = patchMutation('collection/key', { 'foo': increment(2) });
verifyOverlayRoundTrips(doc1, set, update);
});
it('overlay with set then patch on deleted doc', () => {
const doc1 = deletedDoc('collection/key', 1);
const set = setMutation('collection/key', { 'bar': 'bar-value' });
const patch = patchMutation('collection/key', {
'foo': 'foo-patched-value',
'bar.baz': serverTimestamp()
});
verifyOverlayRoundTrips(doc1, set, patch);
});
it('overlay with field deletion of nested field', () => {
const doc1 = doc('collection/key', 1, { 'foo': 1 });
const patch1 = patchMutation('collection/key', {
'foo': 'foo-patched-value',
'bar.baz': increment(1)
});
const patch2 = patchMutation('collection/key', {
'foo': 'foo-patched-value',
'bar.baz': serverTimestamp()
});
const patch3 = patchMutation('collection/key', {
'foo': 'foo-patched-value',
'bar.baz': deleteField()
});
verifyOverlayRoundTrips(doc1, patch1, patch2, patch3);
});
it('overlay created from empty set with merge', () => {
const doc1 = deletedDoc('collection/key', 1);
const merge = mergeMutation('collection/key', {}, []);
verifyOverlayRoundTrips(doc1, merge);
const doc2 = doc('collection/key', 1, { 'foo': 'foo-value' });
verifyOverlayRoundTrips(doc2, merge);
});
// Below tests run on automatically generated mutation list, they are
// deterministic, but hard to debug when they fail. They will print the
// failure case, and the best way to debug is recreate the case manually in a
// separate test.
it('overlay with mutation with multiple deletes', () => {
const docs = [
doc('collection/key', 1, { 'foo': 'foo-value', 'bar.baz': 1 }),
deletedDoc('collection/key', 1),
unknownDoc('collection/key', 1)
];
const mutations = [
setMutation('collection/key', { 'bar': 'bar-value' }),
deleteMutation('collection/key'),
deleteMutation('collection/key'),
patchMutation('collection/key', {
'foo': 'foo-patched-value',
'bar.baz': serverTimestamp()
})
];
const testCases = runPermutationTests(docs, mutations);
// There are 4! * 3 cases
expect(testCases).to.equal(72);
});
it('overlay by combinations and permutations', () => {
const docs: MutableDocument[] = [
doc('collection/key', 1, { 'foo': 'foo-value', 'bar': 1 }),
deletedDoc('collection/key', 1),
unknownDoc('collection/key', 1)
];
const mutations: Mutation[] = [
setMutation('collection/key', { 'bar': 'bar-value' }),
setMutation('collection/key', { 'bar.rab': 'bar.rab-value' }),
deleteMutation('collection/key'),
patchMutation('collection/key', {
'foo': 'foo-patched-value-incr',
'bar': increment(1)
}),
patchMutation('collection/key', {
'foo': 'foo-patched-value-delete',
'bar': deleteField()
}),
patchMutation('collection/key', {
'foo': 'foo-patched-value-st',
'bar': serverTimestamp()
}),
mergeMutation('collection/key', { 'arrays': arrayUnion(1, 2, 3) }, [
field('arrays')
])
];
// Take all possible combinations of the subsets of the mutation list, run each combination for
// all possible permutation, for all 3 different type of documents.
let testCases = 0;
computeCombinations(mutations).forEach(combination => {
testCases += runPermutationTests(docs, combination);
});
// There are (0! + 7*1! + 21*2! + 35*3! + 35*4! + 21*5! + 7*6! + 7!) * 3 = 41100 cases.
expect(testCases).to.equal(41100);
}).timeout(10000);
it('overlay by combinations and permutations for array transforms', () => {
const docs: MutableDocument[] = [
doc('collection/key', 1, { 'foo': 'foo-value', 'bar.baz': 1 }),
deletedDoc('collection/key', 1),
unknownDoc('collection/key', 1)
];
const mutations: Mutation[] = [
setMutation('collection/key', { 'bar': 'bar-value' }),
mergeMutation(
'collection/key',
{ 'foo': 'xxx', 'arrays': arrayRemove(2) },
[field('foo')]
),
deleteMutation('collection/key'),
patchMutation('collection/key', {
'foo': 'foo-patched-value-1',
'arrays': arrayUnion(4, 5)
}),
patchMutation('collection/key', {
'foo': 'foo-patched-value-2',
'arrays': arrayRemove(5, 6)
}),
mergeMutation(
'collection/key',
{ 'foo': 'yyy', 'arrays': arrayUnion(1, 2, 3, 999) },
[field('foo')]
)
];
let testCases = 0;
computeCombinations(mutations).forEach(combination => {
testCases += runPermutationTests(docs, combination);
});
// There are (0! + 6*1! + 15*2! + 20*3! + 15*4! + 6*5! + 6!) * 3 = 5871 cases.
expect(testCases).to.equal(5871);
});
it('overlay by combinations and permutations for increments', () => {
const docs: MutableDocument[] = [
doc('collection/key', 1, { 'foo': 'foo-value', 'bar': 1 }),
deletedDoc('collection/key', 1),
unknownDoc('collection/key', 1)
];
const mutations: Mutation[] = [
setMutation('collection/key', { 'bar': 'bar-value' }),
mergeMutation(
'collection/key',
{ 'foo': 'foo-merge', 'bar': increment(2) },
[field('foo')]
),
deleteMutation('collection/key'),
patchMutation('collection/key', {
'foo': 'foo-patched-value-1',
'bar': increment(-1.4)
}),
patchMutation('collection/key', {
'foo': 'foo-patched-value-2',
'bar': increment(3.3)
}),
mergeMutation('collection/key', { 'foo': 'yyy', 'bar': increment(-41) }, [
field('foo')
])
];
let testCases = 0;
computeCombinations(mutations).forEach(combination => {
testCases += runPermutationTests(docs, combination);
});
// There are (0! + 6*1! + 15*2! + 20*3! + 15*4! + 6*5! + 6!) * 3 = 5871 cases.
expect(testCases).to.equal(5871);
});
});