More improvements on modelchangeobserving

This commit is contained in:
michael starke
2016-08-29 17:44:32 +02:00
parent 0bf439e01d
commit cd47237742
8 changed files with 134 additions and 10 deletions

View File

@@ -1,3 +1,3 @@
github "sparkle-project/Sparkle" ~> 1.13.1 github "sparkle-project/Sparkle" ~> 1.13.1
github "mstarke/KeePassKit" "c53c045a93b1e8496e5d2122bc4bee32c08194a6" github "mstarke/KeePassKit" "bf47781a618fc514288315995a966b5f630f6918"
github "mstarke/HNHUi" ~> 1.0 github "mstarke/HNHUi" ~> 1.0

View File

@@ -1,3 +1,3 @@
github "mstarke/HNHUi" "1.0.1" github "mstarke/HNHUi" "1.0.1"
github "mstarke/KeePassKit" "c53c045a93b1e8496e5d2122bc4bee32c08194a6" github "mstarke/KeePassKit" "722652647b9f1ff0967429ac02da046e3534178f"
github "sparkle-project/Sparkle" "1.14.0" github "sparkle-project/Sparkle" "1.14.0"

View File

@@ -233,6 +233,7 @@
4CE3E62617AB0D2D00D9E4B4 /* MPAttachmentTableDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE3E62517AB0D2D00D9E4B4 /* MPAttachmentTableDataSource.m */; }; 4CE3E62617AB0D2D00D9E4B4 /* MPAttachmentTableDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE3E62517AB0D2D00D9E4B4 /* MPAttachmentTableDataSource.m */; };
4CE501341BBC47F500FB819D /* MPTagsTokenFieldDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE501331BBC47F500FB819D /* MPTagsTokenFieldDelegate.m */; }; 4CE501341BBC47F500FB819D /* MPTagsTokenFieldDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE501331BBC47F500FB819D /* MPTagsTokenFieldDelegate.m */; };
4CE5B54B173AFBA700207B39 /* MPDocument.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE5B549173AFBA700207B39 /* MPDocument.m */; }; 4CE5B54B173AFBA700207B39 /* MPDocument.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE5B549173AFBA700207B39 /* MPDocument.m */; };
4CE7FAFB1D74813500E2C856 /* MPTestModelChangeObservingHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE7FAFA1D74813500E2C856 /* MPTestModelChangeObservingHelper.m */; };
4CE8246F16E2E93400573141 /* MPOverlayWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8246E16E2E93400573141 /* MPOverlayWindowController.m */; }; 4CE8246F16E2E93400573141 /* MPOverlayWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8246E16E2E93400573141 /* MPOverlayWindowController.m */; };
4CE8247516E2F2B900573141 /* MPOverlayView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8247416E2F2B900573141 /* MPOverlayView.m */; }; 4CE8247516E2F2B900573141 /* MPOverlayView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8247416E2F2B900573141 /* MPOverlayView.m */; };
4CE88B9717BA651C0042E078 /* contextTriangleTemplate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 4CE88B9617BA651C0042E078 /* contextTriangleTemplate.pdf */; }; 4CE88B9717BA651C0042E078 /* contextTriangleTemplate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 4CE88B9617BA651C0042E078 /* contextTriangleTemplate.pdf */; };
@@ -656,6 +657,7 @@
4CE501331BBC47F500FB819D /* MPTagsTokenFieldDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPTagsTokenFieldDelegate.m; sourceTree = "<group>"; }; 4CE501331BBC47F500FB819D /* MPTagsTokenFieldDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPTagsTokenFieldDelegate.m; sourceTree = "<group>"; };
4CE5B548173AFBA700207B39 /* MPDocument.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPDocument.h; sourceTree = "<group>"; }; 4CE5B548173AFBA700207B39 /* MPDocument.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPDocument.h; sourceTree = "<group>"; };
4CE5B549173AFBA700207B39 /* MPDocument.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = MPDocument.m; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 4CE5B549173AFBA700207B39 /* MPDocument.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = MPDocument.m; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objc; };
4CE7FAFA1D74813500E2C856 /* MPTestModelChangeObservingHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPTestModelChangeObservingHelper.m; sourceTree = "<group>"; };
4CE8246D16E2E93400573141 /* MPOverlayWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPOverlayWindowController.h; sourceTree = "<group>"; }; 4CE8246D16E2E93400573141 /* MPOverlayWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPOverlayWindowController.h; sourceTree = "<group>"; };
4CE8246E16E2E93400573141 /* MPOverlayWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPOverlayWindowController.m; sourceTree = "<group>"; }; 4CE8246E16E2E93400573141 /* MPOverlayWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPOverlayWindowController.m; sourceTree = "<group>"; };
4CE8247316E2F2B900573141 /* MPOverlayView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPOverlayView.h; sourceTree = "<group>"; }; 4CE8247316E2F2B900573141 /* MPOverlayView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPOverlayView.h; sourceTree = "<group>"; };
@@ -991,6 +993,7 @@
4C45FB2C178E0BCB0010007D /* MPDatabaseLoading.m */, 4C45FB2C178E0BCB0010007D /* MPDatabaseLoading.m */,
4C45FB2F178E0CE20010007D /* MPTestDocument.m */, 4C45FB2F178E0CE20010007D /* MPTestDocument.m */,
4C6BC65F1A36717E00BDDF3D /* MPDatabaseSearch.m */, 4C6BC65F1A36717E00BDDF3D /* MPDatabaseSearch.m */,
4CE7FAFA1D74813500E2C856 /* MPTestModelChangeObservingHelper.m */,
4C45FB1F178E09ED0010007D /* Supporting Files */, 4C45FB1F178E09ED0010007D /* Supporting Files */,
); );
path = MacPassTests; path = MacPassTests;
@@ -1679,6 +1682,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
4CE7FAFB1D74813500E2C856 /* MPTestModelChangeObservingHelper.m in Sources */,
4C45FB2D178E0BCB0010007D /* MPDatabaseLoading.m in Sources */, 4C45FB2D178E0BCB0010007D /* MPDatabaseLoading.m in Sources */,
4C8DEAA21C314D2C00D24C32 /* MPTestAutotypeDelay.m in Sources */, 4C8DEAA21C314D2C00D24C32 /* MPTestAutotypeDelay.m in Sources */,
4C45FB30178E0CE20010007D /* MPTestDocument.m in Sources */, 4C45FB30178E0CE20010007D /* MPTestDocument.m in Sources */,

