From 4fde92229ff465ae0831f84dc7eb390a82cc48e8 Mon Sep 17 00:00:00 2001 From: Drew Volz Date: Sun, 5 Feb 2023 13:50:14 -0800 Subject: [PATCH 1/4] pass accessibility label and testid down to context menu component --- modules/context-menu/index.tsx | 6 ++++++ modules/navigation-buttons/open-settings.tsx | 1 - source/views/home/index.tsx | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/modules/context-menu/index.tsx b/modules/context-menu/index.tsx index 3b957319e6..b82bbd322f 100644 --- a/modules/context-menu/index.tsx +++ b/modules/context-menu/index.tsx @@ -5,6 +5,7 @@ import {ContextMenuButton} from 'react-native-ios-context-menu' import {upperFirst} from 'lodash' interface ContextMenuProps { + accessibilityLabel?: string actions: string[] buttonStyle?: StyleProp children?: React.ReactElement @@ -12,6 +13,7 @@ interface ContextMenuProps { isMenuPrimaryAction?: boolean onPress?: () => void onPressMenuItem: (menuKey: string) => void | Promise + testId?: string title: string } @@ -20,6 +22,7 @@ export const ContextMenu = React.forwardRef< ContextMenuProps >((props, ref): JSX.Element => { const { + accessibilityLabel, actions, buttonStyle, children, @@ -27,6 +30,7 @@ export const ContextMenu = React.forwardRef< isMenuPrimaryAction, onPress, onPressMenuItem, + testId, title, } = props @@ -40,6 +44,7 @@ export const ContextMenu = React.forwardRef< return ( {onPress ? ( diff --git a/modules/navigation-buttons/open-settings.tsx b/modules/navigation-buttons/open-settings.tsx index 07e9cee7ee..60fa80e45b 100644 --- a/modules/navigation-buttons/open-settings.tsx +++ b/modules/navigation-buttons/open-settings.tsx @@ -15,7 +15,6 @@ export function OpenSettingsButton(_props: HeaderBackButtonProps): JSX.Element { ), android: ( diff --git a/source/views/home/index.tsx b/source/views/home/index.tsx index d2bb868cbc..0e13a4c311 100644 --- a/source/views/home/index.tsx +++ b/source/views/home/index.tsx @@ -69,9 +69,11 @@ function HomePage(): JSX.Element { return ( From d7b0d53f0f7ab22d9b79f8e81b6cd2033dd08c98 Mon Sep 17 00:00:00 2001 From: Drew Volz Date: Sun, 5 Feb 2023 13:50:40 -0800 Subject: [PATCH 2/4] update smoke tests to tap the settings button --- e2e/basic-smoke.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/e2e/basic-smoke.spec.ts b/e2e/basic-smoke.spec.ts index a315d4f412..e478e8e236 100644 --- a/e2e/basic-smoke.spec.ts +++ b/e2e/basic-smoke.spec.ts @@ -18,11 +18,13 @@ test('should show the home screen', async () => { test('should show the settings screen after tap', async () => { await element(by.id('button-open-settings')).tap() + await element(by.text('Settings')).tap() await expect(element(by.text('Sign In to St. Olaf'))).toBeVisible() }) test('should show home screen after tap to exit settings screen', async () => { await element(by.id('button-open-settings')).tap() + await element(by.text('Settings')).tap() await expect(element(by.id('screen-homescreen'))).not.toBeVisible() await element(by.text('Done')).tap() await expect(element(by.id('screen-homescreen'))).toBeVisible() From 02e7bdd3d8bad3d64e885248b3680c5f8a792f11 Mon Sep 17 00:00:00 2001 From: Drew Volz Date: Sun, 5 Feb 2023 15:17:12 -0800 Subject: [PATCH 3/4] support for checking if two screenshots had significant pixel differences --- ios/AllAboutOlaf.xcodeproj/project.pbxproj | 20 +++ .../AllAboutOlafUITests.swift | 31 +++++ .../ImageComparison/ImageComparison.swift | 66 +++++++++ .../ImageComparison/Structures.swift | 47 +++++++ .../ImageComparison/UIImage+Extensions.swift | 131 ++++++++++++++++++ 5 files changed, 295 insertions(+) create mode 100644 ios/AllAboutOlafUITests/ImageComparison/ImageComparison.swift create mode 100644 ios/AllAboutOlafUITests/ImageComparison/Structures.swift create mode 100644 ios/AllAboutOlafUITests/ImageComparison/UIImage+Extensions.swift diff --git a/ios/AllAboutOlaf.xcodeproj/project.pbxproj b/ios/AllAboutOlaf.xcodeproj/project.pbxproj index ece04e0ed6..fc74e5725f 100644 --- a/ios/AllAboutOlaf.xcodeproj/project.pbxproj +++ b/ios/AllAboutOlaf.xcodeproj/project.pbxproj @@ -17,6 +17,9 @@ 3ACC8B85273335930069E931 /* windmill@2x~iPad.png in Resources */ = {isa = PBXBuildFile; fileRef = 3ACC8B83273335930069E931 /* windmill@2x~iPad.png */; }; 3ACC8B86273335930069E931 /* windmill@3x~iPad.png in Resources */ = {isa = PBXBuildFile; fileRef = 3ACC8B84273335930069E931 /* windmill@3x~iPad.png */; }; 3AE408501E1E280800F0FD83 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3AE4084F1E1E280800F0FD83 /* LaunchScreen.storyboard */; }; + 3AEA098429904E9F009007FF /* ImageComparison.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEA098329904E9F009007FF /* ImageComparison.swift */; }; + 3AEA098729906E66009007FF /* UIImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEA098629906E66009007FF /* UIImage+Extensions.swift */; }; + 3AEA098929906EE2009007FF /* Structures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEA098829906EE2009007FF /* Structures.swift */; }; 8222E4786CCF790474A60080 /* libPods-AllAboutOlaf-AllAboutOlafUITests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2256074D704451D201AB966D /* libPods-AllAboutOlaf-AllAboutOlafUITests.a */; }; 91E0D7C5F14D215A7C718236 /* libPods-AllAboutOlaf.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9EB28C76433AECBFEDDD439B /* libPods-AllAboutOlaf.a */; }; /* End PBXBuildFile section */ @@ -334,6 +337,9 @@ 3ACC8B83273335930069E931 /* windmill@2x~iPad.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "windmill@2x~iPad.png"; path = "../images/icons/windmill@2x~iPad.png"; sourceTree = ""; }; 3ACC8B84273335930069E931 /* windmill@3x~iPad.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "windmill@3x~iPad.png"; path = "../images/icons/windmill@3x~iPad.png"; sourceTree = ""; }; 3AE4084F1E1E280800F0FD83 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = AllAboutOlaf/LaunchScreen.storyboard; sourceTree = ""; }; + 3AEA098329904E9F009007FF /* ImageComparison.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageComparison.swift; sourceTree = ""; }; + 3AEA098629906E66009007FF /* UIImage+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Extensions.swift"; sourceTree = ""; }; + 3AEA098829906EE2009007FF /* Structures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Structures.swift; sourceTree = ""; }; 5CBB688C2084ADBF20C737EF /* Pods-AllAboutOlaf-AllAboutOlafUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AllAboutOlaf-AllAboutOlafUITests.debug.xcconfig"; path = "Target Support Files/Pods-AllAboutOlaf-AllAboutOlafUITests/Pods-AllAboutOlaf-AllAboutOlafUITests.debug.xcconfig"; sourceTree = ""; }; 67354E87236E0E5900B1E8E8 /* AllAboutOlaf.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AllAboutOlaf.app; sourceTree = BUILT_PRODUCTS_DIR; }; 67354E88236E0E5900B1E8E8 /* AllAboutOlafUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AllAboutOlafUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -438,6 +444,7 @@ 00D33BE01DDA58A8001E830E /* UI Tests */ = { isa = PBXGroup; children = ( + 3AEA098529906E31009007FF /* ImageComparison */, 00D33BE31DDA58A8001E830E /* Info.plist */, 00D33C011DDA5958001E830E /* SnapshotHelper.swift */, 00D33BE11DDA58A8001E830E /* AllAboutOlafUITests.swift */, @@ -512,6 +519,16 @@ name = Products; sourceTree = ""; }; + 3AEA098529906E31009007FF /* ImageComparison */ = { + isa = PBXGroup; + children = ( + 3AEA098329904E9F009007FF /* ImageComparison.swift */, + 3AEA098629906E66009007FF /* UIImage+Extensions.swift */, + 3AEA098829906EE2009007FF /* Structures.swift */, + ); + path = ImageComparison; + sourceTree = ""; + }; 3C5BFA21338636E7EB4E5E0D /* Frameworks */ = { isa = PBXGroup; children = ( @@ -1236,8 +1253,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3AEA098929906EE2009007FF /* Structures.swift in Sources */, 00D33BE21DDA58A8001E830E /* AllAboutOlafUITests.swift in Sources */, 00D33C031DDA598B001E830E /* SnapshotHelper.swift in Sources */, + 3AEA098429904E9F009007FF /* ImageComparison.swift in Sources */, + 3AEA098729906E66009007FF /* UIImage+Extensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/AllAboutOlafUITests/AllAboutOlafUITests.swift b/ios/AllAboutOlafUITests/AllAboutOlafUITests.swift index 39fa6eabe5..02eb1d5f17 100644 --- a/ios/AllAboutOlafUITests/AllAboutOlafUITests.swift +++ b/ios/AllAboutOlafUITests/AllAboutOlafUITests.swift @@ -117,4 +117,35 @@ class AllAboutOlafUITests: XCTestCase { sleep(5) snapshot("13StudentOrgsScreen") } + + func testSettingCustomHomeBackgroundImageScreen() { + sleep(2) + + // screenshot the initial state + XCTAssertTrue(app.buttons["button-open-settings"].isHittable) + let before = XCUIScreen.main.screenshot().image + + // open settings and tap the change background button + app.buttons["button-open-settings"].tap() + app.buttons["Change Background"].tap() + + // ensure we are on the photos tab + XCTAssertTrue(app.buttons["Photos"].isHittable) + app.buttons["Photos"].tap() + + // get the scrollable photos + let photoPredicate = NSPredicate(format: "label BEGINSWITH 'Photo'") + let imageElements = app.scrollViews.otherElements.images.containing(photoPredicate) + + // select the second photo in the image library (the first strangely does not load) + let secondImage = 1 + imageElements.element(boundBy: secondImage).tap() + + // screenshot the changed state + sleep(2) + let after = XCUIScreen.main.screenshot().image + + // visual difference check of before and after with threshold + assert(image: after, matches: before, requiredAccuracy: 1, comparisonName: "Custom background image") + } } diff --git a/ios/AllAboutOlafUITests/ImageComparison/ImageComparison.swift b/ios/AllAboutOlafUITests/ImageComparison/ImageComparison.swift new file mode 100644 index 0000000000..43e971f09e --- /dev/null +++ b/ios/AllAboutOlafUITests/ImageComparison/ImageComparison.swift @@ -0,0 +1,66 @@ +// +// UITests+Extensions.swift +// AllAboutOlafUITests +// +// https://pspdfkit.com/blog/2021/swift-render-tests-in-practice-part-1/ +// https://pspdfkit.com/blog/2021/swift-render-tests-in-practice-part-2/ +// https://pspdfkit.com/blog/2021/swift-render-tests-in-practice-part-3/ +// + +import XCTest + +func assert( + image imageProducer: @autoclosure () -> UIImage, + matches reference: UIImage, + requiredAccuracy: Double, + comparisonName: String, + file: StaticString = #file, + line: UInt = #line +) { + let image = imageProducer() + + do { + try image.ensureMatches(reference: reference, requiredAccuracy: requiredAccuracy) + } catch let ImageComparisonError.imageMismatch( + pixelCount: totalPixels, + acceptableMismatchCount: acceptable, + actualMismatchCount: actual + ) { + XCTContext.runActivity(named: comparisonName) { activity in + let imageAttachment = XCTAttachment(image: image) + imageAttachment.name = "Actual render" + activity.add(imageAttachment) + + let referenceAttachment = XCTAttachment(image: reference) + referenceAttachment.name = "Reference image" + activity.add(referenceAttachment) + + // New: Generate the difference image and attach it. + let fullFrame = CGRect(origin: .zero, size: reference.size) + let renderer = UIGraphicsImageRenderer(size: fullFrame.size) + let difference = renderer.image { context in + // Produce black pixels wherever things are the same. + reference.draw(in: fullFrame) + image.draw(in: fullFrame, blendMode: .difference, alpha: 1) + + // Highlight everything not black — aka the differences — in red. + // This step is optional, but I’ve found it to be helpful in some cases, + // e.g. where the differences are subtle and tinted themselves. + UIColor.red.withAlphaComponent(0.5).setFill() + context.fill(fullFrame, blendMode: .colorDodge) + } + + let differenceAttachment = XCTAttachment(image: difference) + differenceAttachment.name = "Diff image" + activity.add(differenceAttachment) + + XCTFail( + "Image with \(totalPixels) pixels differed from reference by \(actual) pixels (allowed: \(acceptable)", + file: file, + line: line + ) + } + } catch{ + print("Something very bad happened and we're note sure how to recover.") + } +} diff --git a/ios/AllAboutOlafUITests/ImageComparison/Structures.swift b/ios/AllAboutOlafUITests/ImageComparison/Structures.swift new file mode 100644 index 0000000000..327c182acc --- /dev/null +++ b/ios/AllAboutOlafUITests/ImageComparison/Structures.swift @@ -0,0 +1,47 @@ +// +// Structures.swift +// AllAboutOlafUITests +// +// https://pspdfkit.com/blog/2021/swift-render-tests-in-practice-part-1/ +// https://pspdfkit.com/blog/2021/swift-render-tests-in-practice-part-2/ +// https://pspdfkit.com/blog/2021/swift-render-tests-in-practice-part-3/ +// + +import Foundation + +struct RGBA { + let r: UInt8 + let g: UInt8 + let b: UInt8 + let a: UInt8 + + func isSimilar(to other: RGBA) -> Bool { + (r.distance(to: other.r) < 2 + && g.distance(to: other.g) < 2 + && b.distance(to: other.b) < 2 + && a.distance(to: other.a) < 2) + } +} + +/// Comprehensive list of errors that can occur when evaluating a render test. +enum ImageComparisonError: Error { + // :MARK: Test Setup Errors — see last week's article + case unsupportedBackingStore(referenceImage: UIImage, actualImage: UIImage) + case dimensionMismatch(expectedWidth: Int, expectedHeight: Int, actualWidth: Int, actualHeight: Int) + + // :MARK: Actual Comparison Failure + /// The image has the same dimensions as the reference, but too many pixels differ. + /// - Parameters: + /// - pixelCount: + /// The total number of pixels in the image. + /// - acceptableMismatchCount: + /// The maximum number of pixels that would have been allowed to differ from + /// the reference to still be considered a match. + /// - actualMismatchCount: + /// The actual number of pixels in the image that differed from the reference. + case imageMismatch( + pixelCount: Int, + acceptableMismatchCount: Int, + actualMismatchCount: Int + ) +} diff --git a/ios/AllAboutOlafUITests/ImageComparison/UIImage+Extensions.swift b/ios/AllAboutOlafUITests/ImageComparison/UIImage+Extensions.swift new file mode 100644 index 0000000000..f0870c20e8 --- /dev/null +++ b/ios/AllAboutOlafUITests/ImageComparison/UIImage+Extensions.swift @@ -0,0 +1,131 @@ +// +// UIImage+Extensions.swift +// AllAboutOlafUITests +// +// https://pspdfkit.com/blog/2021/swift-render-tests-in-practice-part-1/ +// https://pspdfkit.com/blog/2021/swift-render-tests-in-practice-part-2/ +// https://pspdfkit.com/blog/2021/swift-render-tests-in-practice-part-3/ +// + +import XCTest +import UIKit + +extension UIImage { + + func ensureMatches(reference: UIImage, requiredAccuracy: Double) throws { + // For simplicity, require images to be backed by Core Graphics. + // This will be true for most images. Notable exceptions are images + // obtained from Core Image. + guard + let pixels = cgImage, + let referencePixels = reference.cgImage + else { + throw ImageComparisonError.unsupportedBackingStore( + referenceImage: reference, + actualImage: self + ) + } + + // We cannot compare images that don't have the same dimensions. + guard + referencePixels.width == pixels.width, + referencePixels.height == pixels.height + else { + throw ImageComparisonError.dimensionMismatch( + expectedWidth: referencePixels.width, + expectedHeight: referencePixels.height, + actualWidth: pixels.width, + actualHeight: pixels.height + ) + } + + // Unfortunately, Core Graphics doesn’t offer direct access to the + // underlying uncompressed buffer for an image. We can use this to + // our advantage to give us some flexibility when it comes to + // storing/representing images for different purposes, which we’ll do + // in a helper function. + let deviceRGB = CGColorSpaceCreateDeviceRGB() + let imageBuffer = try makeRGBABuffer( + for: pixels, + colorSpace: deviceRGB + ) + // Since the function returns an unmanaged buffer — which makes sense + // for what we want — never let it escape without deallocating it! + defer { + imageBuffer.deallocate() + } + let referenceBuffer = try makeRGBABuffer( + for: referencePixels, + colorSpace: deviceRGB + ) + defer { + referenceBuffer.deallocate() + } + + // We already checked this above, but in case we made a mistake in our + // helper, fail here. + let pixelCount = imageBuffer.count + precondition( + pixelCount == referenceBuffer.count, + "Cannot compare contents of buffer that differ in size" + ) + // Now we iterate over all the pixels, counting the mismatches. + var mismatchingPixelCount = 0 + for i in 0.. UnsafeMutableBufferPointer + { + precondition( + colorSpace.numberOfComponents == 3 + && colorSpace.model == .rgb, + "For RGBA, we need a compatible colorspace" + ) + + let bitsPerComponent = 8 + let bytesPerPixel = 4 // 3 components plus alpha. + let width = image.width + let height = image.height + + // We make a buffer that can fit all the pixels to back a `CGContext`. + let imageBuffer = UnsafeMutableBufferPointer + .allocate(capacity: width * height) + + // And we make sure the buffer is empty. + imageBuffer.initialize(repeating: .init(r: 0, g: 0, b: 0, a: 0)) + + // Since we’ve handpicked the configuration, we know this can’t fail. + let context = CGContext( + data: imageBuffer.baseAddress, + width: width, + height: height, + bitsPerComponent: bitsPerComponent, + bytesPerRow: width * bytesPerPixel, + space: colorSpace, + bitmapInfo: ( + CGImageAlphaInfo.premultipliedFirst.rawValue + | CGBitmapInfo.byteOrder32Big.rawValue) + )! + + // Draw the image into the context to get the pixel data into our buffer. + context.draw(image, in: .init(x: 0, y: 0, width: width, height: height)) + + return imageBuffer + } +} From 8a365297e331036f25eaf909a2b3f1a8d704df8a Mon Sep 17 00:00:00 2001 From: Drew Volz Date: Sun, 5 Feb 2023 17:06:13 -0800 Subject: [PATCH 4/4] wording --- ios/AllAboutOlafUITests/ImageComparison/ImageComparison.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/AllAboutOlafUITests/ImageComparison/ImageComparison.swift b/ios/AllAboutOlafUITests/ImageComparison/ImageComparison.swift index 43e971f09e..7d9f1119a2 100644 --- a/ios/AllAboutOlafUITests/ImageComparison/ImageComparison.swift +++ b/ios/AllAboutOlafUITests/ImageComparison/ImageComparison.swift @@ -61,6 +61,6 @@ func assert( ) } } catch{ - print("Something very bad happened and we're note sure how to recover.") + print("Something very bad happened during the visual regression checking and we're not sure how to recover.") } }