From 984e6dc47aa499cff55629ccabed81be59c69999 Mon Sep 17 00:00:00 2001 From: sgschantz Date: Mon, 8 Apr 2024 16:24:22 +0700 Subject: [PATCH 1/2] improve compliance detection, accounting for all delete and insert combinations --- .../Keyman4MacIM.xcodeproj/project.pbxproj | 4 + .../Keyman4MacIM/KMInputMethodEventHandler.m | 4 +- .../Keyman4MacIM/TextApiCompliance.h | 4 +- .../Keyman4MacIM/TextApiCompliance.m | 163 ++++++++++------- .../KeymanTests/InputMethodTests.m | 31 ---- .../KeymanTests/TextApiComplianceTests.m | 171 ++++++++++++++++++ 6 files changed, 280 insertions(+), 97 deletions(-) create mode 100644 mac/Keyman4MacIM/KeymanTests/TextApiComplianceTests.m diff --git a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj index cf83375ba8..902fca2460 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj +++ b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ 299B5E2129126CA20070841D /* keyman-icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 299B5E1F29126CA20070841D /* keyman-icon.png */; }; 29B42A572728339900EDD5D3 /* KMInfoWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 29B42A592728339900EDD5D3 /* KMInfoWindowController.xib */; }; 29B42A602728343B00EDD5D3 /* KMKeyboardHelpWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 29B42A622728343B00EDD5D3 /* KMKeyboardHelpWindowController.xib */; }; + 29B6FB732BC39DD60074BF7F /* TextApiComplianceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B6FB722BC39DD60074BF7F /* TextApiComplianceTests.m */; }; 37A245C12565DFA6000BBF92 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 37A245C02565DFA6000BBF92 /* Assets.xcassets */; }; 37AE5C9D239A7B770086CC7C /* qrcode.min.js in Resources */ = {isa = PBXBuildFile; fileRef = 37AE5C9C239A7B770086CC7C /* qrcode.min.js */; }; 37C2B0CB25FF2C350092E16A /* Help in Resources */ = {isa = PBXBuildFile; fileRef = 37C2B0CA25FF2C340092E16A /* Help */; }; @@ -227,6 +228,7 @@ 29B6BB5E292239B4008F04BD /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/KMKeyboardHelpWindowController.strings"; sourceTree = ""; }; 29B6BB5F292239B4008F04BD /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/MainMenu.strings"; sourceTree = ""; }; 29B6BB60292239DC008F04BD /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; + 29B6FB722BC39DD60074BF7F /* TextApiComplianceTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TextApiComplianceTests.m; sourceTree = ""; }; 29B70A23283CB6680086B580 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/KMAboutWindowController.strings"; sourceTree = ""; }; 29B70A24283CB6690086B580 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/preferences.strings"; sourceTree = ""; }; 29B70A25283CB66A0086B580 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/KMInfoWindowController.strings"; sourceTree = ""; }; @@ -682,6 +684,7 @@ E211769E20E18C0B00F8065D /* AppleCompliantTestClient.h */, E211769F20E18C5200F8065D /* AppleCompliantTestClient.m */, 2999C69829D3E4CE006366F5 /* InputMethodTests.m */, + 29B6FB722BC39DD60074BF7F /* TextApiComplianceTests.m */, ); path = KeymanTests; sourceTree = ""; @@ -1002,6 +1005,7 @@ 2999C69929D3E4CE006366F5 /* InputMethodTests.m in Sources */, E211769D20E182DD00F8065D /* NoContextTestClient.m in Sources */, E2D5529D20DD8E0200D77CFB /* LegacyTestClient.m in Sources */, + 29B6FB732BC39DD60074BF7F /* TextApiComplianceTests.m in Sources */, E21176A020E18C5200F8065D /* AppleCompliantTestClient.m in Sources */, E2585A8120DD7CF100CBB994 /* TestAppDelegate.m in Sources */, E28914D62024E4010048E71E /* KMInputMethodEventHandler.m in Sources */, diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m index 0cb52f7797..4c2c553d82 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m @@ -199,7 +199,7 @@ NSString* const kEasterEggKmxName = @"EnglishSpanish.kmx"; [self.appDelegate logDebugMessage:@"KMInputMethodHandler initWithClient checkTextApiCompliance: %@", _apiCompliance]; } else if (self.apiCompliance.isComplianceUncertain) { // We have a valid TextApiCompliance object, but compliance is uncertain, so test it - [self.apiCompliance testCompliance:client]; + [self.apiCompliance checkCompliance:client]; } } @@ -454,7 +454,7 @@ NSString* const kEasterEggKmxName = @"EnglishSpanish.kmx"; [client insertText:text replacementRange:replacementRange]; if (self.apiCompliance.isComplianceUncertain) { - [self.apiCompliance testComplianceAfterInsert:client]; + [self.apiCompliance checkComplianceAfterInsert:client delete:textToDelete insert:text]; } } diff --git a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.h b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.h index a705ce1b8d..6c80417faf 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.h @@ -16,8 +16,8 @@ NS_ASSUME_NONNULL_BEGIN @property (readonly) NSString *clientApplicationId; -(instancetype)initWithClient:(id) client applicationId:(NSString *)appId; --(void)testCompliance:(id) client; --(void)testComplianceAfterInsert:(id) client; +-(void)checkCompliance:(id) client; +-(void) checkComplianceAfterInsert:(id) client delete:(NSString *)textToDelete insert:(NSString *)textToInsert; -(BOOL)isComplianceUncertain; -(BOOL)canReadText; -(BOOL)mustBackspaceUsingEvents; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m index e854b9326e..6ecc5e13ff 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m @@ -64,7 +64,7 @@ NSString *const kKMLegacyApps = @"KMLegacyApps"; // if we do not have hard-coded noncompliance, then test the app if (![self applyNoncompliantAppLists:appId]) { - [self testCompliance:client]; + [self checkCompliance:client]; } } return self; @@ -76,79 +76,118 @@ return [NSString stringWithFormat:@"complianceUncertain: %d, hasCompliantSelecti } /** test to see if the API selectedRange functions properly for the text input client */ --(void) testCompliance:(id) client { - BOOL selectionApiVerified = NO; - +-(void) checkCompliance:(id) client { // confirm that the API actually exists (this always seems to return true) - selectionApiVerified = [client respondsToSelector:@selector(selectedRange)]; + self.hasCompliantSelectionApi = [client respondsToSelector:@selector(selectedRange)]; - if (!selectionApiVerified) { + if (!self.hasCompliantSelectionApi) { self.complianceUncertain = NO; - self.hasCompliantSelectionApi = NO; } else { // if API exists, call it and see if it works as expected - NSRange selectionRange = [client selectedRange]; - self.initialSelection = selectionRange; - [self.appDelegate logDebugMessage:@"TextApiCompliance testCompliance, location = %lu, length = %lu", selectionRange.location, selectionRange.length]; - - if (selectionRange.location == NSNotFound) { - // NSNotFound may just mean that we don't have the focus yet; say NO for - // now, but this may toggle back to YES after the first insertText - selectionApiVerified = NO; - self.complianceUncertain = YES; - [self.appDelegate logDebugMessage:@"TextApiCompliance testCompliance not compliant but uncertain, range is NSNotFound"]; - } else if (selectionRange.location >= 0) { - // location greater than or equal to zero may just mean that the client - // returns an inaccurate value; say YES for now, but set back to NO if the - // first insertText does not cause a change in the selection - selectionApiVerified = YES; - self.complianceUncertain = YES; - [self.appDelegate logDebugMessage:@"TextApiCompliance testCompliance compliant but uncertain, location >= 0"]; - } + self.initialSelection = [client selectedRange]; + [self.appDelegate logDebugMessage:@"TextApiCompliance testCompliance, location = %lu, length = %lu", self.initialSelection.location, self.initialSelection.length]; + [self checkComplianceUsingInitialSelection]; } - [self.appDelegate logDebugMessage:@"TextApiCompliance testCompliance workingSelectionApi for app %@: set to %@", self.clientApplicationId, selectionApiVerified?@"yes":@"no"]; - - self.hasCompliantSelectionApi = selectionApiVerified; + [self.appDelegate logDebugMessage:@"TextApiCompliance testCompliance workingSelectionApi for app %@: set to %@", self.clientApplicationId, self.complianceUncertain?@"YES":@"NO"]; } -/** if complianceUncertain is true, checking the selection after an insert can make it clear */ --(void) testComplianceAfterInsert:(id) client { - if(self.complianceUncertain) { - NSRange selectionRange = [client selectedRange]; - [self.appDelegate logDebugMessage:@"TextApiCompliance testSelectionApiAfterInsert, location = %lu, length = %lu", selectionRange.location, selectionRange.length]; - - if (selectionRange.location == NSNotFound) { - // NO for certain, insertText means we have focus, NSNotFound means that the selection API does not work - self.hasCompliantSelectionApi = NO; - self.complianceUncertain = NO; - [self.appDelegate logDebugMessage:@"TextApiCompliance testComplianceAfterInsert certain, non-compliant, range is NSNotFound"]; - } else if (selectionRange.location == 0) { - // NO for certain, after an insertText we cannot be at location 0 - self.hasCompliantSelectionApi = NO; - self.complianceUncertain = NO; - [self.appDelegate logDebugMessage:@"TextApiCompliance testComplianceAfterInsert certain, non-compliant, location = 0"]; - } else if (selectionRange.location > 0) { - if (NSEqualRanges(selectionRange, self.initialSelection)) { - // NO for certain, when the selection is unchanged after an insert - self.hasCompliantSelectionApi = NO; - self.complianceUncertain = NO; - [self.appDelegate logDebugMessage:@"TextApiCompliance testComplianceAfterInsert certain, non-compliant, selection the same: initial location = %@, new location = %@", NSStringFromRange(self.initialSelection), NSStringFromRange(selectionRange)]; - } else { - // YES for certain, when the selection is changed after an insert - self.hasCompliantSelectionApi = YES; - self.complianceUncertain = NO; - [self.appDelegate logDebugMessage:@"TextApiCompliance testComplianceAfterInsert certain, compliant, selection changed from initial location = %@ to new location = %@", NSStringFromRange(self.initialSelection), NSStringFromRange(selectionRange)]; - } - } - - [self.appDelegate logDebugMessage:@"TextApiCompliance testComplianceAfterInsert, self.hasWorkingSelectionApi = %@ for app %@", self.hasCompliantSelectionApi?@"yes":@"no", self.clientApplicationId]; - [self.appDelegate logDebugMessage:@"TextApiCompliance testComplianceAfterInsert TextApiCompliance: %@", self]; - } else { - [self.appDelegate logDebugMessage:@"TextApiCompliance testSelectionApiAfterInsert, compliance is already known"]; +-(void) checkComplianceUsingInitialSelection { + if (self.initialSelection.location == NSNotFound) { + /** + * NSNotFound may just mean that we don't have the focus yet; say NO for now + * now, but this may toggle back to YES after the first insertText + */ + self.hasCompliantSelectionApi = NO; + self.complianceUncertain = YES; + [self.appDelegate logDebugMessage:@"TextApiCompliance testCompliance not compliant but uncertain, range is NSNotFound"]; + } else if (self.initialSelection.location >= 0) { + /** + * location greater than or equal to zero may just mean that the client + * returns an inaccurate value; say YES for now, but set back to NO if the + * if the selection is not consistent with the first insert + */ + self.hasCompliantSelectionApi = YES; + self.complianceUncertain = YES; + [self.appDelegate logDebugMessage:@"TextApiCompliance testCompliance compliant but uncertain, location >= 0"]; } } +/** + * If complianceUncertain is true, checking the location after an insert can confirm whether the app is compliant. + * Delete and insert are what core instructed us to do. + * Text that was selected in the client when the key was processed is irrelevant as it does not affect the location. + */ +-(void) checkComplianceAfterInsert:(id) client delete:(NSString *)textToDelete insert:(NSString *)textToInsert { + + // return if compliance is already certain + if(!self.complianceUncertain) { + [self.appDelegate logDebugMessage:@"TextApiCompliance testSelectionApiAfterInsert, compliance is already known"]; + return; + } + + NSRange selectionRange = [client selectedRange]; + if ([self validateNewLocation:selectionRange.location delete:textToDelete]) { + BOOL changeExpected = [self isLocationChangeExpectedOnInsert:textToDelete insert:textToInsert]; + BOOL locationChanged = [self hasLocationChanged:selectionRange]; + [self validateLocationChange:changeExpected hasLocationChanged:locationChanged]; + } + + [self.appDelegate logDebugMessage:@"TextApiCompliance testComplianceAfterInsert, self.hasWorkingSelectionApi = %@ for app %@", self.hasCompliantSelectionApi?@"YES":@"NO", self.clientApplicationId]; +} + +- (BOOL)validateNewLocation:(NSUInteger)location delete:(NSString *)textToDelete { + BOOL validLocation = NO; + + if (location == NSNotFound) { + // invalid location: insertText means we have focus, NSNotFound means selection API not functioning + self.hasCompliantSelectionApi = NO; + self.complianceUncertain = NO; + [self.appDelegate logDebugMessage:@"TextApiCompliance validateNewLocation = NO, location is NSNotFound"]; + } else if ((location == 0) && (textToDelete.length > 0)) { + // invalid location: cannot have text to delete at location zero + self.hasCompliantSelectionApi = NO; + self.complianceUncertain = NO; + [self.appDelegate logDebugMessage:@"TextApiCompliance validateNewLocation = NO, location is zero with textToDelete.length > 0"]; + } else { + // location is valid, but do not know if it is compliant yet + validLocation = YES; + [self.appDelegate logDebugMessage:@"TextApiCompliance validateNewLocation = YES, newLocation = %d, oldLocation = %d", location, self.initialSelection.location]; + } + return validLocation; +} + +- (void) validateLocationChange:(BOOL) changeExpected hasLocationChanged:(BOOL) locationChanged { + + if (changeExpected == locationChanged) { + // YES for certain, the location is where we expect it + self.hasCompliantSelectionApi = YES; + self.complianceUncertain = NO; + [self.appDelegate logDebugMessage:@"TextApiCompliance validateLocationChange compliant, locationChanged = %@, changeExpected = %@", locationChanged?@"YES":@"NO", changeExpected?@"YES":@"NO"]; + } else if (changeExpected != locationChanged) { + // NO for certain, when the selection is unchanged after an insert + self.hasCompliantSelectionApi = NO; + self.complianceUncertain = NO; + [self.appDelegate logDebugMessage:@"TextApiCompliance validateLocationChange non-compliant, locationChanged = %@, changeExpected = %@", locationChanged?@"YES":@"NO", changeExpected?@"YES":@"NO"]; + } +} + +- (BOOL)isLocationChangeExpectedOnInsert:(NSString *)textToDelete insert:(NSString *)textToInsert { + BOOL changeExpected = textToInsert.length != textToDelete.length; + [self.appDelegate logDebugMessage:@"TextApiCompliance isLocationChangeExpected, changeExpected = %@", changeExpected?@"YES":@"NO"]; + + return changeExpected; +} + +- (BOOL)hasLocationChanged:(NSRange)newSelection { + NSUInteger newLocation = newSelection.location; + NSUInteger oldLocation = self.initialSelection.location; + BOOL locationChanged = newLocation != oldLocation; + [self.appDelegate logDebugMessage:@"TextApiCompliance hasLocationChanged, currentSelection = %lu, length = %lu", newSelection.location, newSelection.length]; + + return locationChanged; +} + /** * Apply the hard-coded non-compliant app list and the user-managed (via user defaults) non-compliant app list. * If true, mark the app as non-compliant and certain (not complianceUncertain). diff --git a/mac/Keyman4MacIM/KeymanTests/InputMethodTests.m b/mac/Keyman4MacIM/KeymanTests/InputMethodTests.m index ab19175bb4..0b06b99fee 100644 --- a/mac/Keyman4MacIM/KeymanTests/InputMethodTests.m +++ b/mac/Keyman4MacIM/KeymanTests/InputMethodTests.m @@ -13,20 +13,12 @@ #import "KMInputMethodEventHandler.h" #import "AppleCompliantTestClient.h" #import "TextApiCompliance.h" -#import "AppleCompliantTestClient.h" KMInputMethodEventHandler *testEventHandler = nil; @interface InputMethodTests : XCTestCase @end -// included following interface that we can see and test private methods of TextApiCompliance -@interface TextApiCompliance (Testing) - -- (BOOL)arrayContainsApplicationId:(NSString *)clientAppId fromArray:(NSArray *)legacyApps; - -@end - @interface KMInputMethodEventHandler (Testing) - (instancetype)initWithClient:(NSString *)clientAppId client:(id) sender; @@ -46,29 +38,6 @@ KMInputMethodEventHandler *testEventHandler = nil; // Put teardown code here. This method is called after the invocation of each test method in the class. } -- (void)testIsClientAppLegacy_unlistedClientAppId_returnsNo { - id client = [[AppleCompliantTestClient alloc] init]; - NSString *clientAppId = @"com.compliant.app"; - TextApiCompliance *apiCompliance = [[TextApiCompliance alloc]initWithClient:client applicationId:clientAppId]; - - NSArray *legacyAppsArray = [NSArray arrayWithObjects:@"com.microsoft.VSCode",@"com.adobe.Photoshop",nil]; - - BOOL isLegacy = [apiCompliance arrayContainsApplicationId:clientAppId fromArray:legacyAppsArray]; - NSLog(@"isLegacy = %@", isLegacy?@"yes":@"no"); - XCTAssertFalse(isLegacy, @"App not expected to be in legacy list"); -} - -- (void)testIsClientAppLegacy_listedClientAppId_returnsYes { - id client = [[AppleCompliantTestClient alloc] init]; - NSString *clientAppId = @"com.microsoft.VSCode"; - TextApiCompliance *apiCompliance = [[TextApiCompliance alloc]initWithClient:client applicationId:clientAppId]; - - NSArray *legacyAppsArray = [NSArray arrayWithObjects:@"com.adobe.Photoshop",@"com.microsoft.VSCode",nil]; - - BOOL isLegacy = [apiCompliance arrayContainsApplicationId:clientAppId fromArray:legacyAppsArray]; - XCTAssertTrue(isLegacy, @"App expected to be in legacy list"); -} - - (void)testCalculateInsertRange_noDelete_returnsCurrentLocation { NSRange selectionRange = NSMakeRange(1, 0); NSRange insertRange = [testEventHandler calculateInsertRangeForDeletedText:@"" selectionRange:selectionRange]; diff --git a/mac/Keyman4MacIM/KeymanTests/TextApiComplianceTests.m b/mac/Keyman4MacIM/KeymanTests/TextApiComplianceTests.m new file mode 100644 index 0000000000..8b1c3be94d --- /dev/null +++ b/mac/Keyman4MacIM/KeymanTests/TextApiComplianceTests.m @@ -0,0 +1,171 @@ +/** + * Keyman is copyright (C) SIL International. MIT License. + * + * TextApiComplianceTests.m + * KeymanTests + * + * Created by Shawn Schantz on 2024-04-08. + * + * Unit tests of TextApiCompliance class + */ + +#import +#import "TextApiCompliance.h" +#import "AppleCompliantTestClient.h" + +@interface TextApiComplianceTests : XCTestCase +@end + +// included following interface that we can see and test private methods of TextApiCompliance +@interface TextApiCompliance (Testing) +@property NSRange initialSelection; +- (BOOL)validateNewLocation:(NSUInteger)location delete:(NSString *)textToDelete; +- (void) validateLocationChange:(BOOL) changeExpected hasLocationChanged:(BOOL) locationChanged; +- (BOOL)isLocationChangeExpectedOnInsert:(NSString *)textToDelete insert:(NSString *)textToInsert; +- (BOOL)hasLocationChanged:(NSRange)newSelection; +- (BOOL)arrayContainsApplicationId:(NSString *)clientAppId fromArray:(NSArray *)legacyApps; +@end + +@implementation TextApiComplianceTests + +- (void)setUp { + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the class. +} + +- (void)testValidateNewLocation_locationNotFound_returnsNo { + id client = [[AppleCompliantTestClient alloc] init]; + NSString *clientAppId = @"com.compliant.app"; + TextApiCompliance *apiCompliance = [[TextApiCompliance alloc]initWithClient:client applicationId:clientAppId]; + BOOL isNewLocationValid = [apiCompliance validateNewLocation:NSNotFound delete:@"a"]; + XCTAssertFalse(isNewLocationValid, @"New location is not valid if equal to NSNotFound"); +} + +- (void)testValidateNewLocation_deleteAtLocationZero_returnsNo { + id client = [[AppleCompliantTestClient alloc] init]; + NSString *clientAppId = @"com.compliant.app"; + TextApiCompliance *apiCompliance = [[TextApiCompliance alloc]initWithClient:client applicationId:clientAppId]; + BOOL isNewLocationValid = [apiCompliance validateNewLocation:0 delete:@"a"]; + XCTAssertFalse(isNewLocationValid, @"New location is not valid if attempting to delete at location of zero"); +} + +- (void)testValidateNewLocation_deleteAtLocationOne_returnsYes { + id client = [[AppleCompliantTestClient alloc] init]; + NSString *clientAppId = @"com.compliant.app"; + TextApiCompliance *apiCompliance = [[TextApiCompliance alloc]initWithClient:client applicationId:clientAppId]; + BOOL isNewLocationValid = [apiCompliance validateNewLocation:1 delete:@"a"]; + XCTAssertTrue(isNewLocationValid, @"New location is valid if attempting to delete at location of one"); +} + +- (void)testIsLocationChangeExpectedOnInsert_nilDeleteString_returnsYes { + id client = [[AppleCompliantTestClient alloc] init]; + NSString *clientAppId = @"com.compliant.app"; + TextApiCompliance *apiCompliance = [[TextApiCompliance alloc]initWithClient:client applicationId:clientAppId]; + BOOL isExpected = [apiCompliance isLocationChangeExpectedOnInsert:nil insert:@"a"]; + XCTAssertTrue(isExpected, @"Location change is expected for nil delete string and non-empty insert string"); +} + +- (void)testIsLocationChangeExpectedOnInsert_emptyDeleteString_returnsYes { + id client = [[AppleCompliantTestClient alloc] init]; + NSString *clientAppId = @"com.compliant.app"; + TextApiCompliance *apiCompliance = [[TextApiCompliance alloc]initWithClient:client applicationId:clientAppId]; + BOOL isExpected = [apiCompliance isLocationChangeExpectedOnInsert:@"" insert:@"a"]; + XCTAssertTrue(isExpected, @"Location change is expected for empty delete string and non-empty insert string"); +} + +- (void)testIsLocationChangeExpectedOnInsert_deleteAndInsertOneCharacter_returnsNo { + id client = [[AppleCompliantTestClient alloc] init]; + NSString *clientAppId = @"com.compliant.app"; + TextApiCompliance *apiCompliance = [[TextApiCompliance alloc]initWithClient:client applicationId:clientAppId]; + BOOL isExpected = [apiCompliance isLocationChangeExpectedOnInsert:@"a" insert:@"X"]; + XCTAssertFalse(isExpected, @"Location change not expected for delete string and insert string of same length"); +} + +- (void)testIsLocationChangeExpectedOnInsert_deleteAndInsertSurrogatePair_returnsNo { + id client = [[AppleCompliantTestClient alloc] init]; + NSString *clientAppId = @"com.compliant.app"; + TextApiCompliance *apiCompliance = [[TextApiCompliance alloc]initWithClient:client applicationId:clientAppId]; + BOOL isExpected = [apiCompliance isLocationChangeExpectedOnInsert:@"𑀓" insert:@"𑀢"]; + XCTAssertFalse(isExpected, @"Location change not expected for delete string and insert string of same length"); +} + +- (void)testHasLocationChanged_zeroForBoth_returnsNo { + id client = [[AppleCompliantTestClient alloc] init]; + NSString *clientAppId = @"com.compliant.app"; + TextApiCompliance *apiCompliance = [[TextApiCompliance alloc]initWithClient:client applicationId:clientAppId]; + NSRange zeroSelection = NSMakeRange(0, 0); + apiCompliance.initialSelection = zeroSelection; + + BOOL hasChanged = [apiCompliance hasLocationChanged:zeroSelection]; + XCTAssertFalse(hasChanged, @"Location change unexpected for matching selections"); +} + +/** + should not encounter the unintuitive case + of moving from a discrete location to NSNotFound because we will + catch this first with the method validateNewLocation + */ +- (void)testHasLocationChanged_newLocationNotFound_returnsNo { + id client = [[AppleCompliantTestClient alloc] init]; + NSString *clientAppId = @"com.compliant.app"; + TextApiCompliance *apiCompliance = [[TextApiCompliance alloc]initWithClient:client applicationId:clientAppId]; + NSRange notFoundSelection = NSMakeRange(NSNotFound, NSNotFound); + apiCompliance.initialSelection = notFoundSelection; + + BOOL hasChanged = [apiCompliance hasLocationChanged:notFoundSelection]; + XCTAssertFalse(hasChanged, @"Location change unexpected for selection of NSNotFound"); +} + +/** + * unlikely scenario, because if the initial location is notFound, then the app is + * probably non-complianat because we are still unable to get the current selection + */ +- (void)testHasLocationChanged_initialLocationNotFound_returnsYes { + id client = [[AppleCompliantTestClient alloc] init]; + NSString *clientAppId = @"com.compliant.app"; + TextApiCompliance *apiCompliance = [[TextApiCompliance alloc]initWithClient:client applicationId:clientAppId]; + NSRange notFoundSelection = NSMakeRange(NSNotFound, NSNotFound); + apiCompliance.initialSelection = notFoundSelection; + + BOOL hasChanged = [apiCompliance hasLocationChanged:NSMakeRange(0, 0)]; + XCTAssertTrue(hasChanged, @"Location change unexpected for selection of NSNotFound"); +} + +- (void)testHasLocationChanged_fromZeroToOne_returnsYes { + id client = [[AppleCompliantTestClient alloc] init]; + NSString *clientAppId = @"com.compliant.app"; + TextApiCompliance *apiCompliance = [[TextApiCompliance alloc]initWithClient:client applicationId:clientAppId]; + apiCompliance.initialSelection = NSMakeRange(0, 0); + + BOOL hasChanged = [apiCompliance hasLocationChanged:NSMakeRange(1, 0)]; + XCTAssertTrue(hasChanged, @"Location change expected for inserting one char"); +} + + +- (void)testIsClientAppLegacy_unlistedClientAppId_returnsNo { + id client = [[AppleCompliantTestClient alloc] init]; + NSString *clientAppId = @"com.compliant.app"; + TextApiCompliance *apiCompliance = [[TextApiCompliance alloc]initWithClient:client applicationId:clientAppId]; + + NSArray *legacyAppsArray = [NSArray arrayWithObjects:@"com.microsoft.VSCode",@"com.adobe.Photoshop",nil]; + + BOOL isLegacy = [apiCompliance arrayContainsApplicationId:clientAppId fromArray:legacyAppsArray]; + NSLog(@"isLegacy = %@", isLegacy?@"yes":@"no"); + XCTAssertFalse(isLegacy, @"App not expected to be in legacy list"); +} + +- (void)testIsClientAppLegacy_listedClientAppId_returnsYes { + id client = [[AppleCompliantTestClient alloc] init]; + NSString *clientAppId = @"com.microsoft.VSCode"; + TextApiCompliance *apiCompliance = [[TextApiCompliance alloc]initWithClient:client applicationId:clientAppId]; + + NSArray *legacyAppsArray = [NSArray arrayWithObjects:@"com.adobe.Photoshop",@"com.microsoft.VSCode",nil]; + + BOOL isLegacy = [apiCompliance arrayContainsApplicationId:clientAppId fromArray:legacyAppsArray]; + XCTAssertTrue(isLegacy, @"App expected to be in legacy list"); +} + +@end From 5b9e5d80f2fbffdc7e84553b0c2828fc2f091e18 Mon Sep 17 00:00:00 2001 From: sgschantz Date: Fri, 19 Apr 2024 14:19:58 +0700 Subject: [PATCH 2/2] correct method names in log statements --- mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m index 6ecc5e13ff..38cc99dba2 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m @@ -86,10 +86,10 @@ return [NSString stringWithFormat:@"complianceUncertain: %d, hasCompliantSelecti else { // if API exists, call it and see if it works as expected self.initialSelection = [client selectedRange]; - [self.appDelegate logDebugMessage:@"TextApiCompliance testCompliance, location = %lu, length = %lu", self.initialSelection.location, self.initialSelection.length]; + [self.appDelegate logDebugMessage:@"TextApiCompliance checkCompliance, location = %lu, length = %lu", self.initialSelection.location, self.initialSelection.length]; [self checkComplianceUsingInitialSelection]; } - [self.appDelegate logDebugMessage:@"TextApiCompliance testCompliance workingSelectionApi for app %@: set to %@", self.clientApplicationId, self.complianceUncertain?@"YES":@"NO"]; + [self.appDelegate logDebugMessage:@"TextApiCompliance checkCompliance workingSelectionApi for app %@: set to %@", self.clientApplicationId, self.complianceUncertain?@"YES":@"NO"]; } -(void) checkComplianceUsingInitialSelection { @@ -100,7 +100,7 @@ return [NSString stringWithFormat:@"complianceUncertain: %d, hasCompliantSelecti */ self.hasCompliantSelectionApi = NO; self.complianceUncertain = YES; - [self.appDelegate logDebugMessage:@"TextApiCompliance testCompliance not compliant but uncertain, range is NSNotFound"]; + [self.appDelegate logDebugMessage:@"TextApiCompliance checkComplianceUsingInitialSelection not compliant but uncertain, range is NSNotFound"]; } else if (self.initialSelection.location >= 0) { /** * location greater than or equal to zero may just mean that the client @@ -109,7 +109,7 @@ return [NSString stringWithFormat:@"complianceUncertain: %d, hasCompliantSelecti */ self.hasCompliantSelectionApi = YES; self.complianceUncertain = YES; - [self.appDelegate logDebugMessage:@"TextApiCompliance testCompliance compliant but uncertain, location >= 0"]; + [self.appDelegate logDebugMessage:@"TextApiCompliance checkComplianceUsingInitialSelection compliant but uncertain, location >= 0"]; } } @@ -122,7 +122,7 @@ return [NSString stringWithFormat:@"complianceUncertain: %d, hasCompliantSelecti // return if compliance is already certain if(!self.complianceUncertain) { - [self.appDelegate logDebugMessage:@"TextApiCompliance testSelectionApiAfterInsert, compliance is already known"]; + [self.appDelegate logDebugMessage:@"TextApiCompliance checkComplianceAfterInsert, compliance is already known"]; return; } @@ -133,7 +133,7 @@ return [NSString stringWithFormat:@"complianceUncertain: %d, hasCompliantSelecti [self validateLocationChange:changeExpected hasLocationChanged:locationChanged]; } - [self.appDelegate logDebugMessage:@"TextApiCompliance testComplianceAfterInsert, self.hasWorkingSelectionApi = %@ for app %@", self.hasCompliantSelectionApi?@"YES":@"NO", self.clientApplicationId]; + [self.appDelegate logDebugMessage:@"TextApiCompliance checkComplianceAfterInsert, self.hasWorkingSelectionApi = %@ for app %@", self.hasCompliantSelectionApi?@"YES":@"NO", self.clientApplicationId]; } - (BOOL)validateNewLocation:(NSUInteger)location delete:(NSString *)textToDelete { @@ -198,7 +198,7 @@ return [NSString stringWithFormat:@"complianceUncertain: %d, hasCompliantSelecti isAppNonCompliant = [self containedInUserManagedNoncompliantAppList:clientAppId]; } - [self.appDelegate logDebugMessage:@"containedInNoncompliantAppLists: for app %@: %@", clientAppId, isAppNonCompliant?@"yes":@"no"]; + [self.appDelegate logDebugMessage:@"applyNoncompliantAppLists: for app %@: %@", clientAppId, isAppNonCompliant?@"YES":@"NO"]; if (isAppNonCompliant) { self.complianceUncertain = NO;