View File

@@ -53,6 +53,7 @@ NSString *const MPEntryTableURLColumnIdentifier = @"MPEntryTableURLColumnIdentif
NSString *const MPEntryTableNotesColumnIdentifier = @"MPEntryTableNotesColumnIdentifier"; NSString *const MPEntryTableNotesColumnIdentifier = @"MPEntryTableNotesColumnIdentifier";
NSString *const MPEntryTableAttachmentColumnIdentifier = @"MPEntryTableAttachmentColumnIdentifier"; NSString *const MPEntryTableAttachmentColumnIdentifier = @"MPEntryTableAttachmentColumnIdentifier";
NSString *const MPEntryTableModfiedColumnIdentifier = @"MPEntryTableModfiedColumnIdentifier"; NSString *const MPEntryTableModfiedColumnIdentifier = @"MPEntryTableModfiedColumnIdentifier";
NSString *const MPEntryTableHistoryColumnIdentifier = @"MPEntryTableHistoryColumnIdentifier";
NSString *const _MPTableImageCellView = @"ImageCell"; NSString *const _MPTableImageCellView = @"ImageCell";
NSString *const _MPTableStringCellView = @"StringCell"; NSString *const _MPTableStringCellView = @"StringCell";
@@ -128,12 +129,15 @@ NSString *const _MPTableSecurCellView = @"PasswordCell";
NSTableColumn *attachmentsColumn = [[NSTableColumn alloc] initWithIdentifier:MPEntryTableAttachmentColumnIdentifier]; NSTableColumn *attachmentsColumn = [[NSTableColumn alloc] initWithIdentifier:MPEntryTableAttachmentColumnIdentifier];
NSTableColumn *notesColumn = [[NSTableColumn alloc] initWithIdentifier:MPEntryTableNotesColumnIdentifier]; NSTableColumn *notesColumn = [[NSTableColumn alloc] initWithIdentifier:MPEntryTableNotesColumnIdentifier];
NSTableColumn *modifiedColumn = [[NSTableColumn alloc] initWithIdentifier:MPEntryTableModfiedColumnIdentifier]; NSTableColumn *modifiedColumn = [[NSTableColumn alloc] initWithIdentifier:MPEntryTableModfiedColumnIdentifier];
NSTableColumn *historyColumn = [[NSTableColumn alloc] initWithIdentifier:MPEntryTableHistoryColumnIdentifier];
notesColumn.minWidth = 40.0; notesColumn.minWidth = 40.0;
attachmentsColumn.minWidth = 40.0; attachmentsColumn.minWidth = 40.0;
modifiedColumn.minWidth = 40.0; modifiedColumn.minWidth = 40.0;
historyColumn.minWidth = 40.0;
[self.entryTable addTableColumn:notesColumn]; [self.entryTable addTableColumn:notesColumn];
[self.entryTable addTableColumn:attachmentsColumn]; [self.entryTable addTableColumn:attachmentsColumn];
[self.entryTable addTableColumn:modifiedColumn]; [self.entryTable addTableColumn:modifiedColumn];
[self.entryTable addTableColumn:historyColumn];
parentColumn.identifier = MPEntryTableParentColumnIdentifier; parentColumn.identifier = MPEntryTableParentColumnIdentifier;
titleColumn.identifier = MPEntryTableTitleColumnIdentifier; titleColumn.identifier = MPEntryTableTitleColumnIdentifier;
@@ -161,6 +165,7 @@ NSString *const _MPTableSecurCellView = @"PasswordCell";
notesColumn.headerCell.stringValue = NSLocalizedString(@"NOTES", ""); notesColumn.headerCell.stringValue = NSLocalizedString(@"NOTES", "");
attachmentsColumn.headerCell.stringValue = NSLocalizedString(@"ATTACHMENTS", ""); attachmentsColumn.headerCell.stringValue = NSLocalizedString(@"ATTACHMENTS", "");
modifiedColumn.headerCell.stringValue = NSLocalizedString(@"MODIFIED", ""); modifiedColumn.headerCell.stringValue = NSLocalizedString(@"MODIFIED", "");
historyColumn.headerCell.stringValue = NSLocalizedString(@"HISTORY", "");
[self.entryTable bind:NSContentBinding toObject:self.entryArrayController withKeyPath:NSStringFromSelector(@selector(arrangedObjects)) options:nil]; [self.entryTable bind:NSContentBinding toObject:self.entryArrayController withKeyPath:NSStringFromSelector(@selector(arrangedObjects)) options:nil];
[self.entryTable bind:NSSortDescriptorsBinding toObject:self.entryArrayController withKeyPath:NSStringFromSelector(@selector(sortDescriptors)) options:nil]; [self.entryTable bind:NSSortDescriptorsBinding toObject:self.entryArrayController withKeyPath:NSStringFromSelector(@selector(sortDescriptors)) options:nil];
@@ -235,6 +240,7 @@ NSString *const _MPTableSecurCellView = @"PasswordCell";
BOOL isAttachmentColumn = [tableColumn.identifier isEqualToString:MPEntryTableAttachmentColumnIdentifier]; BOOL isAttachmentColumn = [tableColumn.identifier isEqualToString:MPEntryTableAttachmentColumnIdentifier];
BOOL isNotesColumn = [tableColumn.identifier isEqualToString:MPEntryTableNotesColumnIdentifier]; BOOL isNotesColumn = [tableColumn.identifier isEqualToString:MPEntryTableNotesColumnIdentifier];
BOOL isModifedColumn = [tableColumn.identifier isEqualToString:MPEntryTableModfiedColumnIdentifier]; BOOL isModifedColumn = [tableColumn.identifier isEqualToString:MPEntryTableModfiedColumnIdentifier];
BOOL isHistoryColumn = [tableColumn.identifier isEqualToString:MPEntryTableHistoryColumnIdentifier];
NSTableCellView *view = nil; NSTableCellView *view = nil;
if(isTitleColumn || isGroupColumn) { if(isTitleColumn || isGroupColumn) {
@@ -320,10 +326,16 @@ NSString *const _MPTableSecurCellView = @"PasswordCell";
[view.textField bind:NSValueBinding toObject:view withKeyPath:notesKeyPath options:options]; [view.textField bind:NSValueBinding toObject:view withKeyPath:notesKeyPath options:options];
} }
else if(isAttachmentColumn) { else if(isAttachmentColumn) {
NSString *binariesCoundKeyPath = [NSString stringWithFormat:@"%@.%@.@count", NSString *binariesCountKeyPath = [NSString stringWithFormat:@"%@.%@.@count",
NSStringFromSelector(@selector(objectValue)), NSStringFromSelector(@selector(objectValue)),
NSStringFromSelector(@selector(binaries))]; NSStringFromSelector(@selector(binaries))];
[view.textField bind:NSValueBinding toObject:view withKeyPath:binariesCoundKeyPath options:nil]; [view.textField bind:NSValueBinding toObject:view withKeyPath:binariesCountKeyPath options:nil];
}
else if(isHistoryColumn) {
NSString *historyCountKeyPath = [NSString stringWithFormat:@"%@.%@.@count",
NSStringFromSelector(@selector(objectValue)),
NSStringFromSelector(@selector(history))];
[view.textField bind:NSValueBinding toObject:view withKeyPath:historyCountKeyPath options:nil];
} }
} }
return view; return view;

