Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,22 @@ public protocol ConnectionsAPI {
/// Fetch all connections .

func getConnections() async throws -> PayloadPager<[Connection]>

/// Send connection request to user
/// - Parameters:
/// - domain: domain info
/// - userID: userID

#if DEBUG
func sendConnectionRequest(domain: String, userId: String) async throws
#endif

/// Accept connection request from user
/// - Parameters:
/// - domain: domain info
/// - userID: userID

#if DEBUG
func acceptConnectionRequest(domain: String, userId: String) async throws
#endif
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,39 @@ class ConnectionsAPIV0: ConnectionsAPI, VersionedAPI {
.parse(code: response.statusCode, data: data)
}
}

func sendConnectionRequest(
domain: String,
userId: String
) async throws {

let request = try URLRequestBuilder(path: "\(pathPrefix)/connections/\(domain)/\(userId)")
.withMethod(.post)
.build()

let (_, response) = try await apiService.executeRequest(request, requiringAccessToken: true)
guard response.statusCode == HTTPStatusCode.created.rawValue else {
throw ConnectionsAPIError.invalidBody
}
}

func acceptConnectionRequest(
domain: String,
userId: String
) async throws {

let body = try JSONEncoder.defaultEncoder.encode(RequestBodyAcceptConnectionRequestV0.accepted)

let request = try URLRequestBuilder(path: "\(pathPrefix)/connections/\(domain)/\(userId)")
.withMethod(.put)
.withBody(body, contentType: .json)
.build()

let (_, response) = try await apiService.executeRequest(request, requiringAccessToken: true)
guard response.statusCode == HTTPStatusCode.ok.rawValue else {
throw ConnectionsAPIError.invalidBody
}
}
}

private struct PaginatedConnectionListV0: Decodable, ToAPIModelConvertible {
Expand Down Expand Up @@ -120,3 +153,8 @@ private struct ConnectionResponseV0: Decodable, ToAPIModelConvertible {
)
}
}

private struct RequestBodyAcceptConnectionRequestV0: Encodable {
static let accepted = Self()
let status: String = "accepted"
}
10 changes: 10 additions & 0 deletions WireUI/Sources/WireLocators/Locators.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ public enum Locators {
case conversationTitleButton
case conversationDetailsButton
case message
case imageCell = "ImageCell"
}

