diff options
Diffstat (limited to 'Firestore/Example/Tests/Core/FSTViewTests.mm')
-rw-r--r-- | Firestore/Example/Tests/Core/FSTViewTests.mm | 618 |
1 files changed, 618 insertions, 0 deletions
diff --git a/Firestore/Example/Tests/Core/FSTViewTests.mm b/Firestore/Example/Tests/Core/FSTViewTests.mm new file mode 100644 index 0000000..e6c4510 --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTViewTests.mm @@ -0,0 +1,618 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/Source/Core/FSTView.h" + +#import <XCTest/XCTest.h> + +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTViewSnapshot.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTDocumentSet.h" +#import "Firestore/Source/Model/FSTFieldValue.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Remote/FSTRemoteEvent.h" + +#import "Firestore/Example/Tests/Util/FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTViewTests : XCTestCase +@end + +@implementation FSTViewTests + +/** Returns a new empty query to use for testing. */ +- (FSTQuery *)queryForMessages { + return [FSTQuery + queryWithPath:[FSTResourcePath pathWithSegments:@[ @"rooms", @"eros", @"messages" ]]]; +} + +- (void)testAddsDocumentsBasedOnQuery { + FSTQuery *query = [self queryForMessages]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/other/messages/1", 0, @{@"text" : @"msg3"}, NO); + + FSTViewSnapshot *_Nullable snapshot = + FSTTestApplyChanges(view, @[ doc1, doc2, doc3 ], + [FSTTargetChange changeWithDocuments:@[ doc1, doc2, doc3 ] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]); + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc2 ])); + + XCTAssertEqualObjects( + snapshot.documentChanges, (@[ + [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded], + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded] + ])); + + XCTAssertFalse(snapshot.isFromCache); + XCTAssertFalse(snapshot.hasPendingWrites); + XCTAssertTrue(snapshot.syncStateChanged); +} + +- (void)testRemovesDocuments { + FSTQuery *query = [self queryForMessages]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{@"text" : @"msg3"}, NO); + + // initial state + FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + + // delete doc2, add doc3 + FSTViewSnapshot *snapshot = + FSTTestApplyChanges(view, @[ FSTTestDeletedDoc(@"rooms/eros/messages/2", 0), doc3 ], + [FSTTargetChange changeWithDocuments:@[ doc1, doc3 ] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]); + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc3 ])); + + XCTAssertEqualObjects( + snapshot.documentChanges, (@[ + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeRemoved], + [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded] + ])); + + XCTAssertFalse(snapshot.isFromCache); + XCTAssertTrue(snapshot.syncStateChanged); +} + +- (void)testReturnsNilIfThereAreNoChanges { + FSTQuery *query = [self queryForMessages]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); + + // initial state + FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + + // reapply same docs, no changes + FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + XCTAssertNil(snapshot); +} + +- (void)testDoesNotReturnNilForFirstChanges { + FSTQuery *query = [self queryForMessages]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[], nil); + XCTAssertNotNil(snapshot); +} + +- (void)testFiltersDocumentsBasedOnQueryWithFilter { + FSTQuery *query = [self queryForMessages]; + FSTRelationFilter *filter = + [FSTRelationFilter filterWithField:FSTTestFieldPath(@"sort") + filterOperator:FSTRelationFilterOperatorLessThanOrEqual + value:[FSTDoubleValue doubleValue:2]]; + query = [query queryByAddingFilter:filter]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"sort" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"sort" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"sort" : @3 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{}, NO); // no sort, no match + FSTDocument *doc5 = FSTTestDoc(@"rooms/eros/messages/5", 0, @{ @"sort" : @1 }, NO); + + FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2, doc3, doc4, doc5 ], nil); + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc5, doc2 ])); + + XCTAssertEqualObjects( + snapshot.documentChanges, (@[ + [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded], + [FSTDocumentViewChange changeWithDocument:doc5 type:FSTDocumentViewChangeTypeAdded], + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded] + ])); + + XCTAssertTrue(snapshot.isFromCache); + XCTAssertTrue(snapshot.syncStateChanged); +} + +- (void)testUpdatesDocumentsBasedOnQueryWithFilter { + FSTQuery *query = [self queryForMessages]; + FSTRelationFilter *filter = + [FSTRelationFilter filterWithField:FSTTestFieldPath(@"sort") + filterOperator:FSTRelationFilterOperatorLessThanOrEqual + value:[FSTDoubleValue doubleValue:2]]; + query = [query queryByAddingFilter:filter]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"sort" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"sort" : @3 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"sort" : @2 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{}, NO); + + FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2, doc3, doc4 ], nil); + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc3 ])); + + FSTDocument *newDoc2 = FSTTestDoc(@"rooms/eros/messages/2", 1, @{ @"sort" : @2 }, NO); + FSTDocument *newDoc3 = FSTTestDoc(@"rooms/eros/messages/3", 1, @{ @"sort" : @3 }, NO); + FSTDocument *newDoc4 = FSTTestDoc(@"rooms/eros/messages/4", 1, @{ @"sort" : @0 }, NO); + + snapshot = FSTTestApplyChanges(view, @[ newDoc2, newDoc3, newDoc4 ], nil); + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ newDoc4, doc1, newDoc2 ])); + + XCTAssertEqualObjects( + snapshot.documentChanges, (@[ + [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeRemoved], + [FSTDocumentViewChange changeWithDocument:newDoc4 type:FSTDocumentViewChangeTypeAdded], + [FSTDocumentViewChange changeWithDocument:newDoc2 type:FSTDocumentViewChangeTypeAdded] + ])); + + XCTAssertTrue(snapshot.isFromCache); + XCTAssertFalse(snapshot.syncStateChanged); +} + +- (void)testRemovesDocumentsForQueryWithLimit { + FSTQuery *query = [self queryForMessages]; + query = [query queryBySettingLimit:2]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{@"text" : @"msg3"}, NO); + + // initial state + FSTTestApplyChanges(view, @[ doc1, doc3 ], nil); + + // add doc2, which should push out doc3 + FSTViewSnapshot *snapshot = + FSTTestApplyChanges(view, @[ doc2 ], + [FSTTargetChange changeWithDocuments:@[ doc1, doc2, doc3 ] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]); + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc2 ])); + + XCTAssertEqualObjects( + snapshot.documentChanges, (@[ + [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeRemoved], + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded] + ])); + + XCTAssertFalse(snapshot.isFromCache); + XCTAssertTrue(snapshot.syncStateChanged); +} + +- (void)testDoesntReportChangesForDocumentBeyondLimitOfQuery { + FSTQuery *query = [self queryForMessages]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"num") + ascending:YES]]; + query = [query queryBySettingLimit:2]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"num" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"num" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"num" : @3 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{ @"num" : @4 }, NO); + + // initial state + FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + + // change doc2 to 5, and add doc3 and doc4. + // doc2 will be modified + removed = removed + // doc3 will be added + // doc4 will be added + removed = nothing + doc2 = FSTTestDoc(@"rooms/eros/messages/2", 1, @{ @"num" : @5 }, NO); + FSTViewDocumentChanges *viewDocChanges = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2, doc3, doc4 ])]; + XCTAssertTrue(viewDocChanges.needsRefill); + // Verify that all the docs still match. + viewDocChanges = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3, doc4 ]) + previousChanges:viewDocChanges]; + FSTViewSnapshot *snapshot = + [view applyChangesToDocuments:viewDocChanges + targetChange:[FSTTargetChange + changeWithDocuments:@[ doc1, doc2, doc3, doc4 ] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]] + .snapshot; + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc3 ])); + + XCTAssertEqualObjects( + snapshot.documentChanges, (@[ + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeRemoved], + [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded] + ])); + + XCTAssertFalse(snapshot.isFromCache); + XCTAssertTrue(snapshot.syncStateChanged); +} + +- (void)testKeepsTrackOfLimboDocuments { + FSTQuery *query = [self queryForMessages]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO); + + FSTViewChange *change = [view + applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1 ])]]; + XCTAssertEqualObjects(change.limboChanges, @[]); + + change = + [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])] + targetChange:[FSTTargetChange + changeWithDocuments:@[] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]]; + XCTAssertEqualObjects( + change.limboChanges, + @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded key:doc1.key] ]); + + change = [view + applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])] + targetChange:[FSTTargetChange changeWithDocuments:@[ doc1 ] + currentStatusUpdate:FSTCurrentStatusUpdateNone]]; + XCTAssertEqualObjects( + change.limboChanges, + @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved key:doc1.key] ]); + + change = [view + applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ])] + targetChange:[FSTTargetChange changeWithDocuments:@[ doc2 ] + currentStatusUpdate:FSTCurrentStatusUpdateNone]]; + XCTAssertEqualObjects(change.limboChanges, @[]); + + change = [view + applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]]; + XCTAssertEqualObjects( + change.limboChanges, + @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded key:doc3.key] ]); + + change = [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ + FSTTestDeletedDoc(@"rooms/eros/messages/2", + 1) + ])]]; // remove + XCTAssertEqualObjects( + change.limboChanges, + @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved key:doc3.key] ]); +} + +- (void)testResumingQueryCreatesNoLimbos { + FSTQuery *query = [self queryForMessages]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + + // Unlike other cases, here the view is initialized with a set of previously synced documents + // which happens when listening to a previously listened-to query. + FSTView *view = [[FSTView alloc] initWithQuery:query + remoteDocuments:FSTTestDocKeySet(@[ doc1.key, doc2.key ])]; + + FSTTargetChange *markCurrent = + [FSTTargetChange changeWithDocuments:@[] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]; + FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[])]; + FSTViewChange *change = [view applyChangesToDocuments:changes targetChange:markCurrent]; + XCTAssertEqualObjects(change.limboChanges, @[]); +} + +- (void)assertDocSet:(FSTDocumentSet *)docSet containsDocs:(NSArray<FSTDocument *> *)docs { + XCTAssertEqual(docs.count, docSet.count); + for (FSTDocument *doc in docs) { + XCTAssertTrue([docSet containsKey:doc.key]); + } +} + +- (void)testReturnsNeedsRefillOnDeleteInLimitQuery { + FSTQuery *query = [[self queryForMessages] queryBySettingLimit:2]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(2, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Remove one of the docs. + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ FSTTestDeletedDoc( + @"rooms/eros/messages/0", 0) ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc2 ]]; + XCTAssertTrue(changes.needsRefill); + XCTAssertEqual(1, [changes.changeSet changes].count); + // Refill it with just the one doc remaining. + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ]) previousChanges:changes]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(1, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testReturnsNeedsRefillOnReorderInLimitQuery { + FSTQuery *query = [self queryForMessages]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"order") + ascending:YES]]; + query = [query queryBySettingLimit:2]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{ @"order" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"order" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"order" : @3 }, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(2, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Move one of the docs. + doc2 = FSTTestDoc(@"rooms/eros/messages/1", 1, @{ @"order" : @2000 }, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertTrue(changes.needsRefill); + XCTAssertEqual(1, [changes.changeSet changes].count); + // Refill it with all three current docs. + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3 ]) + previousChanges:changes]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc3 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(2, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testDoesntNeedRefillOnReorderWithinLimit { + FSTQuery *query = [self queryForMessages]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"order") + ascending:YES]]; + query = [query queryBySettingLimit:3]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{ @"order" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"order" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"order" : @3 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"order" : @4 }, NO); + FSTDocument *doc5 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{ @"order" : @5 }, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3, doc4, doc5 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2, doc3 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(3, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Move one of the docs. + doc1 = FSTTestDoc(@"rooms/eros/messages/0", 1, @{ @"order" : @3 }, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc2, doc3, doc1 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(1, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testDoesntNeedRefillOnReorderAfterLimitQuery { + FSTQuery *query = [self queryForMessages]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"order") + ascending:YES]]; + query = [query queryBySettingLimit:3]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{ @"order" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"order" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"order" : @3 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"order" : @4 }, NO); + FSTDocument *doc5 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{ @"order" : @5 }, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3, doc4, doc5 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2, doc3 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(3, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Move one of the docs. + doc4 = FSTTestDoc(@"rooms/eros/messages/3", 1, @{ @"order" : @6 }, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc4 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2, doc3 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(0, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testDoesntNeedRefillForAdditionAfterTheLimit { + FSTQuery *query = [[self queryForMessages] queryBySettingLimit:2]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(2, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Add a doc that is past the limit. + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 1, @{}, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(0, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testDoesntNeedRefillForDeletionsWhenNotNearTheLimit { + FSTQuery *query = [[self queryForMessages] queryBySettingLimit:20]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(2, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Remove one of the docs. + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ FSTTestDeletedDoc( + @"rooms/eros/messages/1", 0) ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(1, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testHandlesApplyingIrrelevantDocs { + FSTQuery *query = [[self queryForMessages] queryBySettingLimit:2]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(2, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Remove a doc that isn't even in the results. + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ FSTTestDeletedDoc( + @"rooms/eros/messages/2", 0) ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(0, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testComputesMutatedKeys { + FSTQuery *query = [self queryForMessages]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [view applyChangesToDocuments:changes]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[])); + + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, YES); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc3.key ])); +} + +- (void)testRemovesKeysFromMutatedKeysWhenNewDocHasNoLocalChanges { + FSTQuery *query = [self queryForMessages]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, YES); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [view applyChangesToDocuments:changes]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); + + FSTDocument *doc2Prime = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2Prime ])]; + [view applyChangesToDocuments:changes]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[])); +} + +- (void)testRemembersLocalMutationsFromPreviousSnapshot { + FSTQuery *query = [self queryForMessages]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, YES); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [view applyChangesToDocuments:changes]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); + + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]; + [view applyChangesToDocuments:changes]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); +} + +- (void)testRemembersLocalMutationsFromPreviousCallToComputeChangesWithDocuments { + FSTQuery *query = [self queryForMessages]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, YES); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); + + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ]) previousChanges:changes]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); +} + +@end + +NS_ASSUME_NONNULL_END |