View File

@@ -23,15 +23,17 @@ FOUNDATION_EXTERN NSString *const MPDidChangeModelNotification;
FOUNDATION_EXTERN NSString *const MPModelChangeObservingKeyPathKey; FOUNDATION_EXTERN NSString *const MPModelChangeObservingKeyPathKey;
@required @required
- (void)observerModelChangesForKeyPath:(NSString *)keyPath; - (void)beginObservingModelChangesForKeyPath:(NSString *)keyPath;
- (void)endObservingModelChangesForKeyPath:(NSString *)keyPath;
@end @end
/* Use this helper to fire the right notifications */ /* Use this helper to fire the right notifications. You can hold an instance to help in you implementation of setValue:forKeyPath and observerModelChangesForKeyPath: */
@interface MPModelChangeObservingHelper : NSObject @interface MPModelChangeObservingHelper : NSObject
+ (void)willChangeModelKeyPath:(NSString *)keyPath observer:(id<MPModelChangeObserving>)observer; + (void)willChangeModelKeyPath:(NSString *)keyPath observer:(id<MPModelChangeObserving>)observer;
+ (void)didChangeModelKeyPath:(NSString *)keyPath observer:(id<MPModelChangeObserving>)observer; + (void)didChangeModelKeyPath:(NSString *)keyPath observer:(id<MPModelChangeObserving>)observer;
- (void)beginObservingModelChangesForKeyPath:(NSString *)keyPath;
- (void)endObservingModelChangesForKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath forTarget:(id)target; - (void)setValue:(id)value forKeyPath:(NSString *)keyPath forTarget:(id)target;
@end @end