public enum BackupOrRestorePage: String {
Expand Down Expand Up @@ -266,4 +267,13 @@ public enum Locators {
case closeButton
}

public enum ShareExtensionPage: String {

case imageTile = "PXGGridLayout-Info"
case shareButton = "PUOneUpBarButtonItemIdentifierShare"
case chooseConversations = "chevron"
case sendButtonOnShareExtension
case continueButton = "Continue"
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import WireDesign
import WireDomain
import WireFoundation
import WireLinkPreview
import WireLocators
import WireLogging
import WireNetwork
import WireShareEngine
Expand Down Expand Up @@ -185,6 +186,8 @@ final class ShareExtensionViewController: SLComposeServiceViewController {
guard let item = navigationController?.navigationBar.items?.first else { return }
item.rightBarButtonItem?.action = #selector(appendPostTapped)
item.rightBarButtonItem?.title = L10n.ShareExtension.SendButton.title
item.rightBarButtonItem?.accessibilityIdentifier = Locators.ShareExtensionPage.sendButtonOnShareExtension
.rawValue
item
.titleView = UIImageView(
image: WireStyleKit.imageOfLogo(color: UIColor.Wire.primaryLabel)
Expand Down
32 changes: 32 additions & 0 deletions wire-ios/WireUITests/Helper/UserHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class UserHelper {
let teamsAPI: TeamsAPI
let selfUserAPI: SelfUserAPI
let conversationsAPI: ConversationsAPI
let connectionsAPI: ConnectionsAPI

private let cookieStorage = MockCookieStorage()
private let authenticationManager = MockAuthManager()
Expand All @@ -54,6 +55,7 @@ class UserHelper {
self.teamsAPI = TeamsAPIBuilder(apiService: networkStack.apiService)
.makeAPI(for: apiVersion)
self.conversationsAPI = ConversationsAPIBuilder(apiService: networkStack.apiService).makeAPI(for: apiVersion)
self.connectionsAPI = ConnectionsAPIBuilder(apiService: networkStack.apiService).makeAPI(for: apiVersion)
}

func basicAuth(_ backend: BackendTarget = BackendContext.current) -> String {
Expand Down Expand Up @@ -99,11 +101,16 @@ class UserHelper {
verificationCode: nil,
label: nil
)

authenticationManager.accessToken = accessToken

// Set username
try await selfUserAPI.updateHandle(handle: user.username)

// Store id in UserInfo
let selfUser = try await selfUserAPI.getSelfUser()
user.id = selfUser.id.uuidString

createdUsers.append(user)
return user
}
Expand Down Expand Up @@ -279,6 +286,31 @@ class UserHelper {

_ = try await conversationsAPI.createGroupConversation(parameters: params)
}

func sendConnectionRequestToUser(
domain: String,
userId: String
) async throws {

_ = try await connectionsAPI.sendConnectionRequest(domain: domain, userId: userId)
}

func acceptConnectionRequestFromUser(
domain: String,
user1: UserInfo,
userId: String
) async throws {

let (_, accessToken) = try await authenticationAPI.login(
email: user1.email,
password: user1.password,
verificationCode: nil,
label: nil
)
authenticationManager.accessToken = accessToken

try await connectionsAPI.acceptConnectionRequest(domain: domain, userId: userId)
}
}

extension BackendEnvironment {
Expand Down
4 changes: 4 additions & 0 deletions wire-ios/WireUITests/Pages/ActiveConversationPage .swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ class ActiveConversationPage: PageModel {
app.buttons[Locators.ActiveConversationPage.conversationDetailsButton.rawValue]
}

var imageCell: XCUIElement {
app.otherElements[Locators.ActiveConversationPage.imageCell.rawValue]
}

func fetchMessages() -> [String] {
var messages: [String] = []
for i in 0 ..< messageLabels.count {
Expand Down
105 changes: 105 additions & 0 deletions wire-ios/WireUITests/Pages/PhotosAppPage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//

Check failure on line 1 in wire-ios/WireUITests/Pages/PhotosAppPage.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Header comments should be consistent with project patterns (file_header)
// Wire
// Copyright (C) 2025 Wire Swiss GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import WireLocators
import XCTest

class PhotosAppPage: PageModel {
private let photosApp: XCUIApplication
private let timeout: TimeInterval = 2

override var pageMainElement: XCUIElement {
photosApp.windows.firstMatch
}

init(photosApp: XCUIApplication) throws {
self.photosApp = photosApp
try super.init()
}

var continueButtonOnWhatsNewPhotosApp: XCUIElement {
photosApp.buttons[Locators.ShareExtensionPage.continueButton.rawValue].firstMatch
}

var firstImageTile: XCUIElement {
photosApp.images[Locators.ShareExtensionPage.imageTile.rawValue].firstMatch
}

var shareButton: XCUIElement {
photosApp.buttons
.matching(identifier: Locators.ShareExtensionPage.shareButton.rawValue)
.firstMatch
}

var shareToWireApp: XCUIElement {
photosApp.cells["Wire"].firstMatch
}

var chooseConversationButton: XCUIElement {
photosApp.buttons[Locators.ShareExtensionPage.chooseConversations.rawValue].firstMatch
}

var sendButton: XCUIElement {
photosApp.buttons[Locators.ShareExtensionPage.sendButtonOnShareExtension.rawValue].firstMatch
}

func selectConversation(name: String) -> XCUIElement {
photosApp.staticTexts[name].firstMatch
}

@discardableResult
func continueWhatsNewIfPresent() throws -> PhotosAppPage {
if continueButtonOnWhatsNewPhotosApp.waitForExistence(timeout: timeout) {
continueButtonOnWhatsNewPhotosApp.tap()
}
return self
}

@discardableResult
func openFirstImage() throws -> PhotosAppPage {
try continueWhatsNewIfPresent()
XCTAssertTrue(firstImageTile.waitForExistence(timeout: 10))
// NOTE: Use a coordinate tap on center because Photos grid cells are not always directly hittable in UITests
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: interesting do you have reference to this? is it the only way to deal with it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case if I don't use .coordinate..it returns isHittable = false and fails due to this. with co-ordinates works fine as it try to tap on center.

this is what it returns in debug desc

(lldb) po photosApp.images["PXGGridLayout-Info"].firstMatch.isHittable
    t =   251.60s Find the "PXGGridLayout-Info" Image
false

On in logs when fails to tap()

Failed: Not hittable: Image, {{0.0, 136.0}, {132.9, 133.0}}, identifier: 'PXGGridLayout-Info', label: 'Photo, October 09, 2009, 11:09 PM'
    t =    36.48s     Retrying `Tap "PXGGridLayout-Info" Image` (attempt #3)

Tried some other approach navigating through parent hierarchy but still it returns false.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well a image is never hittable in itself, so makes sense, I would expect cells or collectionViewCells in photoApp that you can select. can you have the hierarchy of photosApp?

Copy link
Contributor Author

@findms findms Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the hierarchy:

Attributes: Application, 0x101a18fb0, pid: 4746, label: 'Photos'
Element subtree:
 →Application, 0x101a18fb0, pid: 4746, label: 'Photos'
    Window (Main), 0x101a14dd0, {{0.0, 0.0}, {402.0, 874.0}}
      Other, 0x101a1d830, {{0.0, 0.0}, {402.0, 874.0}}
        Other, 0x101a1d950, {{0.0, 0.0}, {402.0, 874.0}}
          Other, 0x101a1da70, {{0.0, 0.0}, {402.0, 874.0}}
            Other, 0x101a1d590, {{0.0, 0.0}, {402.0, 874.0}}
              Other, 0x101a1d6b0, {{0.0, 0.0}, {402.0, 874.0}}
                Other, 0x101a1c5b0, {{0.0, 0.0}, {402.0, 874.0}}
                  NavigationBar, 0x101a1c6d0, {{0.0, 62.0}, {402.0, 54.0}}, identifier: 'PXCuratedLibraryUIView'
                    Other, 0x101a1c7f0, {{16.0, 45.7}, {111.7, 76.7}}
                      StaticText, 0x101a1c910, {{16.0, 63.7}, {111.7, 40.7}}, label: 'Library'
                      StaticText, 0x101a1ca30, {{16.0, 104.3}, {65.3, 18.0}}, label: '6 Photos'
                    Button, 0x101a1cb50, {{252.7, 66.0}, {36.0, 36.0}}, identifier: 'sortFilterButton', label: 'Sort and Filter'
                    Button, 0x101a1cc70, {{308.7, 66.0}, {73.3, 36.0}}, label: 'Select'
                  Other, 0x101e24af0, {{0.0, 0.0}, {402.0, 874.0}}
                    Other, 0x101e308c0, {{0.0, 0.0}, {402.0, 874.0}}
                      Other, 0x101e30570, {{0.0, 0.0}, {402.0, 874.0}}
                        Other, 0x101e35a90, {{0.0, 0.0}, {402.0, 874.0}}
                          ScrollView, 0x101e3c150, {{0.0, 0.0}, {402.0, 874.0}}, identifier: 'content_scroll_view'
                            Other, 0x101e3c840, {{0.0, 136.0}, {402.0, 277.7}}, identifier: 'PXCuratedLibraryLayout-Group'
                              Other, 0x101e3a560, {{0.0, 0.0}, {402.0, 874.0}}
                              Other, 0x101e3d9a0, {{0.0, 0.0}, {402.0, 874.0}}
                              Other, 0x101e3cbb0, {{0.0, 0.0}, {402.0, 874.0}}
                              Other, 0x101e3d620, {{0.0, 136.0}, {402.0, 267.7}}, identifier: 'PXZoomablePhotosLayout-Group'
                                Other, 0x101e3cf20, {{0.0, 136.0}, {402.0, 267.7}}, identifier: 'PXGZoomLayout-Group'
                                  Other, 0x101e353d0, {{0.0, 136.0}, {402.0, 267.7}}, identifier: 'PXGDecoratingLayout-Group'
                                    Other, 0x101e3d2b0, {{0.0, 136.0}, {402.0, 267.7}}, identifier: 'PXGGridLayout-Group', label: 'Photo, October 09, 2009, 11:09 PM, Photo, March 13, 2011, 1:17 AM, Photo, August 08, 2012, 8:52 PM, Photo, August 08, 2012, 11:29 PM, Photo, August 08, 2012, 11:55 PM, Photo, March 30, 2018, 9:14 PM'
                                      Image, 0x101e397f0, {{0.0, 136.0}, {132.9, 133.0}}, identifier: 'PXGGridLayout-Info', label: 'Photo, October 09, 2009, 11:09 PM'
                                      Image, 0x101e36140, {{134.6, 136.0}, {132.9, 133.0}}, identifier: 'PXGGridLayout-Info', label: 'Photo, March 13, 2011, 1:17 AM'
                                      Image, 0x101e3b350, {{269.1, 136.0}, {132.9, 133.0}}, identifier: 'PXGGridLayout-Info', label: 'Photo, August 08, 2012, 8:52 PM'
                                      Image, 0x101e3ba30, {{0.0, 270.7}, {132.9, 133.0}}, identifier: 'PXGGridLayout-Info', label: 'Photo, August 08, 2012, 11:29 PM'
                                      Image, 0x101e3b6e0, {{134.6, 270.7}, {132.9, 133.0}}, identifier: 'PXGGridLayout-Info', label: 'Photo, August 08, 2012, 11:55 PM'
                                      Image, 0x101e3bdd0, {{269.1, 270.7}, {132.9, 133.0}}, identifier: 'PXGGridLayout-Info', label: 'Photo, March 30, 2018, 9:14 PM'
                                Other, 0x101e3ac60, {{0.0, 136.0}, {402.0, 0.0}}, identifier: 'PXZoomableInlineHeadersLayout-Group'
                              Other, 0x101e3a8e0, {{369.0, 136.0}, {30.0, 655.0}}, label: 'Vertical scroll bar, 1 page', value: 0%
                                Other, 0x101e3afd0, {{396.0, 516.3}, {3.0, 271.7}}
                              Other, 0x101e39490, {{62.0, 758.0}, {278.0, 30.0}}, label: 'Horizontal scroll bar, 1 page', value: 0%
                                Other, 0x101e39b60, {{65.0, 785.0}, {272.0, 3.0}}
                            Other, 0x101e3a200, {{369.0, 136.0}, {30.0, 655.0}}, label: 'Vertical scroll bar, 1 page', value: 0%
                              Other, 0x101e39ea0, {{396.0, 516.3}, {3.0, 271.7}}
                            Other, 0x101e37260, {{62.0, 758.0}, {278.0, 30.0}}, label: 'Horizontal scroll bar, 1 page', value: 0%
                              Other, 0x101e39130, {{65.0, 785.0}, {272.0, 3.0}}
                        Other, 0x101e38a20, {{0.0, 0.0}, {402.0, 874.0}}
                  Other, 0x101e386a0, {{0.0, 0.0}, {402.0, 874.0}}
                    Other, 0x101e38dc0, {{0.0, 0.0}, {402.0, 874.0}}
            Other, 0x101e38350, {{0.0, 791.0}, {402.0, 83.0}}
              Other, 0x101e37ff0, {{0.0, 0.0}, {402.0, 874.0}}
                TabBar, 0x101e37c60, {{0.0, 791.0}, {402.0, 83.0}}, label: 'Tab Bar'
                  Other, 0x101e364d0, {{21.0, 791.0}, {191.3, 62.0}}
                    Other, 0x101e36840, {{21.0, 791.0}, {95.7, 54.0}}
                    Other, 0x101e36f10, {{21.0, 791.0}, {191.3, 62.0}}
                      Button, 0x101e37910, {{25.0, 795.0}, {95.7, 54.0}}, identifier: 'LibraryTab', label: 'Library', Selected
                        Image, 0x101e375c0, {{56.0, 801.3}, {34.3, 28.7}}, identifier: 'photo.fill.on.rectangle.fill', label: 'photo.fill.on.rectangle.fill'
                      Button, 0x101e36b90, {{112.7, 795.0}, {95.7, 54.0}}, identifier: 'CollectionsTab', label: 'Collections'
                        Image, 0x101e35df0, {{145.7, 800.0}, {30.0, 31.3}}, identifier: 'rectangle.stack.fill', label: 'Album'
                    Other, 0x101e35730, {{25.0, 795.0}, {95.7, 54.0}}
                      Other, 0x101e3dd10, {{25.0, 795.0}, {95.7, 54.0}}
                        Other, 0x101e3e070, {{25.0, 795.0}, {95.7, 54.0}}
                        Other, 0x101e3e3c0, {{25.0, 795.0}, {95.7, 54.0}}
                          Other, 0x101e3e740, {{25.0, 795.0}, {95.7, 54.0}}
                            Other, 0x101e3eac0, {{25.0, 795.0}, {95.7, 54.0}}
                              Other, 0x101e3ee10, {{25.0, 795.0}, {95.7, 54.0}}
                            Other, 0x101e3f180, {{25.0, 795.0}, {95.7, 54.0}}
                          Other, 0x101e3f4f0, {{25.0, 795.0}, {95.7, 54.0}}
                  Other, 0x101e3f850, {{319.0, 791.0}, {62.0, 62.0}}
                    Other, 0x101e3fbb0, {{319.0, 791.0}, {62.0, 62.0}}
                      Other, 0x101e3ff20, {{319.0, 791.0}, {62.0, 62.0}}
                        Button, 0x101e40270, {{319.0, 791.0}, {62.0, 62.0}}, identifier: 'SearchTab', label: 'Search'
                          Image, 0x101e405e0, {{336.0, 808.7}, {28.0, 26.7}}, identifier: 'magnifyingglass', label: 'Search'
              Other, 0x101e40980, {{0.0, 791.0}, {402.0, 83.0}}
                Other, 0x101e40ce0, {{0.0, 791.0}, {402.0, 83.0}}
                  Other, 0x101e41060, {{0.0, 791.0}, {402.0, 83.0}}
                    Other, 0x101e413f0, {{0.0, 791.0}, {402.0, 83.0}}
                      Other, 0x101e41790, {{0.0, 791.0}, {402.0, 83.0}}
                        Other, 0x101e41b10, {{0.0, 791.0}, {402.0, 83.0}}
                    Other, 0x101e41eb0, {{0.0, 791.0}, {402.0, 83.0}}
                      Other, 0x101e42210, {{0.0, 791.0}, {402.0, 83.0}}
Path to element:
 →Application, 0x101a18fb0, pid: 4746, label: 'Photos'
Query chain:
 →Find: Application 'com.apple.mobileslideshow'
  Output: {
    Application, 0x101d13460, pid: 4746, label: 'Photos'
  }

tried with scrollview as well but no luck.

Found that the 1st row of images are not hittable due to some reason but 2nd row is? So if I use

po photosApp.images.matching(identifier: Locators.ShareExtensionPage.imageTile.rawValue)..element(boundBy: 4).tap() - works

firstImageTile
.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
.tap()
return self
}

@discardableResult
func shareImageToWire() throws -> PhotosAppPage {
XCTAssertTrue(shareButton.waitForExistence(timeout: timeout))
shareButton.tap()
XCTAssertTrue(shareToWireApp.waitForExistence(timeout: timeout))
shareToWireApp.tap()
return self
}

func chooseConversationAndSend(name: String) throws {
XCTAssertTrue(chooseConversationButton.waitForExistence(timeout: timeout))
chooseConversationButton.tap()

let conversationToSend = selectConversation(name: name)
XCTAssertTrue(conversationToSend.waitForExistence(timeout: timeout))
conversationToSend.tap()

XCTAssertTrue(sendButton.waitForExistence(timeout: timeout))
sendButton.tap()
}

}
73 changes: 73 additions & 0 deletions wire-ios/WireUITests/ShareExtensionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//

Check failure on line 1 in wire-ios/WireUITests/ShareExtensionTests.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Header comments should be consistent with project patterns (file_header)
// Wire
// Copyright (C) 2025 Wire Swiss GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import WireFoundation
import WireLocators
import XCTest

final class ShareExtensionTests: WireUITestCase {

private let photosAppBundleId = XCUIApplication(bundleIdentifier: "com.apple.mobileslideshow")

@MainActor
private func launchPhotosApp() async throws {
photosAppBundleId.launch()
XCTAssertTrue(photosAppBundleId.wait(for: .runningForeground, timeout: 10))
}

@MainActor
private func shareFirstPhotoToWire(name: String) async throws {
let photosApp = try PhotosAppPage(photosApp: photosAppBundleId)
try photosApp
.openFirstImage()
.shareImageToWire()
.chooseConversationAndSend(name: name)
}

@MainActor
private func switchBackToWireApp() async throws {
app.activate()
if !app.wait(for: .runningForeground, timeout: 10) {
app.launch()
_ = app.wait(for: .runningForeground, timeout: 10)
}
}

@MainActor
func test_ShareImageOnetoOne() async throws {

let user1 = try await userHelper.createPersonalUser()
let user2 = try await userHelper.createPersonalUser()
let domain = BackendTarget.staging.domainInfo

try await userHelper.sendConnectionRequestToUser(domain: domain, userId: user1.id)
try await userHelper.acceptConnectionRequestFromUser(domain: domain, user1: user1, userId: user2.id)
let firstTimePage = try app.loginUser(email: user1.email, password: user1.password)
let conversationsPage = try firstTimePage.acceptPopup(with: self)

try await launchPhotosApp()
try await shareFirstPhotoToWire(name: user2.name)
try await switchBackToWireApp()

let activeConversationPage = try conversationsPage.openConversation()

XCTAssertTrue(
activeConversationPage.imageCell.exists, "No Image cell found"
)
}
}
Loading