View File

@@ -13,6 +13,11 @@ NSString *const MPDidChangeModelNotification = @"com.hicknhack.macpass.MPDidChan
NSString *const MPModelChangeObservingKeyPathKey = @"MPModelChangeObservingKeyPathKey"; NSString *const MPModelChangeObservingKeyPathKey = @"MPModelChangeObservingKeyPathKey";
@interface MPModelChangeObservingHelper ()
@property (strong) NSMutableSet<NSString *> *observedPaths;
@property (strong) NSMutableSet<NSString *> *matchedPathsCache;
@end
@implementation MPModelChangeObservingHelper @implementation MPModelChangeObservingHelper
+ (void)willChangeModelKeyPath:(NSString *)keyPath observer:(id<MPModelChangeObserving>)observer { + (void)willChangeModelKeyPath:(NSString *)keyPath observer:(id<MPModelChangeObserving>)observer {
@@ -24,7 +29,47 @@ NSString *const MPModelChangeObservingKeyPathKey = @"MPModelChangeObservingKeyPa
} }
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath forTarget:(id)target { - (void)setValue:(id)value forKeyPath:(NSString *)keyPath forTarget:(id)target {
[MPModelChangeObservingHelper willChangeModelKeyPath:keyPath observer:target];
[target setValue:value forKeyPath:keyPath]; [target setValue:value forKeyPath:keyPath];
[MPModelChangeObservingHelper didChangeModelKeyPath:keyPath observer:target];
}
- (void)beginObservingModelChangesForKeyPath:(NSString *)keyPath {
if(!self.observedPaths) {
self.observedPaths = [[NSMutableSet alloc] initWithCapacity:3];
}
[self.observedPaths addObject:keyPath];
}
- (void)endObservingModelChangesForKeyPath:(NSString *)keyPath {
[self.observedPaths removeObject:keyPath];
/* if we have nothing to observer, just clear the cache and exit */
if(self.observedPaths.count == 0) {
self.matchedPathsCache = nil;
return;
}
NSPredicate *predicat = [NSPredicate predicateWithBlock:^BOOL(id _Nonnull evaluatedObject, NSDictionary<NSString *,id> * _Nullable bindings) {
NSString *cachedPath = evaluatedObject;
return ![cachedPath hasPrefix:keyPath];
}];
[self.matchedPathsCache filterUsingPredicate:predicat];
}
- (BOOL)_isObservingKeyPath:(NSString *)keyPath {
if([self.matchedPathsCache containsObject:keyPath]) {
return YES;
}
for(NSString *observedKeyPath in self.observedPaths) {
if([keyPath hasPrefix:observedKeyPath]) {
if(!self.matchedPathsCache) {
self.matchedPathsCache = [[NSMutableSet alloc] initWithCapacity:3];
}
[self.matchedPathsCache addObject:keyPath];
return YES;
}
}
return NO;
} }
@end @end

View File

@@ -16,6 +16,8 @@
#import "MPAutotypePaste.h" #import "MPAutotypePaste.h"
#import "MPAutotypeKeyPress.h" #import "MPAutotypeKeyPress.h"
#import "MPSettingsHelper.h"
#import "MPKeyMapper.h" #import "MPKeyMapper.h"
@@ -196,7 +198,13 @@
keyPress = commands[0]; keyPress = commands[0];
/* Lower case is ok, since we only need the key, not the sequence to reproduce the string */ /* Lower case is ok, since we only need the key, not the sequence to reproduce the string */
XCTAssertTrue([@"t" isEqualToString:[MPKeyMapper stringForKey:keyPress.keyCode]]); XCTAssertTrue([@"t" isEqualToString:[MPKeyMapper stringForKey:keyPress.keyCode]]);
BOOL useCommandInsteadOfControl = [[NSUserDefaults standardUserDefaults] boolForKey:kMPSettingsKeySendCommandForControlKey];
if(useCommandInsteadOfControl) {
XCTAssertEqual(keyPress.modifierMask, kCGEventFlagMaskCommand); XCTAssertEqual(keyPress.modifierMask, kCGEventFlagMaskCommand);
}
else {
XCTAssertEqual(keyPress.modifierMask, kCGEventFlagMaskControl);
}
/* {USERNAME} */ /* {USERNAME} */
paste = commands[1]; paste = commands[1];
@@ -205,7 +213,12 @@
/* %+^{TAB} */ /* %+^{TAB} */
keyPress = commands[2]; keyPress = commands[2];
XCTAssertEqual(keyPress.keyCode, kVK_Tab); // Tab is a fixed key, no mapping needed XCTAssertEqual(keyPress.keyCode, kVK_Tab); // Tab is a fixed key, no mapping needed
if(useCommandInsteadOfControl) {
XCTAssertEqual(keyPress.modifierMask, (kCGEventFlagMaskCommand | kCGEventFlagMaskShift | kCGEventFlagMaskAlternate)); XCTAssertEqual(keyPress.modifierMask, (kCGEventFlagMaskCommand | kCGEventFlagMaskShift | kCGEventFlagMaskAlternate));
}
else {
XCTAssertEqual(keyPress.modifierMask, (kCGEventFlagMaskControl | kCGEventFlagMaskShift | kCGEventFlagMaskAlternate));
}
/* Whoo{PASSWORD} */ /* Whoo{PASSWORD} */
paste = commands[3]; paste = commands[3];
@@ -228,7 +241,12 @@
keyPress = commands[0]; keyPress = commands[0];
XCTAssertEqualObjects(@"t", [MPKeyMapper stringForKey:keyPress.keyCode]); XCTAssertEqualObjects(@"t", [MPKeyMapper stringForKey:keyPress.keyCode]);
/* TODO - Respect user settings? */ /* TODO - Respect user settings? */
if(useCommandInsteadOfControl) {
XCTAssertEqual(keyPress.modifierMask, kCGEventFlagMaskCommand); XCTAssertEqual(keyPress.modifierMask, kCGEventFlagMaskCommand);
}
else {
XCTAssertEqual(keyPress.modifierMask, kCGEventFlagMaskControl);
}
/*XCTAssertEqual(keyPress.modifierMask, kCGEventFlagMaskControl);*/ /*XCTAssertEqual(keyPress.modifierMask, kCGEventFlagMaskControl);*/
} }

View File

@@ -0,0 +1,43 @@
//
// MPTestModelChangeObservingHelper.m
// MacPass
//
// Created by Michael Starke on 29/08/16.
// Copyright © 2016 HicknHack Software GmbH. All rights reserved.
//
#import <XCTest/XCTest.h>
#import "MPModelChangeObserving.h"
@interface MPTestModelChangeObservingHelper : XCTestCase
@property (strong) MPModelChangeObservingHelper *helper;
@end
@implementation MPTestModelChangeObservingHelper
- (void)setUp {
[super setUp];
self.helper = [[MPModelChangeObservingHelper alloc] init];
}
- (void)tearDown {
self.helper = nil;
[super tearDown];
}
- (void)testAddObserver {
[self.helper beginObservingModelChangesForKeyPath:@"testKey"];
NSMutableSet *set = [self.helper valueForKey:@"observedPaths"];
XCTAssertEqual(set.count, 1, @"Observed paths contains one element");
XCTAssertTrue([set containsObject:@"testKey"], @"Observed set contains testKey");
}
- (void)testRemoveObserver {
NSString *aKeyPath = @"testKeyPath";
[self.helper beginObservingModelChangesForKeyPath:aKeyPath];
[self.helper endObservingModelChangesForKeyPath:aKeyPath];
NSMutableSet *set = [self.helper valueForKey:@"observedPaths"];
XCTAssertFalse([set containsObject:aKeyPath], @"Observed path is removed after end of observation");
}
@end