diff --git a/.gitignore b/.gitignore
index 312e39287c6..b633f97e725 100644
--- a/.gitignore
+++ b/.gitignore
@@ -102,3 +102,12 @@ vendor/*
# Failing Snapshot Tests
SnapshotResults
artifacts/
+
+# Automation Idea IntelliJ
+.idea
+
+# Automation Config
+wire-ios-automation/ios/backendConnections.json
+
+# Automation Output
+wire-ios-automation/out/
diff --git a/.gitmodules b/.gitmodules
index 88b3b1d233c..9c5fb2dc3c8 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
[submodule "wire-ios-build-assets"]
path = wire-ios-build-assets
url = git@github.com:wireapp/wire-ios-build-assets.git
+[submodule "wire-ios-automation/ios-automation-assets"]
+ path = wire-ios-automation/ios-automation-assets
+ url = https://github.com/wireapp/ios-automation-assets
diff --git a/.swiftformat b/.swiftformat
index d18b825642b..486a650d41d 100644
--- a/.swiftformat
+++ b/.swiftformat
@@ -9,7 +9,8 @@
**AutoMockable**, \
wire-ios-protos/Protos/**, \
wire-ios/Templates/**, \
- WireUI/Sources/WireDesign/Icons/Autogenerated/**
+ WireUI/Sources/WireDesign/Icons/Autogenerated/**, \
+ wire-ios-automation/**
# RULES - Explicitly enabled to avoid automatic opt in of new rules
diff --git a/.swiftlint.yml b/.swiftlint.yml
index 09e736025d2..5fb407f7109 100644
--- a/.swiftlint.yml
+++ b/.swiftlint.yml
@@ -99,6 +99,7 @@ excluded:
- wire-ios/Wire-iOS Share Extension/Generated/Strings+Generated.swift
- wire-ios/Wire-iOS/Generated/Assets+Generated.swift
- wire-ios/Wire-iOS/Generated/Strings+Generated.swift
+ - "wire-ios-automation/**"
# In order to deal with the huge number of violations, first set
# very high limits then iteratively reduce the limits and resolve
diff --git a/Jenkinsfile b/Jenkinsfile
index 7130f45841b..b5174ce03c3 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -104,7 +104,7 @@ pipeline {
error("Could not find any ipa at provided location!")
} else {
def lastModifiedFileName = files[-1].name
- build job: 'iOS_Critical_Flows', parameters: [string(name: 'Track', value: 'S3'), string(name: 'AppBuildNumber', value: "ios/development/simulator/PR/$BRANCH_NAME/${lastModifiedFileName}"), string(name: 'TAGS', value: '@flows'), string(name: 'Branch', value: 'main')]
+ build job: 'iOS_Critical_Flows', parameters: [string(name: 'Track', value: 'S3'), string(name: 'AppBuildNumber', value: "ios/development/simulator/PR/$BRANCH_NAME/${lastModifiedFileName}"), string(name: 'TAGS', value: '@flows'), string(name: 'Branch', value: 'main'), string(name: 'TESTINY_RUN_NAME', value: "PR $BRANCH_NAME")]
}
}
}
diff --git a/wire-ios-automation/ios-automation-assets b/wire-ios-automation/ios-automation-assets
new file mode 160000
index 00000000000..c35660097c0
--- /dev/null
+++ b/wire-ios-automation/ios-automation-assets
@@ -0,0 +1 @@
+Subproject commit c35660097c0e421c1441ac571e597f367e940317
diff --git a/wire-ios-automation/ios/.gitignore b/wire-ios-automation/ios/.gitignore
new file mode 100644
index 00000000000..8f665fbea63
--- /dev/null
+++ b/wire-ios-automation/ios/.gitignore
@@ -0,0 +1,5 @@
+/target
+/bin/
+/tests/ios/.idea/
+.classpath
+.settings
diff --git a/wire-ios-automation/ios/.project b/wire-ios-automation/ios/.project
new file mode 100644
index 00000000000..b9d64a61338
--- /dev/null
+++ b/wire-ios-automation/ios/.project
@@ -0,0 +1,23 @@
+
+
+ auto-ios
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/wire-ios-automation/ios/GridDeployment.groovy b/wire-ios-automation/ios/GridDeployment.groovy
new file mode 100644
index 00000000000..71dc39abd4e
--- /dev/null
+++ b/wire-ios-automation/ios/GridDeployment.groovy
@@ -0,0 +1,448 @@
+def gridNodes = []
+
+// Default values for appium + node (Override in the next conditional block if needed)
+env.NODEVERSION = "node-v18"
+env.APPIUMVERSION = "2.5.2"
+
+if ("${GRID}" == "iOS-realdevice") {
+ env.HUB = "iOS_node070"
+ env.HUBHOST = "192.168.2.70"
+ env.HUBPORT = 4444
+ env.WDALOCALPORT = 8101
+ env.APPIUMPORT = 4730
+ // node cannot be updated higher than xcode 14.0.1 which only comes with iOS 16.0
+ gridNodes.add([label: "iOS_node070", GRIDDEVICETYPE: "phone", DEVICE_PER_NODE: 1, IS_SIMULATOR: false, DEVICENAME: "iPhone 11", PLATFORMVERSION: "16.2"])
+} else if ("${GRID}" == "iOS-phones-arm64") {
+ env.HUB = "iOS_node200"
+ env.HUBHOST = "192.168.2.200"
+ env.HUBPORT = 4444
+ env.WDALOCALPORT = 8101
+ env.APPIUMPORT = 4730
+ gridNodes.add([label: "iOS_node200", GRIDDEVICETYPE: "phone", DEVICE_PER_NODE: 5, IS_SIMULATOR: true, DEVICENAME: "iPhone 11", PLATFORMVERSION: "17.5", RUNTIME: "com.apple.CoreSimulator.SimRuntime.iOS-17-5"])
+} else if ("${GRID}" == "iOS-tablets-arm64") {
+ env.HUB = "iOS_node201"
+ env.HUBHOST = "192.168.2.201"
+ env.HUBPORT = 4444
+ env.WDALOCALPORT = 8101
+ env.APPIUMPORT = 4730
+ gridNodes.add([label: "iOS_node201", GRIDDEVICETYPE: "tablet", DEVICE_PER_NODE: 3, IS_SIMULATOR: true, DEVICENAME: "iPad Air (5th generation)", PLATFORMVERSION: "16.4", RUNTIME: "com.apple.CoreSimulator.SimRuntime.iOS-16-4"])
+} else if ("${GRID}" == "iOS-phones-arm64-fast") {
+ env.HUB = "iOS_node203"
+ env.HUBHOST = "192.168.2.203"
+ env.HUBPORT = 4444
+ env.WDALOCALPORT = 8101
+ env.APPIUMPORT = 4730
+ gridNodes.add([label: "iOS_node203", GRIDDEVICETYPE: "phone", DEVICE_PER_NODE: 15, IS_SIMULATOR: true, DEVICENAME: "iPhone 15", PLATFORMVERSION: "17.5", RUNTIME: "com.apple.CoreSimulator.SimRuntime.iOS-17-5"])
+} else {
+ error("Could not find configuration for ${GRID}")
+}
+
+node(env.HUB) {
+
+ env.NODEPORT = 5555
+ env.DEVICEIDS
+ env.LAUNCH_PATH = "/Library/LaunchDaemons"
+
+ // Print xcode version on node
+ xcodeVersion = sh returnStdout: true, script: 'xcodebuild -version'
+ echo(xcodeVersion)
+
+ stage('Hub: Stop and cleanup') {
+
+ echo("Unload former grid hub")
+ try {
+ String output = sh label: 'Get plist files of grid hub', returnStdout: true, script: "ls -1 /Library/LaunchDaemons/com.wire.ios.phone.grid.plist 2>/dev/null"
+ print(output)
+ for (String nodePlist : output.split("\n")) {
+ sh label: 'Unload plist', returnStdout: true, script: "sudo launchctl unload -w $nodePlist"
+ }
+ } catch (e) {
+ print("Unload plist of node failed or no former plists found!");
+ }
+
+ echo("Delete old plist files and logs")
+ try {
+ sh label: 'Delete old plist files', returnStdout: true, script: "sudo rm -f /Library/LaunchDaemons/com.wire.ios.phone.grid.plist"
+ sh label: 'Delete old grid logs', returnStdout: true, script: "sudo rm -rf /Users/jenkins/selenium/ios-phone-hub.* || true"
+ } catch (e) {
+ print("Delete old plist files or logs failed!");
+ }
+
+ }
+
+ gridNodes.each { entry ->
+ env.GRIDDEVICETYPE = entry["GRIDDEVICETYPE"]
+ node(entry["label"]) {
+ stage(entry["label"] + ": Stop nodes and cleanup") {
+ echo("Unload former grid nodes (selenium relay)")
+ try {
+ String output = sh label: 'Get plist files of grid nodes', returnStdout: true, script: "ls -1 /Library/LaunchDaemons/com.wire.ios.node.* 2>/dev/null"
+ print(output)
+ for (String nodePlist : output.split("\n")) {
+ sh label: 'Unload plist of node', returnStdout: true, script: "sudo launchctl unload -w $nodePlist"
+ }
+ } catch (e) {
+ print("Unload plist of node failed or no former plists found!");
+ }
+
+ echo("Unload former grid nodes (appium)")
+ try {
+ String output = sh label: 'Get plist files of grid nodes', returnStdout: true, script: "ls -1 /Library/LaunchDaemons/com.wire.ios.appium.* 2>/dev/null"
+ print(output)
+ for (String nodePlist : output.split("\n")) {
+ sh label: 'Unload plist of node', returnStdout: true, script: "sudo launchctl unload -w $nodePlist"
+ }
+ } catch (e) {
+ print("Unload plist of node failed or no former plists found!");
+ }
+
+ echo("Delete old plist files and logs (selenium relay & appium)")
+ try {
+ sh label: 'Delete nodeconfig files and logs', returnStdout: true, script: "sudo rm -rf /Users/jenkins/selenium/*.log || true"
+ sh label: 'Delete serverconfig files and logs', returnStdout: true, script: "sudo rm -rf /Users/jenkins/selenium/*.err || true"
+ sh label: 'Delete plist files', returnStdout: true, script: "sudo rm -rf /Library/LaunchDaemons/com.wire.ios.appium.* || true"
+ sh label: 'Delete plist files', returnStdout: true, script: "sudo rm -rf /Library/LaunchDaemons/com.wire.ios.node.* || true"
+ } catch (e) {
+ error("Delete old plist files or logs failed!");
+ }
+ }
+ }
+ }
+
+ stage('Hub: Prepare needed software') {
+ // download selenium-standalone-server if needed
+ try {
+ sh script: """
+ mkdir -p /Users/jenkins/selenium/
+ if [ ! -e /Users/jenkins/selenium/selenium-server-4.7.0.jar ] ; then
+ curl -L https://github.com/SeleniumHQ/selenium/releases/download/selenium-4.7.0/selenium-server-4.7.0.jar -o /Users/jenkins/selenium/selenium-server-4.7.0.jar
+ fi
+ """
+ } catch (e) {
+ error("Downloading selenium standalone server failed!");
+ }
+ // download applesimutils if needed
+ try {
+ sh script: """
+ mkdir -p /Users/jenkins/selenium/
+ if [ ! -e /Users/jenkins/selenium/applesimutils/0.9.9/bin/applesimutils ] ; then
+ rm -rf /Users/jenkins/selenium/applesimutils.tar.gz
+ ARCH=""
+ # Set architecture prefix if on M1 machine
+ if uname -m | grep arm64; then
+ ARCH="arm64_"
+ fi
+ curl -L https://github.com/wix/AppleSimulatorUtils/releases/download/0.9.9/applesimutils-0.9.9.\${ARCH}big_sur.bottle.tar.gz -o /Users/jenkins/selenium/applesimutils.tar.gz
+ cd /Users/jenkins/selenium/
+ tar -zvxf applesimutils.tar.gz
+ sudo xattr -rd com.apple.quarantine /Users/jenkins/selenium/applesimutils/0.9.9/bin/applesimutils
+ fi
+ """
+ } catch (e) {
+ error("Downloading applesimutils failed! " + e);
+ }
+ }
+
+ stage('Create grid config file and start') {
+ // create plist file for grid
+ sh '''
+ GRIDPLIST=com.wire.ios.${GRIDDEVICETYPE}.grid
+ cat < $GRIDPLIST.plist
+
+
+
+
+ Label
+ $GRIDPLIST
+ RunAtLoad
+
+ KeepAlive
+
+ NetworkState
+
+
+ ThrottleInterval
+ 30
+ WorkingDirectory
+ /Users/jenkins/selenium/
+ ProgramArguments
+
+ /usr/bin/java
+ -jar
+ selenium-server-4.7.0.jar
+ hub
+ --host
+ 0.0.0.0
+ --port
+ $HUBPORT
+ --session-request-timeout
+ 240
+
+ StandardErrorPath
+ /Users/jenkins/selenium/ios-$GRIDDEVICETYPE-hub.err
+ StandardOutPath
+ /dev/null
+
+
+EOF
+ sudo mv $GRIDPLIST.plist $LAUNCH_PATH/
+ sudo chown root:wheel $LAUNCH_PATH/$GRIDPLIST.plist
+ '''
+
+ // load grid hub
+ try {
+ sh label: 'Load plist of grid', returnStdout: true, script: "sudo launchctl load -w /Library/LaunchDaemons/com.wire.ios.${GRIDDEVICETYPE}.grid.plist"
+ } catch (e) {
+ error("Load plist of grid failed!");
+ }
+ }
+
+ gridNodes.each { entry ->
+ env.LABEL = entry["label"]
+ env.GRIDDEVICETYPE = entry["GRIDDEVICETYPE"]
+ env.DEVICE_PER_NODE = entry["DEVICE_PER_NODE"]
+ env.PLATFORMVERSION = entry["PLATFORMVERSION"]
+ env.RUNTIME = entry["RUNTIME"]
+ env.DEVICENAME = entry["DEVICENAME"]
+ node(entry["label"]) {
+ stage(entry["label"] + ": Create config files and start nodes") {
+
+ // download selenium-standalone-server if needed
+ try {
+ sh script: """
+ mkdir -p /Users/jenkins/selenium/
+ if [ ! -e /Users/jenkins/selenium/selenium-server-4.7.0.jar ] ; then
+ curl -L https://github.com/SeleniumHQ/selenium/releases/download/selenium-4.7.0/selenium-server-4.7.0.jar -o /Users/jenkins/selenium/selenium-server-4.7.0.jar
+ fi
+ """
+ } catch (e) {
+ error("Downloading selenium standalone server failed!");
+ }
+
+ // Install Carthage
+ sh([script: """
+ if [ "`/usr/local/bin/carthage version`" != '0.38.0' ]; then
+ curl -L https://github.com/Carthage/Carthage/releases/download/0.38.0/Carthage.pkg -o Carthage.pkg
+ sudo installer -pkg Carthage.pkg -target /
+ fi
+ """])
+
+ nodejs(env.NODEVERSION) {
+ sh '[ "$(appium -v)" == ${APPIUMVERSION} ] || npm install -g appium@${APPIUMVERSION}'
+
+ // Need to run this once to make appium find the driver
+ sh returnStatus: true, script: '${NODEJS_HOME}/bin/appium driver install xcuitest'
+ // Unfortunately it is impossible to pin the version of the driver, we just get latest through update:
+ sh returnStatus: true, script: '${NODEJS_HOME}/bin/appium driver update xcuitest --unsafe'
+
+ env.PATH = "${env.NODEJS_HOME}/bin:/usr/local/bin:/usr/bin:/Users/jenkins/selenium/applesimutils/0.9.9/bin/:${env.PATH}"
+ echo "PATH = " + env.PATH
+ def udids = []
+
+ if (entry["IS_SIMULATOR"]) {
+ // Delete all devices with missing runtime etc.
+ sh "xcrun simctl delete unavailable"
+ sh "sudo xcrun simctl delete unavailable"
+ // Get udid of available device with certain device name
+ def output = sh label: 'Get iOS phone/tablet UDIDs', returnStdout: true, script: 'xcrun simctl list -j devices available'
+ def json = readJSON text: output
+ def sims = json["devices"][env.RUNTIME]
+ for (sim in sims) {
+ if (sim["name"] == env.DEVICENAME) {
+ def udid = sim["udid"]
+ udids.add(udid)
+ echo("Simulators with udid " + udid)
+ // If device is not booted then boot it up
+ if (sim["state"] != "Booted") {
+ sh "xcrun simctl boot ${udid}"
+ }
+ }
+ }
+ echo(udids.size() + " simulators found on jenkins node " + entry["label"])
+ // TODO: Create simulators if no available ones with the platformVersion and name are existing
+ while (udids.size() < (env.DEVICE_PER_NODE as Integer)) {
+ def newUDID = sh returnStdout: true, script: 'xcrun simctl create "${DEVICENAME}" "${DEVICENAME}" "${RUNTIME}"'
+ udids.add(newUDID)
+ }
+ } else {
+ def output = sh label: 'Get real device iOS phone/tablet UDIDs', returnStdout: true, script: 'system_profiler SPUSBDataType -json'
+ def json = readJSON text: output
+ def devices = []
+ def dataType = json.SPUSBDataType._items*._items.flatten()
+ dataType.each {
+ if (it != null) {
+ def name = it["_name"]
+ if (name == "iPhone") {
+ devices.add(it)
+ }
+ }
+ }
+ for (device in devices) {
+ if (device["serial_num"] != null) {
+ def serial_num = device["serial_num"]
+ def udid = serial_num.substring(0, 8) + "-" + serial_num.substring(8, serial_num.length())
+ udids.add(udid)
+ echo("Real device with udid " + udid)
+ }
+ }
+ echo(udids.size() + " real devices found on jenkins node " + entry["label"])
+ }
+ env.DEVICEIDS = udids.join(" ")
+
+ print(DEVICEIDS)
+
+ sh 'mkdir -p /Users/jenkins/selenium/'
+
+ // create new grid node config files
+ sh '''
+ DEVICECOUNT=0
+ for UDID in $DEVICEIDS ; do
+ ((DEVICECOUNT++))
+ ((WDALOCALPORT++))
+ ((APPIUMPORT++))
+ ((NODEPORT++))
+
+ IPADDRESS=`ifconfig en0 inet | tail -1 | cut -d ' ' -f 2`
+ SERVERCONFIG=/Users/jenkins/selenium/serverconfig-localhost-$GRIDDEVICETYPE${DEVICECOUNT}
+ NODECONFIG=/Users/jenkins/selenium/nodeconfig-ios-$GRIDDEVICETYPE${DEVICECOUNT}
+ mkdir -p /Users/jenkins/selenium/WDA/$UDID
+
+ cat < $NODECONFIG.toml
+[server]
+port = $NODEPORT
+
+[node]
+detect-drivers = false
+
+[relay]
+url = "http://localhost:$APPIUMPORT"
+status-endpoint = "/status"
+configs = [
+ "1", "{\\"platformName\\": \\"ios\\", \\"adbExecTimeout\\": 40000,\\"version\\": \\"$PLATFORMVERSION\\", \\"wdaLocalPort\\": $WDALOCALPORT}"
+]
+
+EOF
+
+SERVERPLIST=com.wire.ios.appium.${GRIDDEVICETYPE}${DEVICECOUNT}.plist
+ echo "SERVERPLIST = $SERVERPLIST"
+
+ cat < $SERVERPLIST
+
+
+
+
+ EnvironmentVariables
+
+ PATH
+ $PATH
+ HOME
+ /Users/jenkins/
+
+ UserName
+ jenkins
+ Label
+ com.wire.ios.node.${GRIDDEVICETYPE}${DEVICECOUNT}
+ RunAtLoad
+
+ KeepAlive
+
+ ProgramArguments
+
+ ${NODEJS_HOME}/bin/appium
+ --relaxed-security
+ -p
+ $APPIUMPORT
+ --log-timestamp
+ --log-no-colors
+ --default-capabilities
+ {"appium:udid":"$UDID","appium:wdaLocalPort":$WDALOCALPORT,"appium:derivedDataPath":"/Users/jenkins/selenium/WDA/$UDID"}
+
+ StandardErrorPath
+ $SERVERCONFIG.err
+ StandardOutPath
+ $SERVERCONFIG.log
+
+
+
+EOF
+
+ sudo mv $SERVERPLIST $LAUNCH_PATH/
+ sudo chown root:wheel $LAUNCH_PATH/$SERVERPLIST
+
+ NODEPLIST=com.wire.ios.node.${GRIDDEVICETYPE}${DEVICECOUNT}.plist
+ echo "NODEPLIST = $NODEPLIST"
+
+ cat < $NODEPLIST
+
+
+
+
+ Label
+ $NODEPLIST
+ RunAtLoad
+
+ KeepAlive
+
+ ThrottleInterval
+ 30
+ WorkingDirectory
+ /Users/jenkins/selenium/
+ UserName
+ jenkins
+ ProgramArguments
+
+ /usr/bin/java
+ -jar
+ selenium-server-4.7.0.jar
+ node
+ --config
+ $NODECONFIG.toml
+ --hub
+ http://$HUBHOST:$HUBPORT
+
+ StandardErrorPath
+ $NODECONFIG.err
+ StandardOutPath
+ $NODECONFIG.log
+
+
+
+EOF
+
+ sudo mv $NODEPLIST $LAUNCH_PATH/
+ sudo chown root:wheel $LAUNCH_PATH/$NODEPLIST
+ done
+ '''
+ }
+ }
+
+ stage('Start new grid and nodes') {
+
+ // load appium server nodes
+ try {
+ String output = sh label: 'Get new plist files of appium servers', returnStdout: true, script: 'ls -1 /Library/LaunchDaemons/* | grep "com.wire.ios.appium.${GRIDDEVICETYPE}*"'
+ print(output)
+ for (String serverPlist : output.split("\n")) {
+ sh label: 'Load plist of node', returnStdout: true, script: "sudo launchctl load -w $serverPlist"
+ }
+ } catch (e) {
+ error("Load new plist of node failed!");
+ }
+
+ sleep 5
+
+ // load grid nodes
+ try {
+ String output = sh label: 'Get new plist files of nodes', returnStdout: true, script: 'ls -1 /Library/LaunchDaemons/* | grep "com.wire.ios.node.${GRIDDEVICETYPE}*"'
+ print(output)
+ for (String nodePlist : output.split("\n")) {
+ sh label: 'Load plist of node', returnStdout: true, script: "sudo launchctl load -w $nodePlist"
+ }
+ } catch (e) {
+ error("Load new plist of node failed!");
+ }
+ }
+ }
+ }
+}
+
diff --git a/wire-ios-automation/ios/NodeMaintenance.groovy b/wire-ios-automation/ios/NodeMaintenance.groovy
new file mode 100644
index 00000000000..4009193700f
--- /dev/null
+++ b/wire-ios-automation/ios/NodeMaintenance.groovy
@@ -0,0 +1,85 @@
+node('built-in') {
+
+ env.NODE_LABELS = "ios_tablet || ios"
+
+ def jenkinsbot_secret = ""
+ withCredentials([string(credentialsId: "JENKINSBOT_IOS", variable: 'JENKINSBOT_SECRET')]) {
+ jenkinsbot_secret = env.JENKINSBOT_SECRET
+ }
+
+ nodes = nodesByLabel label: "$NODE_LABELS", offline: true
+
+ stage('Check availability of nodes') {
+ nodes.each {
+ echo("Checking if node " + it + " is online...")
+ if (Jenkins.instance.getNode(it).toComputer().isOnline()) {
+ echo("Checking if node " + it + " is busy...")
+ for (slave in hudson.model.Hudson.instance.slaves) {
+ if (slave.name.equals(it)) {
+ final comp = slave.getComputer()
+ if (comp.isOnline()) {
+ if (comp.countBusy() > 0) {
+ currentBuild.result = 'ABORTED'
+ error(slave.getNodeName() + " seems to be busy. Aborting the build...")
+ }
+ }
+ }
+ }
+ } else {
+ echo("Node " + it + " is offline!")
+ }
+ }
+ }
+
+ def offline_nodes = []
+
+ stage('Do maintenance') {
+ def jobs = [:]
+
+ for (int i = 0; i < nodes.size(); i++) {
+ def nodeIndex = i
+ jobs[nodes[nodeIndex]] = {
+ if (Jenkins.instance.getNode(nodes[nodeIndex]).toComputer().isOnline()) {
+ node(nodes[nodeIndex]) {
+ echo("Reboot OS on node " + nodes[nodeIndex] + "...")
+ env.JENKINS_NODE_COOKIE = "dontKillMe"
+ sh '(sleep 5 && sudo shutdown -r now) &'
+ sleep 5
+ }
+ echo("Wait until node " + nodes[nodeIndex] + " shuts down...")
+ try {
+ timeout(1) {
+ waitUntil {
+ return !Jenkins.instance.getNode(nodes[nodeIndex]).toComputer().isOnline()
+ }
+ }
+ } catch (e) {
+ echo("Not restarted?")
+ }
+ echo("Wait until node " + nodes[nodeIndex] + " comes back...")
+ try {
+ timeout(3) {
+ waitUntil {
+ return Jenkins.instance.getNode(nodes[nodeIndex]).toComputer().isOnline()
+ }
+ }
+ } catch (e) {
+ echo("Node " + nodes[nodeIndex] + " is still offline!")
+ offline_nodes.add(nodes[nodeIndex])
+ }
+ } else {
+ // The old script went to the parallels host machine and killed the parallels process
+ echo("Node " + nodes[nodeIndex] + " is offline!")
+ offline_nodes.add(nodes[nodeIndex])
+ }
+ }
+ }
+ parallel jobs
+ }
+
+ if (offline_nodes.size() > 0) {
+ echo("Offline nodes: " + offline_nodes.join(","))
+ wireSend secret: "$jenkinsbot_secret", message: "Several iOS nodes are offline: " + offline_nodes.join(", ")
+ }
+
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/Powercycle.groovy b/wire-ios-automation/ios/Powercycle.groovy
new file mode 100644
index 00000000000..218a40daa13
--- /dev/null
+++ b/wire-ios-automation/ios/Powercycle.groovy
@@ -0,0 +1,56 @@
+pipeline {
+ agent {
+ label 'iOS_simu_node070'
+ }
+
+ options {
+ disableConcurrentBuilds()
+ }
+
+ // NOTE: run every hour
+ triggers {
+ pollSCM 'H * * * *'
+ }
+
+ stages {
+ stage('Requirements') {
+ steps {
+ sh 'pip3 install --user brainstem --upgrade'
+ writeFile file: 'powercycle.py', text: '''import datetime
+import time
+
+from brainstem import discover
+from brainstem.link import Spec
+from brainstem.stem import USBHub2x4
+
+NOW = datetime.datetime.now()
+
+if __name__ == \'__main__\':
+ stem = USBHub2x4()
+ spec = discover.findFirstModule(Spec.USB)
+ if spec is None:
+ raise RuntimeError("No USBHub is connected!")
+ stem.connectFromSpec(spec)
+ print(\'Current hour is: {}\'.format(NOW.hour))
+ if (NOW.hour < 10) or (NOW.hour > 20):
+ print(\'Power ON for all USB ports\')
+ stem.usb.setPowerEnable(0)
+ stem.usb.setPowerEnable(1)
+ stem.usb.setPowerEnable(2)
+ stem.usb.setPowerEnable(3)
+ else:
+ stem.usb.setPowerDisable(0)
+ stem.usb.setPowerDisable(1)
+ stem.usb.setPowerDisable(2)
+ stem.usb.setPowerDisable(3)
+ print(\'Power OFF for all USB ports\')
+'''
+ }
+ }
+ stage('Run') {
+ steps {
+ sh 'python3 powercycle.py'
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/README.md b/wire-ios-automation/ios/README.md
new file mode 100644
index 00000000000..f6a2276bd07
--- /dev/null
+++ b/wire-ios-automation/ios/README.md
@@ -0,0 +1,258 @@
+# Setup
+## Simulator Automation Setup
+
+1. Install Xcode from the Appstore.
+ - Install the additional requirements. You will be prompted when opening Xcode for the first time.
+2. In Xcode, navigate to `Settings -> Locations` and set the value of **Command Line Tools** to your current Xcode version.
+3. In Xcode, navigate to `Settings -> Platforms` and install **iOS** and matching simulator via **+** button
+3. Install Carthage.
+
+ brew install carthage
+4. Install NPM with `brew install node`
+5. Check [GridDeployment.groovy](GridDeployment.groovy) for currently used versions (APPIUMVERSION, XCUITESTVERSION)
+6. Install **Appium** with the version above: `npm install appium@` (Please don't use `-g` option)
+7. Install **Appium driver**: `npx appium driver install xcuitest` (Please don't use `-g` option)
+8. Install [applesimutils](https://github.com/wix/AppleSimulatorUtils/releases) required for setting permissions in simulator
+9. If applesimutils cannot be executed run: `xattr -d com.apple.quarantine applesimutils`
+```
+brew tap wix/brew
+brew install applesimutils
+```
+9. Run appium with: `PATH=$PATH: npx appium`
+
+# Test execution
+
+## Simulator Run
+* Create new Run configuration `Run -> Edit Configurations -> Add new -> Maven`
+* Give the configuration the desired name (e.g. `iOS`)
+* Navigate to the Parameters tab (should be open by default)
+* Set Working directory to the zautomation root, for example `/Users/USERNAME/code/zautomation/tests/`
+* Set Command line to `--also-make --projects ios clean install`
+ * if you want to execute the iPad testcases with this configuration, this should be `--also-make --projects ios-tablet clean install`
+* Click on Modify next to Java Options
+ ![img.png](img.png)
+* Select **Properties** and **Environment Variables**
+* Add the following properties:
+ * `picklejar.tags` @torun or id of desired testcase to run
+ * `appPath` Path to the wire.ipa for the simulator
+ * `backendType` To set the default backend
+ * `deviceName` Use `xcrun simctl list` to find a simulator that Appium will match and reuse. If deviceName is not given then a simulator is newly created and destroyed for every run.
+ * `Url` Set http://127.0.0.1:4723 for local execution
+* Set environment variables for [Credentials](../../README.md#credentials) and [Federation](../../README.md#federation)
+* Run appium with: `PATH=$PATH: npx appium`
+* Start the created Run Configuration in Intellij
+
+## Real Device Automation Setup
+
+1. Launch Xcode, go to Xcode menu option: `Preferences -> Accounts`. Add an account using Apple ID. Use `qa1@wire.com` apple ID. Login credentials for this can be found in the QA.kdbx file in our Git directory.
+ After loggin in, click on "Manage Certificates..." and download all.
+2. Navigate to the Applications folder. Right click on Appium and select "Open Package Contents"
+
+ From there, open the folder `Contents/Resources/app/node_modules/appium/node_modules/appium-webdriveragent`
+
+3. Open this folder in a terminal window and execute: `bash Scripts/bootstrap.sh -d`
+
+4. Open the `WebDriverAgent.xcodeproj` Xcode project file in this folder
+
+5. - Click on the top-level project in the left side-bar, called `WebDriverAgent`.
+- Click on *WebDriverAgentLib* in the **TARGETS section**
+- Click on **Signing & Capabilities** in the top bar, make sure Automatically manage signing is checked and set Team value to `Zeta Project, Inc. (Ent)` and change `Bundle Identifier` to `com.wire.WebDriverAgentLib`
+- Click on *WebDriverAgentRunner* in the **TARGETS** section
+- Click on **Build Settings** in the top bar and change the value of `Product Bundle Identifier` to `com.wire.WebDriverAgentRunner`
+- Click on **Signing & Capabilities** in the top bar, make sure Automatically manage signing is checked and change the team value to `Zeta Project, Inc. (Ent)`
+- Click on *IntegrationApp* in the **TARGETS section**
+- Click on **Signing & Capabilities** in the top bar, make sure Automatically manage signing is checked and set Team value to `Zeta Project, Inc. (Ent)` and change `Bundle Identifier` to `com.wire.WebDriverAgentLib`
+
+ Note: If you are running in to signing issues, try out the Troubleshooting section in this document. If this does not fix it, you could try clearing the keychain of old signing certificates, or installing Appium again.
+
+6. Try to build WebDriverAgentRunner with your real device as a target. It will ask for keychain access a few times during the first run. If this passes, the setup inside Xcode is finished.
+
+## Real device Run
+* Go to `Run -> Edit Configurations`
+* Duplicate the iOS Simulator run configuration and give the configuration the desired name (e.g. `iOS Real`)
+* Go to **Runner**
+* Change the following properties:
+ * `picklejar.tags`
+
+ @torun or id of desired testcase to run
+ * `appPath`
+
+ Path to the wire.ipa for the real device
+ * `platformVersion`
+
+ The installed version of iOS on the device you want to test on
+ * `deviceName`
+
+ Name of the real device you want to execute the testcase on (Example: iPhone X)
+ * `isSimulator`
+
+ Should be set to false
+
+ * `UDID`
+
+ UDID of the connected device
+* Start Appium: `npx appium`
+* Start the created Run Configuration in Intellij
+
+## Debug runs
+* Go to `Run -> Edit Configurations`
+* Duplicate the run configuration with which you want to debug and give the configuration the desired name (e.g. `iOS debug`)
+* Navigate to the Parameters tab (should be open by default)
+* Set Working directory to the ios project root, for example ` /Users/USERNAME/IdeaProjects/zautomation/tests/ios`
+* Set Command line to `clean -Dmaven.surefire.debug test`
+* Set Profiles to `iOS`
+* Add a new Run Configuration by clicking on the `+` in the left upper toolbar and select `Remote`
+ * The default values are valid here
+* Add a breakpoint to the desired line of code that you want to debug
+* Start Appium Desktop
+* Enter `localhost` for the Host field
+* Click on Start Server
+* Start the created Debug Run Configuration in Intellij
+* Start the Remote run configuration
+
+# Finding locators
+There are two possibilities for finding locators with [Appium-Inspector](https://github.com/appium/appium-inspector/releases):
+- By running a testcase with a break point, and attaching the appium server to this session
+- by starting an appium session directly through appium inspector
+
+The benefit of using a break point is that it can take care of setting up the test, so that you don't need to manually create the circumstances in which the locator will show. It can however, be a bit tricky to connect to the session sometimes.
+
+### By running a test
+
+* Run a testcase which has a breakpoint in debug mode
+
+ Alternative possibillity: you can add a step `And I wait for 200 seconds`
+ to make the test execution pause for X seconds, which can be quicker than
+ starting a debug run. Just make sure to remove this before commiting.
+* Open the active Appium Desktop server
+* Press **Command + n**
+* Click **Attach to session...**
+* Click the refresh button on the drop-down bar below until the Session ID shows up
+* Click Attach to Session
+
+### Without running a test
+
+* Open Appium desktop and start a server
+* Press **Command + n** or press the magnifier icon
+* Underneath "Desired Capabilities", add the following values:
+```
+{
+ "platformName": "iOS",
+ "platformVersion": "14.1", // The platform version of your simulator (usually resambles the xcode version)
+ "app": "/Users//Downloads/Wire.ipa", // Path to the wire.ipa for the simulator
+ "deviceName": "iPhone 11", // The device name of your simulator/device
+ "processArguments": { // These arguments are needed to run the app as the automation would do it (for e.g. using staging backend)
+ "args": [
+ "-use-app-center",
+ "0",
+ "--disable-interactive-keyboard-dismissal",
+ "--disable-call-quality-survey",
+ "-BackendEnvironmentTypeOverrideKey",
+ "staging",
+ "--persist-backend-type",
+ "--disable-autocorrection",
+ "--debug-log=Network,SessionManager,event-processing,SyncStatus,OperationStatus,Push,cryptobox,background-activity,ephemeral,Authentication",
+ "-com.apple.CoreData.ConcurrencyDebug",
+ "1",
+ "--disable-push-alert",
+ "-UseAnalytics",
+ "0"
+ ],
+ "env": {
+ "ZMLOG_TAGS": "AVS,calling"
+ }
+ }
+}
+```
+* Click on "Save as..." and save the values so that you do not have to re-enter this the next time
+* Click on "Start Session"
+
+# Special testcases
+
+## Fast Login
+
+If a test uses fast login the entering of credentials will be skipped by providing
+the app with command line parameters `--loginemail=` and `--loginpassword=`
+
+### Deprecated method
+
+Fast login can be activated for a test by adding the @fastlogin tag.
+
+But it only works when test sets the "is me" value on one of the following steps:
+* User X is me
+* There (?:is|are) (\d+) users? where (.*) is me
+* There (?:are|is) (\d+) users? with email address only where (.*) is me
+* I prepare Wire to perform fast log in by email as (.*)
+
+**Warning:** Fast login also skips the First Time Overlay, so it is not needed to use
+this step in the test after the login.
+
+### New method
+
+Instead of using the @fastlogin tag use the step `I sign in user with fast login`
+and remove implicit and explicit setting of "is me" and `Myself`.
+
+The step will automatically tap on login button on the welcome page.
+
+You can only use the step if the driver was not created before.
+
+## Upgrade
+### Locally
+To test upgrade testcases locally, you need to set the old build you want to upgrade from. You can do this by adding the property `oldAppPath` in the run configuration in IntelliJ. The value of this should lead to the .ipa that you want to upgrade from.
+
+### Automation Grid
+In case you want to perform upgrade tests on our automation grid, you can specify the build number of which version you want to upgrade from in the `oldBuildNumber` parameter. If this is left empty, the job will grab the fourth oldest build. In case of a RC build, it will take the previously released build by default.
+
+When the `AppBuildNumber` parameter is provided in the Jenkins Job, the `OldBuildNumber` needs to be provided too in case you want to test an upgrade. Otherwise, it will perform a re-install of the same version.
+
+# Troubleshooting
+
+#### General
+* If you are unable to sign WebDriverAgentRunner with the real device selected as a target (missing certificate iPhone Developer -name-), make sure you are added to the wildcard for our testing devices in the iOS Development team.
+* If there is a project file missing in your WebDriverAgent Xcode project, you should navigate to the WebDriverAgent project location in terminal and execute the following command: `bash Scripts/bootstrap.sh -d`
+This will fetch the needed dependencies using Carthage. If it only shows "Fetching dependencies" before finsihing, re-install carthage using the commands below and execute the bootstrap command again.
+
+ rm -rf Carthage
+ brew uninstall carthage
+ brew install carthage
+
+#### sudo: a terminal is required to read the password
+* The NOPASSWD privilege for the user `jenkins` is needed on the machine. See
+[Confluence](https://wearezeta.atlassian.net/wiki/spaces/QA/pages/383058074/Node+Configuration) on how to set this up.
+
+#### Fail to lock /Users/jenkins/workspace/ios_tablet_pipeline_staging/Wire.ipa
+* The job was probably canceled while it was downloading the ipa from S3. The easiest way is to login to the machine and
+delete the whole workspace with `rm -rf /Users/jenkins/workspace/ios_tablet_pipeline_staging/` and then disconnect and
+reconnect the node in Jenkins node settings.
+
+#### Error running 'install': An error was encountered processing the command... Failed to load Info.plist from bundle
+* The error message should contain the udid of the simulator. Use this udid to find the machine on which the affected
+simulator is running. Connect to the machine and make a full reset of the simulator.
+
+#### Real device: No signing certificate "iOS Development" found: No "iOS Development" signing certificate matching team ID "..." with a private key was found.
+* Check the capability called `appium:xcodeOrgId`. It should contain the team id that you can find when logging in with
+qa1@wire.com on [developer.apple.com](https://developer.apple.com/) under Membership | Team ID.
+
+#### Real device: WebDriverAgent xcodebuild exited with code '65'
+* WebDriverAgent probably could not be signed. Check if the capability `appium:xcodeOrgId` is used. In the nodeconfig
+log you can find the full command for xcodebuild. When you run it on the machine you will get a log with better error
+messages.
+
+#### Real device: WebDriverAgent xcodebuild exited with code '1'
+* WebDriverAgent probably could not be build through missing dependencies or something else. In the nodeconfig log you
+can find the path to appium-webdriveragent node module. Go into this directory and run `./Scripts/bootstrap.sh` In the
+nodeconfig log you can also find the full command for xcodebuild. When you run it on the machine you will get a log with
+better error messages.
+
+#### Real device: Xcode Project does not compile due to missing files
+* Try to re-install carthage and execute the `bash Scripts/boostrap.sh -d` command again from step 4 of the real device automation setup guide.
+
+
+#### All tests freeze on simulator on the screen with the Wire shield logo with black background**
+* Solution: Restart simulator
+
+#### Simulator architecture is unsupported by the '.../Wire.app' application
+* You probably have tried to run the real device builds on a simulator. Check if `is_simulator = true` for the choosen grid.
+
+#### MacOS Big Sur: JavaSeekerTest.testGetLoadedClasses:39 NoSuchField classes
+* It is possible that the update to MacOS Big Sur has resulted into the JAVA_HOME variable being set incorrectly. Try to run `- export JAVA_HOME=$(/usr/libexec/java_home)` in Terminal and see if that fixes the issue.
diff --git a/wire-ios-automation/ios/Release_Tests.Jenkinsfile.groovy b/wire-ios-automation/ios/Release_Tests.Jenkinsfile.groovy
new file mode 100644
index 00000000000..a8abfe22a9d
--- /dev/null
+++ b/wire-ios-automation/ios/Release_Tests.Jenkinsfile.groovy
@@ -0,0 +1,281 @@
+/*
+
+TAGS as String parameter
+Grid as Choice parameter
+Track as Choice parameter: dev, release, avs, qa-playground
+AppBuildNumber as String parameter
+CALLING_SERVICE_ENV as Choice parameter with choices: master, dev
+TESTINY_RUN_NAME as String parameter
+Branch as String parameter main
+surefire.rerunFailingTestsCount as String parameter 1
+test as String parameter
+
+ */
+import hudson.tasks.test.AbstractTestResultAction
+
+@NonCPS
+def testStatuses() {
+ AbstractTestResultAction testResultAction = currentBuild.rawBuild.getAction(AbstractTestResultAction.class)
+ if (testResultAction != null) {
+ def total = testResultAction.totalCount
+ def failed = testResultAction.failCount
+ def skipped = testResultAction.skipCount
+ def passed = total - failed - skipped
+ int percent = (total > 0) ? (passed * 100) / (total - skipped) : 0
+ testStatus = "Tests passed: ${passed}, Failed: ${failed} ${testResultAction.failureDiffString}, Skipped: ${skipped}, Total: ${total} (${percent}%)"
+ } else {
+ testStatus = "Could not find test results!"
+ }
+ return testStatus
+}
+
+@NonCPS
+def isTestSuccessful() {
+ AbstractTestResultAction testResultAction = currentBuild.rawBuild.getAction(AbstractTestResultAction.class)
+ if (testResultAction != null && testResultAction.failCount > 0) {
+ return false
+ }
+ return true
+}
+
+node("Job_distributor") {
+
+ def aborted = false
+ def TAGS = "${params.TAGS}"
+ def filename = ""
+
+ // Select bot credentials for specific Wire conversation based on job name
+ credentialsId = "JENKINSBOT_IOS_REGRESSION"
+
+ // Select grid
+ hubUrl = "http://192.168.2.200:4444/wd/hub"
+ is_simulator = true
+ deviceName = "iPhone 11"
+ platformVersion = "16.4"
+ nodeLabels = "iOS_node200"
+ MAX_PARALLEL = 5
+
+ withCredentials([
+ file(credentialsId: 'KUBECONFIG_anta', variable: 'KUBECONFIG_anta'),
+ file(credentialsId: 'KUBECONFIG_bella', variable: 'KUBECONFIG_bella'),
+ file(credentialsId: 'KUBECONFIG_chala', variable: 'KUBECONFIG_chala'),
+ file(credentialsId: 'KUBECONFIG_foma', variable: 'KUBECONFIG_foma'),
+ file(credentialsId: 'KUBECONFIG_gudja_offline_ios', variable: 'KUBECONFIG_gudja_offline_ios'),
+ file(credentialsId: 'KUBECONFIG_bund_next_column_1', variable: 'KUBECONFIG_bund_next_column_1'),
+ file(credentialsId: 'KUBECONFIG_bund_qa_column_1', variable: 'KUBECONFIG_bund_qa_column_1'),
+ string(credentialsId: "${credentialsId}", variable: 'JENKINSBOT_SECRET'),
+ string(credentialsId: 'BLACKLIST_S3_SECRET', variable: 'BLACKLIST_S3_SECRET'),
+ string(credentialsId: 'OKTA_API_KEY', variable: 'OKTA_API_KEY'),
+ string(credentialsId: 'KEYCLOAK_PASSWORD', variable: 'KEYCLOAK_PASSWORD'),
+ string(credentialsId: 'LH_SERVICE_AUTH_TOKEN', variable: 'LH_SERVICE_AUTH_TOKEN'),
+ string(credentialsId: "TESTINY_API_KEY_IOS", variable: 'TESTINY_API_KEY'),
+ string(credentialsId: 'STRIPE_API_KEY', variable: 'STRIPE_API_KEY')]) {
+
+ // Checkout
+ stage('Checkout & Clean') {
+ echo("Checkout common")
+ checkout(
+ [$class: 'GitSCM',
+ branches: [[name: '*/${Branch}']],
+ doGenerateSubmoduleConfigurations: false,
+ extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'common'],
+ [$class: 'SparseCheckoutPaths', sparseCheckoutPaths: [[path: 'tests/common']]],
+ [$class: 'CheckoutOption', timeout: 30],
+ [$class: 'CloneOption', depth: 0, noTags: true, reference: '', shallow: true, timeout: 30],
+ [$class: 'BuildChooserSetting', buildChooser: [$class: 'DefaultBuildChooser']]],
+ submoduleCfg: [],
+ userRemoteConfigs: [[credentialsId: 'zautomation', url: 'git@github.com:zinfra/zautomation.git']]])
+
+ checkout([$class: 'GitSCM',
+ branches: [[name: '*/${Branch}']],
+ doGenerateSubmoduleConfigurations: false,
+ extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'wire-ios-automation'],
+ [$class: 'SparseCheckoutPaths', sparseCheckoutPaths: [[path: 'wire-ios-automation/ios'], [path: 'wire-ios-automation/pom.xml'], [path: 'wire-ios-automation/tools']]],
+ [$class: 'CheckoutOption', timeout: 30],
+ [$class: 'CloneOption', depth: 0, noTags: true, reference: '', shallow: true, timeout: 30],
+ [$class: 'BuildChooserSetting', buildChooser: [$class: 'DefaultBuildChooser']]],
+ submoduleCfg: [],
+ userRemoteConfigs: [[credentialsId: 'zautomation', url: 'git@github.com:wireapp/wire-ios.git']]])
+
+ echo("Installing kubectl if not already installed")
+ kubeCtlSetup = readFile("${WORKSPACE}/common/tests/common/kubectlSetup.sh")
+ sh kubeCtlSetup
+ }
+
+ // TODO: Move this back into the test stage when a solution is found that does not override Wire.ipa in a second parallel run
+ lock("${Grid}") {
+
+ stage('Download build from S3 to grid nodes') {
+ load('wire-ios-automation/ios/RetrieveIPA.Jenkinsfile.groovy').setIPAFromS3(track, appBuildNumber, is_simulator)
+
+ // Work on selenium grid nodes
+ nodes = nodesByLabel(nodeLabels)
+ nodes.each {
+ node(it) {
+ // Cleanup the workspace before getting the ipa ready
+ sh "rm -rf $WORKSPACE/Payload"
+ sh "rm -rf $WORKSPACE/Wire.ipa"
+
+ // TODO: Find a better place for saving the ipa (maybe containing the grid name)
+ env.OLD_APP_PATH = "$WORKSPACE/Previous/Payload/Wire.app"
+ env.APP_PATH = "$WORKSPACE/Payload/Wire.app"
+
+ // Download builds via s3 proxy
+ sh "curl http://192.168.2.39:8000/z-lohika/${S3AppPath} -o \"$WORKSPACE/Wire.ipa\""
+
+ // grab wire.app from the ipa
+ sh "unzip -o $WORKSPACE/Wire.ipa"
+
+ // Gather bundle id from ipa
+ env.BUNDLE_ID = sh returnStdout: true, script: "plutil -extract CFBundleIdentifier raw Payload/Wire.app/Info.plist -o - | tr -d '\\n'"
+ echo("BUNDLE_ID: " + env.BUNDLE_ID)
+
+ echo("Delete old version")
+ sh "rm -rf ${OldS3AppPath}"
+
+ echo("Download old version")
+ if ("${OldS3AppPath}" == "") {
+ echo("No OldS3AppPath given.")
+ } else {
+ // Download old build via s3 proxy
+ sh "curl http://192.168.2.39:8000/z-lohika/${OldS3AppPath} -o \"$WORKSPACE/Previous.ipa\""
+ sh "unzip -o $WORKSPACE/Previous.ipa -d $WORKSPACE/Previous/"
+ }
+
+ // See section "Files generated by test runs" on http://appium.io/docs/en/drivers/ios-xcuitest/
+ // If it fails we shutdown the simulator first and try deleting them again
+ sh returnStatus: true, script: 'rm -rf $HOME/Library/Logs/CoreSimulator/* || xcrun simctl shutdown booted && rm -Rf $HOME/Library/Logs/CoreSimulator/*'
+
+ // Delete cached ipa files to prevent installation error "Failed to load Info.plist from bundle at"
+ sh 'rm -rf /var/folders/* || true'
+
+ // Delete all files used by former history backup tests
+ sh 'find $HOME/Library/Developer/CoreSimulator/Devices/ -name *.ios_wbu -delete'
+ }
+ }
+ }
+
+ // Set build description
+ filename = env.S3AppPath.tokenize('/')[-1]
+ currentBuild.description = filename + "\n" + TAGS
+
+ // Keep build forever when RC is made
+ if (params.TESTINY_RUN_NAME != "") {
+ currentBuild.description = params.TESTINY_RUN_NAME + "\n" + currentBuild.description
+ currentBuild.keepLog = true
+ }
+
+ withMaven(jdk: 'AdoptiumJDK11', maven: 'M3', mavenOpts: '-Xmx1024m -XX:MaxPermSize=128m', mavenLocalRepo: '.repository', options: [junitPublisher(disabled: true), jacocoPublisher(disabled: true)]) {
+
+ stage('Build common') {
+ env.RC_TESTS_COMMENT_PATH = "$WORKSPACE/rc_comment.txt"
+ sh 'echo "${JOB_NAME} ${BUILD_DISPLAY_NAME} - Cucumber Report: ${JENKINS_URL}job/${JOB_NAME}/${BUILD_NUMBER}/cucumber-html-reports/" > $RC_TESTS_COMMENT_PATH'
+
+ sh """
+mvn clean install \\
+-f "$WORKSPACE/common/tests/common/pom.xml" \\
+-DbackendType="${backendType}" \\
+-DtestinyProjectName="Wire iOS" \\
+-DtestinyRunName="$TESTINY_RUN_NAME" \\
+-DrcTestsCommentPath="${RC_TESTS_COMMENT_PATH}" \\
+-DcallingServiceUrl='loadbalanced' \\
+-Dcom.wire.calling.env='${CALLING_SERVICE_ENV}' \\
+-DsyncIsAutomated=true
+"""
+ }
+
+ // This is needed because of https://issues.jenkins-ci.org/browse/JENKINS-7180
+ def RERUN = currentBuild.getRawBuild().actions.find { it instanceof ParametersAction }?.parameters.find {
+ it.name == 'surefire.rerunFailingTestsCount'
+ }?.value
+
+ // This is needed when we want the tests to only run on a specific device
+ if (!browserName.isEmpty()) {
+ env.MAX_PARALLEL = 1
+ }
+ stage('Run tests') {
+ try {
+ timeout(time: 6, unit: 'HOURS') {
+ realtimeJUnit(keepLongStdio: true, testDataPublishers: [[$class: 'JUnitFlakyTestDataPublisher']], testResults: 'wire-ios-automation/ios/target/xml-reports/TEST*.xml') {
+ sh """
+mvn clean integration-test \\
+-f "$WORKSPACE/wire-ios-automation/ios/pom.xml" \\
+-P isOnGrid \\
+-DUrl='${hubUrl}' \\
+-Dpicklejar.parallelism='${MAX_PARALLEL}' \\
+-DisSimulator=${is_simulator} \\
+-Dpicklejar.tags='$TAGS' \\
+-DappPath="${APP_PATH}" \\
+-DoldAppPath="${OLD_APP_PATH}" \\
+-DdeviceName="${deviceName}" \\
+-DplatformVersion='${platformVersion}' \\
+-DrealBuildNumber='${REAL_BUILD_NUMBER}' \\
+-DbundleId='${BUNDLE_ID}' \\
+-Dsurefire.rerunFailingTestsCount=${RERUN} \\
+-Dtest="${params.test}" \\
+-DbrowserName="${browserName}"
+"""
+ }
+ }
+ } catch (e) {
+ print e
+ if (e instanceof hudson.AbortException) {
+ aborted = true
+ }
+ }
+ }
+ }
+
+ } // End of lock
+
+ stage('Generate test results') {
+ try {
+ // Generate Jenkins cucumber HTML reports and archive JSON
+ archiveArtifacts artifacts: '**/target/*report*.json', followSymlinks: false
+ cucumber failedFeaturesNumber: -1, failedScenariosNumber: -1, failedStepsNumber: -1, fileIncludePattern: '**/target/*report*.json', jsonReportDirectory: "${WORKSPACE}/wire-ios-automation/ios/", mergeFeaturesById: true, pendingStepsNumber: -1, skippedStepsNumber: -1, sortingMethod: 'ALPHABETICAL', undefinedStepsNumber: -1
+ } catch (e) {
+ print e
+ if (e instanceof hudson.AbortException) {
+ aborted = true
+ }
+ }
+ try {
+ // Generate zip-able files
+ // Zip and archive cucumber HTML reports
+ node("built-in") {
+ def foldername = env.JOB_NAME
+ def down = "../.."
+ if (foldername.indexOf("/") > -1) {
+ foldername = env.JOB_NAME.replace("/", "/jobs/")
+ down = "../../.."
+ } else {
+ foldername = env.JOB_BASE_NAME
+ }
+ sh returnStatus: true, script: 'rm -rf cucumber-report*.zip'
+ zip archive: true, defaultExcludes: false, dir: "${down}/jobs/${foldername}/builds/${BUILD_NUMBER}/cucumber-html-reports/", overwrite: true, zipFile: "cucumber-report_iOS_build_${REAL_BUILD_NUMBER}.zip"
+ }
+
+ } catch (e) {
+ print e
+ if (e instanceof hudson.AbortException) {
+ aborted = true
+ }
+ }
+ }
+
+ stage('Report test results') {
+ def testResult = testStatuses()
+ if (isTestSuccessful()) {
+ if (!aborted) {
+ wireSend secret: env.JENKINSBOT_SECRET, message: "✅ **${JOB_NAME} ${BUILD_DISPLAY_NAME}**\n${filename}\n${TAGS}\nSee [JUnit Reports](${BUILD_URL}testReport/) or [Cucumber Reports](${BUILD_URL}cucumber-html-reports)\n${testResult}"
+ } else {
+ wireSend secret: env.JENKINSBOT_SECRET, message: "⚠️ **${JOB_NAME} ${BUILD_DISPLAY_NAME} was aborted**\n${filename}\n${TAGS}\nSee [Console log](${BUILD_URL}console)\n${testResult}"
+ }
+ } else {
+ wireSend secret: env.JENKINSBOT_SECRET, message: "❌ **${JOB_NAME} ${BUILD_DISPLAY_NAME}**\n${filename}\n${TAGS}\nSee [JUnit Reports](${BUILD_URL}testReport/) or [Cucumber Reports](${BUILD_URL}cucumber-html-reports)\n${testResult}"
+ }
+ }
+
+ }
+
+}
diff --git a/wire-ios-automation/ios/RetrieveIPA.Jenkinsfile.groovy b/wire-ios-automation/ios/RetrieveIPA.Jenkinsfile.groovy
new file mode 100644
index 00000000000..09afec23c2b
--- /dev/null
+++ b/wire-ios-automation/ios/RetrieveIPA.Jenkinsfile.groovy
@@ -0,0 +1,153 @@
+/*
+
+TAGS as String parameter
+Grid as Choice parameter
+Track as Choice parameter: dev, release, avs, qa-playground
+AppBuildNumber as String parameter
+CALLING_SERVICE_ENV as Choice parameter with choices: master, dev
+TESTINY_RUN_NAME as String parameter
+Branch as String parameter main
+surefire.rerunFailingTestsCount as String parameter 1
+test as String parameter
+ */
+
+@NonCPS
+def sortByModified(list) {
+ list.sort {
+ it.getLastModified()
+ }
+}
+
+def setIPAFromS3(def track, def appBuildNumber, def is_simulator) {
+ if (track == "Development") {
+ // Track "Development": AppBuildNumber can be set to build number or "latest"
+ if (is_simulator) {
+ S3_FOLDER = "ios/development/simulator/"
+ } else {
+ S3_FOLDER = "ios/development/device/debug/"
+ }
+ GLOB = 'Wire-development-develop-*/*.ipa'
+ // Get list of files on S3
+ def files = []
+ withAWS(region: 'eu-west-1', credentials: "S3_CREDENTIALS") {
+ files = s3FindFiles bucket: "z-lohika", path: S3_FOLDER, onlyFiles: true, glob: GLOB
+ }
+ print("Found:")
+ files.each {
+ print(it.path)
+ }
+ if (appBuildNumber == "latest") {
+ print("Get latest develop build...")
+ files = sortByModified(files)
+ env.S3AppPath = S3_FOLDER + files[-1].path
+ } else {
+ print("Find develop build with matching build number...")
+ files.each {
+ if (it.name.contains("${appBuildNumber}")) {
+ env.S3AppPath = S3_FOLDER + it.path
+ }
+ }
+ if (env.S3AppPath == null) {
+ error("Could not find develop build in ${S3_FOLDER} with name containing ${appBuildNumber}")
+ }
+ }
+ } else if (track == "S3") {
+ if ("${appBuildNumber}".endsWith(".ipa") == 0) {
+ error("AppBuildNumber does needs to end with .ipa: ${appBuildNumber}")
+ }
+ if (appBuildNumber.startsWith("ios/") == 0) {
+ error("AppBuildNumber should look like this: ios/development/simulator/Wire-development-develop-simulator-10310/Wire-development-develop-simulator-10310.ipa")
+ }
+ env.S3AppPath = appBuildNumber
+ } else if (track == "Release") {
+ print("Trying to get latest release...")
+ S3_FOLDER = "ios/release/testflight/"
+ GLOB = 'Wire-beta-release*-simulator-*/*.ipa'
+ withAWS(region: 'eu-west-1', credentials: "S3_CREDENTIALS") {
+ files = s3FindFiles bucket: "z-lohika", path: S3_FOLDER, onlyFiles: true, glob: GLOB
+ }
+ print("Search complete")
+ print("Found:")
+ files.each {
+ print(it.path)
+ }
+
+ if (appBuildNumber == "latest") {
+ print("Get latest release build...")
+ files = sortByModified(files)
+ env.S3AppPath = S3_FOLDER + files[-1].path
+ } else {
+ print("Find develop build with matching build number...")
+ files.each {
+ if (it.name.contains("${appBuildNumber}")) {
+ env.S3AppPath = S3_FOLDER + it.path
+ }
+ }
+ if (env.S3AppPath == null) {
+ error("Could not find develop build in ${S3_FOLDER} with name containing ${appBuildNumber}")
+ }
+ }
+ } else if (track.startsWith("Bund-")) {
+ if (track == "Bund-column1-dev") {
+ if (is_simulator) {
+ S3_FOLDER = "ios/custom/bund/column1/development/simulator/"
+ } else {
+ S3_FOLDER = "ios/custom/bund/column1/development/device/release/"
+ }
+ GLOB = '*.ipa'
+ } else if (track == "Bund-column1-release") {
+ if (is_simulator) {
+ S3_FOLDER = "ios/custom/bund/column1/release/simulator/"
+ } else {
+ S3_FOLDER = "ios/custom/bund/column1/release/device/release/"
+ }
+ GLOB = 'Wire-rc-release_*/*.ipa'
+ } else if (track == "Bund-column3-release") {
+ if (is_simulator) {
+ S3_FOLDER = "ios/custom/bund/column3/release/simulator/"
+ } else {
+ S3_FOLDER = "ios/custom/bund/column3/release/device/release/"
+ }
+ GLOB = 'Wire-rc-release_*/*.ipa'
+ } else {
+ error("Unknown Track: " + track)
+ }
+ // Get list of files on S3
+ def files = []
+ print(S3_FOLDER)
+ print(GLOB)
+ withAWS(region: 'eu-west-1', credentials: s3_credentials) {
+ files = s3FindFiles bucket: "z-lohika", path: S3_FOLDER, onlyFiles: true, glob: GLOB
+ }
+ print("Search complete")
+ print("Found:")
+ files.each {
+ print(it.path)
+ }
+ if (appBuildNumber == "latest") {
+ print("Get latest build...")
+ files = sortByModified(files)
+ env.S3AppPath = S3_FOLDER + files[-1].path
+ } else {
+ print("Find develop build with matching build number...")
+ files.each {
+ if (it.name.contains(appBuildNumber)) {
+ env.S3AppPath = S3_FOLDER + it.path
+ }
+ }
+ if (env.S3AppPath == null) {
+ error("Could not find develop build in ${S3_FOLDER} with name containing ${appBuildNumber}")
+ }
+ }
+ } else {
+ error("Track ${track} is not known. Please use either 'Development', 'S3' or one of the bund tracks.")
+ }
+
+ echo("S3AppPath: " + env.S3AppPath)
+
+ env.REAL_BUILD_NUMBER = (env.S3AppPath =~ /([0-9]+).ipa/) [0] [1]
+ echo("REAL_BUILD_NUMBER: " + env.REAL_BUILD_NUMBER)
+ return env.S3AppPath
+}
+
+return this
\ No newline at end of file
diff --git a/wire-ios-automation/ios/Tests.Jenkinsfile.groovy b/wire-ios-automation/ios/Tests.Jenkinsfile.groovy
new file mode 100644
index 00000000000..f7334285ea0
--- /dev/null
+++ b/wire-ios-automation/ios/Tests.Jenkinsfile.groovy
@@ -0,0 +1,367 @@
+/*
+
+TAGS as String parameter
+Grid as Choice parameter
+Track as Choice parameter: dev, release, avs, qa-playground
+AppBuildNumber as String parameter
+CALLING_SERVICE_ENV as Choice parameter with choices: master, dev
+TESTINY_RUN_NAME as String parameter
+Branch as String parameter main
+surefire.rerunFailingTestsCount as String parameter 1
+test as String parameter
+
+ */
+import hudson.tasks.test.AbstractTestResultAction
+
+@NonCPS
+def testStatuses() {
+ AbstractTestResultAction testResultAction = currentBuild.rawBuild.getAction(AbstractTestResultAction.class)
+ if (testResultAction != null) {
+ def total = testResultAction.totalCount
+ def failed = testResultAction.failCount
+ def skipped = testResultAction.skipCount
+ def passed = total - failed - skipped
+ int percent = (total > 0) ? (passed * 100) / (total - skipped) : 0
+ testStatus = "Tests passed: ${passed}, Failed: ${failed} ${testResultAction.failureDiffString}, Skipped: ${skipped}, Total: ${total} (${percent}%)"
+ } else {
+ testStatus = "Could not find test results!"
+ }
+ return testStatus
+}
+
+@NonCPS
+def isTestSuccessful() {
+ AbstractTestResultAction testResultAction = currentBuild.rawBuild.getAction(AbstractTestResultAction.class)
+ if (testResultAction != null && testResultAction.failCount > 0) {
+ return false
+ }
+ return true
+}
+
+@NonCPS
+def sortByModified(list) {
+ list.sort {
+ it.getLastModified()
+ }
+}
+
+node("Job_distributor") {
+
+ def aborted = false
+ def TAGS = "${params.TAGS}"
+ def filename = ""
+
+ // Select bot credentials for specific Wire conversation based on job name
+ if ("${JOB_NAME}" =~ /pipeline_Regression/) {
+ credentialsId = "JENKINSBOT_IOS_REGRESSION"
+ } else if ("${JOB_NAME}" =~ /pipeline_Call/) {
+ credentialsId = "JENKINSBOT_IOS"
+ } else if ("${JOB_NAME}" =~ /pipeline_realdevice_Regression/) {
+ credentialsId = "JENKINSBOT_IOS_REGRESSION"
+ } else if ("${JOB_NAME}" =~ /pipeline_realdevice_Staging/) {
+ credentialsId = "JENKINSBOT_IOS_STAGING"
+ } else if ("${JOB_NAME}" =~ /pipeline_realdevice_exp/) {
+ credentialsId = "JENKINSBOT_IOS_STAGING"
+ } else if ("${JOB_NAME}" =~ /exp_pipeline/) {
+ credentialsId = "JENKINSBOT_IOS_EXP"
+ } else if ("${JOB_NAME}" =~ /exp2_pipeline/) {
+ credentialsId = "JENKINSBOT_IOS_EXP"
+ } else if ("${JOB_NAME}" =~ /knownbug|unstable/) {
+ credentialsId = "JENKINSBOT_IOS_STAGING"
+ } else if ("${JOB_NAME}" =~ /pipeline_RC/) {
+ credentialsId = "JENKINSBOT_IOS_RC"
+ } else if ("${JOB_NAME}" =~ /pipeline_Smoke/) {
+ credentialsId = "JENKINSBOT_IOS_SMOKE"
+ } else if ("${JOB_NAME}" =~ /_Bund_/) {
+ credentialsId = "JENKINSBOT_BUND"
+ } else if ("${JOB_NAME}" =~ /pipeline_large_team/) {
+ credentialsId = "JENKINSBOT_IOS_REGRESSION"
+ } else if ("${JOB_NAME}" =~ /pipeline_experiment/) {
+ credentialsId = "JENKINSBOT_IOS_STAGING"
+ } else if ("${JOB_NAME}" =~ /pipeline_regression_x86/) {
+ credentialsId = "JENKINSBOT_IOS_REGRESSION"
+ } else if ("${JOB_NAME}" =~ /iOS_Critical_Flows/) {
+ credentialsId = "JENKINSBOT_IOS_SMOKE"
+ } else if ("${JOB_NAME}" =~ /iOS_Navigation_Overhaul/) {
+ credentialsId = "JENKINSBOT_IOS_SMOKE"
+ } else {
+ credentialsId = "JENKINSBOT_IOS_EXP"
+ }
+
+ // Select grid
+ if (params.Grid == "iOS-realdevice") {
+ hubUrl = "http://192.168.2.70:4444/wd/hub"
+ is_simulator = false
+ deviceName = "iPhone 11"
+ platformVersion = "16.3"
+ nodeLabels = "iOS_node070"
+ MAX_PARALLEL = nodesByLabel(nodeLabels).size()
+ } else if (params.Grid == "iOS-phones-arm64") {
+ hubUrl = "http://192.168.2.200:4444/wd/hub"
+ is_simulator = true
+ deviceName = "iPhone 11"
+ platformVersion = "16.4"
+ nodeLabels = "iOS_node200"
+ MAX_PARALLEL = 5
+ } else if (params.Grid == "iOS-phones-arm64-fast") {
+ hubUrl = "http://192.168.2.203:4444/wd/hub"
+ is_simulator = true
+ deviceName = "iPhone 11"
+ platformVersion = "16.4"
+ nodeLabels = "iOS_node203"
+ MAX_PARALLEL = 15
+ } else {
+ error("Grid " + params.Grid + " is not supported yet!")
+ }
+
+ echo("Configure 1Password integration")
+ def config = [
+ serviceAccountCredentialId: '1PasswordServiceAccountToken',
+ opCLIPath: "/usr/local/bin/"
+ ]
+ def secrets = [
+ [envVar: 'OKTA_API_KEY', secretRef: 'op://QA automation/OKTA_API_KEY/password'],
+ [envVar: 'KEYCLOAK_PASSWORD', secretRef: 'op://QA automation/KEYCLOAK_PASSWORD/password'],
+ [envVar: 'LH_SERVICE_AUTH_TOKEN', secretRef: 'op://QA automation/LH_SERVICE_AUTH_TOKEN/password'],
+ [envVar: 'STRIPE_API_KEY', secretRef: 'op://QA automation/STRIPE_API_KEY/password'],
+ [envVar: 'MS_EMAIL', secretRef: 'op://QA automation/MS_CREDENTIALS/username'],
+ [envVar: 'MS_PASSWORD', secretRef: 'op://QA automation/MS_CREDENTIALS/password'],
+ [envVar: 'BLACKLIST_S3_SECRET', secretRef: 'op://QA automation/BLACKLIST_S3_SECRET/password'],
+ [envVar: 'TESTINY_API_KEY', secretRef: 'op://QA automation/TESTINY_API_KEY_IOS/password'],
+ ]
+
+ // Use 1Password secrets
+ withSecrets(config: config, secrets: secrets) {
+ // Use Jenkins credentials
+ withCredentials([
+ string(credentialsId: '1PasswordServiceAccountToken', variable: 'OP_SERVICE_ACCOUNT_TOKEN'),
+ file(credentialsId: 'KUBECONFIG_anta', variable: 'KUBECONFIG_anta'),
+ file(credentialsId: 'KUBECONFIG_bella', variable: 'KUBECONFIG_bella'),
+ file(credentialsId: 'KUBECONFIG_chala', variable: 'KUBECONFIG_chala'),
+ file(credentialsId: 'KUBECONFIG_foma', variable: 'KUBECONFIG_foma'),
+ file(credentialsId: 'KUBECONFIG_gudja_offline_ios', variable: 'KUBECONFIG_gudja_offline_ios'),
+ file(credentialsId: 'KUBECONFIG_bund_next_column_1', variable: 'KUBECONFIG_bund_next_column_1'),
+ file(credentialsId: 'KUBECONFIG_bund_qa_column_1', variable: 'KUBECONFIG_bund_qa_column_1'),
+ string(credentialsId: "${credentialsId}", variable: 'JENKINSBOT_SECRET')
+ ]) {
+
+ // Checkout
+ stage('Checkout & Clean') {
+
+ echo("Checkout common")
+ checkout(
+ [$class: 'GitSCM',
+ branches: [[name: '*/main']],
+ doGenerateSubmoduleConfigurations: false,
+ extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'common'],
+ [$class: 'SparseCheckoutPaths', sparseCheckoutPaths: [[path: 'tests/common']]],
+ [$class: 'CheckoutOption', timeout: 30],
+ [$class: 'CloneOption', depth: 0, noTags: true, reference: '', shallow: true, timeout: 30],
+ [$class: 'BuildChooserSetting', buildChooser: [$class: 'DefaultBuildChooser']]],
+ submoduleCfg: [],
+ userRemoteConfigs: [[credentialsId: 'zautomation', url: 'git@github.com:zinfra/zautomation.git']]])
+
+ checkout([$class: 'GitSCM',
+ branches: [[name: '*/${Branch}']],
+ doGenerateSubmoduleConfigurations: true,
+ extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'wire-ios'],
+ [$class: 'SparseCheckoutPaths', sparseCheckoutPaths: [[path: 'wire-ios-automation/ios'], [path: 'wire-ios-automation/tools']]],
+ [$class: 'CheckoutOption', timeout: 30],
+ [$class: 'SubmoduleOption', disableSubmodules: false, parentCredentials: false, recursiveSubmodules: true, reference: '', trackingSubmodules: false, depth: 1, shallow: true],
+ [$class: 'CloneOption', depth: 0, noTags: true, reference: '', shallow: true, timeout: 30],
+ [$class: 'BuildChooserSetting', buildChooser: [$class: 'DefaultBuildChooser']]],
+ submoduleCfg: [],
+ userRemoteConfigs: [[credentialsId: 'zautomation', url: 'git@github.com:wireapp/wire-ios.git']]])
+
+ echo("Installing kubectl if not already installed")
+ kubeCtlSetup = readFile("${WORKSPACE}/common/tests/common/kubectlSetup.sh")
+ sh kubeCtlSetup
+ }
+
+ // TODO: Move this back into the test stage when a solution is found that does not override Wire.ipa in a second parallel run
+ lock("${Grid}") {
+ stage('Download build from S3 to grid nodes') {
+ // Get ipa from S3 depending on track name
+ load('wire-ios/wire-ios-automation/ios/RetrieveIPA.Jenkinsfile.groovy').setIPAFromS3(env.Track, appBuildNumber, is_simulator)
+ echo("OldS3AppPath: ${OldS3AppPath}")
+
+ env.REAL_BUILD_NUMBER = (env.S3AppPath =~ /([0-9]+).ipa/)[0][1]
+ echo("REAL_BUILD_NUMBER: " + env.REAL_BUILD_NUMBER)
+
+ // Work on selenium grid nodes
+ nodes = nodesByLabel(nodeLabels)
+ nodes.each {
+ node(it) {
+ // Cleanup the workspace before getting the ipa ready
+ sh "rm -rf $WORKSPACE/Payload"
+ sh "rm -rf $WORKSPACE/Wire.ipa"
+
+ // TODO: Find a better place for saving the ipa (maybe containing the grid name)
+ env.OLD_APP_PATH = "$WORKSPACE/Previous/Payload/Wire.app"
+ env.APP_PATH = "$WORKSPACE/Payload/Wire.app"
+
+ // Download builds via s3 proxy
+ sh "curl http://192.168.2.39:8000/z-lohika/${S3AppPath} -o \"$WORKSPACE/Wire.ipa\""
+
+ // grab wire.app from the ipa
+ sh "unzip -o $WORKSPACE/Wire.ipa"
+
+ // Gather bundle id from ipa
+ env.BUNDLE_ID = sh returnStdout: true, script: "plutil -extract CFBundleIdentifier raw Payload/Wire.app/Info.plist -o - | tr -d '\\n'"
+ echo("BUNDLE_ID: " + env.BUNDLE_ID)
+
+ echo("Delete old version")
+ sh "rm -rf ${OldS3AppPath}"
+
+ echo("Download old version")
+ if ("${OldS3AppPath}" == "") {
+ echo("No OldS3AppPath given.")
+ } else {
+ // Download old build via s3 proxy
+ sh "curl http://192.168.2.39:8000/z-lohika/${OldS3AppPath} -o \"$WORKSPACE/Previous.ipa\""
+ sh "unzip -o $WORKSPACE/Previous.ipa -d $WORKSPACE/Previous/"
+ }
+
+ // See section "Files generated by test runs" on http://appium.io/docs/en/drivers/ios-xcuitest/
+ // If it fails we shutdown the simulator first and try deleting them again
+ sh returnStatus: true, script: 'rm -rf $HOME/Library/Logs/CoreSimulator/* || xcrun simctl shutdown booted && rm -Rf $HOME/Library/Logs/CoreSimulator/*'
+
+ // Delete cached ipa files to prevent installation error "Failed to load Info.plist from bundle at"
+ sh 'rm -rf /var/folders/* || true'
+
+ // Delete all files used by former history backup tests
+ sh 'find $HOME/Library/Developer/CoreSimulator/Devices/ -name *.ios_wbu -delete'
+ }
+ }
+ }
+
+ // Set build description
+ filename = env.S3AppPath.tokenize('/')[-1]
+ currentBuild.description = filename + "\n" + TAGS
+
+ // Keep build forever when RC is made
+ if (params.TESTINY_RUN_NAME != "") {
+ currentBuild.description = params.TESTINY_RUN_NAME + "\n" + currentBuild.description
+ currentBuild.keepLog = true
+ }
+
+ withMaven(jdk: 'JDK17', maven: 'M3', mavenOpts: '-Xmx1024m', mavenLocalRepo: '.repository', options: [junitPublisher(disabled: true), jacocoPublisher(disabled: true)]) {
+
+ stage('Build common') {
+ echo("Check 1Password Installation")
+ sh "sh $WORKSPACE/common/tests/common/src/main/resources/install1PasswordCLIOnNode.sh"
+
+ echo("Get backend connections...")
+ sh "sh $WORKSPACE/common/tests/common/src/main/resources/backendConnections.sh"
+
+ env.RC_TESTS_COMMENT_PATH = "$WORKSPACE/rc_comment.txt"
+ sh 'echo "${JOB_NAME} ${BUILD_DISPLAY_NAME} - Cucumber Report: ${JENKINS_URL}job/${JOB_NAME}/${BUILD_NUMBER}/cucumber-html-reports/" > $RC_TESTS_COMMENT_PATH'
+
+ sh """
+mvn clean install \\
+-f "$WORKSPACE/common/tests/common/pom.xml" \\
+-DbackendType="${backendType}" \\
+-DbackendConnections="${WORKSPACE}/backendConnections.json" \\
+-DtestinyProjectName="Wire iOS" \\
+-DtestinyRunName="$TESTINY_RUN_NAME" \\
+-DrcTestsCommentPath="${RC_TESTS_COMMENT_PATH}" \\
+-DcallingServiceUrl='loadbalanced' \\
+-Dcom.wire.calling.env='${CALLING_SERVICE_ENV}' \\
+-DsyncIsAutomated=true
+"""
+ }
+
+ // This is needed because of https://issues.jenkins-ci.org/browse/JENKINS-7180
+ def RERUN = currentBuild.getRawBuild().actions.find { it instanceof ParametersAction }?.parameters.find {
+ it.name == 'surefire.rerunFailingTestsCount'
+ }?.value
+
+ // This is needed when we want the tests to only run on a specific device
+ if (!browserName.isEmpty()) {
+ env.MAX_PARALLEL = 1
+ }
+
+ stage('Run tests') {
+ try {
+ timeout(time: 6, unit: 'HOURS') {
+ realtimeJUnit(keepLongStdio: true, testDataPublishers: [[$class: 'JUnitFlakyTestDataPublisher']], testResults: 'wire-ios/wire-ios-automation/ios/target/xml-reports/TEST*.xml') {
+ sh """
+mvn clean integration-test \\
+-f "$WORKSPACE/wire-ios/wire-ios-automation/ios/pom.xml" \\
+-P isOnGrid \\
+-DUrl='${hubUrl}' \\
+-Dpicklejar.parallelism='${MAX_PARALLEL}' \\
+-DisSimulator=${is_simulator} \\
+-Dpicklejar.tags='$TAGS' \\
+-DappPath="${APP_PATH}" \\
+-DoldAppPath="${OLD_APP_PATH}" \\
+-DdeviceName="${deviceName}" \\
+-DplatformVersion='${platformVersion}' \\
+-DrealBuildNumber='${REAL_BUILD_NUMBER}' \\
+-DbundleId='${BUNDLE_ID}' \\
+-Dsurefire.rerunFailingTestsCount=${RERUN} \\
+-Dtest="${params.test}" \\
+-DbrowserName="${browserName}"
+"""
+ }
+ }
+ } catch (e) {
+ print e
+ if (e instanceof hudson.AbortException) {
+ aborted = true
+ }
+ }
+ }
+ }
+
+ } // End of lock
+
+ stage('Generate test results') {
+ try {
+ // Generate Jenkins cucumber HTML reports and archive JSON
+ archiveArtifacts artifacts: '**/target/*report*.json', followSymlinks: false
+ cucumber failedFeaturesNumber: -1, failedScenariosNumber: -1, failedStepsNumber: -1, fileIncludePattern: '**/target/*report*.json', jsonReportDirectory: "${WORKSPACE}/wire-ios/wire-ios-automation/ios/", mergeFeaturesById: true, pendingStepsNumber: -1, skippedStepsNumber: -1, sortingMethod: 'ALPHABETICAL', undefinedStepsNumber: -1
+ } catch (e) {
+ print e
+ if (e instanceof hudson.AbortException) {
+ aborted = true
+ }
+ }
+ try {
+ // Generate zip-able files
+ // Zip and archive cucumber HTML reports
+ node("built-in") {
+ def foldername = env.JOB_NAME
+ def down = "../.."
+ if (foldername.indexOf("/") > -1) {
+ foldername = env.JOB_NAME.replace("/", "/jobs/")
+ down = "../../.."
+ } else {
+ foldername = env.JOB_BASE_NAME
+ }
+ sh returnStatus: true, script: 'rm -rf cucumber-report*.zip'
+ zip archive: true, defaultExcludes: false, dir: "${down}/jobs/${foldername}/builds/${BUILD_NUMBER}/cucumber-html-reports/", overwrite: true, zipFile: "cucumber-report_iOS_build_${REAL_BUILD_NUMBER}.zip"
+ }
+ } catch (e) {
+ print e
+ if (e instanceof hudson.AbortException) {
+ aborted = true
+ }
+ }
+ }
+
+ stage('Report test results') {
+ def testResult = testStatuses()
+ if (isTestSuccessful()) {
+ if (!aborted) {
+ wireSend secret: env.JENKINSBOT_SECRET, message: "✅ **${JOB_NAME} ${BUILD_DISPLAY_NAME}**\n${filename}\n${TAGS}\nSee [JUnit Reports](${BUILD_URL}testReport/) or [Cucumber Reports](${BUILD_URL}cucumber-html-reports)\n${testResult}"
+ } else {
+ wireSend secret: env.JENKINSBOT_SECRET, message: "⚠️ **${JOB_NAME} ${BUILD_DISPLAY_NAME} was aborted**\n${filename}\n${TAGS}\nSee [Console log](${BUILD_URL}console)\n${testResult}"
+ }
+ } else {
+ wireSend secret: env.JENKINSBOT_SECRET, message: "❌ **${JOB_NAME} ${BUILD_DISPLAY_NAME}**\n${filename}\n${TAGS}\nSee [JUnit Reports](${BUILD_URL}testReport/) or [Cucumber Reports](${BUILD_URL}cucumber-html-reports)\n${testResult}"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/Trigger.Jenkinsfile.groovy b/wire-ios-automation/ios/Trigger.Jenkinsfile.groovy
new file mode 100644
index 00000000000..e36ce193d8a
--- /dev/null
+++ b/wire-ios-automation/ios/Trigger.Jenkinsfile.groovy
@@ -0,0 +1,85 @@
+node('built-in') {
+
+ // Select bot credentials for specific Wire conversation based on job name
+ credentialsId = "JENKINSBOT_IOS_SMOKE"
+
+ def jenkinsbot_secret = ""
+ withCredentials([string(credentialsId: "${credentialsId}", variable: 'JENKINSBOT_SECRET')]) {
+ jenkinsbot_secret = env.JENKINSBOT_SECRET
+ }
+
+ def commit_hash = ""
+ def commit_msg = ""
+ stage('Checkout wire-ios-mono') {
+ def scmVars = git branch: "$GIT_BRANCH_IOS", url: 'https://github.com/wireapp/wire-ios-mono'
+ commit_hash = scmVars.GIT_COMMIT
+ commit_msg = sh returnStdout: true, script: 'git log -n 1 --pretty=format:"%an: %s"'
+ commit_msg = "[${commit_msg}](https://github.com/wireapp/wire-ios-mono/commit/${commit_hash})"
+ }
+
+ stage('Check build state') {
+ try {
+ withCredentials([usernameColonPassword(credentialsId: 'GITHUB_API_WEBAPP', variable: 'CREDENTIALS')]) {
+
+ timeout(time: 3, unit: 'MINUTES') {
+ waitUntil {
+ def output = sh label: 'Get runs', returnStdout: true, script: 'curl -L -u ${CREDENTIALS} https://api.github.com/repos/wireapp/wire-ios-mono/actions/workflows/61594641/runs'
+ def json = readJSON text: output
+ if (json['message']) {
+ echo("Output: " + output)
+ error("**Trigger script failed:** " + json['message'])
+ }
+ def runs = json['workflow_runs']
+ echo("Looking for hash " + commit_hash)
+ for (run in runs) {
+ if (run['head_sha'] == commit_hash) {
+ echo("Found hash " + run['head_sha'])
+ echo("status: " + run['status'])
+ // status can be queued, in_progress, or completed
+ if (run['status'] == 'queued' || run['status'] == 'in_progress' || run['status'] == 'completed') {
+ return true
+ }
+ }
+ }
+ sleep(20)
+ return false
+ }
+ }
+
+ timeout(time: 30, unit: 'MINUTES') {
+ waitUntil {
+ def output = sh label: 'Get runs', returnStdout: true, script: 'curl -L -u ${CREDENTIALS} https://api.github.com/repos/wireapp/wire-ios-mono/actions/workflows/61594641/runs'
+ def json = readJSON text: output
+ def runs = json['workflow_runs']
+ echo("Looking for hash " + commit_hash)
+ for (run in runs) {
+ if (run['head_sha'] == commit_hash) {
+ echo("Found hash " + run['head_sha'])
+ echo("status: " + run['status'])
+ echo("conclusion: " + run['conclusion'])
+ // conclusion can be: success, failure, neutral, cancelled, skipped, timed_out, or action_required
+ if (run['conclusion'] == 'success') {
+ return true
+ } else if (run['conclusion'] == 'failure') {
+ error("❌ **Build failed for branch '${GIT_BRANCH_IOS}'** See [Github Actions](" + run['url'] + ")")
+ } else if (run['conclusion'] == 'cancelled') {
+ error("⚠️ **Build aborted for branch '${GIT_BRANCH_IOS}'** See [Github Actions](" + run['url'] + ")")
+ }
+ }
+ }
+ sleep(20)
+ return false;
+ }
+ }
+ }
+ } catch(e) {
+ wireSend secret: "$jenkinsbot_secret", message: "❌ **Github Action issue or took longer than 30 minutes** \n${commit_msg}\nSee https://github.com/wireapp/wire-webapp/branches"
+ error("$e")
+ }
+ wireSend secret: "$jenkinsbot_secret", message: "✅ **Build finished for branch '$GIT_BRANCH_IOS'**\n${commit_msg}"
+ }
+
+ stage('Trigger test job') {
+ build job: "$TEST_JOB", parameters: [string(name: 'TAGS', value: "$TAGS"), string(name: 'GIT_BRANCH', value: "$GIT_BRANCH"), string(name: 'Track', value: "Development"), string(name: 'AppBuildNumber', value: "latest")], wait: false
+ }
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/img.png b/wire-ios-automation/ios/img.png
new file mode 100644
index 00000000000..1f8df74d42a
Binary files /dev/null and b/wire-ios-automation/ios/img.png differ
diff --git a/wire-ios-automation/ios/nbactions.xml b/wire-ios-automation/ios/nbactions.xml
new file mode 100644
index 00000000000..0e4c7688178
--- /dev/null
+++ b/wire-ios-automation/ios/nbactions.xml
@@ -0,0 +1,33 @@
+
+
+
+ test.single
+
+ *
+
+
+ test-compile
+ surefire:test
+
+
+ ${packageClassName}
+ false
+
+
+ iOS
+
+
+
+ rebuild
+
+ *
+
+
+ clean
+ install
+
+
+ iOS
+
+
+
diff --git a/wire-ios-automation/ios/pom.xml b/wire-ios-automation/ios/pom.xml
new file mode 100644
index 00000000000..6a7d149102a
--- /dev/null
+++ b/wire-ios-automation/ios/pom.xml
@@ -0,0 +1,262 @@
+
+ 4.0.0
+
+ com.wearezeta.auto
+ auto-ios
+ 1.0-SNAPSHOT
+ jar
+
+ auto-ios
+ http://maven.apache.org
+
+
+ UTF-8
+
+ 17
+ false
+ DevRun
+ http://127.0.0.1:4723
+
+
+ 16.0
+
+
+ Wire
+ true
+ false
+
+ 10
+ smoketester@wire.com
+ bsmquxrcdverabbp
+ smoketester.android@wire.com
+ esjasabkpuugatao
+ iPhone 11
+ 1
+ 11
+ staging
+ {user.home}/Documents/performance.json
+ false
+ true
+
+
+ 1
+ com.wearezeta.auto.ios.steps
+ com.wearezeta.auto.ios
+ target/
+ report.json
+ https://wire-account-staging.zinfra.io/
+
+ 0
+
+ 0
+
+
+
+
+ central
+ Maven Central Repository
+ https://repo1.maven.org/maven2
+
+
+ github
+ https://maven.pkg.github.com/zinfra/zautomation
+
+ true
+
+
+
+
+
+
+ com.wire.qa
+ qa-common
+ 1.0-SNAPSHOT
+ jar
+
+
+ org.seleniumhq.selenium
+ selenium-java
+
+
+
+
+
+ org.seleniumhq.selenium
+ selenium-java
+ 4.7.0
+
+
+
+ io.appium
+ java-client
+
+ 8.5.0
+
+
+ org.seleniumhq.selenium
+ selenium-api
+
+
+ org.seleniumhq.selenium
+ selenium-remote-driver
+
+
+ org.seleniumhq.selenium
+ selenium-support
+
+
+
+
+
+ com.googlecode.plist
+ dd-plist
+ 1.18
+
+
+
+
+
+
+ src/main/resources
+ true
+
+
+
+ src/main/java
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+ UTF-8
+
+ 17
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.3.0
+
+
+
+ test-jar
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+
+ UTF-8
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.1.0
+
+
+ integration-test
+
+ exec
+
+
+
+
+ java
+ true
+ false
+ ${skipTests}
+ test
+
+
+
+ --add-opens
+ java.base/java.lang=ALL-UNNAMED
+ -classpath
+
+ -DappPath=${appPath}
+ -DoldAppPath=${oldAppPath}
+ -DplatformVersion=${platformVersion}
+ -DrealBuildNumber=${realBuildNumber}
+ -DbundleId=${bundleId}
+ -DisSimulator=${isSimulator}
+ -DenableAppiumOutput=${enableAppiumOutput}
+ -DbrowserName=${browserName}
+ -DdeviceName=${deviceName}
+ -Dpicklejar.tags=${picklejar.tags}
+ -Djunit.jupiter.execution.parallel.enabled=true
+ -Djunit.jupiter.execution.parallel.mode.default=concurrent
+ -Djunit.jupiter.execution.parallel.mode.classes.default=concurrent
+ -Djunit.jupiter.execution.parallel.config.strategy=fixed
+ -Djunit.jupiter.execution.parallel.config.strategy=custom
+
+ -Djunit.jupiter.execution.parallel.config.custom.class=com.wire.qa.picklejar.engine.CustomStrategy
+
+ -Dpicklejar.parallelism=${picklejar.parallelism}
+ -Dmaven.test.failure.ignore=${maven.test.failure.ignore}
+
+
+ -Dtest=${test}
+ -Dsurefire.rerunFailingTestsCount=${surefire.rerunFailingTestsCount}
+ com.wire.qa.picklejar.launcher.PicklejarLauncher
+ ${project.build.directory}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.1.2
+
+ true
+
+
+
+
+
+
+
+ windows
+
+
+ Windows
+
+
+
+ ;${basedir}\target\test-classes
+
+
+
+ Unix
+
+
+ unix
+
+
+
+ :${basedir}/target/test-classes
+
+
+
+ isOnGrid
+
+ http://192.168.2.110:44110/wd/hub
+ true
+
+
+
+
+
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/common/ElementState.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/common/ElementState.java
new file mode 100644
index 00000000000..4971de61032
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/common/ElementState.java
@@ -0,0 +1,84 @@
+package com.wearezeta.auto.ios.common;
+
+import com.wearezeta.auto.common.ImageUtil;
+import com.wearezeta.auto.common.log.ZetaLogger;
+import com.wearezeta.auto.common.misc.Timedelta;
+import java.util.logging.Logger;
+import org.openqa.selenium.StaleElementReferenceException;
+
+import java.awt.image.BufferedImage;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+
+public class ElementState {
+ private static final Logger log = ZetaLogger.getLog(ElementState.class.getSimpleName());
+
+ private static final Timedelta INTERVAL = Timedelta.ofMillis(500);
+
+ private Optional previousScreenshot = Optional.empty();
+ private Supplier screenshotFunction;
+
+ public ElementState(Supplier screenshotFunction) {
+ this.screenshotFunction = screenshotFunction;
+ }
+
+ public ElementState remember() throws Exception {
+ final int maxRetries = 3;
+ int nTry = 0;
+ Exception savedException;
+ do {
+ try {
+ this.previousScreenshot = Optional.of(screenshotFunction.get());
+ return this;
+ } catch (StaleElementReferenceException e) {
+ savedException = e;
+ nTry++;
+ INTERVAL.sleep();
+ }
+ } while (nTry < maxRetries);
+ throw savedException;
+ }
+
+ private boolean checkState(Function checkerFunc, Timedelta timeout) {
+ return checkState(checkerFunc, timeout, ImageUtil.RESIZE_TEMPLATE_TO_REFERENCE_RESOLUTION);
+ }
+
+ private boolean checkState(Function checkerFunc, Timedelta timeout, int resizeMode) {
+ final Timedelta started = Timedelta.now();
+ do {
+ try {
+ final BufferedImage currentState = screenshotFunction.get();
+ final double score = ImageUtil.getOverlapScore(
+ this.previousScreenshot.orElseThrow(
+ () -> new IllegalStateException("Please remember the previous element state first")),
+ currentState, resizeMode);
+ log.fine(String.format("Actual score: %.4f; Time left: %s", score,
+ timeout.sum(started).diff(Timedelta.now()).toString()));
+ if (checkerFunc.apply(score)) {
+ return true;
+ }
+ } catch (StaleElementReferenceException e) {
+ log.fine(String.format("Actual score: ; Time left: %s",
+ timeout.sum(started).diff(Timedelta.now()).toString()));
+ }
+ INTERVAL.sleep();
+ } while (Timedelta.now().isDiffLessOrEqual(started, timeout));
+ return false;
+ }
+
+ public boolean isChanged(Timedelta timeout, double minScore) {
+ log.fine(String.format(
+ "Checking if element state has been changed in %s (Min score: %.4f)...",
+ timeout.toString(), minScore));
+ return checkState((x) -> x < minScore, timeout);
+ }
+
+ public boolean isNotChanged(Timedelta timeout, double minScore) {
+ log.fine(String.format(
+ "Checking if element state has NOT been changed in %s (Min score: %.4f)...",
+ timeout, minScore));
+ return checkState((x) -> x >= minScore, timeout);
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/common/IOSDriverBuilder.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/common/IOSDriverBuilder.java
new file mode 100644
index 00000000000..0cb9cd84b65
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/common/IOSDriverBuilder.java
@@ -0,0 +1,228 @@
+package com.wearezeta.auto.ios.common;
+
+import com.google.common.collect.ImmutableMap;
+import com.wearezeta.auto.common.Config;
+import com.wearezeta.auto.common.log.ZetaLogger;
+import com.wearezeta.auto.common.usrmgmt.ClientUser;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.logging.Logger;
+
+import io.appium.java_client.ios.IOSDriver;
+import org.json.JSONObject;
+import org.openqa.selenium.Capabilities;
+import org.openqa.selenium.MutableCapabilities;
+import org.openqa.selenium.ScreenOrientation;
+import org.openqa.selenium.WebDriver;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Consumer;
+
+public class IOSDriverBuilder {
+
+ private static final Logger log = ZetaLogger.getLog(IOSDriverBuilder.class.getSimpleName());
+
+ private URL hubUrl = null;
+ private Capabilities capabilities = null;
+ private MutableCapabilities extraCapabilities = new MutableCapabilities();
+ private List processArgs = new ArrayList<>();
+ private Consumer logListenerHandler = null;
+ private Boolean fullReset = false;
+ private Boolean allowLocation = true;
+ private Boolean allowTouchID = true;
+ private Boolean allowMicrophoneAccess = false;
+ private Boolean allowCameraAccess = false;
+ private Boolean allowAccessToAllPhotos = false;
+ private Boolean allowNotifications = false;
+ private Boolean uninstallAllOtherVersions = false;
+ private static final String[] bundleIDs = loadBundleIDs();
+
+ public IOSDriverBuilder withCapabilities(Capabilities capabilities) {
+ this.capabilities = capabilities;
+ return this;
+ }
+
+ private static String[] loadBundleIDs() {
+ Path filePath = new File(System.getProperty("user.dir") + "/../ios-automation-assets/bundleIDs").toPath();
+ Charset charset = Charset.defaultCharset();
+ List stringList = null;
+ try {
+ stringList = Files.readAllLines(filePath, charset);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ String[] stringArray = stringList.toArray(new String[]{});
+ return stringArray;
+ }
+
+ public IOSDriverBuilder withProcessArgs(String... args) {
+ for (String arg : args) {
+ processArgs.add(arg);
+ }
+ return this;
+ }
+
+ public IOSDriverBuilder withHub(URL url) {
+ this.hubUrl = url;
+ return this;
+ }
+
+ public IOSDriverBuilder withFastLoginUser(ClientUser fastLoginUser) {
+ if (fastLoginUser != null) {
+ processArgs.addAll(Arrays.asList(
+ // https://wearezeta.atlassian.net/browse/ZIOS-6747
+ "--loginemail=" + fastLoginUser.getEmail(),
+ "--loginpassword=" + fastLoginUser.getPassword()
+ ));
+ }
+ return this;
+ }
+
+ public IOSDriverBuilder withLogListener(Consumer logListenerHandler) {
+ this.logListenerHandler = logListenerHandler;
+ return this;
+ }
+
+ public IOSDriverBuilder withFullReset(boolean fullReset) {
+ this.fullReset = fullReset;
+ return this;
+ }
+
+ public void withMicrophoneAccess() {
+ allowMicrophoneAccess = true;
+ }
+
+ public void withCameraAccess() {
+ allowCameraAccess = true;
+ }
+
+ public void withAccessToAllPhotos() {
+ allowAccessToAllPhotos = true;
+ }
+
+ public void withNotifications() {
+ allowNotifications = true;
+ }
+
+ public void withUninstallingAllVersionsOfWire() {
+ uninstallAllOtherVersions = true;
+ }
+
+ public WebDriver build() {
+ if (hubUrl == null) {
+ throw new RuntimeException("No hub url specified.");
+ }
+
+ if (capabilities == null) {
+ throw new RuntimeException("No capabilities specified.");
+ }
+
+ if (!processArgs.contains("-BackendEnvironmentTypeOverrideKey")) {
+ String backendType = Config.current().getBackendType(this.getClass());
+ processArgs.add("-BackendEnvironmentTypeOverrideKey");
+ if (backendType.equals("qa-column-1")) {
+ // Using staging here because qa-column-1 is configured in the backend bundle for staging
+ // See: https://github.com/wireapp/wire-ios-build-assets/blob/master/BK-CI-configuration/AppStore/Backend.bundle/staging.json
+ processArgs.add("staging");
+ } else if (backendType.equals("qa-column-3")) {
+ // See: https://github.com/wireapp/wire-ios-build-assets/blob/master/COLUMN3-CI-configuration/RC/Backend.bundle/staging.json
+ processArgs.add("staging");
+ } else {
+ processArgs.add(backendType);
+ }
+ }
+
+ // TODO: Find a better way for adding this capability
+ final JSONObject processArguments = new JSONObject();
+ processArguments.put("args", processArgs);
+ // Enable logging for AVS and calling modules by default
+ processArguments.put("env", ImmutableMap.of("ZMLOG_TAGS", "AVS,calling"));
+ extraCapabilities.setCapability("appium:processArguments", processArguments.toString());
+
+ if (fullReset) {
+ // Set full reset option (app is uninstalled) if needed
+ extraCapabilities.setCapability("appium:fullReset", true);
+ // Uninstall app before and after test on real device / Destroy Simulator before and after test
+ // (this needs to be set because after step is skipped if resetOnSessionStartOnly = true)
+ extraCapabilities.setCapability("appium:resetOnSessionStartOnly", false);
+ } else {
+ // reset app but leave simulator running (might not work. see https://github.com/appium/appium/issues/18814)
+ extraCapabilities.setCapability("appium:noReset", false);
+ extraCapabilities.setCapability("appium:fullReset", false);
+ extraCapabilities.setCapability("appium:resetOnSessionStartOnly", true);
+ }
+
+ JSONObject permissions = new JSONObject();
+
+ if (allowLocation) {
+ permissions.put("location", "yes");
+ }
+
+ if (allowMicrophoneAccess) {
+ permissions.put("microphone", "yes");
+ }
+
+ if (allowCameraAccess) {
+ permissions.put("camera", "yes");
+ }
+
+ if (allowAccessToAllPhotos) {
+ permissions.put("photos", "yes");
+ }
+
+ if (allowNotifications) {
+ permissions.put("notifications", "yes");
+ }
+
+ if (allowTouchID) {
+ permissions.put("faceid", "yes");
+ }
+
+ // Use applesimutils for controlling permissions
+ // See https://github.com/wix/AppleSimulatorUtils#usage for permission names
+ if (allowLocation || allowMicrophoneAccess || allowCameraAccess || allowAccessToAllPhotos || allowNotifications) {
+ JSONObject bundles = new JSONObject();
+ bundles.put(Lifecycle.getBundleId(), permissions);
+ extraCapabilities.setCapability("appium:permissions", bundles.toString());
+ }
+
+ // merge all extra capabilites into the capabilites
+ capabilities.merge(extraCapabilities);
+
+ log.info(String.format("Creating webdriver with capabilities: %s", capabilities));
+
+ final IOSDriver iosDriver = new IOSDriver(hubUrl, capabilities);
+
+ iosDriver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
+ iosDriver.setClipboardText("wire");
+
+ if (uninstallAllOtherVersions) {
+ log.info("Remove all version except with bundle id " + Lifecycle.getBundleId());
+ for (String bundleID : bundleIDs) {
+ if (!bundleID.equals(Lifecycle.getBundleId())) {
+ iosDriver.removeApp(bundleID);
+ }
+ }
+ }
+
+ //start iPad testcases in Landscape by default
+ if (Config.current().isTablet(getClass())) {
+ (iosDriver).rotate(
+ ScreenOrientation.LANDSCAPE);
+ }
+
+ String udid = iosDriver.getSessionId().toString();
+ log.info("sessionDetail: " + udid);
+
+ return iosDriver;
+ }
+
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/common/IOSTestContext.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/common/IOSTestContext.java
new file mode 100644
index 00000000000..2e411f70f71
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/common/IOSTestContext.java
@@ -0,0 +1,361 @@
+package com.wearezeta.auto.ios.common;
+
+import com.wearezeta.auto.common.*;
+import com.wearezeta.auto.common.backend.BackendConnections;
+import com.wearezeta.auto.common.calling2.v1.UiCallingStatTracker;
+import com.wearezeta.auto.common.log.LogListener;
+import com.wearezeta.auto.common.log.ZetaLogger;
+import com.wearezeta.auto.common.misc.Timedelta;
+import com.wearezeta.auto.common.usrmgmt.ClientUser;
+import com.wearezeta.auto.common.usrmgmt.ClientUsersManager;
+import com.wire.qa.picklejar.engine.gherkin.model.Scenario;
+import io.appium.java_client.ios.IOSDriver;
+import org.json.JSONObject;
+import org.openqa.selenium.WebDriver;
+
+import java.awt.image.BufferedImage;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.Future;
+import java.util.function.Supplier;
+import java.util.logging.Logger;
+
+public class IOSTestContext extends CommonTestContext {
+
+ private static final Logger log = ZetaLogger.getLog(IOSTestContext.class.getSimpleName());
+
+ private final String testname;
+ private static final String CALLING_STATS = "@call_stats";
+ public static final Timedelta NO_EXPIRATION = Timedelta.ofSeconds(0);
+ private final IOSDriverBuilder driverBuilder;
+
+ private WebDriver driver;
+ private PagesCollection pagesCollection = null;
+ private final Pinger pinger;
+
+ private LogListener deviceLogListener;
+ private UiCallingStatTracker uiCallingStatTracker;
+ private final Scenario scenario;
+ private final Map> EPHEMERAL_TIMEOUTS_MAP = new HashMap<>();
+ private List additionalScreenshots = new ArrayList<>();
+
+ // remember states
+ private ElementState likeIconState = null;
+ private ElementState profilePictureState = null;
+ private ElementState colorPickerState = null;
+ private String currentDeviceId = null;
+ private Future activationMessage = null;
+ private Future verificationMessage = null;
+ private Future accountRemovalConfirmation;
+ private ClientUser userToRegister = null;
+ private String rememberedCertificate = null;
+
+ public IOSTestContext(Scenario scenario, boolean useSpecialEmail, IOSDriverBuilder driverBuilder) {
+ super(useSpecialEmail);
+ this.uiCallingStatTracker = new UiCallingStatTracker();
+ this.scenario = scenario;
+ this.pinger = new Pinger(this);
+ this.testname = scenario.getName();
+ this.driverBuilder = driverBuilder;
+ }
+
+ public IOSDriver getDriver() {
+ if (!isDriverCreated()) {
+ log.info("Driver is not created yet. Using driver builder...");
+ this.driver = driverBuilder.build();
+ }
+ return (IOSDriver) this.driver;
+ }
+
+ public boolean isDriverCreated() {
+ return driver != null;
+ }
+
+ public void doFullReset() {
+ if (!isDriverCreated()) {
+ driverBuilder.withFullReset(true);
+ } else {
+ throw new RuntimeException("Driver was already created. Cannot add capabilities anymore.");
+ }
+ }
+
+ public void startAppOnProductionBackend() {
+ if (!isDriverCreated()) {
+ driverBuilder.withProcessArgs("-BackendEnvironmentTypeOverrideKey", "production");
+ log.info("Starting app on Production backend");
+ } else {
+ throw new RuntimeException("Driver was already created. Cannot add capabilities anymore.");
+ }
+ }
+
+ public void uninstallAllVersionsOfWire() {
+ if (!isDriverCreated()) {
+ driverBuilder.withUninstallingAllVersionsOfWire();
+ } else {
+ throw new RuntimeException("Cannot uninstall other versions of Wire after driver was created.");
+ }
+ }
+
+ public void enrollSimulatorTouchID() {
+ getDriver().toggleTouchIDEnrollment(true);
+ }
+
+ public void setFastLoginUser(ClientUser fastLoginUser) {
+ if (!isDriverCreated()) {
+ driverBuilder.withFastLoginUser(fastLoginUser);
+ } else {
+ throw new RuntimeException("Driver was already created. Cannot add capabilities anymore.");
+ }
+ }
+
+ public void allowMicrophoneAccess() {
+ if (!isDriverCreated()) {
+ driverBuilder.withMicrophoneAccess();
+ } else {
+ throw new RuntimeException("Cannot allow permissions after driver was created.");
+ }
+ }
+
+ public void allowCameraAccess() {
+ if (!isDriverCreated()) {
+ driverBuilder.withCameraAccess();
+ } else {
+ throw new RuntimeException("Cannot allow permissions after driver was created.");
+ }
+ }
+
+ public void allowAccessToAllPhotos() {
+ if (!isDriverCreated()) {
+ driverBuilder.withAccessToAllPhotos();
+ } else {
+ throw new RuntimeException("Cannot allow permissions after driver was created.");
+ }
+ }
+
+ public PagesCollection getPagesCollection() {
+ if (pagesCollection == null) {
+ log.info("Pages collection not initialized yet...");
+ try {
+ // The driver needs to be finally initialized here so that the method isDriverCreated() works in the context
+ getDriver();
+ } catch (Exception e) {
+ log.severe("Driver initialization failed");
+ throw new RuntimeException("Driver initialization failed: " + e.getMessage(), e);
+ }
+ pagesCollection = new PagesCollection(this.driver);
+ }
+ return pagesCollection;
+ }
+
+ public void reset() {
+ // noop
+ }
+
+ public void startPinging() {
+ if(isDriverCreated()) {
+ pinger.startPinging();
+ }
+ }
+
+ public void stopPinging() {
+ if(isDriverCreated()) {
+ pinger.stopPinging();
+ }
+ }
+
+ /**
+ * From MobileTestContext
+ */
+
+ public boolean isRealDevice() {
+ return !Config.current().isSimulator(getClass());
+ }
+
+ public Scenario getScenario() {
+ return scenario;
+ }
+
+ public Optional getLogListener() {
+ return Optional.ofNullable(deviceLogListener);
+ }
+
+ public void setLogListener(LogListener logListener) {
+ this.deviceLogListener = logListener;
+ }
+
+ public UiCallingStatTracker getUiCallingStatTracker() {
+ return uiCallingStatTracker;
+ }
+
+ public boolean isCallingTrackerEnabled() {
+ return scenario.hasTag(CALLING_STATS);
+ }
+
+ public void printCallingStatsIfEnabled() {
+ try {
+ if (isCallingTrackerEnabled()) {
+ UiCallingStatTracker tracker = getUiCallingStatTracker();
+ log.info(String.join("\n", tracker.getSummary()));
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public Timedelta getSelfDeletingMessageTimeout(String userAlias, String conversationName) {
+ final ClientUser user = getUsersManager().findUserByNameOrNameAlias(userAlias);
+
+ // only team users support enforced self-deleting messages
+ if (user.getTeamId() != null) {
+ JSONObject selfDeletingMessagesSettings = BackendConnections.get(user).getSelfDeletingMessagesSettings(user);
+
+ if (selfDeletingMessagesSettings.getString("status").equals("enabled")) {
+ int timeoutInSeconds = selfDeletingMessagesSettings.getJSONObject("config").getInt("enforcedTimeoutSeconds");
+ if (timeoutInSeconds != 0) {
+ // timeout value is enforced in team settings
+ return Timedelta.ofSeconds(timeoutInSeconds);
+ }
+ } else {
+ // timeout is disabled
+ return NO_EXPIRATION;
+ }
+ }
+
+ // Personal user or team user without set enforced self-deleting message setting
+
+ // follow conversation settings if there is any
+ conversationName = getUsersManager().replaceAliasesOccurrences(conversationName,
+ ClientUsersManager.FindBy.NAME_ALIAS);
+ int conversationMessageTimer = getCommonSteps().getConversationMessageTimer(user, conversationName);
+ if (conversationMessageTimer > 0) {
+ return Timedelta.ofMillis(conversationMessageTimer);
+ }
+
+ // otherwise check for local/client-side self-deleting message timeout
+ return getLocalSelfDeletingMessageTimeout(userAlias, conversationName);
+ }
+
+ private Timedelta getLocalSelfDeletingMessageTimeout(String userAlias, String conversationName) {
+ final ClientUser user = getUsersManager().findUserByNameOrNameAlias(userAlias);
+ conversationName = getUsersManager().replaceAliasesOccurrences(conversationName,
+ ClientUsersManager.FindBy.NAME_ALIAS);
+ final String conversationId = BackendConnections.get(user).getConversationByName(user, conversationName).getId();
+ if (EPHEMERAL_TIMEOUTS_MAP.containsKey(user) && EPHEMERAL_TIMEOUTS_MAP.get(user).containsKey(conversationId)) {
+ return EPHEMERAL_TIMEOUTS_MAP.get(user).get(conversationId);
+ }
+ return NO_EXPIRATION;
+ }
+
+ /**
+ * From TestContext
+ */
+
+ public void addAdditionalScreenshots(BufferedImage screenshot) {
+ additionalScreenshots.add(screenshot);
+ }
+
+ public void enableFederation() {
+ if (!isDriverCreated()) {
+ driverBuilder.withProcessArgs("-FederationEnabled", "1");
+ log.info("Starting app with Federation enabled");
+ } else {
+ throw new RuntimeException("Driver was already created. Cannot add federation enabled capabilities anymore.");
+ }
+ }
+
+ public void enableApiVersioning(int version) {
+ if (!isDriverCreated()) {
+ driverBuilder.withProcessArgs(String.format("--preferred-api-version=%s", version));
+ log.info(String.format("Starting app with Api versioning %s enabled", version));
+ } else {
+ throw new RuntimeException("Driver was already created. Cannot add federation enabled capabilities anymore.");
+ }
+ }
+
+ public void enableMLSSupport() {
+ if (!isDriverCreated()) {
+ driverBuilder.withProcessArgs("--enable-mls-support", "1");
+ log.info("Starting app with mls support enabled");
+ } else {
+ throw new RuntimeException("Driver was already created. Cannot add federation enabled capabilities anymore.");
+ }
+ }
+
+ public ElementState getLikeIconState() {
+ return likeIconState;
+ }
+
+ public void setLikeIconState(Supplier screenshotFunction) throws Exception {
+ this.likeIconState = new ElementState(screenshotFunction);
+ likeIconState.remember();
+ }
+
+ public ElementState getProfilePictureState() {
+ return profilePictureState;
+ }
+
+ public void setProfilePictureState(Supplier screenshotFunction) throws Exception {
+ this.profilePictureState = new ElementState(screenshotFunction);
+ this.profilePictureState.remember();
+ }
+
+ public ElementState getColorPickerState() {
+ return colorPickerState;
+ }
+
+ public void setColorPickerState(Supplier screenshotFunction) throws Exception {
+ this.colorPickerState = new ElementState(screenshotFunction);
+ this.colorPickerState.remember();
+ }
+
+ public String getCurrentDeviceId() {
+ return this.currentDeviceId;
+ }
+
+ public void setCurrentDeviceId(String deviceId) {
+ this.currentDeviceId = deviceId;
+ }
+
+ public Future getActivationMessage() {
+ return activationMessage;
+ }
+
+ public Future getVerificationMessage() {
+ return verificationMessage;
+ }
+
+ public void setActivationMessage(Future message) {
+ this.activationMessage = message;
+ }
+
+ public void setVerificationMessage(Future message) {
+ this.verificationMessage = message;
+ }
+
+ public Future getAccountRemovalConfirmation() {
+ return accountRemovalConfirmation;
+ }
+
+ public void setAccountRemovalConfirmation(Future message) {
+ this.accountRemovalConfirmation = message;
+ }
+
+ public ClientUser getUserToRegister() {
+ return userToRegister;
+ }
+
+ public void setUserToRegister(ClientUser user) {
+ this.userToRegister = user;
+ }
+
+ public String getRememberedCertificate() {
+ return rememberedCertificate;
+ }
+
+ public void setRememberedCertificate(String remembered) {
+ this.rememberedCertificate = remembered;
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/common/IPAInspector.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/common/IPAInspector.java
new file mode 100644
index 00000000000..b263b6413cd
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/common/IPAInspector.java
@@ -0,0 +1,62 @@
+package com.wearezeta.auto.ios.common;
+
+import com.dd.plist.NSDictionary;
+import com.dd.plist.PropertyListParser;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+public class IPAInspector {
+
+ private byte[] plist;
+ private String bundleId;
+
+ public IPAInspector(String path) {
+ final File fPath = new File(path);
+ if (path.toLowerCase().endsWith(".app")) {
+ // no need to extract. Just load Info.plist as byte[]
+ final File plistFile = new File(fPath.getAbsolutePath(), "Info.plist");
+ try {
+ plist = Files.readAllBytes(plistFile.toPath());
+ } catch (IOException e) {
+ throw new RuntimeException("Could not read " + fPath.getAbsolutePath(), e);
+ }
+ } else if (path.toLowerCase().endsWith(".ipa")) {
+ // extract Info.plist from .ipa
+ try {
+ ZipFile zipFile = new ZipFile(fPath);
+ ZipEntry zipEntry = zipFile.getEntry("Payload/Wire.app/Info.plist");
+ InputStream is = zipFile.getInputStream(zipEntry);
+ plist = new byte[is.available()];
+ is.read(plist);
+ is.close();
+ } catch (Exception e) {
+ throw new RuntimeException("Could not extract Info.plist from " + fPath.getAbsolutePath(), e);
+ }
+ } else {
+ throw new IllegalArgumentException(
+ String.format("Only .ipa and .app packages are supported. %s is given instead", path));
+ }
+ }
+
+ public String getBundleId() {
+ if (bundleId == null) {
+ bundleId = parseProperty(plist, "CFBundleIdentifier");
+ }
+ return this.bundleId;
+ }
+
+ private static String parseProperty(byte[] plist, String propertyName) {
+ final NSDictionary rootDict;
+ try {
+ rootDict = (NSDictionary) PropertyListParser.parse(plist);
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ return rootDict.objectForKey(propertyName).toString();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/common/Lifecycle.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/common/Lifecycle.java
new file mode 100644
index 00000000000..d22f65710e3
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/common/Lifecycle.java
@@ -0,0 +1,407 @@
+package com.wearezeta.auto.ios.common;
+
+import com.wearezeta.auto.common.Config;
+import com.wearezeta.auto.common.Platform;
+import com.wearezeta.auto.common.TestScreenshotHelper;
+import com.wearezeta.auto.common.calling2.v1.UiCallingStatTracker;
+import com.wearezeta.auto.common.driver.AppiumLocalServer;
+import com.wearezeta.auto.common.log.LogListener;
+import com.wearezeta.auto.common.log.LogsConverter;
+import com.wearezeta.auto.common.log.ZetaLogger;
+import com.wearezeta.auto.common.misc.Timedelta;
+import com.wearezeta.auto.common.misc.WireBlacklist;
+import com.wearezeta.auto.common.testiny.ScenarioResultToTestinyTransformer;
+import com.wearezeta.auto.common.testiny.TestinySync;
+import com.wearezeta.auto.ios.pages.IOSPage;
+import com.wire.qa.picklejar.engine.TestContext;
+import com.wire.qa.picklejar.engine.annotations.AfterEachScenario;
+import com.wire.qa.picklejar.engine.annotations.AfterEachStep;
+import com.wire.qa.picklejar.engine.annotations.BeforeEachScenario;
+import com.wire.qa.picklejar.engine.annotations.BeforeEachStep;
+import com.wire.qa.picklejar.engine.gherkin.model.Embeddings;
+import com.wire.qa.picklejar.engine.gherkin.model.Scenario;
+import com.wire.qa.picklejar.engine.gherkin.model.Step;
+import org.openqa.selenium.OutputType;
+import org.openqa.selenium.logging.LogEntry;
+import org.openqa.selenium.remote.DesiredCapabilities;
+
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.time.Duration;
+import java.util.*;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+import static com.wearezeta.auto.common.CommonUtils.isRunningOnJenkinsNode;
+
+public class Lifecycle {
+
+ private static final Duration DEFAULT_COMMAND_TIMEOUT = Duration.ofMinutes(10);
+
+ private static Logger log = ZetaLogger.getLog(Lifecycle.class.getSimpleName());
+
+ private static final String TAG_NAME_BLACKLIST = "@blacklist";
+ private static final String SSO = "@sso";
+ private static final String SCIM = "@scim";
+
+ private static final Platform CURRENT_PLATFORM = Platform.iOS;
+
+ private final TestScreenshotHelper screenshotHelper = new TestScreenshotHelper();
+
+ private static final int MAX_SCREENSHOT_WIDTH = 800;
+ private static final int MAX_SCREENSHOT_HEIGHT = 400;
+
+ private static boolean isOnGrid() {
+ return Config.current().isOnGrid(Lifecycle.class);
+ }
+
+ private static String getUrl() {
+ return Config.current().getAppiumUrl(Lifecycle.class);
+ }
+
+ public static String getAppPath() {
+ return Config.current().getIosApplicationPath(Lifecycle.class);
+ }
+
+ public static String getOldAppPath() {
+ return Config.current().getOldAppPath(Lifecycle.class);
+ }
+
+ public static String getBundleId() {
+ if (isRunningOnJenkinsNode() && isOnGrid()) {
+ return Config.current().getBundleId(Lifecycle.class);
+ } else {
+ return new IPAInspector(getAppPath()).getBundleId();
+ }
+ }
+
+ private static String getAppName() {
+ return Config.current().getIOSAppName(Lifecycle.class);
+ }
+
+ @BeforeEachScenario
+ public TestContext setup(Scenario scenario) throws Exception {
+
+ IOSDriverBuilder driverBuilder = new IOSDriverBuilder();
+
+ boolean useSpecialEmail = false;
+
+ if (scenario.hasTag("useSpecialEmail")) {
+ useSpecialEmail = true;
+ }
+
+ // The appPath is the path to the ipa on the node where the simulator runs
+ String appPath = getAppPath();
+
+ // Start appium server if this test is run manually and appium not already running (for e.g. via appium-desktop)
+ if (!isOnGrid() && !AppiumLocalServer.isRunning()) {
+ AppiumLocalServer.start();
+ }
+
+ // TODO: Can be removed once we migrated to the new grid
+ if (isRunningOnJenkinsNode() && !isOnGrid()) {
+ // Add hostname to scenario description (yellow line)
+ InetAddress ip = InetAddress.getLocalHost();
+ String hostname = ip.getHostName();
+ scenario.setDescription(hostname + ": ");
+ }
+
+ final boolean isRealDevice = !Config.current().isSimulator(getClass());
+
+ final DesiredCapabilities capabilities = new DesiredCapabilities();
+ capabilities.setCapability("newCommandTimeout", DEFAULT_COMMAND_TIMEOUT.getSeconds());
+ capabilities.setCapability("app", appPath);
+ // Only set the platformVersion if you have multiple sim runtimes installed
+ //capabilities.setCapability("appium:platformVersion", getPlatformVersion());
+
+ String bundleId = getBundleId();
+ log.info("bundleId: " + bundleId);
+ capabilities.setCapability("appium:automationName", "XCUITest");
+ capabilities.setCapability("appium:bundleId", bundleId);
+ capabilities.setCapability("appium:autoLaunch", true);
+ capabilities.setCapability("appium:clearSystemFiles", true);
+ capabilities.setCapability("appium:appName", getAppName());
+ capabilities.setCapability("appium:language", "en");
+ capabilities.setCapability("appium:locale", "en_US");
+
+ // Increase the default timeout to start up a simulator from 120s to 240s
+ capabilities.setCapability("appium:simulatorStartupTimeout", 240000);
+
+ //Temporary enable full XCode Log
+ capabilities.setCapability("appium:showXcodeLog", true);
+
+ if (!isOnGrid()) {
+ // If deviceName is set appium tries to match the first available simulator/device with the given name
+ capabilities.setCapability("deviceName",
+ Config.current().getDeviceName(this.getClass()));
+ }
+
+ if (isRealDevice && !isOnGrid()) {
+ capabilities.setCapability("appium:udid", RealDevice.getUDID());
+ }
+
+ // Disable update notifications from Hockey
+ driverBuilder.withProcessArgs("-use-app-center", "0");
+
+ driverBuilder.withProcessArgs("--disable-interactive-keyboard-dismissal");
+
+ driverBuilder.withProcessArgs("--disable-call-quality-survey");
+
+ driverBuilder.withProcessArgs("--persist-backend-type");
+
+ // https://wearezeta.atlassian.net/browse/ZIOS-5769
+ driverBuilder.withProcessArgs("--disable-autocorrection");
+
+ driverBuilder.withProcessArgs("--debug-log=Network,SessionManager,event-processing,SyncStatus,OperationStatus,"
+ + "Push,cryptobox,background-activity,ephemeral,Authentication");
+
+ driverBuilder.withProcessArgs("-com.apple.CoreData.ConcurrencyDebug", "1");
+
+ if (!scenario.hasTag("@notifications")) {
+ driverBuilder.withProcessArgs("--disable-push-alert");
+ }
+
+ driverBuilder.withProcessArgs("-UseAnalytics", "0");
+
+ String url = getUrl();
+ log.info("URL: " + url);
+
+ final URL serverAddress;
+ try {
+ serverAddress = new URL(url);
+ } catch (MalformedURLException e) {
+ throw new IllegalStateException(e);
+ }
+
+ driverBuilder.withHub(serverAddress);
+
+ driverBuilder.withCapabilities(capabilities);
+
+ final LogListener logListener = new LogListener();
+ driverBuilder.withLogListener(logListener::addLogMessage);
+
+ IOSTestContext context = new IOSTestContext(scenario, useSpecialEmail, driverBuilder);
+ context.setLogListener(logListener);
+ return context;
+ }
+
+ @BeforeEachStep
+ public void beforeEachStep(IOSTestContext context, Scenario scenario, Step step) {
+
+ }
+
+ @AfterEachStep
+ public void afterEachStep(IOSTestContext context, Scenario scenario, Step step) {
+ // Make screenshot
+ try {
+ if (context.isDriverCreated() && !"SKIPPED".equalsIgnoreCase(step.getResult().getStatus())) {
+ byte[] screenshot = context.getDriver().getScreenshotAs(OutputType.BYTES);
+ // Jenkins 1: screenshotHelper.saveScreenshot(step, scenario, scenario.getCurrentFeatureName(), screenshot);
+ Embeddings embedding = new Embeddings(screenshotHelper.encodeToBase64(screenshot, MAX_SCREENSHOT_WIDTH, MAX_SCREENSHOT_HEIGHT), "image/jpeg");
+ step.addEmbedding(embedding);
+ }
+ } catch (Exception e) {
+ log.warning("Could not make a screenshot: " + e.getMessage());
+ }
+
+ // Attach logs
+ try {
+ if (context.isDriverCreated() && step.getResult().getErrorMessage() != null && !step.getResult().getErrorMessage().isEmpty()) {
+
+ log.info("Get logs...");
+
+ IOSPage page = context.getPagesCollection().getPage(IOSPage.class);
+
+ final Map> attachmentLogsData = new LinkedHashMap<>();
+ final List applicationLogs = page.getWireLogs();
+ final List deviceLogs = context.getLogListener()
+ .map(LogListener::getLogs)
+ .orElse(Collections.emptyList());
+ final List crashLogs = page.getCrashLogs().getAll()
+ .stream()
+ .map(LogEntry::getMessage)
+ .collect(Collectors.toList());
+ final List serverLogs = page.getAppiumLogs().getAll()
+ .stream()
+ .map(LogEntry::getMessage)
+ .collect(Collectors.toList());
+ attachmentLogsData.put("crash.log", crashLogs);
+ attachmentLogsData.put("appium.log", serverLogs);
+ attachmentLogsData.put("system.txt", deviceLogs);
+ attachmentLogsData.put("application.txt", applicationLogs);
+
+ if (context.isCallingTrackerEnabled()) {
+ UiCallingStatTracker tracker = context.getUiCallingStatTracker();
+ attachmentLogsData.put("callingStats.log", tracker.getSummary());
+ }
+
+ if (!attachmentLogsData.isEmpty()) {
+ Embeddings embedding = new Embeddings(
+ LogsConverter.toZipBytearray(attachmentLogsData),
+ LogsConverter.ZIP_MIME_TYPE);
+ step.addEmbedding(embedding);
+ }
+ }
+ } catch (Exception e) {
+ log.warning("Could not attach logs: " + e.getMessage());
+ }
+ }
+
+ @AfterEachScenario
+ public void tearDown(IOSTestContext context, Scenario scenario) {
+ try {
+ TestinySync.syncExecutedScenarioWithTestiny(scenario.getName(),
+ new ScenarioResultToTestinyTransformer(scenario).transform(),
+ scenario.getTags());
+ } catch (Exception e) {
+ log.warning(e.getMessage());
+ }
+
+ try {
+ if (context != null && context.isDriverCreated()) {
+ if (context.isRealDevice()) {
+ context.getDriver().switchTo().alert().accept();
+ } else {
+ context.getPagesCollection().getPage(IOSPage.class).acceptAlert(Timedelta.ofSeconds(1));
+ }
+ }
+ } catch (Exception e) {
+ log.warning(e.getMessage());
+ }
+
+ if (scenario.hasTag(SSO) || scenario.hasTag(SCIM)) {
+ try {
+ if (context != null && context.isDriverCreated()) {
+ context.getPagesCollection().getPage(IOSPage.class).setClipboard(" ");
+ }
+ } catch (Exception e) {
+ log.warning("Could not empty clipboard: " + e.getMessage());
+ }
+ }
+
+ try {
+ if (!scenario.hasTag("maintenance")) {
+ log.fine("Delete application in okta");
+ context.getCommonSteps().cleanUpOkta();
+ }
+ } catch (Exception e) {
+ log.warning(e.getMessage());
+ }
+ try {
+ if (scenario.hasTag(TAG_NAME_BLACKLIST)) {
+ WireBlacklist.uploadDefault(CURRENT_PLATFORM);
+ }
+ if (scenario.hasTag("notifications")) {
+ if (context != null && context.isDriverCreated()) {
+ context.getPagesCollection().getPage(IOSPage.class).pressHomeButton();
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ // Driver
+ try {
+ log.fine("Closing webdriver");
+ if (context != null && context.isDriverCreated()) {
+ context.getDriver().quit();
+ }
+ } catch (Exception e) {
+ log.warning("Closing webdriver failed: " + e.getMessage());
+ e.printStackTrace();
+ }
+ // Federation
+ try {
+ for (String fromBackend: context.getCommonSteps().defederatedBackends.keySet()) {
+ log.info("Repair defederation");
+ context.getCommonSteps().federateBackends(fromBackend,
+ context.getCommonSteps().defederatedBackends.get(fromBackend));
+ }
+ } catch (Exception e) {
+ log.warning(e.getMessage());
+ }
+ try {
+ for (String backendName: context.getCommonSteps().touchedFederator) {
+ log.info("Turn federator back on");
+ context.getCommonSteps().turnFederatorInBackendOn(backendName);
+ }
+ } catch (Exception e) {
+ log.warning(e.getMessage());
+ }
+ try {
+ for (String backendName: context.getCommonSteps().touchedBrig) {
+ log.info("Turn brig back on");
+ context.getCommonSteps().turnBrigInBackendOn(backendName);
+ }
+ } catch (Exception e) {
+ log.warning(e.getMessage());
+ }
+ try {
+ for (String backendName: context.getCommonSteps().touchedGalley) {
+ log.info("Turn galley back on");
+ context.getCommonSteps().turnGalleyInBackendOn(backendName);
+ }
+ } catch (Exception e) {
+ log.warning(e.getMessage());
+ }
+ try {
+ for (String backendName: context.getCommonSteps().touchedIngress) {
+ log.info("Turn ingress back on");
+ context.getCommonSteps().turnIngressInBackendOn(backendName);
+ }
+ } catch (Exception e) {
+ log.warning(e.getMessage());
+ }
+ try {
+ for (String backendName: context.getCommonSteps().touchedSFT) {
+ log.info("Turn SFT back on");
+ context.getCommonSteps().turnSFTInBackendOn(backendName);
+ }
+ } catch (Exception e) {
+ log.warning(e.getMessage());
+ }
+ // Local appium
+ try {
+ if (!isOnGrid()) {
+ log.fine("Stopping appium");
+ AppiumLocalServer.stop();
+ }
+ } catch (Exception e) {
+ log.warning("Stopping appium failed: " + e.getMessage());
+ }
+
+ try {
+ if (!isOnGrid()) {
+ // TODO: This breaks parallel runs and ends in a loop
+ context.printCallingStatsIfEnabled();
+ }
+ if (context != null) {
+ context.reset();
+ }
+ } catch (Exception e) {
+ log.severe("Could not reset context: " + e.getMessage());
+ }
+
+ try {
+ log.fine("Releasing Testservice instances");
+ context.getCommonSteps().cleanUpTestServiceInstances();
+ } catch (Exception e) {
+ log.warning(e.getMessage());
+ }
+ try {
+ log.fine("Cleaning up calling instances");
+ context.getCallingManager().cleanup();
+ } catch (Exception e) {
+ log.warning(e.getMessage());
+ }
+ try {
+ if (!scenario.hasTag("maintenance")) {
+ log.fine("Cleanup backends");
+ context.getCommonSteps().cleanUpBackends();
+ }
+ } catch (Exception e) {
+ log.warning(e.getMessage());
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/common/PagesCollection.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/common/PagesCollection.java
new file mode 100644
index 00000000000..3578a3aa7c6
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/common/PagesCollection.java
@@ -0,0 +1,32 @@
+package com.wearezeta.auto.ios.common;
+
+import com.wearezeta.auto.common.log.ZetaLogger;
+import io.appium.java_client.pagefactory.AppiumFieldDecorator;
+import java.util.logging.Logger;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.support.PageFactory;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+
+public class PagesCollection {
+ private final Logger log = ZetaLogger.getLog(this.getClass().getSimpleName());
+
+ private final WebDriver driver;
+
+ public PagesCollection(WebDriver driver) {
+ this.driver = driver;
+ }
+
+ public T getPage(Class pageClass) {
+ log.info("Page: " + pageClass.getSimpleName());
+ try {
+ final Constructor> constructor = pageClass.getConstructor(WebDriver.class);
+ T instance = pageClass.cast(constructor.newInstance(this.driver));
+ PageFactory.initElements(new AppiumFieldDecorator(this.driver), instance);
+ return instance;
+ } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException("Could not instantiate page " + pageClass, e);
+ }
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/common/Pinger.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/common/Pinger.java
new file mode 100644
index 00000000000..e2bc6ece595
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/common/Pinger.java
@@ -0,0 +1,56 @@
+package com.wearezeta.auto.ios.common;
+
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import com.wearezeta.auto.common.log.ZetaLogger;
+import java.util.logging.Logger;
+
+public class Pinger {
+
+ public static final Logger log = ZetaLogger.getLog(Pinger.class.getSimpleName());
+
+ // Should not be higher than session timeout on selenium grid
+ private static final int PINGER_POLLING_PERIOD = 30;
+
+ private final ScheduledThreadPoolExecutor PING_EXECUTOR = new ScheduledThreadPoolExecutor(1);
+ private ScheduledFuture> RUNNING_PINGER;
+ private IOSTestContext context;
+ private final Runnable PINGER = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ log.fine("Pinging driver");
+ context.getDriver().manage().window().getSize();
+ } catch (Exception ex) {
+ log.warning(String.format("Could not ping driver: %s", ex.getMessage()));
+ }
+ }
+ };
+
+ public Pinger(IOSTestContext context) {
+ PING_EXECUTOR.setRemoveOnCancelPolicy(true);
+ this.context = context;
+ }
+
+ public void startPinging() {
+ if (RUNNING_PINGER == null) {
+ log.info("Scheduling pinger task");
+ RUNNING_PINGER = PING_EXECUTOR.scheduleAtFixedRate(PINGER, 0, PINGER_POLLING_PERIOD, TimeUnit.SECONDS);
+ } else {
+ log.warning("Driver pinger is already running - Please stop the driver pinger before starting it again");
+ }
+ }
+
+ public void stopPinging() {
+ if (RUNNING_PINGER != null) {
+ if (!RUNNING_PINGER.cancel(true)) {
+ log.warning("Could not stop driver pinger");
+ }
+ RUNNING_PINGER = null;
+ } else {
+ log.warning("No pinger to stop");
+ }
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/common/RealDevice.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/common/RealDevice.java
new file mode 100644
index 00000000000..c35aaa2047a
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/common/RealDevice.java
@@ -0,0 +1,49 @@
+package com.wearezeta.auto.ios.common;
+
+import com.wearezeta.auto.common.CommonUtils;
+import com.wearezeta.auto.common.Config;
+
+import java.util.*;
+
+public class RealDevice {
+ private static Optional udid = Optional.empty();
+
+ private static RealDevice instance = null;
+
+ public static synchronized RealDevice getInstance() {
+ if (instance == null) {
+ instance = new RealDevice();
+ }
+ return instance;
+ }
+
+ public static String getUDID() {
+ if(!udid.isPresent()) {
+ udid = Config.current().getUDID(RealDevice.class);
+ }
+
+ if (!udid.isPresent()) {
+ for (String deviceName : new String[]{"iPhone", "iPad"}) {
+ final String result;
+ try {
+ result = CommonUtils.executeOsXCommandWithOutput(new String[]{
+ "/bin/bash",
+ "-c",
+ "system_profiler SPUSBDataType | sed -n '/"
+ + deviceName
+ + "/,/Serial/p' | grep 'Serial Number:' | awk -F ': ' '{print $2}'"}).trim();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ if (result.length() > 0) {
+ udid = Optional.of(result);
+ break;
+ }
+ }
+ }
+ return udid.orElseThrow(
+ () -> new IllegalStateException("No connected iOS devices can be detected")
+ );
+ }
+
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ActionsSheetPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ActionsSheetPage.java
new file mode 100644
index 00000000000..80eb9678d87
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ActionsSheetPage.java
@@ -0,0 +1,60 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import java.util.function.Function;
+
+public class ActionsSheetPage extends IOSPage {
+
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeSheet/**/XCUIElementTypeButton[`visible == 1 AND label != ''`][-1]")
+ private WebElement declineActionButton;
+
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeButton[`visible == 1 AND label != ''`]")
+ private WebElement confirmActionButton;
+
+ private static final Function predicateNewActionButtonByName = text ->
+ MobileBy.iOSNsPredicateString(String.format("type == 'XCUIElementTypeButton' AND label == '%s'", text));
+
+ private static final Function predicateActionSheetLabelByText = text ->
+ String.format("type == 'XCUIElementTypeStaticText' AND label CONTAINS '%s'", text);
+
+ public ActionsSheetPage(WebDriver driver) {
+ super(driver);
+ }
+
+ private By getButtonLocatorByName(String name) {
+ return predicateNewActionButtonByName.apply(name);
+ }
+
+ public void tapMenuItem(String name) {
+ getDriver().findElement(getButtonLocatorByName(name)).click();
+ }
+
+ public boolean isItemVisible(String name) {
+ return getDriver().findElement(getButtonLocatorByName(name)).isDisplayed();
+ }
+
+ public boolean isItemInvisible(String name) {
+ return isLocatorInvisible(getButtonLocatorByName(name));
+ }
+
+ public boolean isActionSheetContainsText(String text) {
+ return getDriver().findElement(MobileBy.iOSNsPredicateString(predicateActionSheetLabelByText.apply(text)))
+ .isDisplayed();
+ }
+
+ public boolean isActionSheetDoesNotContainsText(String text) {
+ return isLocatorInvisible(MobileBy.iOSNsPredicateString(predicateActionSheetLabelByText.apply(text)));
+ }
+
+ public void confirm() {
+ confirmActionButton.click();
+ }
+
+ public void decline() {
+ declineActionButton.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ArchivePage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ArchivePage.java
new file mode 100644
index 00000000000..cac2ea889aa
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ArchivePage.java
@@ -0,0 +1,43 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import com.wearezeta.auto.common.misc.Timedelta;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import java.util.function.Function;
+
+public class ArchivePage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "archiveCloseButton")
+ private WebElement closeArchiveButton;
+
+ private static final String strNameConversation = "title";
+
+ private static final Function predicateStrConversationByLabel = text ->
+ MobileBy.iOSNsPredicateString(String.format("name == '%s' AND label == '%s'", strNameConversation, text));
+
+ public ArchivePage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void clickCloseArchivePageButton() {
+ closeArchiveButton.click();
+ }
+
+ public boolean isConversationInList(String name) {
+ final By locator = predicateStrConversationByLabel.apply(name);
+ return isLocatorDisplayed(locator, Timedelta.ofSeconds(5));
+ }
+
+ public boolean isConversationNotInList(String name) {
+ final By locator = predicateStrConversationByLabel.apply(name);
+ return isLocatorInvisible(locator, Timedelta.ofSeconds(5));
+ }
+
+ public void tapConversationsListItem(String name) {
+ final By locator = predicateStrConversationByLabel.apply(name);
+ getElement(locator).click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/BackupPasswordOverlayPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/BackupPasswordOverlayPage.java
new file mode 100644
index 00000000000..4ab584ee81e
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/BackupPasswordOverlayPage.java
@@ -0,0 +1,27 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class BackupPasswordOverlayPage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "password input")
+ private WebElement passwordInput;
+
+ @iOSXCUITFindBy(accessibility = "Next")
+ private WebElement nextButton;
+
+ public BackupPasswordOverlayPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void typePassword(String password) {
+ passwordInput.clear();
+ passwordInput.sendKeys(password);
+ }
+
+ public void tapNextButton() {
+ nextButton.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/BottomNavigationBarPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/BottomNavigationBarPage.java
new file mode 100644
index 00000000000..fe101741c4b
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/BottomNavigationBarPage.java
@@ -0,0 +1,51 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class BottomNavigationBarPage extends IOSPage {
+
+
+ @iOSXCUITFindBy(accessibility = "bottomBarRecentListButton")
+ private WebElement recentConversationsButton;
+
+ /**
+ @iOSXCUITFindBy(accessibility = "bottomBarFolderListButton")
+ private WebElement folderButton;
+*/
+ @iOSXCUITFindBy(accessibility = "bottomBarArchivedButton")
+ private WebElement openArchiveButton;
+
+ @iOSXCUITFindBy(accessibility = "gearshape")
+ WebElement settingsButton;
+
+ public BottomNavigationBarPage(WebDriver driver) {
+ super(driver);
+ }
+
+
+ public void openArchivedConversations() {
+ tapElementWithRetryIfStillDisplayed(openArchiveButton);
+ }
+
+ public boolean isArchiveButtonVisible() {
+ return openArchiveButton.isDisplayed();
+ }
+
+ public boolean isArchiveButtonInvisible() {
+ return isElementInvisible(openArchiveButton);
+ }
+
+ /** public void tapGroupedConversationsButton() {
+ folderButton.click();
+ }
+*/
+ public void tapRecentConversationsButton() {
+ recentConversationsButton.click();
+ }
+
+ public void tapSettingsButton() {
+ settingsButton.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/CameraPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/CameraPage.java
new file mode 100644
index 00000000000..116aa3aa78d
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/CameraPage.java
@@ -0,0 +1,48 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class CameraPage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "Take Photo")
+ private WebElement takePhotoButton;
+
+ @iOSXCUITFindBy(accessibility = "VideoCapture")
+ private WebElement takeVideoButton;
+
+ @iOSXCUITFindBy(accessibility = "Use Video")
+ private WebElement useVideoButton;
+
+ @iOSXCUITFindBy(accessibility = "Choose from Library")
+ private WebElement chooseFromLibrary;
+
+ public CameraPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void tapTakePhoto() {
+ takePhotoButton.click();
+ }
+
+ public void tapTakeVideo() {
+ takeVideoButton.click();
+ }
+
+ public void tapUseVideo() {
+ useVideoButton.click();
+ }
+
+ public boolean isTakePhotoButtonVisible() {
+ return takePhotoButton.isDisplayed();
+ }
+
+ public boolean isChooseFromLibraryVisible() {
+ return chooseFromLibrary.isDisplayed();
+ }
+
+ public void tapChooseFromLibrary() {
+ chooseFromLibrary.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/CameraRollPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/CameraRollPage.java
new file mode 100644
index 00000000000..bce6b1ebefd
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/CameraRollPage.java
@@ -0,0 +1,26 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class CameraRollPage extends IOSPage {
+
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeScrollView/**/XCUIElementTypeImage[`label BEGINSWITH 'Photo'`][-1]")
+ private WebElement cameraRollPicture;
+
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeScrollView/**/XCUIElementTypeImage[`label BEGINSWITH 'Photo'`]")
+ private WebElement firstCameraRollPicture;
+
+ public CameraRollPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void selectPicture() {
+ cameraRollPicture.click();
+ }
+
+ public void selectFirstPicture() {
+ firstCameraRollPicture.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/CollectionPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/CollectionPage.java
new file mode 100644
index 00000000000..c250b055727
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/CollectionPage.java
@@ -0,0 +1,53 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import java.util.function.Function;
+
+public class CollectionPage extends IOSPage {
+
+ @iOSXCUITFindBy(className = "XCUIElementTypeCollectionView")
+ private WebElement collectionViewRoot;
+
+ @iOSXCUITFindBy(accessibility = "back")
+ private WebElement backButton;
+
+ @iOSXCUITFindBy(accessibility = "close")
+ private WebElement closeButton;
+
+ @iOSXCUITFindBy(accessibility = "fullScreenPage")
+ private WebElement fullScreenPage;
+
+ private static final Function classChainStrPictureCollectionItemByIndex = idx ->
+ String.format("**/XCUIElementTypeCell[" +
+ "$type == 'XCUIElementTypeImage' AND name == 'image'$][%s]", idx);
+
+ public CollectionPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void tapPictureItemByIndex(int index, boolean isLongTap) {
+ final By locator = MobileBy.iOSClassChain(classChainStrPictureCollectionItemByIndex.apply(index));
+ final WebElement dstElement = collectionViewRoot.findElement(locator);
+ if (isLongTap) {
+ longTapWithScript(dstElement, 50, 50);
+ } else {
+ tapAtTheCenterOfElement(dstElement);
+ }
+ }
+
+ public boolean isFullScreenImagePreviewVisible() {
+ return fullScreenPage.isDisplayed();
+ }
+
+ public void tapBackButton() {
+ backButton.click();
+ }
+
+ public void tapCloseButton() {
+ closeButton.click();
+ }
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ContactsUiPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ContactsUiPage.java
new file mode 100644
index 00000000000..29f755f5011
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ContactsUiPage.java
@@ -0,0 +1,77 @@
+package com.wearezeta.auto.ios.pages;
+
+import java.util.function.Function;
+import com.wearezeta.auto.common.misc.Timedelta;
+import io.appium.java_client.MobileBy;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import org.openqa.selenium.WebDriver;
+
+public class ContactsUiPage extends IOSPage {
+
+ @iOSXCUITFindBy(iOSNsPredicate = "name == 'textViewSearch' AND visible == 1")
+ private WebElement searchInput;
+
+ @iOSXCUITFindBy(accessibility = "Invite Others")
+ private WebElement inviteOthersButton;
+
+ @iOSXCUITFindBy(accessibility = "Go back to conversation details")
+ private WebElement backButton;
+
+ @iOSXCUITFindBy(accessibility = "Back")
+ private WebElement backButtonContactsUI;
+
+ private static final Function classChainStrConvoCellByName = name ->
+ String.format("**/XCUIElementTypeCell[$type == 'XCUIElementTypeStaticText' AND label == '%s'$]", name);
+
+ private static final Function classChainOpenButtonByConvoName = name -> MobileBy.iOSClassChain(
+ String.format("%s/XCUIElementTypeButton[`name == 'Open'`]", classChainStrConvoCellByName.apply(name)));
+
+ public ContactsUiPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public boolean isSearchInputVisible() {
+ return searchInput.isDisplayed();
+ }
+
+ public void inputTextToSearch(String text) {
+ searchInput.click();
+ searchInput.sendKeys(text);
+ }
+
+ public boolean isContactVisible(String contact) {
+ final By locator = MobileBy.iOSClassChain(classChainStrConvoCellByName.apply(contact));
+ return getDriver().findElement(locator).isDisplayed();
+ }
+
+ public void tapOpenButtonNextToUser(String contact) {
+ final By locator = classChainOpenButtonByConvoName.apply(contact);
+ getElement(locator).click();
+ // Wait for animation
+ isLocatorInvisible(locator, Timedelta.ofSeconds(5));
+ }
+
+ public boolean isInviteButtonVisible() {
+ return inviteOthersButton.isDisplayed();
+ }
+
+ public void tapBackButton() {
+ if (isElementVisible(backButton)){
+ backButton.click();
+ } else {
+ backButtonContactsUI.click();
+ }
+ }
+
+ public void tapInviteOthersButton() {
+ inviteOthersButton.click();
+ }
+
+ public boolean isContactInvisible(String contact) {
+ final By locator = MobileBy.iOSClassChain(classChainStrConvoCellByName.apply(contact));
+ return isLocatorInvisible(locator);
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ConversationViewPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ConversationViewPage.java
new file mode 100644
index 00000000000..505741ccbed
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ConversationViewPage.java
@@ -0,0 +1,966 @@
+package com.wearezeta.auto.ios.pages;
+
+import com.wearezeta.auto.common.FilenameHelper;
+import com.wearezeta.auto.common.misc.Timedelta;
+import io.appium.java_client.MobileBy;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+import java.awt.image.BufferedImage;
+import java.util.Optional;
+import java.util.function.Function;
+
+public class ConversationViewPage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "ConversationBackButton")
+ private WebElement conversationBackButton;
+
+ @iOSXCUITFindBy(accessibility = "inputField")
+ private WebElement conversationInput;
+
+ @iOSXCUITFindBy(accessibility = "Call")
+ private WebElement startCallButton;
+
+ @iOSXCUITFindBy(accessibility = "audioCallBarButton")
+ private WebElement audioCallButton;
+
+ @iOSXCUITFindBy(accessibility = "videoCallBarButton")
+ private WebElement videoCallButton;
+
+ @iOSXCUITFindBy(accessibility = "Call anyway")
+ private WebElement callAnywayButton;
+
+ @iOSXCUITFindBy(accessibility = "Cancel")
+ private WebElement cancelButton;
+
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeStaticText[`name == \"Upgrade to Enterprise\"`]")
+ private WebElement upgradeAlert;
+
+ @iOSXCUITFindBy(accessibility = "ImageCell")
+ private WebElement imageCell;
+
+ @iOSXCUITFindBy(accessibility = "VideoCell")
+ private WebElement videoCell;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "name == 'ReplyCell'")
+ private WebElement replyCell;
+
+ @iOSXCUITFindBy(accessibility = "link-attachment")
+ private WebElement youtubePreview;
+
+ @iOSXCUITFindBy(accessibility = "mentionButton")
+ private WebElement mentionButton;
+
+ @iOSXCUITFindBy(accessibility = "sketchButton")
+ private WebElement sketchButton;
+
+ @iOSXCUITFindBy(accessibility = "photoButton")
+ private WebElement addPictureButton;
+
+ @iOSXCUITFindBy(accessibility = "gifButton")
+ private WebElement gifButton;
+
+ @iOSXCUITFindBy(accessibility = "audioButton")
+ private WebElement audioMessageButton;
+
+ @iOSXCUITFindBy(accessibility = "AudioActionButton")
+ private WebElement audioMessageActionButton;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "name == 'AudioActionButton' AND value == 'Pause'")
+ private WebElement audioMessagePauseButton;
+
+ @iOSXCUITFindBy(accessibility = "showOtherRowButton")
+ private WebElement ellipsisButton;
+
+ @iOSXCUITFindBy(accessibility = "pingButton")
+ private WebElement pingButton;
+
+ @iOSXCUITFindBy(accessibility = "uploadFileButton")
+ private WebElement fileTransferButton;
+
+ @iOSXCUITFindBy(accessibility = "locationButton")
+ private WebElement locationButton;
+
+ @iOSXCUITFindBy(accessibility = "videoButton")
+ private WebElement videoMessageButton;
+
+ @iOSXCUITFindBy(accessibility = "sendButton")
+ private WebElement sendButton;
+
+ @iOSXCUITFindBy(accessibility = "VideoActionButton")
+ private WebElement videoMessageActionButton;
+
+ @iOSXCUITFindBy(accessibility = "audioRecorderSend")
+ private WebElement audioRecorderSendButton;
+
+ @iOSXCUITFindBy(accessibility = "ephemeralTimeSelectionButton")
+ private WebElement hourglassButton;
+
+ @iOSXCUITFindBy(accessibility = "collection")
+ private WebElement collectionButton;
+
+ @iOSXCUITFindBy(accessibility = "linkPreview")
+ private WebElement linkPreview;
+
+ @iOSXCUITFindBy(accessibility = "linkPreviewImage")
+ private WebElement linkPreviewImage;
+
+ @iOSXCUITFindBy(accessibility = "confirmButton")
+ private WebElement confirmEdit;
+
+ @iOSXCUITFindBy(accessibility = "cancelButton")
+ private WebElement cancelEdit;
+
+ @iOSXCUITFindBy(accessibility = "hasguests")
+ private WebElement conversationHasGuestsIndicator;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeMap'")
+ private WebElement sharedLocationContainer;
+
+ @iOSXCUITFindBy(accessibility = "quote.type.image")
+ private WebElement quotedImageInReply;
+
+ @iOSXCUITFindBy(className = "XCUIElementTypePickerWheel")
+ private WebElement pickerWheel;
+
+ @iOSXCUITFindBy(accessibility = "likeButton")
+ private WebElement likeButton;
+
+ @iOSXCUITFindBy(accessibility = "MessageToolbox")
+ private WebElement recentMessageToolbox;
+
+ @iOSXCUITFindBy(accessibility = "FileTransferBottomLabel")
+ private WebElement fileTransferBottomLabel;
+
+ @iOSXCUITFindBy(accessibility = "FileTransferTopLabel")
+ private WebElement fileTransferTopLabel;
+
+ @iOSXCUITFindBy(accessibility = "DeliveryStatus")
+ private WebElement messageDeliveryStatus;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "name == 'deep link' AND type == 'XCUIElementTypeLink'")
+ private WebElement deepLinkMessage;
+
+ @iOSXCUITFindBy(accessibility = "Image + MessageRestrictionBottomLabel")
+ private WebElement placeholderPicture;
+
+ @iOSXCUITFindBy(accessibility = "File + MessageRestrictionBottomLabel")
+ private WebElement placeholderFile;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "label CONTAINS 'New Device' AND name CONTAINS 'New Device' AND value CONTAINS 'New Device'")
+ private WebElement degradationAlert;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeNavigationBar'")
+ private WebElement titleBar;
+
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeNavigationBar/*[`name == 'Name'`]")
+ private WebElement conversationName;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "name == 'users_list.label'")
+ private WebElement userListLabel;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "name == 'Learn more'")
+ private WebElement learnMoreDelayedMessage;
+
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeButton[$name == '80 MB file'$]")
+ private WebElement optionFor80MBFile;
+
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeButton[$name == 'CountryCodes.plist'$]")
+ private WebElement optionForCountryCodesFile;
+
+ private static final By classChainAllTextMessages = MobileBy.iOSClassChain("**/XCUIElementTypeCell[$name == 'Message'$]");
+
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeTextView[`name == 'Message' OR label == 'Message'`]")
+ private WebElement lastTextMessage;
+
+ private static final By nameFileTransferActionButton = MobileBy.AccessibilityId("FileTransferActionButton");
+
+ private static final By nameFileActionsMenu = MobileBy.AccessibilityId("ActivityListView");
+
+ private static final By nameImageCell = MobileBy.AccessibilityId("ImageCell");
+
+ private static final By nameVideoCell = MobileBy.AccessibilityId("videoCell");
+
+ private static final By nameFileTransferCell = MobileBy.AccessibilityId("FileTransferTopLabel");
+
+ private static final By namePlaceholderImageCell = MobileBy.AccessibilityId("Image + MessageRestrictionBottomLabel");
+
+ private static final String nameInputPlaceholderStandard = "Type a message";
+
+ private static final By predicateStandardTextInputPlaceholder = MobileBy.iOSNsPredicateString(String.format("type == 'XCUIElementTypeTextView' AND name == 'inputField' AND value ENDSWITH '%s'", nameInputPlaceholderStandard));
+
+ private static final Function predicateInputFieldQuoteType = type -> MobileBy.iOSNsPredicateString(
+ String.format("type == 'XCUIElementTypeOther' AND name == 'replyView' AND label CONTAINS '%s'",
+ type));
+
+ private static final Function predicateStrQuotedMessageByValue = text -> MobileBy.iOSNsPredicateString(
+ String.format("type == 'XCUIElementTypeTextView' AND name =='quote.type.text' AND value CONTAINS '%s'", text));
+
+ private static final By classChainConversationViewEntry = MobileBy.iOSClassChain("XCUIElementTypeCell");
+ private static final String classChainStrAllEntries = "**/XCUIElementTypeTable/XCUIElementTypeCell";
+ private static final By fbClassChainRecentEntry = MobileBy.iOSClassChain(String.format("%s[1]", classChainStrAllEntries));
+ private static final By classConversationViewRoot = By.className("XCUIElementTypeTable");
+ private static final By predicateMissedCallByYourself = MobileBy.iOSNsPredicateString("value == 'You called'");
+
+ private static final Function predicateReactionConversationViewByValue = reaction -> MobileBy.iOSNsPredicateString(String.format("type == 'XCUIElementTypeOther' AND name CONTAINS 'value: %s, count:'", reaction));
+
+ private static final Function classChainMessageToolboxByText = name -> MobileBy.iOSClassChain(
+ String.format("**/XCUIElementTypeCell[`name == 'MessageToolbox'`]/**/XCUIElementTypeAny[`value CONTAINS '%s' AND label CONTAINS '%s'`]", name, name));
+ private static final Function classChainMessageToolboxButtonByText = name -> MobileBy.iOSClassChain(
+ String.format("**/XCUIElementTypeCell[`name == 'MessageToolbox'`]/**/XCUIElementTypeButton[`label CONTAINS '%s'`]", name));
+
+ /**
+ * !!! The actual message order in DOM is reversed relatively to the messages order in the conversation view
+ */
+ private static final Function messageAttributesByTextPartTemplate = text ->
+ String.format("`name == 'Message' AND visible == 1 AND (value CONTAINS '%s' OR label CONTAINS '%s')`", text, text);
+
+ private static final Function classChainMessageByTextPart = text -> MobileBy.iOSClassChain(
+ String.format("%s/**/*[%s]", classChainStrAllEntries, messageAttributesByTextPartTemplate.apply(text)));
+
+ private static final Function predicateSystemMessageByText = text -> MobileBy.iOSNsPredicateString(
+ String.format("type == 'XCUIElementTypeCell' AND label BEGINSWITH[c] '%s' OR type == 'XCUIElementTypeLink' AND label BEGINSWITH[c] '%s' OR type == 'XCUIElementTypeLink' AND value BEGINSWITH[c] '%s' OR type == 'XCUIElementTypeStaticText' AND label BEGINSWITH[c] '%s'", text, text, text, text));
+
+ private static final Function predicatePingMessageByText = text -> MobileBy.iOSNsPredicateString(
+ String.format("type == 'XCUIElementTypeLink' AND value ==[c] '%s'", text));
+
+ private static final Function xpathReplyCellsByCount = count -> By.xpath(
+ String.format("//XCUIElementTypeTable[count(XCUIElementTypeCell[@name='ReplyCell'])=%s]", count));
+
+ private static final String MISSED_CALL_PREFIX = "Missed call from";
+
+ private static Function classChainStrToolbarByPredicateExpr = predicateExpr ->
+ String.format("**/XCUIElementTypeNavigationBar/*[`%s`]", predicateExpr);
+
+ private static final By classChainTopBarShield = MobileBy.iOSClassChain(classChainStrToolbarByPredicateExpr.apply(
+ "name == 'Name' AND label CONTAINS 'Verified'"));
+
+ private static final By classChainTopBarLHIndicator = MobileBy.iOSClassChain(classChainStrToolbarByPredicateExpr.apply(
+ "name == 'Name' AND label CONTAINS 'Legal hold'"));
+
+ private static final Function predicateTransferTopLabelByFileName = name ->
+ String.format("type == 'XCUIElementTypeStaticText' AND name == 'FileTransferTopLabel' AND value == '%s'",
+ name.toUpperCase());
+
+ private static final Function predicateStrTransferBottomLabelByExpr = expr ->
+ String.format("type == 'XCUIElementTypeStaticText' AND name == 'FileTransferBottomLabel' AND %s",
+ expr);
+
+ private static final Function predicateAudioActionButtonByState = value ->
+ String.format("name == 'AudioActionButton' AND value == '%s'",
+ value);
+
+ private static final Function predicateStrMessageDeliveryStatusByText = text ->
+ String.format("name == 'DeliveryStatus' AND value CONTAINS '%s'", text);
+
+ private static final Function predicateStrMessageDetailsByText = text ->
+ String.format("name == 'Details' AND value CONTAINS '%s'", text);
+
+ private static final Timedelta MAX_APPEARANCE_TIME = Timedelta.ofSeconds(20);
+
+ public ConversationViewPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public boolean isInputFieldVisible() {
+ return conversationInput.isDisplayed();
+ }
+
+ public boolean isInputFieldInvisible() {
+ return isElementInvisible(conversationInput);
+ }
+
+ public boolean waitUntilTextMessageIsNotVisible(String msg) {
+ final By locator = classChainMessageByTextPart.apply(msg);
+ return isLocatorInvisible(locator);
+ }
+
+ public void returnToConversationsList() {
+ waitUntilElementClickable(conversationBackButton);
+ conversationBackButton.click();
+ }
+
+ public int getNumberOfMessageEntries() {
+ final WebElement convoViewRoot = getElement(classConversationViewRoot);
+ return selectVisibleElements(convoViewRoot, classChainConversationViewEntry).size();
+ }
+
+ public boolean waitForCursorInputVisible() {
+ return isElementVisible(conversationInput, Timedelta.ofSeconds(10));
+ }
+
+ public boolean waitForCursorInputInvisible() {
+ return isElementInvisible(conversationInput);
+ }
+
+ public void tapTextInput() {
+ conversationInput.click();
+ }
+
+ public void longTapTextInput() throws InterruptedException {
+ longTapWithActionsAPI(conversationInput);
+ }
+
+ public void clearTextInput() {
+ // This is to make sure the input cursor has been put to the tail of the text
+ this.tapByPercentOfElementSize(conversationInput, 95, 50);
+ conversationInput.clear();
+ }
+
+ public String getLastTextMessage() {
+ waitUntilElementVisible(lastTextMessage);
+ return lastTextMessage.getText();
+ }
+
+ public boolean waitUntilMessageInConversation() {
+ return waitUntilElementVisible(lastTextMessage);
+ }
+
+ public boolean isUpperToolbarContainNames(String expectedNames) {
+ return titleBar.getAttribute("name").toUpperCase().equals(expectedNames.toUpperCase());
+ }
+
+ public void openConversationDetails() {
+ waitUntilElementClickable(conversationName);
+ conversationName.click();
+ }
+
+ public void typeMessage(String message, boolean shouldSend) {
+ conversationInput.sendKeys(message);
+ if (shouldSend) {
+ tapSendMessageButton();
+ }
+ }
+
+ public void typeMessage(String message) {
+ typeMessage(message, false);
+ }
+
+ public boolean isShieldIconVisible() {
+ return getDriver().findElement(classChainTopBarShield).isDisplayed();
+ }
+
+ public boolean isShieldIconInvisible() {
+ return isLocatorInvisible(classChainTopBarShield);
+ }
+
+ public boolean areInputToolsVisible() {
+ return isElementVisible(addPictureButton, Timedelta.ofSeconds(8)) || isElementVisible(ellipsisButton, Timedelta.ofSeconds(8));
+ }
+
+ public boolean areInputToolsInvisible() {
+ return isElementInvisible(addPictureButton) && isElementInvisible(ellipsisButton);
+ }
+
+ public boolean isSystemMessageVisible(String expectedMsg) {
+ final By locator = predicateSystemMessageByText.apply(expectedMsg);
+ return getDriver().findElement(locator).isDisplayed();
+ }
+
+ public boolean isSystemMessageInvisible(String expectedMsg) {
+ final By locator = predicateSystemMessageByText.apply(expectedMsg);
+ return isLocatorInvisible(locator);
+ }
+
+ public boolean isPingMessageVisible(String pingMsg) {
+ final By locator = predicatePingMessageByText.apply(pingMsg);
+ return getDriver().findElement(locator).isDisplayed();
+ }
+
+ public boolean isPingMessageInvisible(String pingMsg) {
+ final By locator = predicatePingMessageByText.apply(pingMsg);
+ return isLocatorInvisible(locator);
+ }
+
+ public boolean isYouCalledMessageVisible() {
+ return getDriver().findElement(predicateMissedCallByYourself).isDisplayed();
+ }
+
+ public Optional getRecentPictureScreenshot() {
+ return getElementScreenshot(imageCell);
+ }
+
+ public List getQRCodeFromPicture() {
+ return waitUntilElementContainsQRCode(imageCell);
+ }
+
+ public void tapFileTransferOptionFor80MBFile() {
+ optionFor80MBFile.click();
+ }
+
+ public void tapFileTransferOptionForCountryCodesFile() {
+ optionForCountryCodesFile.click();
+ }
+
+ public boolean isFileTransferTopLabelVisible() {
+ return fileTransferTopLabel.isDisplayed();
+ }
+
+ public boolean isFileTransferTopLabelInvisible() {
+ return isElementInvisible(fileTransferTopLabel);
+ }
+
+ public boolean isFileTransferBottomLabelVisible() {
+ return waitUntilElementVisible(fileTransferBottomLabel);
+ }
+
+ public void tapFileTransferActionButton() {
+ tapElementWithRetryIfNextElementNotAppears(nameFileTransferActionButton, nameFileActionsMenu,
+ Timedelta.ofSeconds(3), 5);
+ }
+
+ public void tapAddPictureButton() {
+ addPictureButton.click();
+ }
+
+ public boolean isAddPictureButtonVisible() {
+ return addPictureButton.isDisplayed();
+ }
+
+ public boolean isAddPictureButtonInvisible() {
+ return isElementInvisible(addPictureButton);
+ }
+
+ public boolean isPingButtonVisible() {
+ return locateCursorToolButton(pingButton).isDisplayed();
+ }
+
+ public boolean isPingButtonInvisible() {
+ return isElementInvisible(pingButton);
+ }
+
+ public boolean isMentionButtonVisible() {
+ return locateCursorToolButton(mentionButton).isDisplayed();
+ }
+
+ public boolean isMentionButtonInvisible() {
+ return isElementInvisible(mentionButton);
+ }
+
+ public boolean isShareLocationButtonVisible() {
+ return locateCursorToolButton(locationButton).isDisplayed();
+ }
+
+ public boolean isShareLocationButtonInvisible() {
+ return isElementInvisible(locationButton);
+ }
+
+ public void tapMentionButton() {
+ mentionButton.click();
+ }
+
+ public void tapPingButton() {
+ pingButton.click();
+ }
+
+ public void tapSketchButton() {
+ sketchButton.click();
+ }
+
+ public void tapShareLocationButton() {
+ locationButton.click();
+ }
+
+ public void tapEllipsisButton() {
+ ellipsisButton.click();
+ }
+
+ public void tapFileTransferButton() {
+ fileTransferButton.click();
+ }
+
+ public void tapVideoMessageButton() {
+ locateCursorToolButton(videoMessageButton).click();
+ }
+
+ public void tapAudioMessageButton() {
+ locateCursorToolButton(audioMessageButton).click();
+ }
+
+ public void longTapAudioMessageButton() {
+ if (waitUntilElementClickable(audioMessageButton)) {
+ try {
+ longTapWithActionsAPI(audioMessageButton);
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Failed to long tap audio message button", e);
+ }
+ } else {
+ throw new RuntimeException("Audio message button is not clickable");
+ }
+
+ }
+
+ public void longTapAudioMessageButtonWithDuration(int seconds) {
+ try {
+ longTapWithActionsAPI(locateCursorToolButton(audioMessageButton), Duration.ofSeconds(seconds));
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Could not long tap audio message button", e);
+ }
+ }
+
+ public void tapGIFButton() {
+ gifButton.click();
+ }
+
+ public boolean isAudioMessageButtonVisible() {
+ return audioMessageButton.isDisplayed();
+ }
+
+ public boolean isAudioMessageButtonInvisible() {
+ return isElementInvisible(audioMessageButton);
+ }
+
+ public boolean isFileTransferButtonVisible() {
+ return locateCursorToolButton(fileTransferButton).isDisplayed();
+ }
+
+ public boolean isSketchButtonVisible() {
+ return locateCursorToolButton(sketchButton).isDisplayed();
+ }
+
+ public boolean isSketchButtonInvisible() {
+ return isElementInvisible(sketchButton);
+ }
+
+ public boolean isVideoMessageButtonVisible() {
+ return locateCursorToolButton(videoMessageButton).isDisplayed();
+ }
+
+ public boolean isVideoMessageButtonInvisible() {
+ return isElementInvisible(videoMessageButton);
+ }
+
+ public boolean isPhotoGalleryButtonVisible() {
+ return addPictureButton.isDisplayed();
+ }
+
+ public boolean isPhotoGalleryButtonInvisible() {
+ return isElementInvisible(addPictureButton);
+ }
+
+ public boolean isGiphyButtonVisible() {
+ return locateCursorToolButton(gifButton).isDisplayed();
+ }
+
+ public boolean isGiphyButtonInvisible() {
+ return isElementInvisible(gifButton);
+ }
+
+ public boolean isDegradationAlertVisible() {
+ return degradationAlert.isEnabled();
+ }
+
+ public boolean isDegradationAlertInvisible() {
+ return isElementInvisible(degradationAlert);
+ }
+
+ public boolean isFileTransferButtonInvisible() {
+ return isElementInvisible(fileTransferButton);
+ }
+
+ public boolean waitUntilDownloadReadyPlaceholderVisible(String filename, String expectedSize, Timedelta timeout) {
+ final By topLabelLocator = MobileBy.iOSNsPredicateString(
+ predicateTransferTopLabelByFileName.apply(FilenameHelper.getBaseName(filename).get()));
+ final By bottomLabelLocator = MobileBy.iOSNsPredicateString(predicateStrTransferBottomLabelByExpr.apply(
+ String.join(" AND ",
+ String.format("value BEGINSWITH '%s'", expectedSize.toUpperCase()),
+ String.format("value CONTAINS '%s'", FilenameHelper.getExtension(filename).get().toUpperCase())
+ )
+ ));
+ return isLocatorDisplayed(topLabelLocator, timeout) &&
+ isLocatorDisplayed(bottomLabelLocator, timeout);
+ }
+
+ public boolean isPlaceholderStandardTextVisible() {
+ return getDriver().findElement(predicateStandardTextInputPlaceholder).isDisplayed();
+ }
+
+ public boolean isPlaceholderStandardTextInvisible() {
+ return isLocatorInvisible(predicateStandardTextInputPlaceholder);
+ }
+
+ public void longTapMessageByText(String message) {
+ final WebElement el = getDriver().findElement(classChainMessageByTextPart.apply(message));
+ getDriver().executeScript("mobile: touchAndHold", Map.ofEntries(
+ Map.entry("elementId", el.getAttribute("UID")),
+ Map.entry("duration", "2.0")
+ ));
+ }
+
+ public void tapMessageByText(String text) {
+ final WebElement el = getDriver().findElement(classChainMessageByTextPart.apply(text));
+ tapAtTheLeftSideOfElement(el);
+ }
+
+ private WebElement locateCursorToolButton(WebElement toolButton) {
+ if (isElementVisible(toolButton)) {
+ return toolButton;
+ } else {
+ this.tapAtTheCenterOfElement(ellipsisButton);
+ return toolButton;
+ }
+ }
+
+ public void tapSendRecordControlButton() {
+ audioRecorderSendButton.click();
+ }
+
+ public void tapAudioMessagePlayButton() {
+ waitUntilElementClickable(audioMessageActionButton);
+ audioMessageActionButton.click();
+ }
+
+ public void longTapPlayAudioMessageButton() {
+ waitUntilElementClickable(audioMessageActionButton);
+ longTapWithScript(audioMessageActionButton);
+ }
+
+ public boolean isPlaceholderAudioMessageButtonState(String buttonState) {
+ final By locator = MobileBy.iOSNsPredicateString(predicateAudioActionButtonByState.apply(buttonState));
+ return getDriver().findElement(locator).isDisplayed();
+ }
+
+ public boolean isAudioMessagePauseButtonVisible() {
+ return waitUntilElementVisible(audioMessagePauseButton);
+ }
+
+ public boolean isLinkPreviewImageVisible() {
+ return isElementVisible(linkPreviewImage, MAX_APPEARANCE_TIME);
+ }
+
+ public boolean isLinkPreviewImageInvisible() {
+ return isElementInvisible(linkPreviewImage);
+ }
+
+ public void selectDeleteMenuItem(String name) {
+ getElement(MobileBy.AccessibilityId(name)).click();
+ }
+
+ public void tapConfirmEditControlButton() {
+ confirmEdit.click();
+ }
+
+ public void tapCancelEditControlButton() {
+ cancelEdit.click();
+ }
+
+ public void tapImageInConversation() {
+ imageCell.click();
+ }
+
+ public void longTapImageInConversation() {
+ longTapWithScript(imageCell);
+ }
+
+ public void longTapFileTransferPlaceholder() {
+ longTapWithScript(fileTransferBottomLabel);
+ }
+
+ public void tapFileTransferPlaceholder() {
+ fileTransferBottomLabel.click();
+ }
+
+ public void tapVideoMessage() {
+ videoMessageActionButton.click();
+ }
+
+ public void longTapVideoMessage() {
+ longTapWithScript(videoMessageActionButton);
+ }
+
+ public void tapLocationMap() {
+ sharedLocationContainer.click();
+ }
+
+ public void longTapLinkPreview() {
+ longTapWithScript(linkPreview);
+ }
+
+ public void singleTapYoutubePreview() {
+ youtubePreview.click();
+ }
+
+ public boolean isVideoMessageVisible() {
+ return videoMessageActionButton.isDisplayed();
+ }
+
+ public boolean isVideoMessageInvisible() {
+ return isElementInvisible(videoMessageActionButton);
+ }
+
+ public boolean isLinkPreviewVisible() {
+ return linkPreview.isDisplayed();
+ }
+
+ public boolean isLinkPreviewInvisible() {
+ return isElementInvisible(linkPreview);
+ }
+
+ public boolean isAudioMessageVisible() {
+ return waitUntilElementVisible(audioMessageActionButton);
+ }
+
+ public boolean isAudioMessageInvisible() {
+ return waitUntilElementInvisible(audioMessageActionButton);
+ }
+
+ public boolean isLocationMapVisible() {
+ return sharedLocationContainer.isDisplayed();
+ }
+
+ public boolean isLocationMapInvisible() {
+ return isElementInvisible(sharedLocationContainer);
+ }
+
+ public BufferedImage getLikeIconState() {
+ return getElementScreenshot(likeButton).orElseThrow(
+ () -> new IllegalStateException("Cannot take a screenshot of Like/Unlike button")
+ );
+ }
+
+ public void tapLikeIcon() {
+ likeButton.click();
+ }
+
+ public boolean isLikeIconVisible() {
+ return likeButton.isDisplayed() && recentMessageToolbox.isDisplayed();
+ }
+
+ public boolean isLikeIconInvisible() {
+ return isElementInvisible(likeButton) || isElementInvisible(recentMessageToolbox);
+ }
+
+ public void tapAtRecentMessage(int pWidth, int pHeight) {
+ this.tapByPercentOfElementSize(getElement(fbClassChainRecentEntry), pWidth, pHeight);
+ }
+
+ public void tapRecentMessageToolbox() {
+ recentMessageToolbox.click();
+ }
+
+ public void tapAtDeepLinkMessage() {
+ deepLinkMessage.click();
+ }
+
+ public boolean waitUntilAllTextMessageAreNotVisible() {
+ return isLocatorInvisible(classChainAllTextMessages);
+ }
+
+ public int numberOfTextMessagesVisible(int expectedCount) {
+ return waitUntilNumberOfElementsToBe(classChainAllTextMessages, expectedCount);
+ }
+
+ public int numberOfSpecificTextMessagesVisible(String s, int expectedCount) {
+ final By locator = classChainMessageByTextPart.apply(s);
+ return waitUntilNumberOfElementsToBe(locator, expectedCount);
+ }
+
+ public boolean areNoImagesVisible() {
+ return waitUntilElementInvisible(imageCell);
+ }
+
+ public int numberOfImagesVisible(int expectedCount) {
+ return waitUntilNumberOfElementsToBe(nameImageCell, expectedCount);
+ }
+
+ public boolean areNoVideoFilesVisible() {
+ return waitUntilElementInvisible(videoCell);
+ }
+
+ public int numberOfVideoFiles(int expectedCount) {
+ return waitUntilNumberOfElementsToBe(nameVideoCell, expectedCount);
+ }
+
+ public int areXFilesVisible(int expectedCount) {
+ return waitUntilNumberOfElementsToBe(nameFileTransferCell, expectedCount);
+ }
+
+ public String getPlaceholderImageText() {
+ return placeholderPicture.getText();
+ }
+
+ public boolean areNoPlaceholderImagesVisible() {
+ return isElementInvisible(placeholderPicture);
+ }
+
+ public boolean isPlaceholderFileVisible() {
+ return placeholderFile.isDisplayed();
+ }
+
+ public boolean isPlaceholderFileInvisible() {
+ return isElementInvisible(placeholderFile);
+ }
+
+ public void longTapPlaceholderFile() {
+ longTapWithScript(placeholderFile);
+ }
+
+ public int numberOfPlaceholderImages(int expectedCount) {
+ return waitUntilNumberOfElementsToBe(namePlaceholderImageCell, expectedCount);
+ }
+
+ public void tapAudioButton() {
+ audioCallButton.click();
+ }
+
+ public void tapStartCallButton() {
+ startCallButton.click();
+ }
+
+ public void tapVideoCallButton() {
+ tapAtTheCenterOfElement(videoCallButton);
+ }
+
+ public void tapCancelButton() {
+ tapAtTheCenterOfElement(cancelButton);
+ }
+
+ public void tapCallAnywayButton() {
+ waitUntilElementVisible(callAnywayButton);
+ tapAtTheCenterOfElement(callAnywayButton);
+ }
+
+ public void tapSendMessageButton() {
+ sendButton.click();
+ }
+
+ public void tapHourglassButton() {
+ hourglassButton.click();
+ }
+
+ public void tapCollectionButton() {
+ this.tapElementWithRetryIfStillDisplayed(collectionButton);
+ }
+
+ public boolean isMessageDeliveryStatusTextVisible(String expectedText) {
+ final By locator = MobileBy.iOSNsPredicateString(predicateStrMessageDeliveryStatusByText.apply(expectedText));
+ return getDriver().findElement(locator).isDisplayed();
+ }
+
+ public boolean isMessageToolboxDetailsTextVisible(String expectedText) {
+ final By locator = MobileBy.iOSNsPredicateString(predicateStrMessageDetailsByText.apply(expectedText));
+ return getDriver().findElement(locator).isDisplayed();
+ }
+
+ public boolean isMessageToolboxDetailsTextInvisible(String expectedText) {
+ final By locator = MobileBy.iOSNsPredicateString(predicateStrMessageDetailsByText.apply(expectedText));
+ return isLocatorInvisible(locator);
+ }
+
+ public boolean isMessageToolboxTextVisible(String expectedText) {
+ By locator = classChainMessageToolboxByText.apply(expectedText);
+ return waitUntilLocatorVisible(locator);
+ }
+
+ public boolean isMessageToolboxButtonVisible(String expectedText) {
+ By locator = classChainMessageToolboxButtonByText.apply(expectedText);
+ return getDriver().findElement(locator).isDisplayed();
+ }
+
+ public boolean isMessageToolboxButtonInvisible(String expectedText) {
+ By locator = classChainMessageToolboxButtonByText.apply(expectedText);
+ return isLocatorInvisible(locator, MAX_APPEARANCE_TIME);
+ }
+
+ public boolean isMessageToolboxTextInvisible(String expectedText) {
+ final By locator = classChainMessageToolboxByText.apply(expectedText);
+ return isLocatorInvisible(locator);
+ }
+
+ public boolean isMessageDeliveryStatusVisible() {
+ return waitUntilElementVisible(messageDeliveryStatus, MAX_APPEARANCE_TIME.asDuration());
+ }
+
+ public boolean isMessageDeliveryStatusInvisible() {
+ return isElementInvisible(messageDeliveryStatus);
+ }
+
+ public void setMessageExpirationTimer(String value) {
+ pickerWheel.sendKeys(value);
+ }
+
+ private static final int MAX_SCROLLS = 3;
+
+ public void scrollToTheTop() {
+ for (int i = 0; i < MAX_SCROLLS; i++) {
+ swipe(getElement(classConversationViewRoot), SwipeDirection.DOWN);
+ }
+ }
+
+ public void scrollToTheBottom() {
+ for (int i = 0; i < MAX_SCROLLS; i++) {
+ swipe(getElement(classConversationViewRoot), SwipeDirection.UP);
+ }
+ }
+
+ public boolean isConversationHasGuestsVisible() {
+ return conversationHasGuestsIndicator.isDisplayed();
+ }
+
+ public boolean isConversationHasGuestsInvisible() {
+ return isElementInvisible(conversationHasGuestsIndicator);
+ }
+
+ public boolean isReplyVisible() {
+ return replyCell.isDisplayed();
+ }
+
+ public boolean isReplyInvisible() {
+ return isElementInvisible(replyCell);
+ }
+
+ public boolean isInputFieldQuoteOfTypeVisible(String type) {
+ return isLocatorExist(predicateInputFieldQuoteType.apply(type));
+ }
+
+ public boolean isQuotedImageVisible() {
+ return quotedImageInReply.isDisplayed();
+ }
+
+ public boolean isQuotedMessageVisible(String text) {
+ return getDriver().findElement(predicateStrQuotedMessageByValue.apply(text)).isDisplayed();
+ }
+
+ public boolean isNumberOfReplyCellsVisible(int numberOfCells) {
+ return getDriver().findElement(xpathReplyCellsByCount.apply(numberOfCells)).isDisplayed();
+ }
+
+ public boolean isLegalHoldIndicatorVisible() {
+ return isLocatorDisplayed(classChainTopBarLHIndicator, Timedelta.ofSeconds(10));
+ }
+
+ public boolean isLegalHoldIndicatorInvisible() {
+ return isLocatorInvisible(classChainTopBarLHIndicator);
+ }
+
+ public boolean isUserWillGetYourMessageLaterVisible(String name) {
+ return userListLabel.getText().equals(String.format("%s will get your message later. Learn more", name));
+ }
+
+ public void iTapOnLearnMoreLinkOnDelayedMessage() {
+ learnMoreDelayedMessage.click();
+ }
+
+ public boolean isReactionVisible(String reaction) {
+ return getDriver().findElement(predicateReactionConversationViewByValue.apply(reaction)).isDisplayed();
+ }
+
+ public boolean isReactionInvisible(String reaction) {
+ return isLocatorInvisible(predicateReactionConversationViewByValue.apply(reaction));
+ }
+
+ public boolean enterpriseUpgradeAlertPresent() {
+ try {
+ return upgradeAlert.isDisplayed();
+ } catch(Exception e) {
+ return false;
+ }
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ConversationsListPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ConversationsListPage.java
new file mode 100644
index 00000000000..79c37d894ca
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ConversationsListPage.java
@@ -0,0 +1,310 @@
+package com.wearezeta.auto.ios.pages;
+
+import com.wearezeta.auto.common.CommonUtils;
+import com.wearezeta.auto.common.Config;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import com.wearezeta.auto.common.log.ZetaLogger;
+import com.wearezeta.auto.common.misc.Timedelta;
+import io.appium.java_client.MobileBy;
+import java.util.logging.Logger;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+public class ConversationsListPage extends IOSPage {
+ private static final Logger log = ZetaLogger.getLog(ConversationsListPage.class.getSimpleName());
+
+ @iOSXCUITFindBy(iOSNsPredicate = "name == 'conversation list'")
+ private WebElement conversationsListRoot;
+
+ @iOSXCUITFindBy(className = "XCUIElementTypeCollectionView")
+ private WebElement classConversationsListRoot;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "name BEGINSWITH 'Start a conversation'")
+ private WebElement inviteHelpText;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "name == 'action_button' AND type == 'XCUIElementTypeButton'")
+ private WebElement joinButton;
+
+ @iOSXCUITFindBy(accessibility = "EVERYTHING ARCHIVED")
+ private WebElement conversationListPlaceholder;
+
+ @iOSXCUITFindBy(accessibility = "ClassificationBannerClassified")
+ private WebElement classifiedDomainLabel;
+
+ @iOSXCUITFindBy(accessibility = "ClassificationBannerUnclassified")
+ private WebElement unClassifiedDomainLabel;
+
+ @iOSXCUITFindBy(accessibility = "contactRequests - conversation_list_cell")
+ private WebElement conversationPendingRequest;
+
+ @iOSXCUITFindBy(accessibility = "Mark as Read")
+ private WebElement markAsRead;
+
+ @iOSXCUITFindBy(accessibility = "Mute")
+ private WebElement mute;
+
+ @iOSXCUITFindBy(accessibility = "Nothing")
+ private WebElement nothing;
+
+ @iOSXCUITFindBy(accessibility = "Notifications…")
+ private WebElement notificationsMenu;
+
+ @iOSXCUITFindBy(accessibility = "Archive")
+ private WebElement archive;
+
+ @iOSXCUITFindBy(accessibility = "Add to Favorites")
+ private WebElement addToFavorites;
+
+ @iOSXCUITFindBy(accessibility = "Remove from Favorites")
+ private WebElement removeFromFavorites;
+
+ @iOSXCUITFindBy(accessibility = "Move to…")
+ private WebElement moveTo;
+
+ @iOSXCUITFindBy(accessibility = "Clear Content…")
+ private WebElement clearContent;
+
+ @iOSXCUITFindBy(accessibility = "Clear")
+ private WebElement clearInClearContent;
+
+ @iOSXCUITFindBy(accessibility = "Block…")
+ private WebElement block;
+
+ @iOSXCUITFindBy(accessibility = "Block")
+ private WebElement blockConfirm;
+
+ @iOSXCUITFindBy(accessibility = "Leave Group…")
+ private WebElement leaveGroup;
+
+ @iOSXCUITFindBy(accessibility = "Leave and clear content")
+ private WebElement leaveAndClearConfirm;
+
+ @iOSXCUITFindBy(accessibility = "Name")
+ private WebElement name;
+
+ private static final Function predicateStrConversationByLabel = text ->
+ MobileBy.iOSNsPredicateString(String.format("name == 'title' AND label == '%s'", text));
+
+ private static final Function xpathStrFirstConversationRelativeEntryByName = name ->
+ MobileBy.xpath(String.format("//XCUIElementTypeOther[1]/XCUIElementTypeButton[@label='%s']", name));
+
+ private static final BiFunction predicateStrSecondaryLine = (label, value) ->
+ MobileBy.iOSNsPredicateString(String.format("name == 'title' AND label == '%s' AND value BEGINSWITH '%s'", label, value));
+
+ private static final Function classChainStrRelativeConversationStatus = name -> MobileBy.iOSClassChain(String.format(
+ "XCUIElementTypeCell[$type == 'XCUIElementTypeButton' AND label == '%s' AND `name == 'title' $]",
+ name));
+
+ private static final BiFunction predicatedStrRelativeConversationStatusByValue = (label, value) ->
+ MobileBy.iOSNsPredicateString(String.format("name == 'title' AND label == '%s' AND value ENDSWITH '%s'", label, value));
+
+ public ConversationsListPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void tapConversationItemRecentList(String name) {
+ final By locator = predicateStrConversationByLabel.apply(name);
+ getDriver().findElement(locator).click();
+ }
+
+ // TODO: Not sure if I like how handling the context menu at the moment
+ public void tapMarkAsRead() {
+ markAsRead.click();
+ }
+
+ public void tapNotificationsMenu() {
+ notificationsMenu.click();
+ }
+
+ public void tapNothing() {
+ nothing.click();
+ }
+
+ public void tapArchive() {
+ archive.click();
+ }
+
+ public void tapFavorite() {
+ addToFavorites.click();
+ }
+
+ public void tapRemoveFromFavorite() {
+ removeFromFavorites.click();
+ }
+
+ public void tapMoveTo() {
+ moveTo.click();
+ }
+
+ public void tapClearContent() {
+ clearContent.click();
+ }
+
+ public void tapBlock() {
+ block.click();
+ }
+
+ public void tapLeaveGroup() {
+ leaveGroup.click();
+ }
+
+ public void longTapConversationItemRecentList(String name) {
+ final By locator = predicateStrConversationByLabel.apply(name);
+ try {
+ longTapWithActionsAPI(getDriver().findElement(locator));
+ } catch (InterruptedException e) {
+ throw new RuntimeException("The long tap action was interrupted", e);
+ }
+ }
+
+ public boolean isConversationInList(String name) {
+ return this.isConversationInList(name,
+ Timedelta.ofSeconds(Integer.parseInt(Config.current().getDriverTimeout(getClass()))));
+ }
+
+ public boolean isConversationInList(String name, Timedelta timeout) {
+ final By locator = predicateStrConversationByLabel.apply(name);
+ return isLocatorDisplayed(locator, timeout);
+ }
+
+ public void swipeRightOnConversation(String name) {
+ final By locator = predicateStrConversationByLabel.apply(name);
+ final WebElement convoListItem = getDriver().findElement(locator);
+ if (!CommonUtils.waitUntilTrue(Timedelta.ofSeconds(5), Timedelta.ofMillis(1), () -> {
+ swipe(convoListItem, SwipeDirection.RIGHT);
+ return isElementInvisible(convoListItem, Timedelta.ofSeconds(1));
+ })) {
+ log.warning(String.format("The conversation item '%s' is still visible after being swiped to the right", name));
+ }
+ }
+
+ public boolean isPendingRequestInContactList() {
+ return conversationPendingRequest.isDisplayed();
+ }
+
+ public boolean pendingRequestInContactListIsNotShown() {
+ return isElementInvisible(conversationPendingRequest);
+ }
+
+ public void tapPendingRequest() {
+ conversationPendingRequest.click();
+ }
+
+ public boolean isConversationNotInList(String name, Timedelta timeout) {
+ final By locator = predicateStrConversationByLabel.apply(name);
+ return isLocatorInvisible(locator, timeout);
+ }
+
+ public boolean isConversationNotInList(String name) {
+ return isConversationNotInList(name, getDefaultLookupTimeout());
+ }
+
+ public boolean isFirstConversationName(String convoName) {
+ return isLocatorDisplayed(classConversationsListRoot, xpathStrFirstConversationRelativeEntryByName.apply(convoName));
+ }
+
+ public boolean isConversationsListPlaceholderVisible() {
+ return conversationListPlaceholder.isDisplayed();
+ }
+
+ public boolean isConversationItemWithStatusVisible(String status, String conversation) {
+ return isLocatorDisplayed(classConversationsListRoot, predicatedStrRelativeConversationStatusByValue.apply(conversation, status));
+ }
+
+ public boolean isConversationItemWithStatusInvisible(String status, String conversation) {
+ return isLocatorInvisible(classConversationsListRoot, predicatedStrRelativeConversationStatusByValue.apply(conversation, status));
+ }
+
+ public boolean isConversationItemStatusInvisible(String conversation) {
+ return isLocatorInvisible(classConversationsListRoot, classChainStrRelativeConversationStatus.apply(conversation));
+ }
+
+ public boolean isConversationItemStatusVisible(String conversation) {
+ return isLocatorDisplayed(classConversationsListRoot, classChainStrRelativeConversationStatus.apply(conversation));
+ }
+
+ public boolean isSecondaryLineVisible(String conversation, String secondaryLine) {
+ final By locator = predicateStrSecondaryLine.apply(conversation, secondaryLine);
+ return isLocatorDisplayed(locator, Timedelta.ofSeconds(3));
+ }
+
+ public boolean isVisible() {
+ return isElementVisible(conversationsListRoot, Timedelta.ofSeconds(10));
+ }
+
+ public boolean isInvisible() {
+ return isElementInvisible(conversationsListRoot);
+ }
+
+ public void tapJoinButtonNextTo(String name) {
+ isLocatorDisplayed(classConversationsListRoot, predicatedStrRelativeConversationStatusByValue.apply(name, "JOIN"));
+ joinButton.click();
+ }
+
+ public boolean isClassifiedLabelVisible() {
+ return classifiedDomainLabel.isDisplayed();
+ }
+
+ public boolean isClassifiedLabelInvisible() {
+ return isElementInvisible(classifiedDomainLabel);
+ }
+
+ public boolean isNotClassifiedLabelVisible() {
+ return unClassifiedDomainLabel.isDisplayed();
+ }
+
+ public boolean isNotClassifiedLabelInvisible() {
+ return isElementInvisible(classifiedDomainLabel);
+ }
+
+ public boolean isClassifiedLabelVisibleConvo() {
+ return classifiedDomainLabel.isDisplayed();
+ }
+
+ public boolean isClassifiedLabelInvisibleConvo() {
+ return isElementInvisible(classifiedDomainLabel);
+ }
+
+ public boolean isNotClassifiedLabelVisibleConvo() {
+ return unClassifiedDomainLabel.isDisplayed();
+ }
+
+ public boolean isNotClassifiedLabelInvisibleConvo() {
+ return isElementInvisible(unClassifiedDomainLabel);
+ }
+
+ public boolean isClassifiedLabelVisibleUserProfile() {
+ return classifiedDomainLabel.isDisplayed();
+ }
+
+ public boolean isClassifiedLabelInvisibleUserProfile() {
+ return isElementInvisible(classifiedDomainLabel);
+ }
+
+ public boolean isNotClassifiedLabelVisibleUserProfile() {
+ return unClassifiedDomainLabel.isDisplayed();
+ }
+
+ public boolean isNotClassifiedLabelInvisibleUserProfile() {
+ return isElementInvisible(unClassifiedDomainLabel);
+ }
+
+ public void tapClearInClearContent() {
+ clearInClearContent.click();
+ }
+
+ public void tapBlockConfirm() {
+ blockConfirm.click();
+ }
+
+ public void tapLeaveAndClearConfirm() {
+ leaveAndClearConfirm.click();
+ }
+
+ public boolean isCertified() {
+ return name.getAttribute("label").contains("all your devices have a valid end-to-end identity certificate");
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/CreateFolderPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/CreateFolderPage.java
new file mode 100644
index 00000000000..def0526d606
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/CreateFolderPage.java
@@ -0,0 +1,42 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import com.wearezeta.auto.common.log.ZetaLogger;
+import java.util.logging.Logger;
+import org.openqa.selenium.WebElement;
+
+public class CreateFolderPage extends IOSPage {
+ public CreateFolderPage(WebDriver driver) {
+ super(driver);
+ }
+
+ @iOSXCUITFindBy(accessibility = "Create New Folder")
+ private WebElement createNewFolderTitle;
+
+ @iOSXCUITFindBy(accessibility = "textfield.newfolder.name")
+ private WebElement folderNameTextField;
+
+ @iOSXCUITFindBy(accessibility = "button.newfolder.create")
+ private WebElement createButton;
+
+ @iOSXCUITFindBy(accessibility = "back")
+ private WebElement backButton;
+
+ public boolean isVisible() {
+ return isElementVisible(createNewFolderTitle);
+ }
+
+ public void tapBackButton() {
+ backButton.click();
+ }
+
+ public void tapCreateButton() {
+ createButton.click();
+ }
+
+ public void enterFolderName(String groupName) {
+ folderNameTextField.clear();
+ folderNameTextField.sendKeys(groupName);
+ }
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/CustomBackendRedirectionPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/CustomBackendRedirectionPage.java
new file mode 100644
index 00000000000..bfbc20361f0
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/CustomBackendRedirectionPage.java
@@ -0,0 +1,43 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.AppiumBy;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class CustomBackendRedirectionPage extends IOSPage {
+
+ public CustomBackendRedirectionPage(WebDriver driver) {
+ super(driver);
+ }
+
+ @iOSXCUITFindBy(accessibility = "Redirecting...")
+ private WebElement leavingWire;
+
+ @iOSXCUITFindBy(accessibility = "ProgressView.Timer")
+ private WebElement wireLogo;
+
+ @iOSXCUITFindBy(accessibility = "Proceed")
+ private WebElement proceedButton;
+
+ @iOSXCUITFindBy(accessibility = "Redirect to an on-premises backend?")
+ private WebElement redirectionTitle;
+
+ public boolean isVisible() {
+ return isElementVisible(wireLogo) && leavingWire.isDisplayed();
+ }
+
+ public void tapProceedButton() {
+ proceedButton.click();
+ }
+
+ public boolean isRedirectionTitleVisible() {
+ return redirectionTitle.isDisplayed();
+ }
+
+ public boolean isTextVisible(String text) {
+ By locator = AppiumBy.accessibilityId(text);
+ return waitUntilLocatorVisible(locator);
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/CustomBackendWelcomePage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/CustomBackendWelcomePage.java
new file mode 100644
index 00000000000..7c1fbb122c7
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/CustomBackendWelcomePage.java
@@ -0,0 +1,34 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import java.util.function.Function;
+
+public class CustomBackendWelcomePage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "Login")
+ private WebElement emailLoginButton;
+
+ private static final Function predicateWelcomeToBackend = backendName -> MobileBy.iOSNsPredicateString(String.format("label CONTAINS '%s'", backendName));
+
+ public CustomBackendWelcomePage(WebDriver driver) {
+ super(driver);
+ }
+
+ public boolean isTextVisible(String backendName) {
+ return isLocatorDisplayed(predicateWelcomeToBackend.apply(backendName));
+ }
+
+ public boolean isConnectionMessageVisible(String backendName) {
+ return isLocatorDisplayed(predicateWelcomeToBackend.apply(backendName));
+ }
+
+ public void tapOnLoginWithEmailButton() {
+ waitUntilElementClickable(emailLoginButton);
+ emailLoginButton.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/E2EIPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/E2EIPage.java
new file mode 100644
index 00000000000..320046d0c87
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/E2EIPage.java
@@ -0,0 +1,27 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class E2EIPage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "Get Certificate")
+ private WebElement getCertificateBtn;
+
+ @iOSXCUITFindBy(accessibility = "confirmationButton")
+ private WebElement OkBtn;
+
+ public E2EIPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void tapGetCertificateButton() {
+ waitUntilElementClickable(getCertificateBtn);
+ getCertificateBtn.click();
+ }
+
+ public void tapOkButton() {
+ OkBtn.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/EncryptionAtRestOverlay.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/EncryptionAtRestOverlay.java
new file mode 100644
index 00000000000..9e8520be530
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/EncryptionAtRestOverlay.java
@@ -0,0 +1,37 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class EncryptionAtRestOverlay extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "Wire Bund")
+ private WebElement application;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeImage' AND name == 'wire-logo-shield'")
+ private WebElement wireShieldLogo;
+
+ public EncryptionAtRestOverlay(WebDriver driver) {
+ super(driver);
+ }
+
+ public boolean isPasscodeOverlayVisible() {
+ return wireShieldLogo.isDisplayed();
+ }
+
+ public boolean isPasscodeOverlayInvisible() {
+ // currently not possible as on iOS 17 it is not possible to interact with the passcode overlay
+ // return isElementInvisible(passcodeField);
+ return true;
+ }
+
+ public void typePasscode(String passcode) {
+ application.sendKeys(passcode);
+ }
+
+ public void pressEnter() {
+ application.sendKeys(Keys.ENTER);
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/EnterpriseLoginPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/EnterpriseLoginPage.java
new file mode 100644
index 00000000000..ff67fca5d7d
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/EnterpriseLoginPage.java
@@ -0,0 +1,34 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class EnterpriseLoginPage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "textfield.sso.code")
+ private WebElement emailSSOCodeTextField;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeAlert' AND name == 'Enterprise Login'")
+ private WebElement predicateEnterpriseLogInPopup;
+
+ public EnterpriseLoginPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void typeCodeIntoEmailSSOField(String code) {
+ emailSSOCodeTextField.sendKeys(code);
+ }
+
+ public boolean isEnterpriseLoginBoxVisible() {
+ return predicateEnterpriseLogInPopup.isDisplayed();
+ }
+
+ public boolean isEnterpriseLoginBoxInvisible() {
+ return isElementInvisible(predicateEnterpriseLogInPopup);
+ }
+
+ public boolean isCancelOptionVisible() {
+ return isAlertButtonVisible("Cancel");
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/FileInspectionPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/FileInspectionPage.java
new file mode 100644
index 00000000000..db03a3fa595
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/FileInspectionPage.java
@@ -0,0 +1,26 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class FileInspectionPage extends IOSPage {
+
+ public FileInspectionPage(WebDriver driver) {
+ super(driver);
+ }
+
+ @iOSXCUITFindBy(accessibility = "QLOverlayDefaultActionButtonAccessibilityIdentifier")
+ private WebElement shareButton;
+
+ @iOSXCUITFindBy(accessibility = "QLOverlayDoneButtonAccessibilityIdentifier")
+ private WebElement doneButton;
+
+ public void tapShareButton() {
+ shareButton.click();
+ }
+
+ public void tapDoneButton() {
+ doneButton.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/FirstTimeOverlay.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/FirstTimeOverlay.java
new file mode 100644
index 00000000000..7a54881fe21
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/FirstTimeOverlay.java
@@ -0,0 +1,42 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class FirstTimeOverlay extends IOSPage {
+
+ @iOSXCUITFindBy(iOSNsPredicate = "label == 'It’s the first time you’re using Wire on this device.'")
+ private WebElement heading;
+
+ @iOSXCUITFindBy(accessibility = "ignore_backup")
+ private WebElement oKButton;
+
+ @iOSXCUITFindBy(accessibility = "restore_backup")
+ private WebElement restoreButton;
+
+ public FirstTimeOverlay(WebDriver driver) {
+ super(driver);
+ }
+
+ public boolean isHeadingVisible() {
+ return waitUntilElementVisible(heading);
+ }
+
+ public boolean waitUntilVisible() {
+ return waitUntilElementVisible(restoreButton);
+ }
+
+ public boolean waitUntilInvisible() {
+ return waitUntilElementInvisible(restoreButton);
+ }
+
+ public void accept() {
+ oKButton.click();
+ }
+
+ public void tapRestoreButton() {
+ waitUntilElementClickable(restoreButton);
+ restoreButton.click();
+ }
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/FolderViewPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/FolderViewPage.java
new file mode 100644
index 00000000000..a8fcff21efa
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/FolderViewPage.java
@@ -0,0 +1,256 @@
+package com.wearezeta.auto.ios.pages;
+
+import com.wearezeta.auto.common.CommonUtils;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import com.wearezeta.auto.common.log.ZetaLogger;
+import com.wearezeta.auto.common.misc.Timedelta;
+import io.appium.java_client.MobileBy;
+
+import java.util.*;
+import java.util.logging.Logger;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public class FolderViewPage extends IOSPage {
+ private static final Logger log = ZetaLogger.getLog(ConversationsListPage.class.getSimpleName());
+
+ @iOSXCUITFindBy(xpath = "//XCUIElementTypeCollectionView[@name=\"conversation list\"]/*")
+ List conversationListItems;
+
+ @iOSXCUITFindBy(accessibility = "PEOPLE")
+ WebElement peopleFolder;
+
+ @iOSXCUITFindBy(xpath = "//XCUIElementTypeCell[@name=\"contacts - conversation_list_cell\"]/*")
+ WebElement peopleFolderItems;
+
+ public FolderViewPage(WebDriver driver) {
+ super(driver);
+ }
+
+ private static final By nameFolderViewRoot =
+ MobileBy.iOSNsPredicateString("name ENDSWITH 'bottomBarFolderListButton' AND label == 'List of conversations organized in folders'");
+
+ private static final String classStrFolderViewRoot = "XCUIElementTypeCollectionView";
+ private static final By classFolderViewRoot = By.className(classStrFolderViewRoot);
+
+ private static final By nameGroupFolder = MobileBy.AccessibilityId("GROUPS");
+ private static final String classChainStrGroupsFolderRoot =
+ "**/XCUIElementTypeCell[`name == 'groups - conversation_list_cell'`]";
+ private static final By nameFavoritesFolder = MobileBy.AccessibilityId("FAVORITES");
+ private static final String classChainStrFavoritesFolderRoot =
+ "**/XCUIElementTypeCell[`name == 'favorites - conversation_list_cell'`]";
+ private static final String strNameConversation = "title";
+ private static final String strFolderCollapsed = "collapsed";
+ private static final String strFolderExpanded = "expanded";
+
+ private static final By classChainConversationInList =
+ MobileBy.iOSClassChain("**/XCUIElementTypeButton[`name == 'title'`]");
+
+ private static final Function predicateStrConversationByLabel = text ->
+ MobileBy.iOSNsPredicateString(String.format("name == '%s' AND label == '%s'", strNameConversation, text));
+
+ private static final Function predicateStrIsFolderCollapsed = folderName ->
+ MobileBy.iOSNsPredicateString(String.format("name == '%s' AND value BEGINSWITH '%s'", folderName, strFolderCollapsed));
+
+ private static final Function predicateStrIsFolderExpanded = folderName ->
+ MobileBy.iOSNsPredicateString(String.format("name == '%s' AND value BEGINSWITH '%s'", folderName, strFolderExpanded));
+
+ private static final Function classChainStrConversationInGroupsFolderByName = (nameConversation) -> String.format(
+ "%s/**/XCUIElementTypeButton[`name == 'title' && label == '%s'`]",
+ classChainStrGroupsFolderRoot, nameConversation);
+
+ private static final Function classChainStrConversationInFavoritesFolderByName = (nameConversation) -> String.format(
+ "%s/**/XCUIElementTypeButton[`name == 'title' && label == '%s'`]",
+ classChainStrFavoritesFolderRoot, nameConversation);
+
+ private static final BiFunction predicatedStrRelativeConversationStatusByValue = (label, value) ->
+ MobileBy.iOSNsPredicateString(String.format("name == '%s' AND label == '%s' AND value ENDSWITH '%s'", strNameConversation, label, value));
+
+ private static final BiFunction predicateStrSecondaryLine = (label, value) ->
+ MobileBy.iOSNsPredicateString(String.format("name == '%s' AND label == '%s' AND value BEGINSWITH '%s'", strNameConversation, label, value));
+
+ private static final BiFunction predicateStrFolderBadgeCount = (folderName, number) -> String.format(
+ "name == '%s' AND value ENDSWITH ' %s'", folderName, number);
+
+ private static final Function predicateStrFolderBadge = (folderName) -> String.format(
+ "name == '%s' AND value CONTAINS 'New messages:'", folderName);
+
+ public boolean isVisible() {
+ return isLocatorDisplayed(nameFolderViewRoot, Timedelta.ofSeconds(10));
+ }
+
+ public boolean isPeopleFolderVisible() {
+ return waitUntilElementVisible(peopleFolder);
+ }
+
+ public boolean isPeopleFolderInvisible() {
+ return waitUntilElementInvisible(peopleFolder);
+ }
+
+ public boolean isFavoritesFolderVisible() {
+ return isLocatorDisplayed(nameFavoritesFolder);
+ }
+
+ public boolean isFavoritesFolderInvisible() {
+ return isLocatorInvisible(nameFavoritesFolder);
+ }
+
+ public void tapPeopleFolder() {
+ peopleFolder.click();
+ }
+
+ public void tapFavoritesFolder() {
+ getElement(nameFavoritesFolder).click();
+ }
+ public void tapCustomFolder(String folderName) {
+ getElement(MobileBy.AccessibilityId(folderName.toUpperCase())).click();
+ }
+
+ public boolean isGroupFolderVisible() {
+ return isLocatorDisplayed(nameGroupFolder);
+ }
+
+ public boolean isGroupFolderInvisible() {
+ return isLocatorInvisible(nameGroupFolder);
+ }
+
+ public boolean isCustomFolderVisible(String name) {
+ return isLocatorDisplayed(MobileBy.AccessibilityId(name.toUpperCase()));
+ }
+
+ public boolean isCustomFolderInvisible(String name) {
+ return isLocatorInvisible(MobileBy.AccessibilityId(name.toUpperCase()));
+ }
+
+ public boolean isFolderExpanded(String folderName) {
+ return isLocatorDisplayed(predicateStrIsFolderExpanded.apply(folderName.toUpperCase()));
+ }
+
+ public boolean isFolderCollapsed(String folderName) {
+ return isLocatorDisplayed(predicateStrIsFolderCollapsed.apply(folderName.toUpperCase()));
+ }
+
+ public boolean isConversationInGroupsFolder(String conversationName) {
+ final By locator = MobileBy.iOSClassChain(classChainStrConversationInGroupsFolderByName.apply(conversationName));
+ return isLocatorDisplayed(locator);
+ }
+
+ public boolean isConversationNotInGroupsFolder(String conversationName) {
+ final By locator = MobileBy.iOSClassChain(classChainStrConversationInGroupsFolderByName.apply(conversationName));
+ return isLocatorInvisible(locator);
+ }
+
+ public boolean isConversationInFavoritesFolder(String conversationName) {
+ final By locator = MobileBy.iOSClassChain(classChainStrConversationInFavoritesFolderByName.apply(conversationName));
+ return isLocatorDisplayed(locator);
+ }
+
+ public boolean isConversationNotInFavoritesFolder(String conversationName) {
+ final By locator = MobileBy.iOSClassChain(classChainStrConversationInFavoritesFolderByName.apply(conversationName));
+ return isLocatorInvisible(locator);
+ }
+
+ public List getConversationOfPeopleFolder() {
+ List buttons = peopleFolderItems.findElements(classChainConversationInList);
+ return buttons.stream().map(button -> button.getAttribute("label")).collect(Collectors.toList());
+ }
+
+ public List getConversationsInCustomFolder(String folderName) {
+ List folders = getConversationsWithFolderInformation();
+ for (Folder folder : folders) {
+ log.info("Folder " + folder.name + " contains: " + String.join(",", folder.conversations));
+ if (folder.name.equalsIgnoreCase(folderName)) {
+ return folder.conversations;
+ }
+ }
+ return Collections.emptyList();
+ }
+
+ // data structure to hold folder names and conversations in folder
+ private class Folder {
+ public String name;
+ public List conversations = new ArrayList<>();
+
+ public Folder(String name) {
+ this.name = name;
+ }
+
+ public void addConversation(String name) {
+ conversations.add(name);
+ }
+ }
+
+ // This method creates a map containing the conversation name and the custom folder in which it is
+ public List getConversationsWithFolderInformation() {
+ List folders = new ArrayList<>();
+ Folder currentFolder = null;
+ // Create
+ for (WebElement element : conversationListItems) {
+ if (element.getAttribute("type").equals("XCUIElementTypeOther")) {
+ // If folder element: remember which is the current custom folder
+ currentFolder = new Folder(element.getAttribute("label"));
+ folders.add(currentFolder);
+ } else if (element.getAttribute("type").equals("XCUIElementTypeCell")) {
+ // If conversation element: get individual conversation element
+ WebElement button = element.findElement(classChainConversationInList);
+ currentFolder.addConversation(button.getAttribute("label"));
+ }
+ }
+ return folders;
+ }
+
+ public boolean isConversationItemWithStatusVisible(String status, String conversation) {
+ final WebElement root = getElement(classFolderViewRoot);
+ return isLocatorDisplayed(root, predicatedStrRelativeConversationStatusByValue.apply(conversation, status));
+ }
+
+ public boolean isConversationItemWithStatusInvisible(String status, String conversation) {
+ final WebElement root = getElement(classFolderViewRoot);
+ return isLocatorInvisible(root, predicatedStrRelativeConversationStatusByValue.apply(conversation, status));
+ }
+
+ public boolean isSecondaryLineVisible(String conversation, String secondaryLine) {
+ final By locator = predicateStrSecondaryLine.apply(conversation, secondaryLine);
+ return isLocatorDisplayed(locator, Timedelta.ofSeconds(3));
+ }
+
+ public boolean isSecondaryLineInvisible(String conversation, String secondaryLine) {
+ final By locator = predicateStrSecondaryLine.apply(conversation, secondaryLine);
+ return isLocatorInvisible(locator, Timedelta.ofSeconds(3));
+ }
+
+ public void tapConversationItemGroupedList(String conversationName) {
+ getConversationsListItem(conversationName, Timedelta.ofSeconds(10)).click();
+ }
+
+ public void swipeRightOnGroupedConversation(String name) {
+ final WebElement convoListItem = getConversationsListItem(name, Timedelta.ofSeconds(5));
+ if (!CommonUtils.waitUntilTrue(Timedelta.ofSeconds(5), Timedelta.ofMillis(1), () -> {
+ swipe(convoListItem, SwipeDirection.RIGHT);
+ return isElementInvisible(convoListItem, Timedelta.ofSeconds(1));
+ })) {
+ log.warning(String.format("The conversation item '%s' is still visible after being swiped to the right", name));
+ }
+ }
+
+ private WebElement getConversationsListItem(String name, Timedelta timeout) {
+ final By locator = predicateStrConversationByLabel.apply(name);
+ return getElement(locator,
+ String.format("The conversation '%s' is not visible in the list after %s", name, timeout), timeout);
+ }
+
+ public boolean isBadgeCountForFolder(String folderName, int number) {
+ final By locator = MobileBy.iOSNsPredicateString(predicateStrFolderBadgeCount.apply(folderName.toUpperCase(), number));
+ return isLocatorDisplayed(locator);
+ }
+
+ public boolean isBadgeCountInvisibleForFolder(String folderName) {
+ final By locator = MobileBy.iOSNsPredicateString(predicateStrFolderBadge.apply(folderName.toUpperCase()));
+ return isLocatorInvisible(locator);
+ }
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ForwardPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ForwardPage.java
new file mode 100644
index 00000000000..710825578b9
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ForwardPage.java
@@ -0,0 +1,90 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import java.util.function.Function;
+
+public class ForwardPage extends IOSPage {
+
+ public ForwardPage(WebDriver driver) {
+ super(driver);
+ }
+
+ @iOSXCUITFindBy(accessibility = "shield icon")
+ private WebElement shieldIcon;
+
+ @iOSXCUITFindBy(accessibility = "legalHoldIcon")
+ private WebElement legalHoldIcon;
+
+ @iOSXCUITFindBy(accessibility = "guestUserIcon")
+ private WebElement guestUserIcon;
+
+ @iOSXCUITFindBy(accessibility = "img.external")
+ private WebElement externalIcon;
+
+ @iOSXCUITFindBy(accessibility = "send")
+ private WebElement sendButton;
+
+ @iOSXCUITFindBy(accessibility = "close")
+ private WebElement closeButton;
+
+ private static final Function conversationLocatorByName = name -> MobileBy.iOSClassChain(
+ String.format("**/XCUIElementTypeOther[$name == 'textViewSearch'$]" +
+ "/**/XCUIElementTypeCell[$type == 'XCUIElementTypeStaticText' AND name == '%s'$]",
+ name));
+
+ public void selectConversation(String name) {
+ getElement(conversationLocatorByName.apply(name)).click();
+ }
+
+ public void tapSendButton() {
+ sendButton.click();
+ }
+
+ public void tapCloseButton() {
+ closeButton.click();
+ }
+
+ public boolean isConversationVisible(String name) {
+ return getDriver().findElement(conversationLocatorByName.apply(name)).isDisplayed();
+ }
+
+ public boolean isConversationInvisible(String name) {
+ return isLocatorInvisible(conversationLocatorByName.apply(name));
+ }
+
+ public boolean isShieldIconVisible() {
+ return shieldIcon.isDisplayed();
+ }
+
+ public boolean isShieldIconInvisible() {
+ return isElementInvisible(shieldIcon);
+ }
+
+ public boolean isLegalHoldIndicatorVisible() {
+ return legalHoldIcon.isDisplayed();
+ }
+
+ public boolean isLegalHoldIndicatorInvisible() {
+ return isElementInvisible(legalHoldIcon);
+ }
+
+ public boolean isGuestIconVisible() {
+ return guestUserIcon.isDisplayed();
+ }
+
+ public boolean isGuestIconInvisible() {
+ return isElementInvisible(guestUserIcon);
+ }
+
+ public boolean isExternalIconVisible() {
+ return externalIcon.isDisplayed();
+ }
+
+ public boolean isExternalIconInvisible() {
+ return isElementInvisible(externalIcon);
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/GiphyPreviewPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/GiphyPreviewPage.java
new file mode 100644
index 00000000000..edf8cfe27b7
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/GiphyPreviewPage.java
@@ -0,0 +1,49 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class GiphyPreviewPage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "CANCEL")
+ private WebElement cancelButton;
+
+ @iOSXCUITFindBy(accessibility = "SEND")
+ private WebElement sendButton;
+
+ @iOSXCUITFindBy(accessibility = "giphyCollectionView")
+ private WebElement previewGrid;
+
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeCollectionView/*[1]")
+ private WebElement firstGiphyInGrid;
+
+ public GiphyPreviewPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public boolean isGridVisible() {
+ return previewGrid.isDisplayed();
+ }
+
+ public void selectFirstItem() {
+ waitUntilElementVisible(firstGiphyInGrid);
+ firstGiphyInGrid.click();
+ }
+
+ public void tapSendButton() {
+ sendButton.click();
+ }
+
+ public void tapCancelButton() {
+ cancelButton.click();
+ }
+
+ public boolean isSendButtonVisible() {
+ return sendButton.isDisplayed();
+ }
+
+ public boolean isCancelButtonVisible() {
+ return cancelButton.isDisplayed();
+ }
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/HistoryBackupPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/HistoryBackupPage.java
new file mode 100644
index 00000000000..e61737c4474
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/HistoryBackupPage.java
@@ -0,0 +1,26 @@
+package com.wearezeta.auto.ios.pages;
+
+import com.wearezeta.auto.common.misc.Timedelta;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class HistoryBackupPage extends IOSPage {
+
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeCell[$type == 'XCUIElementTypeStaticText' AND value == 'Back Up Now'$]")
+ private WebElement backUpNowButton;
+
+ public static final Timedelta BACKUP_TIMEOUT = Timedelta.ofSeconds(15);
+
+ public HistoryBackupPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void initiateHistoryBackup() {
+ backUpNowButton.click();
+ }
+
+ public boolean isBackupNowButtonShown() {
+ return isElementVisible(backUpNowButton, BACKUP_TIMEOUT);
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/IOSPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/IOSPage.java
new file mode 100644
index 00000000000..88e16480200
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/IOSPage.java
@@ -0,0 +1,1065 @@
+package com.wearezeta.auto.ios.pages;
+
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.time.Duration;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import com.google.common.collect.ImmutableMap;
+import com.wearezeta.auto.common.Config;
+import com.wearezeta.auto.common.ImageUtil;
+import com.wearezeta.auto.common.backend.Backend;
+import com.wearezeta.auto.common.backend.BackendConnections;
+import com.wearezeta.auto.common.email.MailboxProvider;
+import com.wearezeta.auto.common.email.handlers.ISupportsMessagesPolling;
+import com.wearezeta.auto.common.email.messages.VerificationMessage;
+import com.wearezeta.auto.common.email.messages.WireMessage;
+import com.wearezeta.auto.common.imagecomparator.QRCode;
+import com.wearezeta.auto.common.log.ZetaLogger;
+import com.wearezeta.auto.common.misc.Timedelta;
+import com.wearezeta.auto.common.usrmgmt.ClientUser;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.common.Lifecycle;
+import io.appium.java_client.AppiumBy;
+import io.appium.java_client.MobileBy;
+import io.appium.java_client.ios.IOSDriver;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+
+import java.util.logging.Logger;
+
+import org.openqa.selenium.*;
+import com.wearezeta.auto.ios.pages.keyboard.IOSKeyboard;
+import org.openqa.selenium.Dimension;
+import org.openqa.selenium.NoSuchElementException;
+import org.openqa.selenium.Point;
+import org.openqa.selenium.Rectangle;
+import org.openqa.selenium.TimeoutException;
+import org.openqa.selenium.interactions.Actions;
+import org.openqa.selenium.logging.LogEntries;
+import org.openqa.selenium.remote.RemoteWebDriver;
+import org.openqa.selenium.remote.RemoteWebElement;
+import org.openqa.selenium.support.ui.*;
+
+import javax.annotation.Nullable;
+import javax.imageio.ImageIO;
+
+public class IOSPage {
+ private static final Logger log = ZetaLogger.getLog(IOSPage.class.getSimpleName());
+
+ @iOSXCUITFindBy(accessibility = "Allow Access to All Photos")
+ private WebElement allowAccessToAllPhotosItem;
+
+ @iOSXCUITFindBy(accessibility = "Not Now")
+ private WebElement notNowButton;
+
+ private static final String CRASHLOG = "crashlog";
+ private static final String SERVER_LOG = "server";
+ private static final String STRING_NOTIFICATION_ALERT = "Would Like to Send You Notifications";
+
+ private static final int DEFAULT_RETRY_COUNT = 2;
+
+ private static final Function predicateAlertLabelByText = text ->
+ MobileBy.iOSNsPredicateString(String.format("label CONTAINS '%s'", text));
+
+ private static final By classAlertTitle = By.className("XCUIElementTypeAlert");
+
+ private static final By classAlertDescription = By.xpath("//XCUIElementTypeAlert//XCUIElementTypeStaticText[2]");
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeButton' AND name == 'Cancel'")
+ private WebElement cancelButton;
+
+ // Self node can only be found by xpath
+ @iOSXCUITFindBy(xpath = "//XCUIElementTypeApplication[@name='Maps']")
+ private WebElement mapsApplicationButton;
+
+ private IOSKeyboard onScreenKeyboard;
+
+ protected final WebDriver driver;
+
+ public IOSPage(WebDriver driver) {
+ this.driver = driver;
+ }
+
+ protected IOSDriver getDriver() {
+ return (IOSDriver) this.driver;
+ }
+
+ private IOSKeyboard getOnScreenKeyboard() {
+ if (this.onScreenKeyboard == null) {
+ this.onScreenKeyboard = new IOSKeyboard(getDriver());
+ }
+ return this.onScreenKeyboard;
+ }
+
+ public boolean isKeyboardVisible() {
+ return this.getOnScreenKeyboard().isVisible();
+ }
+
+ public boolean isKeyboardInvisible(Timedelta timeout) {
+ return this.getOnScreenKeyboard().isInvisible(timeout);
+ }
+
+ public void tapHideKeyboardButton() {
+ //The Appium method hideKeyboard() is known to be unstable
+ // https://developers.perfectomobile.com/display/TT/iOS+Limitation%3A+Appium+hideKeyboard+Method
+ //getDriver().hideKeyboard();
+ this.getOnScreenKeyboard().pressHideButton();
+ }
+
+ public void tapSpaceKeyboardButton() {
+ this.getOnScreenKeyboard().pressSpaceButton();
+ }
+
+ public void tapNextKeyboardButton() {
+ this.getOnScreenKeyboard().pressNextButton();
+ }
+
+ public void tapKeyboardCommitButton() {
+ this.getOnScreenKeyboard().pressCommitButton();
+ }
+
+ public boolean waitUntilAlertIsVisible(Duration timeout) {
+ new WebDriverWait(getDriver(), timeout)
+ .ignoring(NoAlertPresentException.class)
+ .withMessage("No alert has been shown")
+ .until(ExpectedConditions.alertIsPresent());
+ return true;
+ }
+
+ public boolean waitUntilAlertIsVisible(int seconds) {
+ return getElementIfDisplayed(classAlertTitle, Timedelta.ofSeconds(seconds)).isPresent();
+ }
+
+ public boolean acceptAlertIfVisible() {
+ try {
+ acceptAlert(Timedelta.ofSeconds(5));
+ return true;
+ } catch (TimeoutException e) {
+ log.info(String.format("Did not accept the alert: %s", e.getMessage()));
+ return false;
+ }
+ }
+
+ public boolean isNotNowOnPasswordPromptVisible() {
+ log.info("Password keychain shown?");
+ try {
+ getDriver().manage().timeouts().implicitlyWait(Duration.ZERO);
+ new WebDriverWait(getDriver(), Duration.ofSeconds(2))
+ .ignoring(StaleElementReferenceException.class)
+ .until(driver -> !driver.findElements(AppiumBy.accessibilityId("Not Now")).isEmpty());
+ log.info("Password keychain shown");
+ return true;
+ } catch (TimeoutException e) {
+ log.info("Password keychain question was not shown");
+ return false;
+ } finally {
+ getDriver().manage().timeouts().implicitlyWait(Duration.ofSeconds(getDefaultLookupTimeoutSeconds()));
+ }
+ }
+
+ public void tapNotNowOnPasswordPrompt() {
+ notNowButton.click();
+ }
+
+ public void acceptNotificationAlertIfVisible() {
+ if (isAlertContainsText(STRING_NOTIFICATION_ALERT)) {
+ acceptAlert();
+ }
+ }
+
+ public void acceptAlert() {
+ acceptAlert(getDefaultLookupTimeout());
+ }
+
+ public void acceptAlert(Timedelta timeout) {
+ if (waitUntilAlertIsVisible(timeout.asDuration())) {
+ getDriver().switchTo().alert().accept();
+ } else {
+ throw new TimeoutException("Alert did not appear after " + timeout.toString());
+ }
+ }
+
+ public void acceptAccessToAllPhotos() {
+ allowAccessToAllPhotosItem.click();
+ }
+
+ public void performTouchID(boolean match) {
+ getDriver().performTouchID(match);
+ }
+
+ public void typeAlertText(String text) {
+ getDriver().switchTo().alert().sendKeys(text);
+ }
+
+ private enum AlertAction {
+ ACCEPT, DISMISS, TAP_BUTTON
+ }
+
+ private void handleAlert(AlertAction action, @Nullable String buttonLabel, Timedelta timeout) {
+ if (action != AlertAction.TAP_BUTTON && buttonLabel != null) {
+ log.warning(String.format("Button caption '%s' is only supported for '%s' alert action", buttonLabel,
+ AlertAction.TAP_BUTTON.name()));
+ }
+ final Timedelta started = Timedelta.now();
+ do {
+ try {
+ switch (action) {
+ case ACCEPT:
+ getDriver().switchTo().alert().accept();
+ return;
+ case DISMISS:
+ getDriver().switchTo().alert().dismiss();
+ return;
+ case TAP_BUTTON:
+ assert buttonLabel != null;
+ getElement(MobileBy.AccessibilityId(buttonLabel)).click();
+ return;
+ default:
+ throw new IllegalArgumentException(String.format("Illegal alert action '%s'", action.name()));
+ }
+ } catch (NoAlertPresentException e) {
+ Timedelta.ofSeconds(1).sleep();
+ }
+ } while (Timedelta.now().isDiffLessOrEqual(started, timeout));
+ throw new IllegalStateException(
+ String.format("No alert has been shown or it cannot be %s after %s",
+ (action == AlertAction.ACCEPT) ? "accepted" : "dismissed", timeout)
+ );
+ }
+
+ public String getAlertTitle() {
+ waitUntilLocatorVisible(classAlertTitle);
+ return getDriver().findElement(classAlertTitle).getText();
+ }
+
+ public String getAlertDescription() {
+ waitUntilLocatorVisible(classAlertDescription);
+ return getDriver().findElement(classAlertDescription).getText();
+ }
+
+ public boolean isAlertContainsText(String expectedText) {
+ final By locator = predicateAlertLabelByText.apply(expectedText);
+ final Optional alert = getElementIfExists(classAlertTitle);
+ return alert.isPresent() && isLocatorDisplayed(alert.get(), locator);
+ }
+
+ public static boolean isTablet() {
+ return Config.current().isTablet(IOSPage.class);
+ }
+
+ public boolean isAlertDoesNotContainsText(String expectedText) {
+ final By locator = predicateAlertLabelByText.apply(expectedText);
+ final Optional alert = getElementIfExists(classAlertTitle);
+ return alert.isEmpty() || isLocatorInvisible(alert.get(), locator);
+ }
+
+ public void putWireToBackgroundFor(Timedelta duration) {
+ this.getDriver().runAppInBackground(duration.asDuration());
+ }
+
+ public void pressHomeButton() {
+ getDriver().runAppInBackground(Duration.ofMillis(-1));
+ }
+
+ public void rotateScreen(ScreenOrientation orientation) {
+ switch (orientation) {
+ case LANDSCAPE:
+ rotateLandscape();
+ break;
+ case PORTRAIT:
+ rotatePortrait();
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("Unknown orientation '%s'",
+ orientation));
+ }
+ }
+
+ private void rotateLandscape() {
+ this.getDriver().rotate(ScreenOrientation.LANDSCAPE);
+ }
+
+ private void rotatePortrait() {
+ this.getDriver().rotate(ScreenOrientation.PORTRAIT);
+ }
+
+ public void lockScreen(Timedelta duration) {
+ this.getDriver().lockDevice(duration.asDuration());
+ }
+
+ protected void tapElementWithRetryIfStillDisplayed(WebElement el, Timedelta delay, int retryCount) {
+ int counter = 0;
+ do {
+ try {
+ el.click();
+ } catch (WebDriverException | IllegalStateException e) {
+ log.fine(e.getMessage());
+ continue;
+ }
+ if (isElementInvisible(el, delay)) {
+ return;
+ }
+ } while (++counter < retryCount);
+ throw new IllegalStateException(String.format("Locator %s is still displayed after %s tap retries",
+ el, retryCount));
+ }
+
+ protected void tapElementWithRetryIfStillDisplayed(WebElement el) {
+ tapElementWithRetryIfStillDisplayed(el, Timedelta.ofSeconds(3), DEFAULT_RETRY_COUNT);
+ }
+
+ protected void tapElementWithRetryIfNextElementNotAppears(By locator, By nextLocator, Timedelta delay, int retryCount) {
+ int counter = 0;
+ do {
+ final Optional dstElement = getElementIfExists(locator);
+ if (dstElement.isPresent()) {
+ dstElement.get().click();
+ if (isLocatorExist(nextLocator, delay)) {
+ return;
+ }
+ }
+ } while (++counter < retryCount);
+ throw new IllegalStateException(String.format("Locator %s did't appear", nextLocator));
+ }
+
+ //region Elements location
+
+ protected WebElement getElement(WebElement parent, By locator) {
+ return this.getElement(parent, locator,
+ String.format("The element '%s' is not visible", locator),
+ getDefaultLookupTimeout()
+ );
+ }
+
+ @Deprecated // Please use @iOSXCUITFindBy or getDriver().findElement() instead
+ protected WebElement getElement(By locator) {
+ return this.getElement(locator,
+ String.format("The element '%s' is not visible", locator),
+ getDefaultLookupTimeout());
+ }
+
+ protected WebElement getElement(WebElement parent, By locator, String message) {
+ return this.getElement(parent, locator, message, getDefaultLookupTimeout());
+ }
+
+ @Deprecated // Please use @iOSXCUITFindBy or getDriver().findElement() instead
+ protected WebElement getElement(By locator, String message) {
+ return this.getElement(locator, message, getDefaultLookupTimeout());
+ }
+
+ private static final double MAX_EXISTENCE_DELAY_MS = 2000.0;
+ private static final long MIN_EXISTENCE_ITERATIONS_COUNT = 2;
+
+ protected WebElement getElement(@Nullable WebElement parent, By locator, String message,
+ Timedelta timeout) {
+ final Timedelta started = Timedelta.now();
+ int iterationNumber = 1;
+ do {
+ try {
+ final WebElement el;
+ if (parent == null) {
+ el = getDriver().findElement(locator);
+ } else {
+ el = parent.findElement(locator);
+ }
+ if (el.isDisplayed()) {
+ return el;
+ }
+ } catch (WebDriverException e) {
+ log.info("Failed to get element: " + e.getMessage());
+ }
+ log.fine(String.format("The element '%s' is still not visible after %s",
+ locator, Timedelta.now().diff(started).toString()));
+ Timedelta.ofMillis(MAX_EXISTENCE_DELAY_MS / iterationNumber).sleep();
+ iterationNumber++;
+ } while (Timedelta.now().isDiffLessOrEqual(started, timeout) ||
+ iterationNumber <= MIN_EXISTENCE_ITERATIONS_COUNT);
+ throw new IllegalStateException(message);
+ }
+
+ @Deprecated // Please use @iOSXCUITFindBy or getDriver().findElement() instead
+ protected WebElement getElement(By locator, String message, Timedelta timeout) {
+ return getElement(null, locator, message, timeout);
+ }
+
+ @Deprecated // please use waitUntilLocatorVisible()
+ protected boolean isLocatorExist(By locator) {
+ return this.isLocatorExist(locator, getDefaultLookupTimeout());
+ }
+
+ protected boolean isLocatorExist(By locator, Timedelta timeout) {
+ final Timedelta started = Timedelta.now();
+ int iterationNumber = 1;
+ do {
+ try {
+ final WebElement el = getDriver().findElement(locator);
+ if (el != null) {
+ return true;
+ }
+ } catch (WebDriverException e) {
+ // simply ignore
+ }
+ log.fine(String.format("The element '%s' is still not present after %s",
+ locator, Timedelta.now().diff(started).toString()));
+ Timedelta.ofMillis(MAX_EXISTENCE_DELAY_MS / iterationNumber).sleep();
+ iterationNumber++;
+ } while (Timedelta.now().isDiffLessOrEqual(started, timeout) ||
+ iterationNumber <= MIN_EXISTENCE_ITERATIONS_COUNT);
+ return false;
+ }
+
+ protected boolean isLocatorNotExist(By locator) {
+ return isLocatorNotExist(locator, getDefaultLookupTimeout());
+ }
+
+ protected boolean isLocatorNotExist(By locator, Timedelta timeout) {
+ final Timedelta started = Timedelta.now();
+ int iterationNumber = 1;
+ do {
+ try {
+ final WebElement el = getDriver().findElement(locator);
+ if (el == null) {
+ return true;
+ }
+ } catch (WebDriverException e) {
+ return true;
+ }
+ log.fine(String.format("The element '%s' is still present after %s",
+ locator, Timedelta.now().diff(started).toString()));
+ Timedelta.ofMillis(MAX_EXISTENCE_DELAY_MS / iterationNumber).sleep();
+ iterationNumber++;
+ } while (Timedelta.now().isDiffLessOrEqual(started, timeout) ||
+ iterationNumber <= MIN_EXISTENCE_ITERATIONS_COUNT);
+ return false;
+ }
+
+ protected boolean isLocatorDisplayed(WebElement parent, By locator) {
+ return this.isLocatorDisplayed(parent, locator, getDefaultLookupTimeout());
+ }
+
+ @Deprecated // please use waitUntilLocatorVisible()
+ protected boolean isLocatorDisplayed(By locator) {
+ return this.isLocatorDisplayed(locator, getDefaultLookupTimeout());
+ }
+
+ protected boolean isLocatorDisplayed(@Nullable WebElement parent, By locator, Timedelta timeout) {
+ final Timedelta started = Timedelta.now();
+ int iterationNumber = 1;
+ do {
+ try {
+ final WebElement el;
+ if (parent == null) {
+ el = getDriver().findElement(locator);
+ } else {
+ el = parent.findElement(locator);
+ }
+ if (el.isDisplayed()) {
+ return true;
+ }
+ } catch (WebDriverException e) {
+ // simply ignore
+ }
+ log.fine(String.format("The element '%s' is still not visible after %s",
+ locator, Timedelta.now().diff(started).toString()));
+ Timedelta.ofMillis(MAX_EXISTENCE_DELAY_MS / iterationNumber).sleep();
+ iterationNumber++;
+ } while (Timedelta.now().isDiffLessOrEqual(started, timeout) ||
+ iterationNumber <= MIN_EXISTENCE_ITERATIONS_COUNT);
+ return false;
+ }
+
+ protected boolean isLocatorDisplayed(By locator, Timedelta timeout) {
+ return isLocatorDisplayed(null, locator, timeout);
+ }
+
+ protected boolean isLocatorInvisible(By locator) {
+ return this.isLocatorInvisible(locator, getDefaultLookupTimeout());
+ }
+
+ protected boolean isLocatorInvisible(WebElement parent, By locator) {
+ return this.isLocatorInvisible(parent, locator, getDefaultLookupTimeout());
+ }
+
+ protected boolean isLocatorInvisible(@Nullable WebElement parent, By locator, Timedelta timeout) {
+ // TODO: Replace while loop with FluentWait
+ final Timedelta started = Timedelta.now();
+ int iterationNumber = 1;
+ do {
+ try {
+ final WebElement el;
+ if (parent == null) {
+ el = getDriver().findElement(locator);
+ } else {
+ el = parent.findElement(locator);
+ }
+ if (!el.isDisplayed()) {
+ return true;
+ }
+ } catch (WebDriverException e) {
+ return true;
+ }
+ log.fine(String.format("The element '%s' is still visible after %s",
+ locator, Timedelta.now().diff(started).toString()));
+ Timedelta.ofMillis(MAX_EXISTENCE_DELAY_MS / iterationNumber).sleep();
+ iterationNumber++;
+ } while (Timedelta.now().isDiffLessOrEqual(started, timeout) ||
+ iterationNumber <= MIN_EXISTENCE_ITERATIONS_COUNT);
+ return false;
+ }
+
+ protected boolean isLocatorInvisible(By locator, Timedelta timeout) {
+ return isLocatorInvisible(null, locator, timeout);
+ }
+
+ @Deprecated // Please use waitUntilElementInvisible
+ protected boolean isElementInvisible(WebElement element) {
+ return this.isElementInvisible(element, getDefaultLookupTimeout());
+ }
+
+ protected boolean isElementInvisible(WebElement el, Timedelta timeout) {
+ final Timedelta started = Timedelta.now();
+ int iterationNumber = 1;
+ do {
+ try {
+ if (!el.isDisplayed()) {
+ return true;
+ }
+ } catch (WebDriverException e) {
+ return true;
+ }
+ log.fine(String.format("The element '%s' is still visible after %s",
+ el, Timedelta.now().diff(started).toString()));
+ Timedelta.ofMillis(MAX_EXISTENCE_DELAY_MS / iterationNumber).sleep();
+ iterationNumber++;
+ } while (Timedelta.now().isDiffLessOrEqual(started, timeout) ||
+ iterationNumber <= MIN_EXISTENCE_ITERATIONS_COUNT);
+ return false;
+ }
+
+ protected boolean isElementVisible(WebElement element) {
+ return this.isElementVisible(element, getDefaultLookupTimeout());
+ }
+
+ protected boolean isElementVisible(WebElement el, Timedelta timeout) {
+ final Timedelta started = Timedelta.now();
+ int iterationNumber = 1;
+ do {
+ try {
+ if (el.isDisplayed()) {
+ return true;
+ }
+ } catch (WebDriverException e) {
+ // Element might not exist yet, ignore
+ }
+ log.fine(String.format("The element '%s' is still invisible after %s",
+ el, Timedelta.now().diff(started).toString()));
+ Timedelta.ofMillis(MAX_EXISTENCE_DELAY_MS / iterationNumber).sleep();
+ iterationNumber++;
+ } while (Timedelta.now().isDiffLessOrEqual(started, timeout) ||
+ iterationNumber <= MIN_EXISTENCE_ITERATIONS_COUNT);
+ return false;
+ }
+
+ protected Optional getElementIfDisplayed(@Nullable WebElement parent, By locator, Timedelta timeout) {
+ final Timedelta started = Timedelta.now();
+ int iterationNumber = 1;
+ do {
+ try {
+ final WebElement el;
+ if (parent == null) {
+ el = getDriver().findElement(locator);
+ } else {
+ el = parent.findElement(locator);
+ }
+ if (el.isDisplayed()) {
+ return Optional.of(el);
+ }
+ } catch (WebDriverException e) {
+ // simply ignore
+ }
+ log.fine(String.format("The element '%s' is still not visible after %s",
+ locator, Timedelta.now().diff(started).toString()));
+ Timedelta.ofMillis(MAX_EXISTENCE_DELAY_MS / iterationNumber).sleep();
+ iterationNumber++;
+ } while (Timedelta.now().isDiffLessOrEqual(started, timeout));
+ return Optional.empty();
+ }
+
+ protected Optional getElementIfDisplayed(By locator, Timedelta timeout) {
+ return getElementIfDisplayed(null, locator, timeout);
+ }
+
+ protected Optional getElementIfExists(By locator) {
+ return this.getElementIfExists(locator, getDefaultLookupTimeout());
+ }
+
+ protected Optional getElementIfExists(By locator, Timedelta timeout) {
+ final Timedelta started = Timedelta.now();
+ int iterationNumber = 1;
+ do {
+ try {
+ final WebElement el = getDriver().findElement(locator);
+ if (el != null) {
+ return Optional.of(el);
+ }
+ } catch (WebDriverException e) {
+ // simply ignore
+ }
+ log.fine(String.format("The element '%s' is still not present after %s",
+ locator, Timedelta.now().diff(started).toString()));
+ Timedelta.ofMillis(MAX_EXISTENCE_DELAY_MS / iterationNumber).sleep();
+ iterationNumber++;
+ } while (Timedelta.now().isDiffLessOrEqual(started, timeout));
+ return Optional.empty();
+ }
+
+ protected List selectVisibleElements(By locator) {
+ return selectVisibleElements(null, locator, getDefaultLookupTimeout());
+ }
+
+ protected List selectVisibleElements(@Nullable WebElement parent, By locator) {
+ return selectVisibleElements(parent, locator, getDefaultLookupTimeout());
+ }
+
+ protected List selectVisibleElements(By locator, Timedelta timeout) {
+ return selectVisibleElements(null, locator, timeout);
+ }
+
+ protected List selectVisibleElements(@Nullable WebElement parent, By locator, Timedelta timeout) {
+ return selectElements(parent, locator, timeout)
+ .stream()
+ .filter(WebElement::isDisplayed)
+ .collect(Collectors.toList());
+ }
+
+ protected List selectElements(@Nullable WebElement parent, By locator, Timedelta timeout) {
+ final List result = new ArrayList<>();
+ final Timedelta started = Timedelta.now();
+ int iterationNumber = 1;
+ do {
+ if (parent == null) {
+ result.addAll(getDriver().findElements(locator));
+ } else {
+ result.addAll(parent.findElements(locator));
+ }
+ if (result.size() > 0) {
+ return result;
+ }
+ Timedelta.ofMillis(MAX_EXISTENCE_DELAY_MS / iterationNumber).sleep();
+ iterationNumber++;
+ } while (Timedelta.now().isDiffLessOrEqual(started, timeout));
+ return result;
+ }
+
+ //endregion
+
+ @Deprecated // Please create individual page and step classes for such buttons
+ public void tapCancelButton() {
+ waitUntilElementClickable(cancelButton);
+ cancelButton.click();
+ }
+
+ public void installApp(File appFile) {
+ getDriver().installApp(appFile.getAbsolutePath());
+ // simctl returns too early sometimes
+ Timedelta.ofSeconds(3).sleep();
+ }
+
+ private static Point sizePercentsToRelativeCoordinates(WebElement el, int percentX, int percentY) {
+ final Rectangle elRect = el.getRect();
+ final int tapX = elRect.width * percentX / 100;
+ final int tapY = elRect.height * percentY / 100;
+ return new Point(tapX, tapY);
+ }
+
+ protected void longTapWithScript(WebElement el) {
+ getDriver().executeScript("mobile: touchAndHold",
+ ImmutableMap.of("element", ((RemoteWebElement) el).getId(),
+ "duration", (Duration.ofSeconds(3).toMillis() * 1.0 / 1000)));
+ }
+
+ protected void longTapWithScript(WebElement el, int percentX, int percentY) {
+ final Point tapPoint = sizePercentsToRelativeCoordinates(el, percentX, percentY);
+ getDriver().executeScript("mobile: touchAndHold",
+ ImmutableMap.of("element", ((RemoteWebElement) el).getId(),
+ "x", tapPoint.x,
+ "y", tapPoint.y,
+ "duration", (Duration.ofSeconds(3).toMillis() * 1.0 / 1000)));
+ }
+
+ protected void longTapWithActionsAPI(WebElement el) throws InterruptedException {
+ longTapWithActionsAPI(el, Duration.ofSeconds(2));
+ }
+
+ protected void longTapWithActionsAPI(WebElement el, Duration duration) throws InterruptedException {
+ Actions actions = new Actions(getDriver());
+ actions.moveToElement(el).clickAndHold().pause(duration).release().perform();
+ }
+
+ protected void tapByPercentOfElementSize(WebElement el, int percentX, int percentY) {
+ //TODO adjust method to calculate offset needed instead of by percentage, or refactor to remove behaviour in general
+ final Point tapPoint = sizePercentsToRelativeCoordinates(el, percentX, percentY);
+
+ Actions actions = new Actions(getDriver());
+ actions.click(el).perform();
+ }
+
+ protected void tapAtTheCenterOfElement(WebElement el) {
+ tapByPercentOfElementSize(el, 50, 50);
+ }
+
+ protected void tapAtTheLeftSideOfElement(WebElement el) {
+ Actions actions = new Actions(getDriver());
+ actions.click(el).perform();
+ }
+
+ protected void tapScreenAt(int x, int y) {
+ getDriver().executeScript("mobile: tap",
+ ImmutableMap.of("x", x,
+ "y", y));
+ }
+
+ public void tapScreenByPercents(int percentX, int percentY) {
+ final Dimension size = getDriver().manage().window().getSize();
+ tapScreenAt(percentX * size.getWidth() / 100, percentY * size.getHeight() / 100);
+ }
+
+ public Optional readAlertText() {
+ return readAlertText(getDefaultLookupTimeout());
+ }
+
+ private Optional readAlertText(Timedelta timeout) {
+ final Timedelta start = Timedelta.now();
+ do {
+ try {
+ final String text = getDriver().switchTo().alert().getText();
+ if (text != null && text.length() > 0 && !text.equals("null") && !text.equals("{}")) {
+ return Optional.of(text);
+ }
+ } catch (WebDriverException e) {
+ Timedelta.ofSeconds(1).sleep();
+ }
+ } while (Timedelta.now().isDiffLessOrEqual(start, timeout));
+ return Optional.empty();
+ }
+
+ public void tapAlertButton(String caption) {
+ handleAlert(AlertAction.TAP_BUTTON, caption, getDefaultLookupTimeout());
+ }
+
+ public boolean isAlertButtonVisible(String caption) {
+ HashMap args = new HashMap<>();
+ args.put("action", "getButtons");
+ List buttons = (List) getDriver().executeScript("mobile: alert", args);
+
+ for (String button : buttons) {
+ if (button.equals(caption)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public Optional takeScreenshot() {
+ Optional screenshotImage;
+ try {
+ screenshotImage = takeFullScreenShot();
+ } catch (Exception e) {
+ return Optional.empty();
+ }
+ if (screenshotImage.isPresent()) {
+ final Dimension screenSize = getDriver().manage().window().getSize();
+ if (screenshotImage.get().getWidth() != screenSize.getWidth()) {
+ // proportions are expected to be the same
+ final double scale = 1.0 * screenSize.getWidth() / screenshotImage.get().getWidth();
+ screenshotImage = Optional.of(
+ ImageUtil.resizeImage(screenshotImage.get(), (float) scale)
+ );
+ }
+ }
+ return screenshotImage;
+ }
+
+ public Optional getElementScreenshot(WebElement element) {
+ final byte[] srcImage = element.getScreenshotAs(OutputType.BYTES);
+ try {
+ return Optional.ofNullable(ImageIO.read(new ByteArrayInputStream(srcImage)));
+ } catch (IOException e) {
+ e.printStackTrace();
+ return Optional.empty();
+ }
+ }
+
+ public boolean isDefaultMapApplicationVisible() {
+ return waitUntilElementVisible(mapsApplicationButton);
+ }
+
+ public LogEntries getCrashLogs() {
+ return getDriver().manage().logs().get(CRASHLOG);
+ }
+
+ public LogEntries getAppiumLogs() {
+ return getDriver().manage().logs().get(SERVER_LOG);
+ }
+
+ public List getWireLogs() {
+ final String currentLogPath = String.format(Config.current().isSimulator(getClass())
+ ? "@%s:data/Library/Caches/current.log"
+ : "@%s/Library/Caches/current.log",
+ Lifecycle.getBundleId());
+ try {
+ final byte[] content = getDriver().pullFile(currentLogPath);
+ final String logContent = new String(content, Charset.forName("UTF-8"));
+ return Arrays.asList(logContent.split("\n"));
+ } catch (WebDriverException e) {
+ e.printStackTrace();
+ }
+ return Collections.emptyList();
+ }
+
+ public enum SwipeDirection {
+ UP, DOWN, LEFT, RIGHT
+ }
+
+ public void swipe(SwipeDirection direction) {
+ swipe(null, direction);
+ }
+
+ protected WebElement swipe(@Nullable WebElement el, SwipeDirection direction) {
+ final Map args;
+ if (el == null) {
+ args = ImmutableMap.of("direction", direction.name().toLowerCase());
+ } else {
+ args = ImmutableMap.of("direction", direction.name().toLowerCase(),
+ "element", ((RemoteWebElement) el).getId());
+ }
+ getDriver().executeScript("mobile: swipe", args);
+ return el;
+ }
+ public void setClipboard(String text) {
+ getDriver().setClipboardText(text);
+ }
+
+ public void terminateApp(String bundleId) {
+ getDriver().terminateApp(bundleId);
+ }
+
+ public void openURL(String url) {
+ getDriver().get(url);
+ }
+
+ public void openDeepLink(String deeplink, String browser) {
+ log.fine("deeplink: "+deeplink);
+ this.activateApp(browser);
+ this.openURL(deeplink);
+ }
+
+ public void activateApp(String bundleId) {
+ getDriver().activateApp(bundleId);
+ }
+
+ /*
+ * Former DriverUtils methods
+ */
+
+ protected static Timedelta getDefaultLookupTimeout() {
+ return Timedelta.ofSeconds(getDefaultLookupTimeoutSeconds());
+ }
+
+ public static int getDefaultLookupTimeoutSeconds() {
+ return Integer.parseInt(Config.current().getDriverTimeout(IOSPage.class));
+ }
+
+ public boolean waitUntilElementVisible(WebElement element) {
+ return waitUntilElementVisible(element, Duration.ofSeconds(getDefaultLookupTimeoutSeconds()));
+ }
+
+ public boolean waitUntilElementVisible(WebElement element, Duration duration) {
+ new WebDriverWait(getDriver(), duration)
+ .ignoring(StaleElementReferenceException.class)
+ .until(ExpectedConditions.visibilityOf(element));
+ return element.isDisplayed();
+ }
+
+ public boolean waitUntilElementInvisible(WebElement element) {
+ return waitUntilElementInvisible(element, Duration.ofSeconds(getDefaultLookupTimeoutSeconds()));
+ }
+
+ public boolean waitUntilElementInvisible(WebElement element, Duration timeout) {
+ try {
+ // For checking invisibility the implicit wait needs to be deactivated
+ driver.manage().timeouts().implicitlyWait(0, TimeUnit.SECONDS);
+ FluentWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(getDefaultLookupTimeoutSeconds()))
+ .pollingEvery(Duration.ofMillis(500))
+ .ignoring(StaleElementReferenceException.class);
+ // Unfortunately ExpectedConditions.invisibilityOf() cannot be used here, because selenium does not return
+ // true on a NoSuchElementException when using an element (for invisibilityOfElementLocated it works)
+ return wait.until((drv) -> {
+ try {
+ return !element.isDisplayed();
+ } catch (NoSuchElementException e) {
+ return true;
+ }
+ });
+ } catch (TimeoutException e) {
+ return false;
+ } finally {
+ driver.manage().timeouts().implicitlyWait(getDefaultLookupTimeoutSeconds(), TimeUnit.SECONDS);
+ }
+ }
+
+ public boolean waitUntilElementClickable(WebElement element) {
+ try {
+ new WebDriverWait(getDriver(), Duration.ofSeconds(getDefaultLookupTimeoutSeconds()))
+ .ignoring(StaleElementReferenceException.class)
+ .until(ExpectedConditions.elementToBeClickable(element));
+ Wait extends RemoteWebDriver> waitStopped = new FluentWait<>(getDriver())
+ .withTimeout(Duration.ofSeconds(getDefaultLookupTimeoutSeconds()))
+ .pollingEvery(Duration.ofMillis(100))
+ .ignoring(NoSuchElementException.class)
+ .ignoring(StaleElementReferenceException.class);
+ waitStopped.until(elementStoppedMoving(element));
+ return true;
+ } catch (TimeoutException e) {
+ return false;
+ }
+ }
+
+ private ExpectedCondition elementStoppedMoving(final WebElement element) {
+ return new ExpectedCondition() {
+
+ private Point location = null;
+
+ public WebElement apply(WebDriver driver) {
+ if (element.isDisplayed()) {
+ Point currentLocation = element.getLocation();
+ if (currentLocation.equals(location)) {
+ return element;
+ }
+ location = currentLocation;
+ }
+
+ return null;
+ }
+
+ public String toString() {
+ return "steadiness of element " + element;
+ }
+ };
+ }
+
+ public boolean waitUntilLocatorVisible(By locator) {
+ return waitUntilLocatorVisible(locator, getDefaultLookupTimeout().asDuration());
+ }
+
+ public boolean waitUntilLocatorVisible(By locator, Duration duration) {
+ new WebDriverWait(getDriver(), duration)
+ .ignoring(StaleElementReferenceException.class)
+ .until(ExpectedConditions.visibilityOfElementLocated(locator));
+ return true;
+ }
+
+ public int waitUntilNumberOfElementsToBe(By locator, int number) {
+ // very slow on real device, so we increase timeout by 3
+ return new WebDriverWait(getDriver(), Duration.ofSeconds(getDefaultLookupTimeoutSeconds() * 3L))
+ .ignoring(StaleElementReferenceException.class)
+ .until(ExpectedConditions.numberOfElementsToBe(locator, number))
+ .size();
+ }
+
+ public Optional takeFullScreenShot() {
+ try {
+ final byte[] srcImage = getDriver().getScreenshotAs(OutputType.BYTES);
+ final BufferedImage bImageFromConvert = ImageIO.read(new ByteArrayInputStream(srcImage));
+ return Optional.ofNullable(bImageFromConvert);
+ } catch (WebDriverException | NoClassDefFoundError | IOException e) {
+ log.severe("Selenium driver has failed to take the screenshot of the current screen!");
+ }
+ return Optional.empty();
+ }
+
+ public void pushFile(String fileName, String path) {
+ try {
+ getDriver().pushFile(fileName, new File(path));
+ } catch (IOException inputOutputException) {
+ inputOutputException.printStackTrace();
+ throw new IllegalArgumentException(
+ String.format("Something went wrong while pushing the file %s to the device", path));
+ }
+ }
+
+ public boolean doesFileExistOnDevice(String fileName) {
+ try {
+ getDriver().pullFile(fileName);
+ } catch (WebDriverException e) {
+ return false;
+ }
+ return true;
+ }
+
+ public List waitUntilElementContainsQRCode(final WebElement element) {
+ try {
+ Wait extends RemoteWebDriver> wait = new FluentWait<>(getDriver())
+ .withTimeout(Duration.ofSeconds(getDefaultLookupTimeoutSeconds()))
+ .pollingEvery(Duration.ofSeconds(1))
+ .ignoring(NoSuchElementException.class)
+ .ignoring(StaleElementReferenceException.class);
+ return wait.until(elementContainsQRCode(element));
+ } catch (TimeoutException e) {
+ return Collections.emptyList();
+ }
+ }
+
+ private ExpectedCondition> elementContainsQRCode(final WebElement element) {
+ return driver -> {
+ Optional screenshot = getElementScreenshot(element);
+ if (screenshot.isEmpty()) {
+ log.info("Could not get screenshot of element");
+ return null;
+ }
+ BufferedImage actualImage = screenshot.get();
+ List codes;
+ try {
+ codes = QRCode.readMultipleCodes(actualImage);
+ } catch (com.google.zxing.NotFoundException e) {
+ log.info("Element contains no QR code");
+ return null;
+ }
+ if (codes.isEmpty()) {
+ return null;
+ } else {
+ return codes;
+ }
+ };
+ }
+
+ private static final String SAFARI = "com.apple.mobilesafari";
+
+ public void openDeepLinkForDefault() {
+ Backend backend = BackendConnections.getDefault();
+ String protocolHandler = "wire";
+
+ if (backend.getBackendName().contains("column")) {
+ protocolHandler = backend.getBackendName().contains("column-1") ? "wire-bk-test" : "wire-c3-test";
+ }
+
+ String deeplink = backend.getDeeplinkForiOS(protocolHandler);
+ log.fine("deeplink: " + deeplink);
+ activateApp(SAFARI);
+ openURL(deeplink);
+ }
+
+ public void startVerificationEmailMonitoring(ClientUser user, IOSTestContext context) throws Exception {
+ final Map expectedHeaders = new HashMap<>();
+ expectedHeaders.put(WireMessage.ZETA_PURPOSE_HEADER_NAME, VerificationMessage.MESSAGE_PURPOSE);
+
+ ISupportsMessagesPolling mailbox = MailboxProvider.getInstance(BackendConnections.get(user), user.getEmail());
+ context.setVerificationMessage(mailbox.getMessage(expectedHeaders, VerificationMessage.ACTIVATION_TIMEOUT));
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ImageFullScreenPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ImageFullScreenPage.java
new file mode 100644
index 00000000000..978a3911f45
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ImageFullScreenPage.java
@@ -0,0 +1,35 @@
+package com.wearezeta.auto.ios.pages;
+
+import java.awt.image.BufferedImage;
+import java.util.Optional;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class ImageFullScreenPage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "close")
+ private WebElement closeButton;
+
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeScrollView[`name == 'fullScreenPage'`]/XCUIElementTypeImage")
+ private WebElement fullScreenImage;
+
+ public ImageFullScreenPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public boolean isImageFullScreenShown() {
+ return waitUntilElementVisible(fullScreenImage);
+ }
+
+ public void tapFullScreenCloseButton() {
+ waitUntilElementClickable(closeButton);
+ closeButton.click();
+ }
+
+ public Optional getPreviewPictureScreenshot() {
+ return Optional.ofNullable(getElementScreenshot(fullScreenImage)
+ .orElseThrow(() -> new IllegalStateException("No visible images are detected in fullscreen mode")));
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/KeyboardGalleryPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/KeyboardGalleryPage.java
new file mode 100644
index 00000000000..f36566e9560
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/KeyboardGalleryPage.java
@@ -0,0 +1,42 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class KeyboardGalleryPage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "cameraRollButton")
+ private WebElement openCameraRollButton;
+
+ @iOSXCUITFindBy(accessibility = "fullscreenCameraButton")
+ private WebElement fullscreenCameraButton;
+
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeOther[$name == 'cameraRollButton'$]/" +
+ "**/XCUIElementTypeCollectionView/XCUIElementTypeCell[2]")
+ private WebElement firstGalleryPicture;
+
+ public KeyboardGalleryPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void selectFirstPicture() {
+ firstGalleryPicture.click();
+ }
+
+ public void tapCameraRollButton(){
+ openCameraRollButton.click();
+ }
+
+ public void tapFullScreenButton(){
+ fullscreenCameraButton.click();
+ }
+
+ public boolean isFirstItemGalleryVisible(){
+ return firstGalleryPicture.isDisplayed();
+ }
+
+ public boolean isFirstItemGalleryInvisible(){
+ return isElementInvisible(firstGalleryPicture);
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/LegalHoldOverviewPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/LegalHoldOverviewPage.java
new file mode 100644
index 00000000000..95c14601615
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/LegalHoldOverviewPage.java
@@ -0,0 +1,62 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import java.util.function.Function;
+
+public class LegalHoldOverviewPage extends IOSPage {
+
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeCollectionView[`name == 'list.legalhold'`]")
+ private WebElement legalHoldPage;
+
+ private static final String classChainLegalHoldTitle = "**/XCUIElementTypeNavigationBar[`name == 'LEGAL HOLD'`]";
+
+ private static final String strItemName = "user_cell.name";
+
+ private static final String classChainstrViewRoot = "**/XCUIElementTypeCell[`name == 'participants.section.participants.cell'`]";
+
+ private final By classChainCloseButton = MobileBy.iOSClassChain(String.format("%s/XCUIElementTypeButton[$name == '%s'$]",
+ classChainLegalHoldTitle, "close"));
+
+ private final Function classChainStrItemCellByName = name ->
+ String.format("%s/**/XCUIElementTypeStaticText[$name == '%s' AND value CONTAINS '%s'$]",
+ classChainstrViewRoot, strItemName, name);
+
+ private final Function classChainItemCellByName = name -> MobileBy.iOSClassChain(
+ classChainStrItemCellByName.apply(name));
+
+ public LegalHoldOverviewPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public boolean isVisible() {
+ return legalHoldPage.isDisplayed();
+ }
+
+ public boolean isInvisible() {
+ return isElementInvisible(legalHoldPage);
+ }
+
+ public boolean isMyselfVisible(String name) {
+ return isSubjectDisplayNameVisible(name+" (You)");
+ }
+
+ public boolean isSubjectDisplayNameVisible(String name) {
+ return isLocatorDisplayed(classChainItemCellByName.apply(name));
+ }
+
+ public boolean isSubjectDisplayNameInvisible(String name) {
+ return isLocatorInvisible(classChainItemCellByName.apply(name));
+ }
+
+ public void tapOnSubject(String name) {
+ getElement(classChainItemCellByName.apply(name)).click();
+ }
+
+ public void tapCloseButton() {
+ this.tapAtTheCenterOfElement(getElement(classChainCloseButton));
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/LoginPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/LoginPage.java
new file mode 100644
index 00000000000..882514adfed
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/LoginPage.java
@@ -0,0 +1,143 @@
+package com.wearezeta.auto.ios.pages;
+
+import com.wearezeta.auto.common.misc.Timedelta;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.TimeoutException;
+import org.openqa.selenium.WebElement;
+
+import org.openqa.selenium.WebDriver;
+
+import java.time.Duration;
+import java.util.logging.Logger;
+
+public class LoginPage extends IOSPage {
+
+ Logger log = Logger.getLogger(LoginPage.class.getSimpleName());
+
+ @iOSXCUITFindBy(iOSNsPredicate = "name == 'Connect to server'")
+ private WebElement connectToServerAlertTitle;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "name BEGINSWITH 'Open in \"Wire")
+ private WebElement openInWireAlertTitle;
+
+ @iOSXCUITFindBy(accessibility = "UseEmail")
+ private WebElement switchToEmailLogin;
+
+ @iOSXCUITFindBy(accessibility = "UsePhone")
+ private WebElement switchToPhoneLogin;
+
+ @iOSXCUITFindBy(accessibility = "Log in")
+ private WebElement loginScreen;
+
+ @iOSXCUITFindBy(accessibility = "restore_backup")
+ private WebElement restoreButtonOnFirstTimeOverlay;
+
+ @iOSXCUITFindBy(accessibility = "Log In")
+ private WebElement logInButton;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "label == 'Log In' AND name == 'Log In' AND type == 'XCUIElementTypeButton'")
+ private WebElement invinsibleLoginButton;
+
+ @iOSXCUITFindBy(accessibility = "companyLoginButton")
+ private WebElement companyLoginButton;
+
+ @iOSXCUITFindBy(accessibility = "EmailField")
+ private WebElement emailField;
+
+ @iOSXCUITFindBy(accessibility = "PasswordField")
+ private WebElement passwordField;
+
+ @iOSXCUITFindBy(accessibility = "back")
+ private WebElement backButton;
+
+ @iOSXCUITFindBy(accessibility = "bottomBarSettingsButton")
+ private WebElement profileButton;
+
+ public LoginPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public boolean isVisible() {
+ return isElementVisible(loginScreen);
+ }
+
+ public void acceptConnectToServerAlert() {
+ if (waitUntilElementVisible(connectToServerAlertTitle)) {
+ getDriver().switchTo().alert().accept();
+ } else {
+ throw new TimeoutException("Alert to connect to server did not appear");
+ }
+
+ }
+
+ public void acceptOpenInWireAlert() {
+ // We currently have no clue when this alert is shown.
+ // it is indeterministic so we wait for 1 second without implicit waits and timeouts.
+ if (!waitUntilElementInvisible(openInWireAlertTitle, Duration.ofSeconds(1))) {
+ getDriver().switchTo().alert().accept();
+ }
+ }
+
+ public void switchToEmailLogin() {
+ if(isElementVisible(switchToEmailLogin)){
+ switchToEmailLogin.click();
+ }
+ }
+
+ public boolean isPhoneLoginVisible() {
+ return switchToPhoneLogin.isDisplayed();
+ }
+
+ public boolean isPhoneLoginInvisible() {
+ return isElementInvisible(switchToPhoneLogin);
+ }
+
+ public boolean waitForLoginProperly() {
+ log.info("Start wait for login...");
+ boolean noLoginForm = waitUntilElementInvisible(loginScreen);
+ log.info("noLoginForm: " + noLoginForm);
+ boolean noFirstTimeOverlay = waitUntilElementInvisible(restoreButtonOnFirstTimeOverlay);
+ log.info("noFirstTimeOverlay: " + noFirstTimeOverlay);
+ boolean profileButtonShown = waitUntilElementVisible(profileButton);
+ return noLoginForm && noFirstTimeOverlay && profileButtonShown;
+ }
+
+ public void tapLoginButton() {
+ logInButton.click();
+ }
+
+ public boolean isLoginButtonInvisible() {
+ return isElementVisible(invinsibleLoginButton, LOGIN_TIMEOUT);
+ }
+
+ public void setLogin(String login) {
+ waitUntilElementClickable(emailField);
+ emailField.click();
+ emailField.clear();
+ emailField.sendKeys(login);
+ }
+
+ public void setPassword(String password) {
+ passwordField.click();
+ passwordField.clear();
+ passwordField.sendKeys(password);
+ }
+
+ private static final Timedelta LOGIN_TIMEOUT = Timedelta.ofSeconds(30);
+
+ public void tapBackButton() {
+ if (backButton.isDisplayed()) {
+ backButton.click();
+ }
+ }
+
+ public boolean isCompanyLoginButtonInvisible() {
+ return isElementInvisible(companyLoginButton, Timedelta.ofSeconds(1));
+ }
+
+ public void loginAs(String email, String password) {
+ setLogin(email);
+ setPassword(password);
+ tapLoginButton();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ManageDevicesOverlay.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ManageDevicesOverlay.java
new file mode 100644
index 00000000000..8c325dd966f
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ManageDevicesOverlay.java
@@ -0,0 +1,42 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.WebElement;
+
+public class ManageDevicesOverlay extends IOSPage{
+ @iOSXCUITFindBy(accessibility = "manage_devices")
+ private WebElement manageDevicesButton;
+
+ @iOSXCUITFindBy(accessibility = "Delete")
+ private WebElement deleteDeviceButton;
+
+ public ManageDevicesOverlay(WebDriver driver) {
+ super(driver);
+ }
+
+ public boolean waitUntilVisible() {
+ return manageDevicesButton.isDisplayed();
+ }
+
+ public boolean waitUntilInvisible() {
+ return waitUntilElementInvisible(manageDevicesButton);
+ }
+
+ public void tapMangeDevicesButton() {
+ manageDevicesButton.click();
+ }
+
+ private String removeStringFor(String deviceName) {
+ return String.format("name CONTAINS 'Remove %s'", deviceName);
+ }
+
+ public void tapDeleteButtonForDevice(String deviceName) {
+ getDriver().findElement(MobileBy.iOSNsPredicateString(removeStringFor(deviceName))).click();
+ }
+
+ public void tapDeleteButton() {
+ deleteDeviceButton.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/MapViewPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/MapViewPage.java
new file mode 100644
index 00000000000..274e4c597ef
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/MapViewPage.java
@@ -0,0 +1,19 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class MapViewPage extends IOSPage{
+
+ @iOSXCUITFindBy(accessibility = "sendLocation")
+ private WebElement sendLocationButton;
+
+ public MapViewPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void clickSendLocationButton() {
+ sendLocationButton.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/MessageDetailsPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/MessageDetailsPage.java
new file mode 100644
index 00000000000..c43975248ac
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/MessageDetailsPage.java
@@ -0,0 +1,36 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.WebElement;
+
+import java.util.function.Function;
+
+public class MessageDetailsPage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "Close")
+ private WebElement closeButton;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeButton' AND name == 'Tab0' AND label CONTAINS 'Read'")
+ private WebElement readTabActive;
+
+ private static final Function predicateStrUserByName = name ->
+ String.format("name == 'user_cell.name' AND value CONTAINS '%s'", name);
+
+ public MessageDetailsPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public boolean isContactVisible(String name) {
+ return waitUntilLocatorVisible(MobileBy.iOSNsPredicateString(predicateStrUserByName.apply(name)));
+ }
+
+ public void tapCloseButton() {
+ closeButton.click();
+ }
+
+ public boolean isContactVisibleInSeenTab(String name) {
+ return isContactVisible(name) && readTabActive.isDisplayed();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/MoveToCustomFolderPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/MoveToCustomFolderPage.java
new file mode 100644
index 00000000000..8614e5dc3cf
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/MoveToCustomFolderPage.java
@@ -0,0 +1,42 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.WebElement;
+
+public class MoveToCustomFolderPage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "Move To")
+ private WebElement moveToTitle;
+
+ @iOSXCUITFindBy(accessibility = "close")
+ private WebElement closeButton;
+
+ @iOSXCUITFindBy(accessibility = "button.newfolder.create")
+ private WebElement createNewButton;
+
+ public MoveToCustomFolderPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public boolean isVisible() {
+ return waitUntilElementVisible(moveToTitle);
+ }
+
+ public boolean isInvisible() {
+ return waitUntilElementInvisible(moveToTitle);
+ }
+
+ public void tapCloseButton() {
+ closeButton.click();
+ }
+
+ public void tapCreateNewButton() {
+ createNewButton.click();
+ }
+
+ public void tapOnACustomFolder(String name) {
+ getDriver().findElement(MobileBy.AccessibilityId(name)).click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/PasteDialog.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/PasteDialog.java
new file mode 100644
index 00000000000..3b31ae4dbad
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/PasteDialog.java
@@ -0,0 +1,19 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class PasteDialog extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "OK")
+ private WebElement oKButton;
+
+ public PasteDialog(WebDriver driver) {
+ super(driver);
+ }
+
+ public void tapOKButton() {
+ oKButton.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/PicturePreviewPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/PicturePreviewPage.java
new file mode 100644
index 00000000000..d2f7b035840
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/PicturePreviewPage.java
@@ -0,0 +1,40 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class PicturePreviewPage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "sketchButton")
+ private WebElement sketchButton;
+
+ @iOSXCUITFindBy(accessibility = "OK")
+ private WebElement oKButton;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "label == 'OK' AND name == 'OK' AND type == 'XCUIElementTypeButton'")
+ private WebElement iPadOKButton;
+
+ @iOSXCUITFindBy(accessibility = "Use Photo")
+ private WebElement usePhotoButton;
+
+ public PicturePreviewPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void tapSketchButton(){
+ sketchButton.click();
+ }
+
+ public void tapOkButton(){
+ oKButton.click();
+ }
+
+ public void tapOkButtonOnIPad(){
+ iPadOKButton.click();
+ }
+
+ public void tapPhotoButton(){
+ usePhotoButton.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/PollMessagesPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/PollMessagesPage.java
new file mode 100644
index 00000000000..950f8b82f28
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/PollMessagesPage.java
@@ -0,0 +1,63 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import java.util.function.Function;
+
+public class PollMessagesPage extends IOSPage {
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeButton' AND value=='selected'")
+ private WebElement anySelectedPollButton;
+
+ private static final Function pollMessageText = text ->
+ MobileBy.iOSNsPredicateString(String.format("name == 'Message' AND value CONTAINS '%s'", text));
+
+ private static final Function confirmedPollButtonByText = text ->
+ MobileBy.iOSNsPredicateString(String.format("name == '%s' AND value=='confirmed'", text));
+
+ private static final Function unselectedPollButtonByText = text ->
+ MobileBy.iOSNsPredicateString(String.format("name == '%s' AND value=='unselected'", text));
+
+ private static final Function selectedPollButtonByText = text ->
+ MobileBy.iOSNsPredicateString(String.format("name == '%s' AND value=='selected'", text));
+
+ public PollMessagesPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public boolean isPollMessageTextContains(String text) {
+ return waitUntilLocatorVisible(pollMessageText.apply(text));
+ }
+
+ public boolean areAllPollButtonsUnselected() {
+ return waitUntilElementInvisible(anySelectedPollButton);
+ }
+
+ public void tapPollButtonWithTheText(String text) {
+ getDriver().findElement(MobileBy.AccessibilityId(text.toUpperCase())).click();
+ }
+
+ public boolean isPollButtonWithTextConfirmed(String buttonText) {
+ return waitUntilLocatorVisible(confirmedPollButtonByText.apply(buttonText.toUpperCase()));
+ }
+
+ public boolean isPollButtonWithTextUnselected(String buttonText) {
+ return waitUntilLocatorVisible(unselectedPollButtonByText.apply(buttonText.toUpperCase()));
+ }
+
+ public boolean isPollButtonWithTextSelected(String buttonText) {
+ return waitUntilLocatorVisible(selectedPollButtonByText.apply(buttonText.toUpperCase()));
+ }
+
+ public boolean isPollErrorMessageVisible(String error) {
+ return waitUntilLocatorVisible(MobileBy.AccessibilityId(error));
+ }
+
+ public boolean isPollErrorMessageInvisible(String error) {
+ return isLocatorInvisible(MobileBy.AccessibilityId(error));
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ReactionsEditMenuPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ReactionsEditMenuPage.java
new file mode 100644
index 00000000000..50db3f1d00b
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ReactionsEditMenuPage.java
@@ -0,0 +1,170 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.MobileBy;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+import java.util.function.Function;
+
+public class ReactionsEditMenuPage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "Reactions")
+ private WebElement emojiPicker;
+
+ @iOSXCUITFindBy(accessibility = "Forward")
+ private WebElement forwardButton;
+
+ @iOSXCUITFindBy(accessibility = "Copy")
+ private WebElement copyItem;
+
+ @iOSXCUITFindBy(accessibility = "Delete")
+ private WebElement deleteItem;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeButton' AND name == 'Details'")
+ private WebElement detailsItem;
+
+ @iOSXCUITFindBy(accessibility = "Download")
+ private WebElement downloadItem;
+
+ @iOSXCUITFindBy(accessibility = "Edit")
+ private WebElement editItem;
+
+ @iOSXCUITFindBy(accessibility = "Like")
+ private WebElement likeItem;
+
+ @iOSXCUITFindBy(accessibility = "Unlike")
+ private WebElement unlikeItem;
+
+ @iOSXCUITFindBy(accessibility = "Paste")
+ private WebElement pasteItem;
+
+ @iOSXCUITFindBy(accessibility = "Reply")
+ private WebElement replyItem;
+
+ @iOSXCUITFindBy(accessibility = "Reveal")
+ private WebElement revealItem;
+
+ @iOSXCUITFindBy(accessibility = "Save")
+ private WebElement saveItem;
+
+ @iOSXCUITFindBy(accessibility = "Select All")
+ private WebElement selectAll;
+
+ @iOSXCUITFindBy(accessibility = "Share")
+ private WebElement shareItem;
+
+ @iOSXCUITFindBy(accessibility = "Cancel")
+ private WebElement cancelItem;
+
+ private static final Function predicateReactionByValue = reaction -> MobileBy.iOSNsPredicateString(String.format("type == 'XCUIElementTypeButton' AND name == '%s'", reaction));
+
+ public ReactionsEditMenuPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public boolean isVisible() {
+ return waitUntilElementVisible(emojiPicker);
+ }
+
+ public boolean isInvisible() {
+ return waitUntilElementInvisible(emojiPicker);
+ }
+
+ public void iTapQuickReaction(String reaction) {
+ getDriver().findElement(predicateReactionByValue.apply(reaction)).click();
+ }
+
+ public boolean isForwardButtonInvisible() {
+ return waitUntilElementInvisible(forwardButton);
+ }
+
+ public void tapCopy() {
+ copyItem.click();
+ }
+
+ public boolean isCopyVisible() {
+ return waitUntilElementVisible(copyItem);
+ }
+
+ public boolean isCopyInvisible() {
+ return waitUntilElementInvisible(copyItem);
+ }
+
+ public void tapDelete() {
+ deleteItem.click();
+ }
+
+ public boolean isDeleteVisible() {
+ return waitUntilElementVisible(deleteItem);
+ }
+
+ public void tapDetails() {
+ detailsItem.click();
+ }
+
+ public void tapDownload() {
+ downloadItem.click();
+ }
+
+ public boolean isDownloadInvisible() {
+ return waitUntilElementInvisible(downloadItem);
+ }
+
+ public void tapEdit() {
+ editItem.click();
+ }
+
+ public void tapLike() {
+ likeItem.click();
+ }
+
+ public void tapPaste() {
+ pasteItem.click();
+ }
+
+ public boolean isPasteInvisible() {
+ return waitUntilElementInvisible(pasteItem);
+ }
+
+ public void tapReply() {
+ replyItem.click();
+ }
+
+ public boolean isReplyVisible() {
+ return waitUntilElementVisible(replyItem);
+ }
+
+ public void tapSave() {
+ saveItem.click();
+ }
+
+ public boolean isSaveVisible() {
+ return waitUntilElementVisible(saveItem);
+ }
+
+ public boolean isSaveInvisible() {
+ return waitUntilElementInvisible(saveItem);
+ }
+
+ public void tapSelectAll() {
+ selectAll.click();
+ }
+
+ public void tapShare() {
+ shareItem.click();
+ }
+
+ public boolean isShareVisible() {
+ return waitUntilElementVisible(shareItem);
+ }
+
+ public boolean isShareInvisible() {
+ return waitUntilElementInvisible(shareItem);
+ }
+
+ public void tapCancel() {
+ cancelItem.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/RegistrationPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/RegistrationPage.java
new file mode 100644
index 00000000000..0330cc15947
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/RegistrationPage.java
@@ -0,0 +1,133 @@
+package com.wearezeta.auto.ios.pages;
+
+import com.wearezeta.auto.common.backend.BackendConnections;
+import org.openqa.selenium.WebDriver;
+import com.wearezeta.auto.common.misc.Timedelta;
+import com.wearezeta.auto.common.usrmgmt.ClientUser;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.interactions.Actions;
+
+public class RegistrationPage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "VerificationCode")
+ private WebElement verificationCodeInput;
+
+ @iOSXCUITFindBy(accessibility = "Accept")
+ private WebElement acceptTOCButton;
+
+ @iOSXCUITFindBy(accessibility = "resend_button")
+ private WebElement resendCode;
+
+ @iOSXCUITFindBy(accessibility = "ConfirmButton")
+ private WebElement nameConfirmButton;
+
+ @iOSXCUITFindBy(accessibility = "ConfirmButton")
+ private WebElement usernameConfirmButton;
+
+ @iOSXCUITFindBy(accessibility = "RevealButton")
+ private WebElement passwordConfirmButton;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "label IN {'Create an account'}")
+ private WebElement registrationScreen;
+
+ @iOSXCUITFindBy(accessibility = "NameField")
+ private WebElement nameField;
+
+ @iOSXCUITFindBy(accessibility = "EmailField")
+ private WebElement emailField;
+
+ @iOSXCUITFindBy(accessibility = "PasswordField")
+ private WebElement passwordField;
+
+ @iOSXCUITFindBy(accessibility = "UsernameField")
+ private WebElement usernameField;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "value CONTAINS 'Enter the verification code we sent to'")
+ private WebElement mailVerifyPrompt;
+
+ @iOSXCUITFindBy(accessibility = "back")
+ private WebElement backButton;
+
+ @iOSXCUITFindBy(accessibility = "validation-rules")
+ private WebElement passwordRulesDesc;
+
+ @iOSXCUITFindBy(accessibility = "validation-failure")
+ private WebElement passwordfailureMessage;
+
+ public RegistrationPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public boolean isVisible() {
+ return waitUntilElementVisible(registrationScreen);
+ }
+
+ public boolean isPasswordRulesVisible() {
+ return isElementVisible(passwordRulesDesc, Timedelta.ofSeconds(1));
+ }
+
+ public boolean isPasswordFailureVisible() {
+ return isElementVisible(passwordfailureMessage, Timedelta.ofSeconds(1));
+ }
+
+ public void inputActivationCode(ClientUser user) {
+ waitUntilElementVisible(verificationCodeInput);
+ verificationCodeInput.click();
+ final String code = BackendConnections.get(user).getActivationCodeForEmail(user.getEmail());
+ Actions a = new Actions(getDriver());
+ a.sendKeys(code);
+ a.perform();
+ }
+
+ public void clickAcceptTOCButton() {
+ acceptTOCButton.click();
+ }
+
+ public void tapPasswordConfirmButton() {
+ passwordConfirmButton.click();
+ }
+
+ public void tapNameConfirmButton() {
+ nameConfirmButton.click();
+ }
+
+ public void tapUsernameConfirmButton() {
+ usernameConfirmButton.click();
+ }
+
+ public void clearPasswordInput() {
+ passwordField.clear();
+ }
+
+ public void typeEmail(String email) {
+ waitUntilElementClickable(emailField);
+ emailField.click();
+ emailField.sendKeys(email);
+ }
+
+ public void typeName(String name) {
+ waitUntilElementClickable(nameField);
+ nameField.click();
+ nameField.sendKeys(name);
+ }
+
+ public void typeUsername(String name) {
+ waitUntilElementClickable(usernameField);
+ usernameField.click();
+ usernameField.sendKeys(name);
+ }
+
+ public void typePassword(String password) {
+ passwordField.click();
+ passwordField.sendKeys(password);
+ }
+
+ public boolean isEmailVerificationPromptVisible() {
+ return waitUntilElementVisible(mailVerifyPrompt);
+ }
+
+ public void tapBackButton() {
+ backButton.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/SearchUIPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/SearchUIPage.java
new file mode 100644
index 00000000000..a06422e585b
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/SearchUIPage.java
@@ -0,0 +1,161 @@
+package com.wearezeta.auto.ios.pages;
+
+import java.util.function.Function;
+import com.wearezeta.auto.common.misc.Timedelta;
+import com.wearezeta.auto.ios.pages.search.SearchList;
+import io.appium.java_client.MobileBy;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.*;
+
+import org.openqa.selenium.WebDriver;
+
+public class SearchUIPage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "close")
+ private WebElement closeButton;
+
+ @iOSXCUITFindBy(accessibility = "Search by name or username")
+ private WebElement searchBar;
+
+ @iOSXCUITFindBy(accessibility = "Copy")
+ private WebElement copyInviteButton;
+
+ @iOSXCUITFindBy(accessibility = "Invite More People")
+ private WebElement inviteMorePeopleButton;
+
+ /**
+ @iOSXCUITFindBy(accessibility = "button.searchui.creategroup")
+ private WebElement createGroupButton;
+*/
+ @iOSXCUITFindBy(accessibility = "button.searchui.createguestroom")
+ private WebElement createGuestRoomButton;
+
+ @iOSXCUITFindBy(accessibility= "create_group")
+ private WebElement createGroupButton;
+
+ private static final Function classChainTopPeopleAvatarByIdx = idx -> MobileBy.iOSClassChain(
+ String.format("**/XCUIElementTypeCell[`name == 'TopPeopleCell'`][%s]", idx));
+
+ private static final Function predicateStringServiceSearchResult = name -> MobileBy.iOSNsPredicateString(
+ String.format("type == 'XCUIElementTypeStaticText' AND value == '%s'", name));
+
+ private static final Function serviceNameByText = name -> MobileBy.iOSNsPredicateString(
+ String.format("type == 'XCUIElementTypeStaticText' AND value == '%s'", name));
+
+ private static final Function predicateContact = name -> MobileBy.iOSNsPredicateString(
+ String.format("name == 'user_cell.name' AND value == '%s'", name));
+
+ private final SearchList searchList;
+
+ public SearchUIPage(WebDriver driver) {
+ super(driver);
+ this.searchList = new SearchList(driver);
+ }
+
+ public boolean isVisible() {
+ return searchList.isVisible();
+ }
+
+ public void typeSearchQuery(String text, boolean shouldClearFieldBeforeInput) {
+// searchList.typeSearchQuery(text, shouldClearFieldBeforeInput);
+ searchBar.sendKeys(text);
+ // Wait for a user to be found
+ Timedelta.ofSeconds(2).sleep();
+ }
+
+ public void clearSearchInput() {
+ searchList.clearSearchQuery();
+ }
+
+ public void tapCreateGroupButton() {
+ createGroupButton.click();
+ }
+
+ public void tapCloseButton() {
+ closeButton.click();
+ }
+
+ public void tapSendInviteButton() {
+ inviteMorePeopleButton.click();
+ }
+
+ public void tapCopyInviteButton() {
+ copyInviteButton.click();
+ }
+
+ public boolean isElementFoundInSearch(String name) {
+ return searchList.isItemVisible(name);
+ }
+
+ public boolean isElementNotFoundInSearch(String name) {
+ return searchList.isItemInvisible(name);
+ }
+
+ public void selectElementInSearchResults(String name) {
+ searchList.selectItem(name);
+ }
+
+ public void tapTopConnectionsAvatars(int numberToTap) {
+ for (int i = 1; i <= numberToTap; i++) {
+ final By locator = classChainTopPeopleAvatarByIdx.apply(i);
+ getElement(locator).click();
+ }
+ }
+
+ public void tapInstantConnectButton(String name) {
+ searchList.tapInstantConnectButton(name);
+ }
+
+ public void tapOnTopConnectionAvatarByOrder(int i) {
+ final By locator = classChainTopPeopleAvatarByIdx.apply(i);
+ getDriver().findElement(locator).click();
+ }
+
+ public int getOccurrencesCount(String name) {
+ return searchList.getOccurrencesCount(name);
+ }
+
+ public boolean isContactVisible(String text) {
+ final By locator = predicateContact.apply(text);
+ return waitUntilLocatorVisible(locator);
+ }
+
+ public boolean isContactInvisible(String text) {
+ final By locator = predicateContact.apply(text);
+ return isLocatorInvisible(locator);
+ }
+
+ public boolean isServiceVisibleInSearchResult(String serviceName) {
+ final By locator = predicateStringServiceSearchResult.apply(serviceName);
+ return waitUntilLocatorVisible(locator);
+ }
+
+ public boolean isServiceInVisibleInSearchResult(String serviceName) {
+ final By locator = predicateStringServiceSearchResult.apply(serviceName);
+ return isLocatorInvisible(locator);
+ }
+
+ public void tapOnService(String serviceName) {
+ getDriver().findElement(serviceNameByText.apply(serviceName)).click();
+ }
+
+ public boolean isCreateGroupButtonVisible() {
+ return isElementVisible(createGroupButton);
+ }
+
+ public boolean isCreateGroupButtonInvisible() {
+ return waitUntilElementInvisible(createGroupButton);
+ }
+
+ public boolean isCreateGuestRoomButtonVisible() {
+ return isElementVisible(createGuestRoomButton);
+ }
+
+ public boolean isCreateGuestRoomButtonInvisible() {
+ return waitUntilElementInvisible(createGuestRoomButton);
+ }
+
+ public void iOpenCreateGroupScreen() {
+ createGroupButton.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ServiceCreationPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ServiceCreationPage.java
new file mode 100644
index 00000000000..89bda04dbcf
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ServiceCreationPage.java
@@ -0,0 +1,18 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class ServiceCreationPage extends IOSPage {
+ public ServiceCreationPage(WebDriver driver) {
+ super(driver);
+ }
+
+ @iOSXCUITFindBy(accessibility = "close")
+ private WebElement closeButton;
+
+ public void tapCloseButton() {
+ closeButton.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ServiceDetailPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ServiceDetailPage.java
new file mode 100644
index 00000000000..de49561b34c
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/ServiceDetailPage.java
@@ -0,0 +1,19 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+
+public class ServiceDetailPage extends IOSPage{
+ @iOSXCUITFindBy(accessibility = "Add Service")
+ private WebElement nameAddServiceButton;
+
+ public ServiceDetailPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void addService(){
+ nameAddServiceButton.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/SettingsPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/SettingsPage.java
new file mode 100644
index 00000000000..924e09ba77f
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/SettingsPage.java
@@ -0,0 +1,473 @@
+package com.wearezeta.auto.ios.pages;
+
+import com.wearezeta.auto.common.backend.models.AccentColor;
+import com.wearezeta.auto.common.log.ZetaLogger;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.*;
+import org.openqa.selenium.support.ui.ExpectedCondition;
+import org.openqa.selenium.support.ui.WebDriverWait;
+
+import java.awt.image.BufferedImage;
+import java.time.Duration;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.logging.Logger;
+
+public class SettingsPage extends IOSPage {
+
+ private static final Logger log = ZetaLogger.getLog(SettingsPage.class.getSimpleName());
+
+ @iOSXCUITFindBy(accessibility = "close")
+ private WebElement xButton;
+
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeStaticText[`name == \"Advanced\"`]")
+ private WebElement advanced;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeStaticText' AND label == 'Devices'")
+ private WebElement devicesItem;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "label CONTAINS 'This makes audio calls use less data and work better on slower networks. Turn off to use constant bitrate encoding (CBR). This setting only affects 1:1 calls; conference calls always use CBR encoding.'")
+ private WebElement vbrText;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeStaticText' AND label == 'Account'")
+ private WebElement accountItem;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeStaticText' AND label == 'Back Up Conversations'")
+ private WebElement backUpConversationsItem;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeStaticText' AND label == 'Options'")
+ private WebElement optionsItem;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeStaticText' AND label == 'Log Out'")
+ private WebElement logOutItem;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeStaticText' AND label == 'Profile Picture'")
+ private WebElement pictureItem;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeStaticText' AND label == 'Reset Password'")
+ private WebElement resetPasswordItem;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeStaticText' AND label == 'Support'")
+ private WebElement supportItem;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeStaticText' AND label == 'Wire Support Website'")
+ private WebElement wireSupportWebsiteItem;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeStaticText' AND label == 'Delete Account'")
+ private WebElement deleteAccountItem;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeStaticText' AND label == 'Username'")
+ private WebElement usernameItem;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeStaticText' AND label == 'Email'")
+ private WebElement emailItem;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeStaticText' AND label == 'Name'")
+ private WebElement nameItem;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeStaticText' AND label == 'About'")
+ private WebElement aboutItem;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeStaticText' AND label == 'Profile Color'")
+ private WebElement colorItem;
+
+ @iOSXCUITFindBy(accessibility = "Purple")
+ private WebElement colorPurple;
+
+ @iOSXCUITFindBy(accessibility = "DomainFieldDisabled")
+ private WebElement nonEditableDomainLabel;
+
+ @iOSXCUITFindBy(accessibility = "TeamFieldDisabled")
+ private WebElement nonEditableTeamLabel;
+
+ @iOSXCUITFindBy(accessibility = "Terms of Use")
+ private WebElement termsOfUseItem;
+
+ @iOSXCUITFindBy(accessibility = "Privacy Policy")
+ private WebElement privacyPolicyItem;
+
+ @iOSXCUITFindBy(accessibility = "Wire Website")
+ private WebElement wireWebsiteItem;
+
+ @iOSXCUITFindBy(accessibility = "Contact Support")
+ private WebElement contactSupportItem;
+
+ @iOSXCUITFindBy(accessibility = "Report Misuse")
+ private WebElement reportMisuse;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeTable' AND visible == 1")
+ private WebElement predicateOptionsRoot;
+
+ private static final Function classChainStrMenuItemByName = name ->
+ String.format("**/XCUIElementTypeCell[$type == 'XCUIElementTypeStaticText' AND label == '%s'$]", name);
+
+ private static final BiFunction classChainStrSettingsValue =
+ (itemName, expectedValue) -> String.format("%s/*[`value == '%s'`]",
+ classChainStrMenuItemByName.apply(itemName), expectedValue);
+
+ private static final By classChainSelfNameEditField =
+ MobileBy.iOSClassChain(String.format("%s/XCUIElementTypeTextField[-1]",
+ classChainStrMenuItemByName.apply("Name")));
+
+ private static final String xpathStrColorPicker = "//*[@name='COLOR']/following::XCUIElementTypeTable[1]";
+ private static final By xpathColorPicker = By.xpath(xpathStrColorPicker);
+
+ // indexation starts from 1
+ private static final Function xpathSreColorByIdx = idx ->
+ String.format("%s/XCUIElementTypeCell[%s]", xpathStrColorPicker, idx);
+
+ private static final Function predicateStrUniqueUsernameInSettings = name -> MobileBy.iOSNsPredicateString(
+ //FIXME: waiting for correct Id from Alexis String.format("name == 'UsernameFieldDisabled' OR name == 'UsernameField' AND value ='%s'", name));
+ String.format("value ='@%s'", name));
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeImage' AND name == 'imagePreview' AND value == 'image'")
+ private WebElement predicateSettingsProfilePicturePreview;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeImage' AND name == 'imagePreview' AND value == 'color'")
+ private WebElement predicateSettingsProfileColorPreview;
+
+ @iOSXCUITFindBy(xpath = "//XCUIElementTypeStaticText[@name='COLOR']/preceding-sibling::XCUIElementTypeButton")
+ private WebElement xpathColorPickerCloseButton;
+
+ @iOSXCUITFindBy(accessibility = "EmailField")
+ private WebElement nameEmailInput;
+
+ @iOSXCUITFindBy(accessibility = "Verify email")
+ private WebElement nameVerifyEmailTitle;
+
+ @iOSXCUITFindBy(accessibility = "Account")
+ WebElement accountBackButton;
+
+ @iOSXCUITFindBy(accessibility = "Settings")
+ WebElement settingsBackButton;
+
+ private static final Function predicateNavigationButtonByName = name -> MobileBy.iOSNsPredicateString(
+ String.format("type == 'XCUIElementTypeButton' AND name =[c] '%s'", name));
+
+ private static final Function predicateDomainName = name -> MobileBy.iOSNsPredicateString(
+ String.format("name == 'DomainFieldDisabled' AND value == '%s'", name));
+
+ private static final Function predicateTeamName = name -> MobileBy.iOSNsPredicateString(
+ String.format("name == 'TeamFieldDisabled' AND value == '%s'", name));
+
+ private static final Function predicateDomainNameOnUsernameUI = name -> MobileBy.iOSNsPredicateString(
+ String.format("label == '%s'", name));
+
+ private static final Function predicateNonEditableDomainNameOnUsernameUI = name -> MobileBy.iOSNsPredicateString(
+ String.format("label == '%s' AND type == 'XCUIElementTypeStaticText'", name));
+ @iOSXCUITFindBy(accessibility = "ReadReceiptsSwitch")
+ private WebElement nameReadReceiptToggle;
+
+ @iOSXCUITFindBy(accessibility = "Appearance")
+ private WebElement nameAppearanceText;
+
+ @iOSXCUITFindBy(accessibility = "nameProfilePictureLabel")
+ private WebElement nameProfilePictureLabel;
+
+ @iOSXCUITFindBy(accessibility = "Color")
+ private WebElement nameColorLabel;
+
+ @iOSXCUITFindBy(accessibility = "NameFieldDisabled")
+ private WebElement nameDisplayNameDisabled;
+
+ @iOSXCUITFindBy(accessibility = "UsernameFieldDisabled")
+ private WebElement nameUniqueUsernameDisabled;
+
+ @iOSXCUITFindBy(accessibility = "handleTextField")
+ private WebElement nameUserName;
+
+ @iOSXCUITFindBy(accessibility = "Beta Program")
+ private WebElement nameBetaProgram;
+
+ @iOSXCUITFindBy(accessibility = "Beta Toggle")
+ private WebElement nameBetaToggle;
+
+ private static final Function predicateBetaToggleValue = value -> MobileBy.iOSNsPredicateString(
+ String.format("type == 'XCUIElementTypeSwitch' AND name == '%s' AND value == '%s'", "Beta Toggle", value));
+
+ public SettingsPage(WebDriver driver) {
+ super(driver);
+ }
+
+ private WebElement scrollToItem(String itemName) {
+ By locator = MobileBy.iOSClassChain(classChainStrMenuItemByName.apply(itemName));
+ return new WebDriverWait(getDriver(), Duration.ofSeconds(getDefaultLookupTimeoutSeconds()))
+ .ignoring(StaleElementReferenceException.class)
+ .ignoring(NoSuchElementException.class)
+ .until(locatorIsScrolledIntoView(locator));
+ }
+
+ private WebElement scrollToItem(WebElement item) {
+ return new WebDriverWait(getDriver(), Duration.ofSeconds(getDefaultLookupTimeoutSeconds()))
+ .ignoring(StaleElementReferenceException.class)
+ .ignoring(NoSuchElementException.class)
+ .until(elementIsScrolledIntoView(item));
+ }
+
+ private ExpectedCondition elementIsScrolledIntoView(final WebElement element) {
+ return driver -> {
+ if (element.isDisplayed()) {
+ return element;
+ }
+ log.info("Scroll up");
+ swipe(predicateOptionsRoot, SwipeDirection.UP);
+ return null;
+ };
+ }
+
+ private ExpectedCondition locatorIsScrolledIntoView(final By locator) {
+ return driver -> {
+ WebElement element = getDriver().findElement(locator);
+ if (element.isDisplayed()) {
+ return element;
+ }
+ log.info("Scroll up");
+ swipe(predicateOptionsRoot, SwipeDirection.UP);
+ return null;
+ };
+ }
+
+ public void tapAccount() {
+ waitUntilElementClickable(accountItem);
+ accountItem.click();
+ }
+
+ public void tapDevices() {
+ devicesItem.click();
+ }
+
+ public void tapBackUpConversations() {
+ scrollToItem(backUpConversationsItem).click();
+ }
+
+ public void tapOptionsItem() {
+ waitUntilElementClickable(optionsItem);
+ optionsItem.click();
+ }
+
+ public void tapLogOutItem() {
+ scrollToItem(logOutItem).click();
+ }
+
+ public void tapPictureItem() {
+ pictureItem.click();
+ }
+
+ public void tapResetPasswordItem() {
+ scrollToItem(resetPasswordItem);
+ resetPasswordItem.click();
+ }
+
+ public void tapSupportItem() {
+ supportItem.click();
+ }
+
+ public void tapWireSupportWebsiteItem() {
+ wireSupportWebsiteItem.click();
+ }
+
+ public void tapDeleteAccountItem() {
+ scrollToItem(deleteAccountItem).click();
+ }
+
+ public void tapUsernameItem() {
+ usernameItem.click();
+ }
+
+ public void tapEmailItem() {
+ emailItem.click();
+ }
+
+ public void tapNameItem() {
+ nameItem.click();
+ }
+
+ public void tapAboutItem() {
+ aboutItem.click();
+ }
+
+ public void tapColorItem() {
+ scrollToItem(colorItem).click();
+ }
+
+ public void tapColorPurple() {
+ colorPurple.click();
+ }
+
+ public boolean isItemVisible(String itemName) {
+ return scrollToItem(itemName).isDisplayed();
+ }
+
+ public boolean isItemInvisible(String itemName) {
+ try {
+ scrollToItem(itemName);
+ } catch (Exception e) {
+ return true;
+ }
+ return isLocatorInvisible(MobileBy.iOSClassChain(classChainStrMenuItemByName.apply(itemName)));
+ }
+
+ public void tapX() {
+ xButton.click();
+ }
+
+ public void tapNavigationButton(String name) {
+ getDriver().findElement(predicateNavigationButtonByName.apply(name)).click();
+ }
+
+ public boolean isSettingItemValueEqualTo(String itemName, String expectedValue) {
+ final By locator = MobileBy.iOSClassChain(classChainStrSettingsValue.apply(itemName, expectedValue));
+ return waitUntilLocatorVisible(locator);
+ }
+
+ public WebElement clearSelfName() {
+ final WebElement selfName = getElementIfExists(classChainSelfNameEditField).orElseThrow(
+ () -> new IllegalStateException("Name input is not present on the page")
+ );
+ selfName.clear();
+ return selfName;
+ }
+
+ public void clearUsername() {
+ nameUserName.clear();
+ }
+
+ public void setSelfName(String newName) {
+ clearSelfName().sendKeys(newName);
+ }
+
+ public BufferedImage getColorPickerStateScreenshot() {
+ return this.getElementScreenshot(getDriver().findElement(xpathColorPicker)).orElseThrow(
+ () -> new IllegalStateException("Cannot make a screenshot of Color Picker control")
+ );
+ }
+
+ public void closeColorPicker() {
+ xpathColorPickerCloseButton.click();
+ }
+
+ public void selectAccentColor(AccentColor byName) {
+ final By locator = By.xpath(xpathSreColorByIdx.apply(byName.getId()));
+ getDriver().findElement(locator).click();
+ }
+
+ public boolean isUniqueUsernameInSettingsDisplayed(String uniqueName) {
+ return waitUntilLocatorVisible(predicateStrUniqueUsernameInSettings.apply(uniqueName));
+ }
+
+ public boolean isProfilePicturePreviewInvisible() {
+ return waitUntilElementInvisible(predicateSettingsProfilePicturePreview);
+ }
+
+ public void changeEmailAddress(String newEmail) {
+ nameEmailInput.clear();
+ nameEmailInput.sendKeys(newEmail);
+ }
+
+ public boolean waitUntilEmailVerificationHappens(Duration timeout) {
+ return waitUntilElementInvisible(nameVerifyEmailTitle, timeout);
+ }
+
+ public boolean iSeeVBRText() {
+ return vbrText.isDisplayed();
+ }
+
+ public void switchToggleReadReceipts() {
+ nameReadReceiptToggle.click();
+ }
+
+ public boolean isDisplayNameInputFieldStatic() {
+ return isElementVisible(nameDisplayNameDisabled);
+ }
+
+ public boolean isUniqueUsernameInputFieldStatic() {
+ return isElementVisible(nameUniqueUsernameDisabled);
+ }
+
+ public boolean isAppearanceSectionInvisible() {
+ return waitUntilElementInvisible(nameAppearanceText) && isProfilePictureInvisible() && isAccentColorInvisible();
+ }
+
+ public boolean isProfilePictureInvisible() {
+ return waitUntilElementInvisible(nameProfilePictureLabel) && isProfilePicturePreviewInvisible();
+ }
+ public boolean isAccentColorInvisible() {
+ return waitUntilElementInvisible(nameColorLabel) && waitUntilElementInvisible(predicateSettingsProfileColorPreview);
+ }
+
+ public boolean isBetaToggleVisible() {
+ return isElementVisible(nameBetaProgram);
+ }
+
+ public boolean isBetaToggleInvisible() {
+ return waitUntilElementInvisible(nameBetaProgram);
+ }
+
+ public boolean isBetaToggleChecked() {
+ return waitUntilLocatorVisible(predicateBetaToggleValue.apply(1));
+ }
+
+ public boolean isBetaToggleUnchecked() {
+ return waitUntilLocatorVisible(predicateBetaToggleValue.apply(0));
+ }
+
+ public void tapBetaToggle() {
+ nameBetaToggle.click();
+ }
+
+ public boolean isDomainNameVisible(String domainName) {
+ final By locator = predicateDomainName.apply(domainName);
+ return waitUntilLocatorVisible(locator);
+ }
+
+ public boolean isDomainNameVisibleOnUsernameUI(String domainName) {
+ final By locator = predicateDomainNameOnUsernameUI.apply(domainName);
+ return waitUntilLocatorVisible(locator);
+ }
+
+ public boolean isNonEditableDomainNameFieldOnUsernameUI(String domainName) {
+ final By locator = predicateNonEditableDomainNameOnUsernameUI.apply(domainName);
+ return waitUntilLocatorVisible(locator);
+ }
+
+ public boolean isDomainNonEditableOnSettings() {
+ return !nonEditableDomainLabel.isEnabled();
+ }
+
+ public boolean isTeamNonEditableOnSettings() {
+ return !nonEditableTeamLabel.isEnabled();
+ }
+
+ public boolean isTeamNameVisible(String domainName) {
+ final By locator = predicateTeamName.apply(domainName);
+ return waitUntilLocatorVisible(locator);
+ }
+
+ public boolean isTeamNameInvisible(String domainName) {
+ final By locator = predicateTeamName.apply(domainName);
+ return isLocatorInvisible(locator);
+ }
+
+ public void tapTermsOfUse(){ termsOfUseItem.click();}
+
+ public void tapPrivacyPolicy(){ privacyPolicyItem.click();}
+
+ public void tapWireWebsite(){ wireWebsiteItem.click();}
+
+ public void tapContactSupport(){ contactSupportItem.click();}
+
+ public void tapReportMisuse(){ reportMisuse.click();}
+
+ public void tapAdvanced() {
+ advanced.click();
+ }
+
+ public void tapAccountBackButton() {
+ accountBackButton.click();
+ }
+ public void tapSettingsBackButton() {
+ settingsBackButton.click();
+ }
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/SketchPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/SketchPage.java
new file mode 100644
index 00000000000..d78bcf4cfcc
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/SketchPage.java
@@ -0,0 +1,26 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class SketchPage extends IOSPage {
+ @iOSXCUITFindBy(accessibility = "sendButton")
+ private WebElement sendButton;
+
+ @iOSXCUITFindBy(accessibility = "canvas")
+ private WebElement canvasButton;
+
+ public SketchPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void sketchRandomLines() {
+ swipe(canvasButton, SwipeDirection.UP);
+ swipe(canvasButton, SwipeDirection.LEFT);
+ }
+
+ public void tapSendButton(){
+ sendButton.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/StatusActionSheetPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/StatusActionSheetPage.java
new file mode 100644
index 00000000000..d1a74d538d5
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/StatusActionSheetPage.java
@@ -0,0 +1,14 @@
+package com.wearezeta.auto.ios.pages;
+
+import org.openqa.selenium.WebDriver;
+import io.appium.java_client.MobileBy;
+
+public class StatusActionSheetPage extends IOSPage {
+ public StatusActionSheetPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void tapStatusName(String name) {
+ getElement(MobileBy.AccessibilityId(name)).click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/TeamSearchUIPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/TeamSearchUIPage.java
new file mode 100644
index 00000000000..06c34b92b2f
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/TeamSearchUIPage.java
@@ -0,0 +1,19 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class TeamSearchUIPage extends SearchUIPage {
+
+ @iOSXCUITFindBy(accessibility = "Services")
+ private WebElement servicesTabButton;
+
+ public TeamSearchUIPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void tapTeamSearchUITab() {
+ servicesTabButton.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/TopNavigationBarPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/TopNavigationBarPage.java
new file mode 100644
index 00000000000..986dd44a889
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/TopNavigationBarPage.java
@@ -0,0 +1,68 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class TopNavigationBarPage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "bottomBarSettingsButton")
+ private WebElement profileButton;
+
+ @iOSXCUITFindBy(accessibility = "legalhold")
+ private WebElement legalHoldIndicator;
+
+
+// @iOSXCUITFindBy(accessibility = "Name")
+// private WebElement userProfileName;
+//
+
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeNavigationBar[`name == \"Conversations\"`]/XCUIElementTypeOther/XCUIElementTypeButton/XCUIElementTypeOther[1]/XCUIElementTypeImage")
+ private WebElement userProfileImage;
+
+ @iOSXCUITFindBy(accessibility = "create_group_or_search_button")
+ private WebElement openSearchScreenButton;
+
+ @iOSXCUITFindBy(accessibility = "Filter conversations")
+ private WebElement filterButton;
+
+ public TopNavigationBarPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void tapProfileButton() {
+ waitUntilElementClickable(profileButton);
+ profileButton.click();
+ }
+
+ public boolean isSelfProfileButtonVisible() {
+ return profileButton.isDisplayed();
+ }
+
+ public boolean isSelfProfileButtonInvisible() {
+ return waitUntilElementInvisible(profileButton);
+ }
+
+ public void tapProfileImage() {
+ userProfileImage.click();
+ }
+
+ public boolean isLegalHoldIndicatorVisible() {
+ return waitUntilElementVisible(legalHoldIndicator);
+ }
+
+ public boolean isLegalHoldIndicatorInvisible() {
+ return waitUntilElementInvisible(legalHoldIndicator);
+ }
+
+ public void iTapLegalHoldIndicator() {
+ legalHoldIndicator.click();
+ }
+ public void iOpenSearchScreen() {
+ openSearchScreenButton.click();
+ }
+
+ public void iTapOnFilterButton() {
+ filterButton.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/UniqueUsernamePage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/UniqueUsernamePage.java
new file mode 100644
index 00000000000..b55f9877afa
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/UniqueUsernamePage.java
@@ -0,0 +1,33 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class UniqueUsernamePage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "handleTextField")
+ private WebElement input;
+
+ @iOSXCUITFindBy(accessibility = "Save")
+ private WebElement saveButton;
+
+ public UniqueUsernamePage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void tapSaveButton() {
+ saveButton.click();
+ }
+
+ public void inputStringInNameInput(String name) {
+ input.clear();
+ if (name.length() > 0) {
+ input.sendKeys(name);
+ }
+ }
+
+ public boolean isSaveButtonEnabled() {
+ return saveButton.isEnabled();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/UniqueUsernameTakeoverPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/UniqueUsernameTakeoverPage.java
new file mode 100644
index 00000000000..6f6a48c6130
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/UniqueUsernameTakeoverPage.java
@@ -0,0 +1,25 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.*;
+
+public class UniqueUsernameTakeoverPage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "Choose yours")
+ private WebElement chooseYoursButton;
+
+ @iOSXCUITFindBy(accessibility = "Keep this one")
+ private WebElement keepThisOneButton;
+
+ public UniqueUsernameTakeoverPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public boolean isVisible() {
+ return waitUntilElementVisible(chooseYoursButton);
+ }
+
+ public void tapKeepThisOneButton() {
+ keepThisOneButton.click();
+ }
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/VideoPlayerPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/VideoPlayerPage.java
new file mode 100644
index 00000000000..bec059ce349
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/VideoPlayerPage.java
@@ -0,0 +1,33 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class VideoPlayerPage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "Play/Pause")
+ private WebElement playPauseButton;
+
+ @iOSXCUITFindBy(accessibility = "Done")
+ private WebElement nameVideoDoneButton;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeApplication' AND name == 'Safari'")
+ private WebElement predicateWebPlayer;
+
+ public VideoPlayerPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public boolean isVideoPlayerPageOpened() {
+ return predicateWebPlayer.isDisplayed();
+ }
+
+ public boolean isPlayPauseButtonVisible() {
+ return playPauseButton.isDisplayed();
+ }
+
+ public void tapDoneButton() {
+ nameVideoDoneButton.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/VoiceFiltersOverlay.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/VoiceFiltersOverlay.java
new file mode 100644
index 00000000000..6d379741dd5
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/VoiceFiltersOverlay.java
@@ -0,0 +1,76 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import java.util.Random;
+
+public class VoiceFiltersOverlay extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "Start recording")
+ private WebElement startRecordButton;
+
+ @iOSXCUITFindBy(accessibility = "Stop recording")
+ private WebElement stopRecordButton;
+
+ @iOSXCUITFindBy(accessibility = "Send")
+ private WebElement confirmRecordButton;
+
+ @iOSXCUITFindBy(accessibility = "Helium")
+ private WebElement effectHeliumButton;
+
+ @iOSXCUITFindBy(accessibility = "Quick")
+ private WebElement effectHareButton;
+
+ @iOSXCUITFindBy(accessibility = "Deep voice")
+ private WebElement effectJellyfishButton;
+
+ @iOSXCUITFindBy(accessibility = "Hall effect")
+ private WebElement effectCathedralButton;
+
+ @iOSXCUITFindBy(accessibility = "Alien")
+ private WebElement effectAlienButton;
+
+ @iOSXCUITFindBy(accessibility = "Robotic")
+ private WebElement effectVocoderMedButton;
+
+ @iOSXCUITFindBy(accessibility = "High to deep")
+ private WebElement effectRollerCoasterButton;
+
+ private static final Random rand = new Random();
+
+ public VoiceFiltersOverlay(WebDriver driver) {
+ super(driver);
+ }
+
+ public void tapOnStartButton() {
+ startRecordButton.click();
+ }
+
+ public void tapOnStopButton() {
+ stopRecordButton.click();
+ }
+
+ public void tapOnConfirmButton() {
+ confirmRecordButton.click();
+ }
+
+ public boolean isConfirmButtonVisible() {
+ return confirmRecordButton.isDisplayed();
+ }
+
+ public boolean isConfirmButtonInVisible() {
+ return isElementInvisible(confirmRecordButton);
+ }
+
+ public void tapRandomEffectButtons(int count) {
+ final WebElement[] availableButtons = new WebElement[]{
+ effectHeliumButton, effectJellyfishButton, effectHareButton, effectCathedralButton,
+ effectAlienButton, effectVocoderMedButton, effectRollerCoasterButton
+ };
+ for (int i = 0; i < count; i++) {
+ final WebElement locator = availableButtons[rand.nextInt(availableButtons.length)];
+ locator.click();
+ }
+ }
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/WelcomePage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/WelcomePage.java
new file mode 100644
index 00000000000..eb8c73d0ba6
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/WelcomePage.java
@@ -0,0 +1,66 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class WelcomePage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "WireLogo")
+ private WebElement wireLogo;
+
+ @iOSXCUITFindBy(accessibility = "Trying to create a Pro or Enterprise account for your business or organization?")
+ private WebElement welcomeMessage;
+
+ @iOSXCUITFindBy(accessibility = "Create An Account")
+ private WebElement createAnAccountBtn;
+
+ @iOSXCUITFindBy(accessibility = "Log in")
+ private WebElement logInBtn;
+
+ @iOSXCUITFindBy(accessibility = "Enterprise Login")
+ private WebElement enterpriseLogInBtn;
+
+ @iOSXCUITFindBy(xpath = "//XCUIElementTypeStaticText")
+ private List staticTextElements;
+
+ public WelcomePage(WebDriver driver) {
+ super(driver);
+ }
+
+ public boolean isWireLogoVisible() {
+ return isElementVisible(wireLogo);
+ }
+
+ public boolean isWelcomeMessageVisible() {
+ return welcomeMessage.isDisplayed();
+ }
+
+ public boolean isWelcomePageInvisible() {
+ return isElementInvisible(wireLogo) && isElementInvisible(welcomeMessage);
+ }
+
+ public List getStaticTexts() {
+ return staticTextElements.stream().map(WebElement::getText).collect(Collectors.toList());
+ }
+
+ public boolean isEnterpriseLogInButtonVisible() {
+ return enterpriseLogInBtn.isDisplayed();
+ }
+
+ public void tapLoginButton() {
+ waitUntilElementClickable(logInBtn);
+ logInBtn.click();
+ }
+
+ public void tapCreateAnAccountButton() {
+ createAnAccountBtn.click();
+ }
+
+ public void tapEnterpriseLoginButton() {
+ waitUntilElementClickable(enterpriseLogInBtn);
+ enterpriseLogInBtn.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/calling/CallPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/calling/CallPage.java
new file mode 100644
index 00000000000..3ce96ebaee9
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/calling/CallPage.java
@@ -0,0 +1,24 @@
+package com.wearezeta.auto.ios.pages.calling;
+
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class CallPage extends IOSPage {
+
+ @iOSXCUITFindBy(iOSNsPredicate = "label == 'Constant Bit Rate'")
+ private WebElement labelCBR;
+
+ public CallPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public boolean isCBRLabelVisible() {
+ return labelCBR.isDisplayed();
+ }
+
+ public boolean isCBRLabelInvisible() {
+ return isElementInvisible(labelCBR);
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/calling/VideoCallingOverlayPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/calling/VideoCallingOverlayPage.java
new file mode 100644
index 00000000000..1078dc6d5eb
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/calling/VideoCallingOverlayPage.java
@@ -0,0 +1,365 @@
+package com.wearezeta.auto.ios.pages.calling;
+
+import com.google.zxing.NotFoundException;
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.appium.java_client.ios.IOSTouchAction;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import io.appium.java_client.touch.WaitOptions;
+import io.appium.java_client.touch.offset.PointOption;
+import org.openqa.selenium.WebDriver;
+import com.wearezeta.auto.common.imagecomparator.QRCode;
+import com.wearezeta.auto.common.misc.Timedelta;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.Dimension;
+import org.openqa.selenium.WebElement;
+import java.awt.image.BufferedImage;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+public class VideoCallingOverlayPage extends IOSPage {
+
+ @iOSXCUITFindBy(iOSNsPredicate = "name BEGINSWITH 'videoView' AND name CONTAINS 'maximized' AND type == 'XCUIElementTypeButton'")
+ private WebElement maximizedVideoView;
+
+ public VideoCallingOverlayPage(WebDriver driver) {
+ super(driver);
+ }
+
+ private static final String strNameParticipantCell = "user_cell.name";
+
+ private static final String nameStrParticipantCamera = "img.video";
+
+ private static String nameMicrophoneUnmuted = "Microphone on";
+ private static String nameMicrophoneMuted = "Microphone off";
+
+ @iOSXCUITFindBy(accessibility = "ThumbnailView")
+ private WebElement videoSelfPreview;
+
+ @iOSXCUITFindBy(accessibility = "Double tap to go back, pinch to zoom")
+ private WebElement iSeeZoomInMessage;
+
+ @iOSXCUITFindBy(accessibility = "Double tap on a tile for fullscreen")
+ private WebElement iSeeFullScreenMessage;
+
+ @iOSXCUITFindBy(accessibility = "speakers_and_all_toggle.selected.all")
+ private WebElement speakerAndAllTabSelectedAll;
+
+ @iOSXCUITFindBy(accessibility = "speakers_and_all_toggle.selected.speakers")
+ private WebElement speakerAndAllTabSelectedSpeakers;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "label == 'SPEAKERS' AND name == 'SPEAKERS' AND type == 'XCUIElementTypeButton'")
+ private WebElement speakerTab;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "label == 'ALL' AND name == 'ALL' AND value == 'ALL'")
+ private WebElement allTab;
+
+ @iOSXCUITFindBy(accessibility = "No active video speakers...")
+ private WebElement noActivitySpeakerIcon;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "value CONTAINS 'page 1'")
+ private WebElement firstPagePaginationIcon;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "value == 'page 2 of 7'")
+ private WebElement secondPagePaginationIcon;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "value BEGINSWITH 'page'")
+ private WebElement paginationIcon;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "label CONTAINS 'camera'")
+ private WebElement callVideoButton;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "label CONTAINS 'speaker'")
+ private WebElement callSpeakerButton;
+
+ @iOSXCUITFindBy(accessibility = "Turn off microphone")
+ private WebElement activeMuteButton;
+
+ @iOSXCUITFindBy(accessibility = "Turn on microphone")
+ private WebElement inactiveMuteButton;
+
+ @iOSXCUITFindBy(accessibility = "Turn on camera")
+ private WebElement switchONCameraButton;
+
+ @iOSXCUITFindBy(accessibility = "Turn off camera")
+ private WebElement switchOffCameraButton;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "label == 'Minimize calling view'")
+ private WebElement minimizeButton;
+
+ @iOSXCUITFindBy(accessibility = "toast.mutedOnJoin")
+ private WebElement muteIndicatorBanner;
+
+ @iOSXCUITFindBy(accessibility = "btn.close")
+ private WebElement closeButton;
+
+ @iOSXCUITFindBy(accessibility = "End call")
+ private WebElement callLeaveButton;
+
+ @iOSXCUITFindBy(accessibility = "ClassificationBannerClassified")
+ private WebElement nameClassifiedDomainLabel;
+
+ @iOSXCUITFindBy(accessibility = "ClassificationBannerUnclassified")
+ private WebElement nameUnclassifiedDomainLabel;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeButton' AND label == 'Back'")
+ private WebElement tapBackButton;
+
+ @iOSXCUITFindBy(accessibility = "Learn more about Wire’s pricing")
+ private WebElement learnMoreAboutWirePricingButton;
+
+ @iOSXCUITFindBy(accessibility = "Upgrade now")
+ private WebElement upgradeNowButton;
+
+ @iOSXCUITFindBy(accessibility = "Learn more about the Enterprise plan")
+ private WebElement learnMoreAboutWireEnterpriseButton;
+
+ @iOSXCUITFindBy(accessibility = "CallStatusLabel")
+ private WebElement nameCallStatusLabel;
+
+ @iOSXCUITFindBy(accessibility = "Accept")
+ private WebElement acceptButton;
+
+ @iOSXCUITFindBy(accessibility = "OK")
+ private WebElement okButton;
+
+ @iOSXCUITFindBy(accessibility = "Allow")
+ private WebElement allowButton;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "label == 'New Device' AND name == 'New Device' AND value == 'New Device'")
+ private WebElement alertNewDevice;
+
+ @iOSXCUITFindBy(accessibility = "CallFlipCameraButton")
+ private WebElement switchCameraButton;
+
+ @iOSXCUITFindBy(accessibility = "ConferenceCallingBadge")
+ private WebElement nameConferenceCallingBadge;
+
+ @iOSXCUITFindBy(accessibility = "OpenOngoingCallButton")
+ private WebElement nameRestoreOverlayButton;
+
+ // Copied from AVCallingOverlayPage to remove inheritance
+ private static final By nameMinimizeOverlayButton = MobileBy.AccessibilityId("CallDismissOverlayButton");
+ private static final By predicateSeeAllButton = MobileBy.iOSNsPredicateString("name BEGINSWITH 'Show All'");
+ private static final By nameEndCallButton = MobileBy.AccessibilityId("LeaveCallButton");
+ private static final By nameSpeakersButton = MobileBy.AccessibilityId("CallSpeakerButton");
+ private static final By nameMuteCallButton = MobileBy.AccessibilityId("CallMuteButton");
+ protected static final By nameCallVideoButton = MobileBy.iOSNsPredicateString("label CONTAINS 'camera'");
+ private static final By nameAcceptCallButton = MobileBy.AccessibilityId("AcceptButton");
+ private static final By nameBitRateLabel = MobileBy.AccessibilityId("bitrate-indicator");
+
+ private static final By predicateLoudspeakerToggled = MobileBy.iOSNsPredicateString("type == 'XCUIElementTypeButton' AND name == 'CallSpeakerButton' AND value == '1'");
+
+ private static final Function predicateCallingSpeakerByName = name -> MobileBy.iOSNsPredicateString(String.format("name == 'Turn on speaker' AND label CONTAINS '%s'", name));
+
+ private static final Function predicateCallingMuteByName = name -> MobileBy.iOSNsPredicateString(String.format("name == 'Turn off microphone' AND label CONTAINS '%s'", name));
+
+ private static final Function predicateCallingMessageByName = name -> MobileBy.iOSNsPredicateString(String.format("name == '%s' AND label CONTAINS '%s'", name, name));
+
+ private static final Function predicateActiveVideoParticipant = usernameAlias -> MobileBy.iOSNsPredicateString(String.format("label CONTAINS '%s' AND label CONTAINS 'Camera on' AND label CONTAINS 'Microphone on' AND label CONTAINS 'Active speaker' AND type == 'XCUIElementTypeButton'", usernameAlias));
+
+ private static final Function predicateActiveVideoParticipantList = usernameAlias -> MobileBy.iOSNsPredicateString(String.format("label CONTAINS '%s' AND label CONTAINS 'Microphone on' AND type == 'XCUIElementTypeStaticText'", usernameAlias));
+
+ private static final Function predicateMaximizedVideoView = usernameAlias -> MobileBy.iOSNsPredicateString(String.format("name BEGINSWITH 'videoView' AND name CONTAINS '%s' AND name CONTAINS 'maximized' AND type == 'XCUIElementTypeButton'", usernameAlias));
+
+ private static final Function predicateMinimizedVideoView = usernameAlias -> MobileBy.iOSNsPredicateString(String.format("name BEGINSWITH 'videoView' AND name CONTAINS '%s' AND name CONTAINS 'minimized' AND type == 'XCUIElementTypeButton'", usernameAlias));
+
+ private static final Function predicateActiveSpeakerVideoView = usernameAlias -> MobileBy.iOSNsPredicateString(String.format("name BEGINSWITH 'videoView' AND name CONTAINS '%s' AND name CONTAINS 'minimized' AND name ENDSWITH 'active' AND type == 'XCUIElementTypeButton'", usernameAlias));
+
+ private static final Function predicateActiveSpeaker1to1VideoView = usernameAlias -> MobileBy.iOSNsPredicateString(String.format("name BEGINSWITH 'videoView' AND name CONTAINS '%s' AND name CONTAINS 'minimized' AND name ENDSWITH 'active' AND type == 'XCUIElementTypeStaticText'", usernameAlias));
+
+ private static final Function predicateInActiveSpeakerVideoView = usernameAlias -> MobileBy.iOSNsPredicateString(String.format("name BEGINSWITH 'videoView' AND name CONTAINS '%s' AND name CONTAINS 'minimized' AND name ENDSWITH 'inactive' AND type == 'XCUIElementTypeButton'", usernameAlias));
+
+ private static final Function predicateAvatarGroupAudioParticipant = usernameAlias -> MobileBy.iOSNsPredicateString(String.format("label CONTAINS '%s' AND label CONTAINS 'Camera off' AND label CONTAINS 'Microphone' AND type == 'XCUIElementTypeButton'", usernameAlias));
+
+ private static final Function predicateAvatarAudioParticipant = usernameAlias -> MobileBy.iOSNsPredicateString(String.format("label CONTAINS '%s' AND label CONTAINS 'Camera off' AND label CONTAINS 'Microphone' AND type == 'XCUIElementTypeStaticText'", usernameAlias));
+
+ private static final Function nameOfParticipantOnLabel = name -> MobileBy.iOSClassChain(
+ String.format("**/XCUIElementTypeCollectionView/XCUIElementTypeCell[$name == 'GridCell'$]/XCUIElementTypeOther/XCUIElementTypeButton[$name CONTAINS[cd] '%s' OR name CONTAINS[cd] '%s (You)'$]", name, name));
+
+ private static final BiFunction nameOfParticipantOnLabelOnPosition = (position, name) -> MobileBy.iOSClassChain(
+ String.format("**/XCUIElementTypeCollectionView/XCUIElementTypeCell[$name == 'GridCell'$][%s]/**/XCUIElementTypeButton[$label CONTAINS[cd] '%s' OR label CONTAINS[cd] '%s (You)'$]", position, name, name));
+
+ private static final Function muteImageOnParticipantLabel = position -> MobileBy.iOSClassChain(
+ String.format("**/XCUIElementTypeCollectionView/XCUIElementTypeCell[$name == 'GridCell'$][%s]/**/XCUIElementTypeButton[$label CONTAINS[cd] '%s'$]", position, nameMicrophoneMuted));
+
+ private static final Function unmuteImageOnParticipantLabel = position -> MobileBy.iOSClassChain(
+ String.format("**/XCUIElementTypeCollectionView/XCUIElementTypeCell[$name == 'GridCell'$][%s]/**/XCUIElementTypeButton[$label CONTAINS[cd] '%s'$]", position, nameMicrophoneUnmuted));
+
+ private static final Function classChainGroupCallParticipantsCellByIdx = idx ->
+ MobileBy.iOSClassChain(String.format(
+ "**/XCUIElementTypeCollectionView/XCUIElementTypeStaticText/XCUIElementTypeOther/XCUIElementTypeOther/XCUIElementTypeOther/XCUIElementTypeStaticText[$name == '%s'$][%s]",
+ strNameParticipantCell, idx));
+
+ private static final BiFunction classChainGroupCallParticipantsCellByNameAndText =
+ (name, text) -> MobileBy.iOSClassChain(String.format(
+ "**/XCUIElementTypeCollectionView/XCUIElementTypeStaticText/XCUIElementTypeOther/XCUIElementTypeOther/XCUIElementTypeOther/XCUIElementTypeStaticText[$name == '%s'$][$value CONTAINS[cd] '%s'$][$label CONTAINS[cd] '%s'$]", strNameParticipantCell, name, text));
+
+ private static final Function classChainGroupCallParticipantsCellByName = name ->
+ MobileBy.iOSClassChain(String.format(
+ "**/XCUIElementTypeCollectionView/XCUIElementTypeStaticText[$name == '%s'$]/" +
+ "**/*[`value == '%s' OR value == '%s (You)'`]", strNameParticipantCell, name, name));
+
+ private static final BiFunction classChainGroupCallParticipantsCellByNameAndIdx = (name, idx) ->
+ MobileBy.iOSClassChain(String.format(
+ "**/XCUIElementTypeCollectionView/XCUIElementTypeStaticText[$name == '%s'$][%s]/" +
+ "**/*[`value == '%s' OR value == '%s (You)'`]", strNameParticipantCell, idx, name, name));
+
+ private static final BiFunction classChainGroupCallParticipantsCellByNameAndIcon =
+ (name, iconId) -> MobileBy.iOSClassChain(String.format(
+ "**/XCUIElementTypeCollectionView/XCUIElementTypeStaticText[$name == '%s'$][$value == '%s' OR value == '%s (You)'$]/" +
+ "**/*[`name == '%s'`]", strNameParticipantCell, name, name, iconId));
+
+ private static final BiFunction classChainGroupCallParticipantsCellByNameAndMuteIcon =
+ (name, iconId) -> MobileBy.iOSClassChain(String.format(
+ "**/XCUIElementTypeCollectionView/XCUIElementTypeStaticText[`name CONTAINS[cd] '%s'`]/" +
+ "XCUIElementTypeOther/XCUIElementTypeOther/XCUIElementTypeOther/XCUIElementTypeImage[`name == '%s'`]", name, iconId));
+
+ public void tapDeclineButton(){
+ tapAtTheCenterOfElement(callLeaveButton);
+ }
+
+ public boolean isBitRateLabelVisible() {
+ return getDriver().findElement(nameBitRateLabel).isDisplayed();
+ }
+
+ public boolean isBitRateLabelInvisible() {
+ return isLocatorInvisible(nameBitRateLabel);
+ }
+
+ public boolean isMuteIndicatorBannerVisible() {
+ return muteIndicatorBanner.isDisplayed();
+ }
+
+ public boolean isMuteIndicatorBannerInvisible() {
+ return isElementInvisible(muteIndicatorBanner);
+ }
+
+ public void tapCloseButtonOnMuteIndicatorBanner(){
+ closeButton.click();
+ }
+
+ @Deprecated // Please don't add more and instead use individual methods and iOSXCUITFindBy
+ protected By getLocatorByName(final String name) {
+ switch (name) {
+ case "Minimize":
+ return nameMinimizeOverlayButton;
+ case "Mute":
+ return nameMuteCallButton;
+ case "Leave":
+ return nameEndCallButton;
+ case "Accept":
+ return nameAcceptCallButton;
+ case "Call Video":
+ return nameCallVideoButton;
+ case "Call Speaker":
+ return nameSpeakersButton;
+ case "Show All":
+ return predicateSeeAllButton;
+ case "Constant Bitrate":
+ case "VARIABLE BIT RATE":
+ return nameBitRateLabel;
+ default:
+ throw new IllegalArgumentException(String.format("Button name '%s' is unknown", name));
+ }
+ }
+
+ public void iTapOnScreen() {
+ tapScreenByPercents(50,50);
+ }
+
+ public boolean iSeeAvatarGroupAudioParticipants(String usernameAlias) {
+ return isLocatorExist(predicateAvatarGroupAudioParticipant.apply(usernameAlias));
+ }
+
+ public boolean iDontSeeGroupAvatarAudioParticipants(String usernameAlias) {
+ return isLocatorInvisible(predicateAvatarGroupAudioParticipant.apply(usernameAlias));
+ }
+
+ public boolean isCountOfParticipantsEqualTo(int expectedNumberOfParticipants, Timedelta timeout) {
+ assert expectedNumberOfParticipants > 0 : "The expected number of participants should be greater than zero";
+ if (expectedNumberOfParticipants < 8) {
+ final By locator = classChainGroupCallParticipantsCellByIdx.apply(expectedNumberOfParticipants);
+ return isLocatorExist(locator, timeout);
+ } else {
+ return isLocatorExist(MobileBy.AccessibilityId(String.format("PARTICIPANTS (%s)", expectedNumberOfParticipants)));
+ }
+ }
+
+ public boolean isRestoreButtonVisible() {
+ return waitUntilElementVisible(nameRestoreOverlayButton);
+ }
+
+ public void tapRestoreButton() {
+ nameRestoreOverlayButton.click();
+ }
+
+ public void iTapMinimize(){
+ minimizeButton.click();
+ }
+
+ public boolean iSeeLeaveCallButton(){
+ return waitUntilElementVisible(callLeaveButton);
+ }
+
+ public boolean iDontSeeLeaveCallButton(){
+ return waitUntilElementInvisible(callLeaveButton);
+ }
+
+ public boolean isClassifiedLabelVisibleOnCallingOverlay() {
+ return waitUntilElementVisible(nameClassifiedDomainLabel);
+ }
+
+ public boolean isClassifiedLabelInvisibleOnCallingOverlay() {
+ return waitUntilElementInvisible(nameClassifiedDomainLabel);
+ }
+
+ public boolean isUnclassifiedLabelVisibleOnCallingOverlay() {
+ return waitUntilElementVisible(nameUnclassifiedDomainLabel);
+ }
+
+ public boolean isUnclassifiedLabelInvisibleOnCallingOverlay() {
+ return waitUntilElementInvisible(nameUnclassifiedDomainLabel);
+ }
+
+ public void tapBackButton(){
+ tapAtTheCenterOfElement(tapBackButton);
+ }
+
+ public boolean isCallingMessageContainingVisible(String text) {
+ return isLocatorDisplayed(predicateCallingMessageByName.apply(text));
+ }
+
+ public void tapAcceptButton(){
+ acceptButton.click();
+ }
+
+ public void tapOKButton() {
+ waitUntilElementVisible(okButton);
+ okButton.click();
+ }
+
+ public void swipeUpParticipantsList(){
+ Dimension dimension = getDriver().manage().window().getSize();
+ int scrollStart = (int) (dimension.getHeight() * 0.9);
+ int scrollEnd = (int) (dimension.getHeight() * 0.1);
+ IOSTouchAction action = new IOSTouchAction(getDriver());
+ action.press(PointOption.point(1,scrollStart))
+ .waitAction(WaitOptions.waitOptions(Duration.ofMillis(300)))
+ .moveTo(PointOption.point(1, scrollEnd))
+ .release().perform();
+ }
+
+ public boolean iSeeNewDeviceAlert(){
+ waitUntilElementVisible(alertNewDevice);
+ return alertNewDevice.isDisplayed();
+ }
+
+ public boolean iDontSeeNewDeviceAlert(){
+ return isElementInvisible(alertNewDevice);
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/common/CertificateDetailsPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/common/CertificateDetailsPage.java
new file mode 100644
index 00000000000..2f5b66fa7a5
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/common/CertificateDetailsPage.java
@@ -0,0 +1,28 @@
+package com.wearezeta.auto.ios.pages.details_overlay.common;
+
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class CertificateDetailsPage extends IOSPage {
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeButton[`name == \"Show Certificate Details\"`]")
+ private WebElement showCertificate;
+
+ @iOSXCUITFindBy(accessibility = "CertificateDetailsView")
+ private WebElement certificateDetails;
+
+ public CertificateDetailsPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void openCertificateDetails() {
+ showCertificate.click();
+ }
+
+ public boolean isCertificateDetailsPageVisible() {
+ return certificateDetails.isDisplayed();
+ }
+
+ public String getCertificate() { return certificateDetails.getText(); }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/common/DeviceDetailsPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/common/DeviceDetailsPage.java
new file mode 100644
index 00000000000..eacf752ef9f
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/common/DeviceDetailsPage.java
@@ -0,0 +1,75 @@
+package com.wearezeta.auto.ios.pages.details_overlay.common;
+
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import java.util.function.Function;
+
+public class DeviceDetailsPage extends IOSPage {
+
+ // TODO: Once the accessibility identifier for the device verified switch is fixed, clean this up
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeSwitch[`value == \"0\"`][2]")
+ private WebElement verifySwitcher;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "label == 'Device verified' AND name == 'Device verified' AND type == 'XCUIElementTypeSwitch'")
+ private WebElement verifyDeviceSwitcher;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "label == 'Verified' AND name == 'device verified' AND type == 'XCUIElementTypeSwitch'")
+ private WebElement verifySelfDeviceSwitcher;
+
+ @iOSXCUITFindBy(accessibility = "fingerprint")
+ private WebElement fingerprint;
+
+ @iOSXCUITFindBy(accessibility = "Remove Device")
+ private WebElement removeDevice;
+
+ @iOSXCUITFindBy(accessibility = "Go back to device overview")
+ private WebElement backButton;
+
+ @iOSXCUITFindBy(accessibility = "Back")
+ private WebElement backButtonDeviceDetails;
+
+ @iOSXCUITFindBy(accessibility = "Go back to device list")
+ private WebElement backButtonDeviceList;
+
+ @iOSXCUITFindBy(accessibility = "Revoked")
+ private WebElement revoked;
+
+ private static final Function predicateText = text -> MobileBy.iOSNsPredicateString(String.format("type == 'XCUIElementTypeLink' AND value CONTAINS '%s'", text));
+
+ public DeviceDetailsPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void tapVerifyToggle() {
+ if (isElementVisible(verifySwitcher)){
+ verifySwitcher.click();}
+ else if (isElementVisible(verifyDeviceSwitcher)){
+ verifyDeviceSwitcher.click();
+ } else {
+ verifySelfDeviceSwitcher.click();
+ }
+ }
+
+ public void tapBackButton() {
+ if (isElementVisible(backButton)){
+ backButton.click();
+ } else if (isElementVisible(backButtonDeviceDetails)) {
+ backButtonDeviceDetails.click();
+ } else {
+ backButtonDeviceList.click();
+ }
+ }
+
+ public void tapRemoveDeviceButton() {
+ removeDevice.click();
+ }
+
+ public boolean isDeviceRevoked() {
+ return revoked.isDisplayed();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/common/UserDetailsDevicesPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/common/UserDetailsDevicesPage.java
new file mode 100644
index 00000000000..2790f6863eb
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/common/UserDetailsDevicesPage.java
@@ -0,0 +1,60 @@
+package com.wearezeta.auto.ios.pages.details_overlay.common;
+
+import com.wearezeta.auto.ios.pages.IOSPage;
+import org.openqa.selenium.WebDriver;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+
+import java.util.function.Function;
+
+public class UserDetailsDevicesPage extends IOSPage {
+
+ private static final String xpathStrDevicesRoot =
+ "//XCUIElementTypeButton[@label='Devices']/following::XCUIElementTypeCollectionView";
+
+ private static final By predicateStrDevideTitleValue = MobileBy.iOSNsPredicateString("name == 'device_cell.name' AND value == 'Legal Hold'");
+
+ private static final By nameLegalHoldIcon = MobileBy.AccessibilityId("img.device_class.legalhold");
+ private final String notVerifiedLabel = "Not Verified";
+
+ private final Function classChainGetDeviceCellByNumber = (deviceNumber) -> MobileBy.iOSClassChain(String.format("**/XCUIElementTypeCollectionView[`visible == 1`]/XCUIElementTypeCell[%s]", deviceNumber));
+
+ private final Function xpathDeviceByIndex =
+ idx -> By.xpath(String.format("%s/XCUIElementTypeCell[%s]", getDevicesListRootXPath(), idx));
+
+ private final Function xpathDevicesByCount = count ->
+ By.xpath(String.format("//XCUIElementTypeCollectionView[count(XCUIElementTypeCell)=%s]", count));
+
+ public UserDetailsDevicesPage(WebDriver driver) {
+ super(driver);
+ }
+
+ protected String getDevicesListRootXPath() {
+ return xpathStrDevicesRoot;
+ }
+
+ public void openDeviceDetailsPage(int deviceIndex) {
+ final By locator = xpathDeviceByIndex.apply(deviceIndex);
+ getElement(locator).click();
+ }
+
+ public boolean isParticipantDevicesCountEqualTo(int expectedCount) {
+ final By locator = xpathDevicesByCount.apply(expectedCount);
+ return isLocatorDisplayed(locator);
+ }
+
+ public boolean isLegalHoldTheFirstDevice() {
+ final By cellIndexLocator = xpathDeviceByIndex.apply(2);
+ return isLocatorDisplayed(getElement(cellIndexLocator), predicateStrDevideTitleValue) && isLocatorExist(nameLegalHoldIcon);
+ }
+
+ public boolean isDeviceNumberNotVerified(int number) {
+ number++;
+ return getElement(classChainGetDeviceCellByNumber.apply(number)).getAttribute("label").contains(notVerifiedLabel);
+ }
+
+ public boolean isDeviceNumberVerified(int number) {
+ number++;
+ return !getElement(classChainGetDeviceCellByNumber.apply(number)).getAttribute("label").contains(notVerifiedLabel);
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/common/UserSettingsDevicesPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/common/UserSettingsDevicesPage.java
new file mode 100644
index 00000000000..317adcac07c
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/common/UserSettingsDevicesPage.java
@@ -0,0 +1,103 @@
+package com.wearezeta.auto.ios.pages.details_overlay.common;
+
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.appium.java_client.AppiumBy;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import java.util.function.Function;
+
+public class UserSettingsDevicesPage extends IOSPage {
+
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeStaticText[`name == \"device proteus ID\"`][1]")
+ private WebElement deviceID;
+
+ @iOSXCUITFindBy(accessibility = "Delete")
+ private WebElement deleteButton;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeSecureTextField' AND value CONTAINS 'Password'")
+ private WebElement deleteDevicePasswordField;
+
+ @iOSXCUITFindBy(accessibility = "Wrong password")
+ private WebElement wrongPasswordDialog;
+
+ @iOSXCUITFindBy(accessibility = "OK")
+ private WebElement oKButton;
+
+ private static final Function predicateDeleteDeviceButtonByName = deviceName ->
+ MobileBy.iOSNsPredicateString(String.format("type == 'XCUIElementTypeButton' AND label CONTAINS 'Remove %s'",
+ deviceName));
+
+ private static final Function predicateDeleteDeviceButtonByID = deviceID ->
+ MobileBy.iOSNsPredicateString(String.format("name == 'device proteus ID' AND label CONTAINS '%s'",
+ deviceID));
+
+ private static final Function predicateDeviceListEntry = device -> MobileBy.iOSNsPredicateString(
+ String.format("name == 'device name' AND value CONTAINS '%s'", device));
+
+ private final Function xpathDevicesByCount = count ->
+ By.xpath(String.format("//XCUIElementTypeTable[count(XCUIElementTypeCell)=%s]", count));
+
+ private final Function classChainDeviceForIndex =
+ idx -> String.format("**/XCUIElementTypeStaticText[`name == \"device proteus ID\"`][%s]", idx);
+
+ public UserSettingsDevicesPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void tapDeleteDeviceButton(String deviceName) {
+ final By locator = predicateDeleteDeviceButtonByName.apply(deviceName);
+ getElement(locator).click();
+ }
+
+ public void tapDeleteButton() {
+ deleteButton.click();
+ if (!isElementInvisible(deleteButton)) {
+ deleteButton.click();
+ }
+ }
+
+ public void typePasswordToConfirmDeleteDevice(String password) {
+ deleteDevicePasswordField.sendKeys(password);
+ }
+
+ public boolean isDeviceVisibleInList(String device) {
+ final By locator = predicateDeviceListEntry.apply(device);
+ return isLocatorDisplayed(locator);
+ }
+
+ public boolean isDeviceInvisibleInList(String device) {
+ final By locator = predicateDeviceListEntry.apply(device);
+ return isLocatorInvisible(locator);
+ }
+
+ public boolean isUserDevicesCountEqualTo(int expectedCount) {
+ final By locator = xpathDevicesByCount.apply(expectedCount);
+ return isLocatorDisplayed(locator);
+ }
+
+ public String getCurrentDeviceID() {
+ return deviceID.getAttribute("value");
+ }
+
+ public void openDeviceDetailsPage(int deviceIndex) {
+ final By locator = AppiumBy.iOSClassChain(classChainDeviceForIndex.apply(deviceIndex));
+ getDriver().findElement(locator).click();
+ }
+
+ public boolean isWrongPasswordDialogVisible() {
+ return waitUntilElementVisible(wrongPasswordDialog);
+ }
+
+ public void tapOKOnWrongPasswordDialog() {
+ oKButton.click();
+ }
+
+ public void openDeviceDetailsPageById(String deviceId) {
+ final By locator = predicateDeleteDeviceButtonByID.apply(deviceId);
+ getDriver().findElement(locator).click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/group/GroupAddPeoplePage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/group/GroupAddPeoplePage.java
new file mode 100644
index 00000000000..424f06adfcd
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/group/GroupAddPeoplePage.java
@@ -0,0 +1,51 @@
+package com.wearezeta.auto.ios.pages.details_overlay.group;
+
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import com.wearezeta.auto.ios.pages.search.GroupParticipantsSearchList;
+import org.openqa.selenium.WebElement;
+
+public class GroupAddPeoplePage extends IOSPage {
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeButton' AND name == 'Add Participants'")
+ private WebElement addPeopleButton;
+
+ @iOSXCUITFindBy(accessibility = "close")
+ private WebElement exitAddPeopleButton;
+
+ private final GroupParticipantsSearchList participantsSearchList;
+
+ public GroupAddPeoplePage(WebDriver driver) {
+ super(driver);
+ this.participantsSearchList = new GroupParticipantsSearchList(driver);
+ }
+
+ public void tapCloseButton() {
+ exitAddPeopleButton.click();
+ }
+
+ public void tapAddButton() {
+ addPeopleButton.click();
+ }
+
+ public void typeSearchQuery(String text) {
+ participantsSearchList.typeSearchQuery(text);
+ }
+
+ public void selectItem(String name) {
+ participantsSearchList.selectItem(name);
+ }
+
+ public boolean isItemVisible(String name) {
+ return participantsSearchList.isItemVisible(name);
+ }
+
+ public boolean isItemInvisible(String name) {
+ return participantsSearchList.isItemInvisible(name);
+ }
+
+ public boolean waitUntilResultsLabelIsVisible(String msg) {
+ return participantsSearchList.waitUntilResultsLabelIsVisible(msg);
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/group/GroupConnectedParticipantProfilePage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/group/GroupConnectedParticipantProfilePage.java
new file mode 100644
index 00000000000..4a9acf41444
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/group/GroupConnectedParticipantProfilePage.java
@@ -0,0 +1,140 @@
+package com.wearezeta.auto.ios.pages.details_overlay.group;
+
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import java.util.function.Function;
+
+public class GroupConnectedParticipantProfilePage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "left_button")
+ private WebElement leftActionButton;
+
+ @iOSXCUITFindBy(accessibility = "right_button")
+ private WebElement rightActionButton;
+
+ @iOSXCUITFindBy(accessibility = "cell.profile.group_admin_options")
+ private WebElement adminToggle;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "name == 'label.team_role' AND value = 'External'")
+ private WebElement externalIcon;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "name == 'label.group_role' AND value == 'Group Admin'")
+ private WebElement adminIcon;
+
+ @iOSXCUITFindBy(accessibility = "Go back to conversation details")
+ private WebElement backButton;
+
+ @iOSXCUITFindBy(accessibility = "Back")
+ private WebElement back;
+
+ @iOSXCUITFindBy(accessibility = "Devices")
+ private WebElement devicesTab;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeStaticText' AND name IN {'user_profile.name','name'}")
+ private WebElement nameLabel;
+
+ private static final String strOpenConversationButton = "Open conversation";
+ private static final String strConnectButton = "Connect";
+ private Function predicateLeftButtonByLabel = text ->
+ MobileBy.iOSNsPredicateString(String.format("name == 'left_button' AND label == '%s'",text));
+
+ private static final Function predicateNameByValue = name -> MobileBy.iOSNsPredicateString(
+ String.format("type == 'XCUIElementTypeStaticText' AND name IN {'user_profile.name','name'} AND value == '%s'",
+ name));
+
+ public GroupConnectedParticipantProfilePage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void tapRemoveFromConversationButton() {
+ rightActionButton.click();
+ }
+
+ public void tapOpenConversationButton() {
+ leftActionButton.click();
+ }
+
+ public void tapBackButton() {
+ if(isElementVisible(backButton)){
+ backButton.click();
+ } else {
+ back.click();
+ }
+ }
+
+ public void tapOpenMenuButton() {
+ rightActionButton.click();
+ }
+
+ public boolean isOpenConversationButtonVisible() {
+ return isLocatorDisplayed(predicateLeftButtonByLabel.apply(strOpenConversationButton));
+ }
+
+ public boolean isOpenConversationInvisible() {
+ return isLocatorInvisible(predicateLeftButtonByLabel.apply(strOpenConversationButton));
+ }
+
+ public boolean isMoreActionsButtonVisible() {
+ return isElementVisible(rightActionButton);
+ }
+
+ public boolean isMoreActionsButtonInvisible() { return isElementInvisible(rightActionButton);
+ }
+
+ public boolean isConnectButtonVisible() {
+ return isLocatorDisplayed(predicateLeftButtonByLabel.apply(strConnectButton));
+ }
+
+ public boolean isConnectButtonInvisible() {
+ return isLocatorInvisible(predicateLeftButtonByLabel.apply(strConnectButton));
+ }
+
+ public boolean isLeftActionButtonInvisible() {
+ return isElementInvisible(leftActionButton);
+ }
+
+ public boolean isAdminToggleVisible() {
+ return adminToggle.isDisplayed();
+ }
+
+ public boolean isAdminToggleInvisible() {
+ return isElementInvisible(adminToggle);
+ }
+
+ public void tapAdminToggle() {
+ adminToggle.click();
+ }
+
+ public boolean isAdminIconVisible() {
+ return adminIcon.isDisplayed();
+ }
+
+ public boolean isAdminIconInvisible() {
+ return isElementInvisible(adminIcon);
+ }
+
+ public boolean isExternalIconVisible() {
+ return externalIcon.isDisplayed();
+ }
+
+ public boolean isExternalIconInvisible() {
+ return isElementInvisible(externalIcon);
+ }
+
+ public boolean isUserDetailNameVisible(String expectedValue){
+ return isLocatorDisplayed(predicateNameByValue.apply(expectedValue));
+ }
+
+ public boolean isUserDetailNameInvisible() {
+ return isElementInvisible(nameLabel);
+ }
+
+ public void tapDevicesTab() {
+ devicesTab.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/group/GroupDetailsPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/group/GroupDetailsPage.java
new file mode 100644
index 00000000000..ba7af36c728
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/group/GroupDetailsPage.java
@@ -0,0 +1,265 @@
+package com.wearezeta.auto.ios.pages.details_overlay.group;
+
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import com.wearezeta.auto.common.misc.Timedelta;
+import com.wearezeta.auto.ios.pages.search.GroupParticipantsList;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebDriverException;
+import org.openqa.selenium.WebElement;
+
+import java.util.function.Function;
+
+public class GroupDetailsPage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "close")
+ private WebElement exitGroupInfoPageButton;
+
+ @iOSXCUITFindBy(accessibility = "OtherUserMetaControllerLeftButton")
+ private WebElement addPeopleButton;
+
+ @iOSXCUITFindBy(accessibility = "OtherUserMetaControllerRightButton")
+ private WebElement openMenuButton;
+
+ @iOSXCUITFindBy(accessibility = "group_details.list")
+ private WebElement listRoot;
+
+ @iOSXCUITFindBy(accessibility = "NameField")
+ private WebElement conversationNameTextField;
+
+ @iOSXCUITFindBy(accessibility = "ReadReceiptsSwitch")
+ private WebElement toggleReadReceipts;
+
+ @iOSXCUITFindBy(accessibility = "cell.groupdetails.guestoptions")
+ private WebElement guestOptionsCell;
+
+ @iOSXCUITFindBy(accessibility = "cell.groupdetails.servicesoptions")
+ private WebElement servicesOptionsCell;
+
+ @iOSXCUITFindBy(accessibility = "cell.groupdetails.timeoutoptions")
+ private WebElement timedMessageOptionCell;
+
+ @iOSXCUITFindBy(accessibility = "legalhold")
+ private WebElement legalHoldIndicator;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "name BEGINSWITH 'GROUP MEMBERS'")
+ private WebElement membersSection;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "name BEGINSWITH 'Participants'")
+ private WebElement seeAllButton;
+
+ private static final Function predicateConversationNameByText = text -> MobileBy.iOSNsPredicateString(
+ String.format("name == 'NameField' AND value == '%s'", text));
+
+ private static final Function classChainUserInAdminSection = userName ->
+ MobileBy.iOSClassChain(String.format("**/XCUIElementTypeCollectionView[`name == " +
+ "'group_details.list'`]/*[`name == 'Admins - participants.section.participants.cell'`]/**/XCUIElementTypeStaticText[`value == '%s' OR value == '%s (You)'`]", userName, userName));
+
+ private static final By classChainShowAllInAdminSection =
+ MobileBy.iOSClassChain("**/XCUIElementTypeCollectionView[`name == " +
+ "'group_details.list'`]/*[`name == 'Admins - cell.call.show_all_participants'`]/**/XCUIElementTypeStaticText[`value BEGINSWITH 'Show All'`]");
+
+ private static final Function predicateStrMembersCount = count ->
+ String.format("value == 'GROUP MEMBERS (%s)'", count);
+
+ private static final Function predicateStrAdminsCount = count ->
+ String.format("value == 'GROUP ADMINS (%s)'", count);
+
+ private final GroupParticipantsList participantsList;
+
+ public GroupDetailsPage(WebDriver driver) {
+ super(driver);
+ this.participantsList = new GroupParticipantsList(driver);
+ }
+
+
+ public boolean isGroupNameEqualTo(String expectedName) {
+ final By locator = predicateConversationNameByText.apply(expectedName);
+ return isLocatorDisplayed(locator);
+ }
+
+ public void setGroupChatName(String name) {
+ this.isKeyboardVisible();
+ try {
+ this.tapAtTheCenterOfElement(conversationNameTextField);
+ this.tapAtTheCenterOfElement(conversationNameTextField);
+ conversationNameTextField.clear();
+ } catch (WebDriverException e) {
+ this.tapAtTheCenterOfElement(conversationNameTextField);
+ conversationNameTextField.clear();
+ }
+ conversationNameTextField.sendKeys(name);
+ tapKeyboardCommitButton();
+ }
+
+ public boolean isGroupChatNameEnabled(){
+ this.tapAtTheCenterOfElement(conversationNameTextField);
+ return this.isKeyboardVisible();
+ }
+
+ public boolean isNumberOfMembersParticipantsEquals(int expectedNumber) {
+ final By locator = MobileBy.iOSNsPredicateString(predicateStrMembersCount.apply(expectedNumber));
+ if (!isLocatorDisplayed(locator, Timedelta.ofSeconds(3))) {
+ // Sometimes the list is too long, so we need to scroll it a bit
+ this.swipe(listRoot, SwipeDirection.UP);
+ return isLocatorDisplayed(locator, Timedelta.ofSeconds(5));
+ }
+ return true;
+ }
+
+ public boolean isNumberOfAdminsParticipantsEquals(int expectedNumber) {
+ final By locator = MobileBy.iOSNsPredicateString(predicateStrAdminsCount.apply(expectedNumber));
+ if (!isLocatorDisplayed(locator, Timedelta.ofSeconds(3))) {
+ // Sometimes the list is too long, so we need to scroll it a bit
+ this.swipe(listRoot, SwipeDirection.UP);
+ return isLocatorDisplayed(locator, Timedelta.ofSeconds(5));
+ }
+ return true;
+ }
+
+ public int getGroupNameLength() {
+ return conversationNameTextField.getText().length();
+ }
+
+ public void tapAddPeopleButton() {
+ addPeopleButton.click();
+ }
+
+ public void tapOpenMenuButton() {
+ openMenuButton.click();
+ }
+
+ public void tapXButton() {
+ waitUntilElementClickable(exitGroupInfoPageButton);
+ exitGroupInfoPageButton.click();
+ }
+
+ public boolean isAddPeopleButtonVisible() {
+ return addPeopleButton.isDisplayed();
+ }
+
+ public boolean isAddPeopleButtonInvisible() {
+ return isElementInvisible(addPeopleButton);
+ }
+
+ public boolean isExternalIndicatorVisibleFor(String name) {
+ return participantsList.isExternalIconVisibleFor(name);
+ }
+
+ public void openGuestOptions() {
+ guestOptionsCell.click();
+ }
+
+ public boolean isGuestOptionsVisible() {
+ return guestOptionsCell.isDisplayed();
+ }
+
+ public boolean isGuestOptionsInvisible() {
+ return isElementInvisible(guestOptionsCell);
+ }
+
+ public boolean isServicesOptionsVisible() {
+ return servicesOptionsCell.isDisplayed();
+ }
+
+ public void selectParticipant(String name) {
+ if (participantsList.isParticipantVisible(name)) {
+ participantsList.selectParticipant(name);
+ } else {
+ int nScroll = 0;
+ int maxScroll = 2;
+ while (participantsList.isParticipantInvisible(name) && nScroll <= maxScroll) {
+ //scroll down
+ this.swipe(listRoot, SwipeDirection.UP);
+ nScroll++;
+ }
+ if (participantsList.isParticipantInvisible(name)) {
+ nScroll = 0;
+ while(participantsList.isParticipantInvisible(name) && nScroll <= maxScroll){
+ //scroll up
+ this.swipe(listRoot, SwipeDirection.DOWN);
+ nScroll++;
+ }
+ }
+ participantsList.selectParticipant(name);
+ }
+ }
+
+ public int getServicesCount() {
+ return participantsList.getServicesCount();
+ }
+
+ public int getParticipantsCount() {
+ if (!participantsList.isParticipantCellVisible()) {
+ // Sometimes the list is too long, so we need to scroll it a bit
+ this.swipe(listRoot, SwipeDirection.UP);
+ }
+ return participantsList.getPeopleCount();
+ }
+
+ public boolean isParticipantVisible(String name) {
+ return participantsList.isParticipantVisible(name);
+ }
+
+ public boolean isParticipantInvisible(String name) {
+ return participantsList.isParticipantInvisible(name);
+ }
+
+ public boolean isTimedMessagesOptionVisible() {
+ return timedMessageOptionCell.isDisplayed();
+ }
+
+ public boolean isTimedMessagesOptionInvisible() {
+ return isElementInvisible(timedMessageOptionCell);
+ }
+
+ public boolean isReadReceiptsVisible() {
+ return toggleReadReceipts.isDisplayed();
+ }
+
+ public boolean isReadReceiptsInvisible() {
+ return isElementInvisible(toggleReadReceipts);
+ }
+
+ public boolean isLegalHoldIndicatorVisible() {
+ return isElementVisible(legalHoldIndicator);
+ }
+
+ public boolean isLegalHoldIndicatorInvisible() {
+ return isElementInvisible(legalHoldIndicator);
+ }
+
+ public void tapLegalHoldIndicator() {
+ legalHoldIndicator.click();
+ }
+
+ public boolean isMembersSectionVisible() {
+ return membersSection.isDisplayed();
+ }
+
+ public boolean isMembersSectionInvisible() {
+ return isElementInvisible(membersSection);
+ }
+
+ public boolean isUserInAdminsSection(String userName) {
+ return isLocatorDisplayed(classChainUserInAdminSection.apply(userName));
+ }
+
+ public boolean isUserNotInAdminsSection(String userName) {
+ return isLocatorInvisible(classChainUserInAdminSection.apply(userName));
+ }
+
+ public boolean isSeeAllButtonVisibleInAdminsSection() {
+ return isLocatorDisplayed(classChainShowAllInAdminSection);
+ }
+
+ public boolean isSeeAllButtonInvisibleInAdminsSection() {
+ return isLocatorInvisible(classChainShowAllInAdminSection);
+ }
+
+ public void tapSeeAllButton() {
+ seeAllButton.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/group/GroupPendingParticipantIncomingConnectionPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/group/GroupPendingParticipantIncomingConnectionPage.java
new file mode 100644
index 00000000000..992a30479b7
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/group/GroupPendingParticipantIncomingConnectionPage.java
@@ -0,0 +1,23 @@
+package com.wearezeta.auto.ios.pages.details_overlay.group;
+
+import com.wearezeta.auto.ios.pages.IOSPage;
+import org.openqa.selenium.WebDriver;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+
+import java.util.function.Function;
+
+public class GroupPendingParticipantIncomingConnectionPage extends IOSPage {
+
+ private static final Function predicateNameByValue = name -> MobileBy.iOSNsPredicateString(
+ String.format("type == 'XCUIElementTypeStaticText' AND name IN {'user_profile.name','name'} AND value == '%s'",
+ name));
+
+ public GroupPendingParticipantIncomingConnectionPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public boolean isUserDetailNameVisible(String name) {
+ return isLocatorDisplayed(predicateNameByValue.apply(name));
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/group/GroupPendingParticipantOutgoingConnectionPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/group/GroupPendingParticipantOutgoingConnectionPage.java
new file mode 100644
index 00000000000..50e180a2002
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/group/GroupPendingParticipantOutgoingConnectionPage.java
@@ -0,0 +1,47 @@
+package com.wearezeta.auto.ios.pages.details_overlay.group;
+
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import java.util.function.Function;
+
+public class GroupPendingParticipantOutgoingConnectionPage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "Go back to conversation details")
+ private WebElement backButton;
+
+ @iOSXCUITFindBy(accessibility = "right_button")
+ private WebElement openMenuButton;
+
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeButton[`label == 'Connect'`][-1]")
+ private WebElement connectButton;
+
+
+ private static final Function predicateNameByValue = name -> MobileBy.iOSNsPredicateString(
+ String.format("type == 'XCUIElementTypeStaticText' AND name IN {'user_profile.name','name'} AND value == '%s'",
+ name));
+
+ public GroupPendingParticipantOutgoingConnectionPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void tapBackButton() {
+ backButton.click();
+ }
+
+ public void tapOpenMenuButton() {
+ openMenuButton.click();
+ }
+
+ public boolean isUserNameVisible(String value) {
+ return isLocatorDisplayed(predicateNameByValue.apply(value));
+ }
+
+ public boolean isConnectButtonVisible() {
+ return connectButton.isDisplayed();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/group/GroupPeoplePage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/group/GroupPeoplePage.java
new file mode 100644
index 00000000000..340561070c2
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/group/GroupPeoplePage.java
@@ -0,0 +1,67 @@
+package com.wearezeta.auto.ios.pages.details_overlay.group;
+
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import java.util.function.Function;
+
+public class GroupPeoplePage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "GROUP MEMBERS")
+ private WebElement membersSection;
+
+ @iOSXCUITFindBy(accessibility = "GROUP ADMINS")
+ private WebElement adminsSection;
+
+ private static final Function classChainExternalIconForUser = name ->
+ MobileBy.iOSClassChain(String.format("**/XCUIElementTypeCollectionView[`name == 'group_details.full_list'`]/XCUIElementTypeCell[`name ENDSWITH 'participants.section.participants.cell'`][$name == 'user_cell.name' AND value == '%s' OR name == 'user_cell.name' AND value == '%s (You)'$]/**/XCUIElementTypeImage[`name == 'img.external'`]", name, name
+ ));
+
+ private static final Function classChainUserInAdminSection = userName ->
+ MobileBy.iOSClassChain(String.format("**/XCUIElementTypeCollectionView[`name == " +
+ "'group_details.full_list'`]/*[`name == 'Admins - participants.section.participants.cell'`]/**/XCUIElementTypeStaticText[`value == '%s' OR value == '%s (You)'`]", userName, userName));
+
+ private static final Function classChainUser = userName ->
+ MobileBy.iOSClassChain(String.format("**/XCUIElementTypeCollectionView[`name == " +
+ "'group_details.full_list'`]/*[`name ENDSWITH 'participants.section.participants.cell'`]/**/XCUIElementTypeStaticText[`value == '%s' OR value == '%s (You)'`]", userName, userName));
+
+ public GroupPeoplePage(WebDriver driver) {
+ super(driver);
+ }
+
+ public boolean isExternalIndicatorVisibleFor(String name) {
+ return isLocatorDisplayed(classChainExternalIconForUser.apply(name));
+ }
+
+ public boolean isMembersSectionVisible() {
+ return membersSection.isDisplayed();
+ }
+
+ public boolean isMembersSectionInvisible() {
+ return isElementInvisible(membersSection);
+ }
+
+ public boolean isAdminsSectionVisible() {
+ return adminsSection.isDisplayed();
+ }
+
+ public boolean isAdminsSectionInvisible() {
+ return isElementInvisible(adminsSection);
+ }
+
+ public boolean isUserInAdminsSection(String userName) {
+ return isLocatorDisplayed(classChainUserInAdminSection.apply(userName));
+ }
+
+ public boolean isUserNotInAdminsSection(String userName) {
+ return isLocatorInvisible(classChainUserInAdminSection.apply(userName));
+ }
+
+ public void selectParticipantPeoplePage(String name) {
+ getElement(classChainUser.apply(name)).click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/group/GuestOptionsPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/group/GuestOptionsPage.java
new file mode 100644
index 00000000000..9133d40af2f
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/group/GuestOptionsPage.java
@@ -0,0 +1,44 @@
+package com.wearezeta.auto.ios.pages.details_overlay.group;
+
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import java.util.function.Function;
+
+public class GuestOptionsPage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "Go back to conversation details")
+ private WebElement backButton;
+
+ @iOSXCUITFindBy(accessibility = "Create Link")
+ private WebElement createLinkButton;
+
+ private static final Function predicateStrAllowGuestsByValue = text ->
+ MobileBy.iOSNsPredicateString(String.format("name == 'toggle.guestoptions.allowguests' AND value == '%s'", text));
+
+ public GuestOptionsPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void tapBackButton() {
+ backButton.click();
+ }
+
+ public boolean isCreateLinkButtonVisible() {
+ return createLinkButton.isDisplayed();
+ }
+
+ public boolean isCreateLinkButtonInvisible() {
+ return isElementInvisible(createLinkButton);
+ }
+
+ public boolean isAllowGuestsEqualsTo(String expectedValue) {
+ final By locator = predicateStrAllowGuestsByValue.apply(expectedValue);
+ return isLocatorDisplayed(locator);
+ }
+
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/single/ConnectionInboxPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/single/ConnectionInboxPage.java
new file mode 100644
index 00000000000..5bfc8904670
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/single/ConnectionInboxPage.java
@@ -0,0 +1,115 @@
+package com.wearezeta.auto.ios.pages.details_overlay.single;
+
+import java.util.function.Function;
+
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.appium.java_client.MobileBy;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.By;
+
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class ConnectionInboxPage extends IOSPage {
+
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeButton[`name ==[c] 'ignore'`][-1]")
+ private WebElement ignoreButton;
+
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeButton[`name == 'accept' OR name == 'CONNECT'`][-1]")
+ private WebElement connectButton;
+
+ private static final Function predicateNameByValue = name -> MobileBy.iOSNsPredicateString(
+ String.format("type == 'XCUIElementTypeStaticText' AND name IN {'user_profile.name','name'} AND value == '%s'",
+ name));
+ private static final By predicateName = MobileBy.iOSNsPredicateString(
+ "type == 'XCUIElementTypeStaticText' AND name IN {'user_profile.name','name'}");
+
+ private static final Function predicateABNameByName = name -> MobileBy.iOSNsPredicateString(
+ String.format("type == 'XCUIElementTypeStaticText' AND name == 'correlation' AND value='%s'",
+ name.trim().length() > 0 ? (name + " in Contacts") : "in Contacts"));
+ private static final By nameABName = MobileBy.AccessibilityId("correlation");
+
+ private static final Function predicateUniqueUsernameByUsername = username -> MobileBy.iOSNsPredicateString(
+ String.format("type == 'XCUIElementTypeStaticText' AND name IN {'user_profile.username', 'username', 'handle'} AND value == '%s'",
+ username.startsWith("@") ? username : ("@" + username)));
+ private static final By predicateUniqueUsername = MobileBy.iOSNsPredicateString(
+ "type == 'XCUIElementTypeStaticText' AND name IN {'user_profile.username', 'username', 'handle'}");
+
+ private static final Function predicateSSONameByValue = name -> MobileBy.iOSNsPredicateString(
+ String.format("type == 'XCUIElementTypeStaticText' AND name IN {'user_profile.name','name'} AND value CONTAINS '%s'",
+ name));
+
+ private static final By nameTeamName = MobileBy.AccessibilityId("team name");
+ private static final Function predicateTeamName = name -> MobileBy.iOSNsPredicateString(
+ String.format("name == 'team name' AND value == '%s'", name.toUpperCase()));
+
+ public ConnectionInboxPage(WebDriver driver) {
+ super(driver);
+ }
+
+ protected By getUserDetailLocator(String detailName, String expectedValue) {
+ switch (detailName.toLowerCase()) {
+ case "name":
+ return predicateNameByValue.apply(expectedValue);
+ case "unique username":
+ return predicateUniqueUsernameByUsername.apply(expectedValue);
+ case "address book name":
+ return predicateABNameByName.apply(expectedValue);
+ case "team name":
+ return predicateTeamName.apply(expectedValue);
+ case "sso username":
+ return predicateSSONameByValue.apply(expectedValue);
+ default:
+ throw new IllegalArgumentException(String.format("Unknown user detail name '%s'", detailName));
+ }
+ }
+
+ private By getUserDetailLocator(String detailName) {
+ switch (detailName.toLowerCase()) {
+ case "name":
+ return predicateName;
+ case "unique username":
+ return predicateUniqueUsername;
+ case "address book name":
+ return nameABName;
+ case "team name":
+ return nameTeamName;
+ case "create group button":
+ return nameTeamName;
+ default:
+ throw new IllegalArgumentException(String.format("Unknown user detail name '%s'", detailName));
+ }
+ }
+
+ public boolean isUserDetailVisible(String detailName, String value) {
+ final By locator = getUserDetailLocator(detailName, value);
+ return isLocatorDisplayed(locator);
+ }
+
+ public boolean isUserDetailInvisible(String detailName, String value) {
+ final By locator = getUserDetailLocator(detailName, value);
+ return isLocatorInvisible(locator);
+ }
+
+ public boolean isUserDetailVisible(String detailName) {
+ final By locator = getUserDetailLocator(detailName);
+ return isLocatorDisplayed(locator);
+ }
+
+ public boolean isUserDetailInvisible(String detailName) {
+ final By locator = getUserDetailLocator(detailName);
+ return isLocatorInvisible(locator);
+ }
+
+ public void tapIgnoreButton() {
+ ignoreButton.click();
+ }
+
+ public void tapConnectButton() {
+ connectButton.click();
+ }
+
+ public boolean isConnectButtonVisible() {
+ return waitUntilElementVisible(connectButton);
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/single/SelfProfilePage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/single/SelfProfilePage.java
new file mode 100644
index 00000000000..6e48d5301df
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/single/SelfProfilePage.java
@@ -0,0 +1,110 @@
+package com.wearezeta.auto.ios.pages.details_overlay.single;
+
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import java.util.function.Function;
+
+public class SelfProfilePage extends IOSPage {
+/**
+ @iOSXCUITFindBy(iOSNsPredicate = "label == 'Settings' AND name == 'Settings' AND type == 'XCUIElementTypeStaticText'")
+ WebElement settingsButton;
+ */
+
+ @iOSXCUITFindBy(accessibility = "user image")
+ private WebElement profilePicture;
+
+ @iOSXCUITFindBy(accessibility = "Add Account")
+ private WebElement addAccountButton;
+
+ @iOSXCUITFindBy(accessibility = "Manage Team")
+ private WebElement manageTeamButton;
+
+ @iOSXCUITFindBy(accessibility = "Name")
+ private WebElement setStatusButton;
+
+ @iOSXCUITFindBy(accessibility = "close")
+ private WebElement profileCloseButton;
+
+ public SelfProfilePage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void tapProfilePicture() {
+ profilePicture.click();
+ }
+
+ public void tapSetStatusButton() {
+ setStatusButton.click();
+ }
+
+ public void tapProfileCloseButton() {
+ profileCloseButton.click();
+ }
+
+ private static final Function predicateNameByValue = name -> MobileBy.iOSNsPredicateString(
+ String.format("type == 'XCUIElementTypeStaticText' AND name IN {'user_profile.name','name'} AND value == '%s'",
+ name));
+
+ private static final Function predicateABNameByName = name -> MobileBy.iOSNsPredicateString(
+ String.format("type == 'XCUIElementTypeStaticText' AND name == 'correlation' AND value='%s'",
+ name.trim().length() > 0 ? (name + " in Contacts") : "in Contacts"));
+
+ private static final Function predicateUniqueUsernameByUsername = username -> MobileBy.iOSNsPredicateString(
+ String.format("type == 'XCUIElementTypeStaticText' AND name IN {'user_profile.username', 'username', 'handle'} AND value == '%s'",
+ username.startsWith("@") ? username : ("@" + username)));
+
+ private static final Function predicateSSONameByValue = name -> MobileBy.iOSNsPredicateString(
+ String.format("type == 'XCUIElementTypeStaticText' AND name IN {'user_profile.name','name'} AND value CONTAINS '%s'",
+ name));
+
+ private static final Function predicateTeamName = name -> MobileBy.iOSNsPredicateString(
+ String.format("label == 'Team name' AND value == '%s'", name));
+
+ protected By getUserDetailLocator(String detailName, String expectedValue) {
+ switch (detailName.toLowerCase()) {
+ case "name":
+ return predicateNameByValue.apply(expectedValue);
+ case "unique username":
+ return predicateUniqueUsernameByUsername.apply(expectedValue);
+ case "address book name":
+ return predicateABNameByName.apply(expectedValue);
+ case "team name":
+ return predicateTeamName.apply(expectedValue);
+ case "sso username":
+ return predicateSSONameByValue.apply(expectedValue);
+ default:
+ throw new IllegalArgumentException(String.format("Unknown user detail name '%s'", detailName));
+ }
+ }
+
+ public boolean isUserDetailVisible(String detailName, String value) {
+ final By locator = getUserDetailLocator(detailName, value);
+ return isLocatorDisplayed(locator);
+ }
+
+ public boolean isUserDetailInvisible(String detailName, String value) {
+ final By locator = getUserDetailLocator(detailName, value);
+ return isLocatorInvisible(locator);
+ }
+
+ public void tapAddAccountButton() {
+ waitUntilElementClickable(addAccountButton);
+ addAccountButton.click();
+ }
+
+ public void tapManageTeam() {
+ waitUntilElementClickable(manageTeamButton);
+ manageTeamButton.click();
+ }
+/**
+ public void tapSettingsButton() {
+ waitUntilElementClickable(settingsButton);
+ settingsButton.click();
+ }
+ */
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/single/SingleConnectedUserProfilePage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/single/SingleConnectedUserProfilePage.java
new file mode 100644
index 00000000000..b55a3ffa954
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/single/SingleConnectedUserProfilePage.java
@@ -0,0 +1,112 @@
+package com.wearezeta.auto.ios.pages.details_overlay.single;
+
+import java.util.function.Function;
+
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.appium.java_client.MobileBy;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.By;
+
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class SingleConnectedUserProfilePage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "Devices")
+ private WebElement devicesTab;
+
+ @iOSXCUITFindBy(accessibility = "INFORMATION")
+ private WebElement informationLabel;
+
+ @iOSXCUITFindBy(accessibility = "cell.profile.group_admin_options")
+ private WebElement adminToggle;
+
+ @iOSXCUITFindBy(accessibility = "close")
+ private WebElement closeButton;
+
+ @iOSXCUITFindBy(accessibility = "Back")
+ private WebElement backButton;
+
+ @iOSXCUITFindBy(accessibility = "Go back to conversation details")
+ private WebElement goBacktoButton;
+
+ @iOSXCUITFindBy(accessibility = "left_button")
+ private WebElement leftActionButton;
+
+ @iOSXCUITFindBy(accessibility = "right_button")
+ private WebElement rightActionButton;
+
+ @iOSXCUITFindBy(accessibility = "ReadReceiptsStatusFooter")
+ private WebElement readReceiptStatusFooter;
+
+ @iOSXCUITFindBy(accessibility = "Start conversation")
+ private WebElement startConversation;
+
+ private final Function xpathInformationKeyByIndex =
+ idx -> By.xpath(String.format("//XCUIElementTypeCell[%d]/XCUIElementTypeStaticText[2]", idx));
+ private final Function xpathInformationValueByIndex =
+ idx -> By.xpath(String.format("//XCUIElementTypeCell[%d]/XCUIElementTypeStaticText", idx));
+
+ private Function predicateStrInformationKeyValue = text ->
+ String.format("label == '%s' AND value == '%s'", text, text);
+
+ public SingleConnectedUserProfilePage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void tapBackButton() {
+ if (isElementVisible(goBacktoButton)) {
+ goBacktoButton.click();
+ } else {
+ backButton.click();
+ }
+ }
+
+ public boolean isInformationLabelVisible() {
+ return informationLabel.isDisplayed();
+ }
+
+ public boolean isInformationLabelInvisible() {
+ return waitUntilElementInvisible(informationLabel);
+ }
+
+ public boolean isAdminToggleVisible() {
+ return isElementVisible(adminToggle);
+ }
+
+ public boolean isInformationKeyValuePairVisible(String key, String value, int index) {
+ if (isAdminToggleVisible()) {
+ index++;
+ }
+ final By cellKeyLocator = xpathInformationKeyByIndex.apply(index);
+ final By cellValueLocator = xpathInformationValueByIndex.apply(index);
+ final By keyID = MobileBy.iOSNsPredicateString(predicateStrInformationKeyValue.apply(key));
+ final By valueID = MobileBy.iOSNsPredicateString(predicateStrInformationKeyValue.apply(value));
+ return isLocatorDisplayed(getElement(cellKeyLocator), keyID) && isLocatorDisplayed(getElement(cellValueLocator), valueID);
+ }
+
+ public boolean isReadReceiptFooterVisible() {
+ return waitUntilElementVisible(readReceiptStatusFooter);
+ }
+
+ public void switchToDevicesTab() {
+ devicesTab.click();
+ }
+
+ public void tapXButton() {
+ closeButton.click();
+ }
+
+ public void tapCreateGroupButton() {
+ leftActionButton.click();
+ }
+
+ public void tapOpenMenuButton() {
+ rightActionButton.click();
+ }
+
+ public void tapStartConversation() {
+ startConversation.click();
+ }
+
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/single/SinglePendingUserIncomingConnectionProfilePage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/single/SinglePendingUserIncomingConnectionProfilePage.java
new file mode 100644
index 00000000000..1e3d8dff9fd
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/single/SinglePendingUserIncomingConnectionProfilePage.java
@@ -0,0 +1,48 @@
+package com.wearezeta.auto.ios.pages.details_overlay.single;
+
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import java.util.function.Function;
+
+public class SinglePendingUserIncomingConnectionProfilePage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "CONNECT")
+ private WebElement connectButton;
+
+ @iOSXCUITFindBy(accessibility = "IGNORE")
+ private WebElement ignoreButton;
+
+ @iOSXCUITFindBy(accessibility = "Go back to conversation details")
+ private WebElement backButton;
+
+ private Function predicateDisplayName = text ->
+ MobileBy.iOSNsPredicateString(String.format("label == '%s'", text));
+
+ public SinglePendingUserIncomingConnectionProfilePage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void tapConnectInboxStyleButton() {
+ connectButton.click();
+ }
+
+ public void tapIgnoreInboxStyleButton() {
+ ignoreButton.click();
+ }
+
+ public void tapBackButton() {
+ backButton.click();
+ }
+
+ public boolean isDisplayNameVisible(String value) {
+ return isLocatorDisplayed(predicateDisplayName.apply(value));
+ }
+
+ public boolean isDisplayNameInvisible(String value) {
+ return isLocatorInvisible(predicateDisplayName.apply(value));
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/single/SinglePendingUserOutgoingConnectionPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/single/SinglePendingUserOutgoingConnectionPage.java
new file mode 100644
index 00000000000..fcba5b59b1d
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/single/SinglePendingUserOutgoingConnectionPage.java
@@ -0,0 +1,154 @@
+package com.wearezeta.auto.ios.pages.details_overlay.single;
+
+import java.util.function.Function;
+
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+
+import org.openqa.selenium.WebDriver;
+
+public class SinglePendingUserOutgoingConnectionPage extends IOSPage {
+
+ private static final By nameCancelRequest = MobileBy.AccessibilityId("Cancel Request");
+ private static final By nameBackButton = MobileBy.AccessibilityId("ConversationBackButton");
+ private static final By nameBackButtoniPad = MobileBy.AccessibilityId("Go back to conversation details");
+ protected static final By classChainConnectOtherUserButton =
+ MobileBy.iOSClassChain("**/XCUIElementTypeStaticText[`label == 'Connect'`]");
+
+ protected static final By nameArchiveRequestButton = MobileBy.AccessibilityId("archive connection");
+
+ protected static final By classChainCancelRequestButton = MobileBy.iOSClassChain(
+ "**/XCUIElementTypeStaticText[`label == 'Cancel Request' OR name == 'cancel connection'`][-1]");
+
+ public void tapBackButton() {
+ getElement(nameBackButton).click();
+ }
+
+ public void tapBackButtoniPad() {
+ getElement(nameBackButtoniPad).click();
+ }
+
+ protected static final By nameXButton = MobileBy.AccessibilityId("close");
+
+ private static final Function predicateNameByValue = name -> MobileBy.iOSNsPredicateString(
+ String.format("type == 'XCUIElementTypeStaticText' AND name IN {'user_profile.name','name'} AND value == '%s'",
+ name));
+ private static final By predicateName = MobileBy.iOSNsPredicateString(
+ "type == 'XCUIElementTypeStaticText' AND name IN {'user_profile.name','name'}");
+
+ private static final Function predicateABNameByName = name -> MobileBy.iOSNsPredicateString(
+ String.format("type == 'XCUIElementTypeStaticText' AND name == 'correlation' AND value='%s'",
+ name.trim().length() > 0 ? (name + " in Contacts") : "in Contacts"));
+ private static final By nameABName = MobileBy.AccessibilityId("correlation");
+
+ private static final Function predicateUniqueUsernameByUsername = username -> MobileBy.iOSNsPredicateString(
+ String.format("type == 'XCUIElementTypeStaticText' AND name IN {'user_profile.username', 'username', 'handle'} AND value == '%s'",
+ username.startsWith("@") ? username : ("@" + username)));
+ private static final By predicateUniqueUsername = MobileBy.iOSNsPredicateString(
+ "type == 'XCUIElementTypeStaticText' AND name IN {'user_profile.username', 'username', 'handle'}");
+
+ private static final Function predicateSSONameByValue = name -> MobileBy.iOSNsPredicateString(
+ String.format("type == 'XCUIElementTypeStaticText' AND name IN {'user_profile.name','name'} AND value CONTAINS '%s'",
+ name));
+
+ private static final By nameTeamName = MobileBy.AccessibilityId("team name");
+ private static final Function predicateTeamName = name -> MobileBy.iOSNsPredicateString(
+ String.format("name == 'team name' AND value == '%s'", name.toUpperCase()));
+
+ public SinglePendingUserOutgoingConnectionPage(WebDriver driver) {
+ super(driver);
+ }
+
+ protected By getButtonLocatorByName(String name) {
+ switch (name.toLowerCase()) {
+ case "archive":
+ return nameArchiveRequestButton;
+ case "connect":
+ return classChainConnectOtherUserButton;
+ case "cancel request":
+ return classChainCancelRequestButton;
+ case "x":
+ return nameXButton;
+ default:
+ throw new IllegalArgumentException(String.format("Unknown button name '%s'", name));
+ }
+ }
+
+ protected By getUserDetailLocator(String detailName, String expectedValue) {
+ switch (detailName.toLowerCase()) {
+ case "name":
+ return predicateNameByValue.apply(expectedValue);
+ case "unique username":
+ return predicateUniqueUsernameByUsername.apply(expectedValue);
+ case "address book name":
+ return predicateABNameByName.apply(expectedValue);
+ case "team name":
+ return predicateTeamName.apply(expectedValue);
+ case "sso username":
+ return predicateSSONameByValue.apply(expectedValue);
+ default:
+ throw new IllegalArgumentException(String.format("Unknown user detail name '%s'", detailName));
+ }
+ }
+
+ private By getUserDetailLocator(String detailName) {
+ switch (detailName.toLowerCase()) {
+ case "name":
+ return predicateName;
+ case "unique username":
+ return predicateUniqueUsername;
+ case "address book name":
+ return nameABName;
+ case "team name":
+ return nameTeamName;
+ case "create group button":
+ return nameTeamName;
+ default:
+ throw new IllegalArgumentException(String.format("Unknown user detail name '%s'", detailName));
+ }
+ }
+
+ public boolean isUserDetailVisible(String detailName, String value) {
+ final By locator = getUserDetailLocator(detailName, value);
+ return isLocatorDisplayed(locator);
+ }
+
+ public boolean isUserDetailInvisible(String detailName, String value) {
+ final By locator = getUserDetailLocator(detailName, value);
+ return isLocatorInvisible(locator);
+ }
+
+ public boolean isUserDetailVisible(String detailName) {
+ final By locator = getUserDetailLocator(detailName);
+ return isLocatorDisplayed(locator);
+ }
+
+ public boolean isUserDetailInvisible(String detailName) {
+ final By locator = getUserDetailLocator(detailName);
+ return isLocatorInvisible(locator);
+ }
+
+ public void tapButton(String name) {
+ final By locator = getButtonLocatorByName(name);
+ getElement(locator).click();
+ }
+
+ public boolean isButtonVisible(String name) {
+ final By locator = getButtonLocatorByName(name);
+ return isLocatorDisplayed(locator);
+ }
+
+ public boolean isButtonInvisible(String name) {
+ final By locator = getButtonLocatorByName(name);
+ return isLocatorInvisible(locator);
+ }
+
+ public boolean isCancelRequestButtonVisible() {
+ return isLocatorDisplayed(nameCancelRequest);
+ }
+
+ public boolean isCancelRequestButtonInvisible() {
+ return isLocatorInvisible(nameCancelRequest);
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/single/UserProfilePopupPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/single/UserProfilePopupPage.java
new file mode 100644
index 00000000000..454a0718d07
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/details_overlay/single/UserProfilePopupPage.java
@@ -0,0 +1,155 @@
+package com.wearezeta.auto.ios.pages.details_overlay.single;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import java.util.function.Function;
+
+public class UserProfilePopupPage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "close")
+ private WebElement xButton;
+
+ @iOSXCUITFindBy(accessibility = "user image")
+ private WebElement profilePicture;
+
+ @iOSXCUITFindBy(accessibility = "username")
+ private WebElement userNameLabel;
+
+ @iOSXCUITFindBy(accessibility = "INFORMATION")
+ private WebElement informationLabel;
+
+ @iOSXCUITFindBy(accessibility = "DEVICES")
+ private WebElement devicesTab;
+
+ @iOSXCUITFindBy(accessibility = "right_button")
+ private WebElement rightActionButton;
+
+ @iOSXCUITFindBy(accessibility = "Guest")
+ private WebElement guestLabel;
+
+ private Function predicateUsernameByValue = text ->
+ MobileBy.iOSNsPredicateString(String.format("name == 'user_profile.name' AND label == '%s'", text));
+
+ private Function predicateUniqueUsernameByValue = text ->
+ MobileBy.iOSNsPredicateString(String.format("name == 'username' AND label == '%s'", text));
+
+ private static final String classChainInformationKey = "XCUIElementTypeStaticText[2]";
+ private static final String classChainInformationValue = "XCUIElementTypeStaticText";
+
+ private final Function xpathInformationKeyByIndex =
+ idx -> By.xpath(String.format("//XCUIElementTypeCell[%d]/%s", idx, classChainInformationKey));
+ private final Function xpathInformationValueByIndex =
+ idx -> By.xpath(String.format("//XCUIElementTypeCell[%d]/%s", idx, classChainInformationValue));
+
+ private Function predicateStrInformationKeyValue = text ->
+ String.format("label == '%s' AND value == '%s'", text, text);
+
+ private static final String strLeftActionButton = "left_button";
+ private static final String strOpenConversationButton = "Open conversation";
+ private static final String strConnectButton = "CONNECT";
+ private static final String strOpenSelfProfileButton = "Open Profile";
+ private Function predicateLeftButtonByLabel = text ->
+ MobileBy.iOSNsPredicateString(String.format("name == '%s' AND label == '%s'", strLeftActionButton,text));
+
+
+ public UserProfilePopupPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void tapXButton() {
+ xButton.click();
+ }
+
+ public void tapOpenConversationButton() {
+ getElement(predicateLeftButtonByLabel.apply(strOpenConversationButton)).click();
+ }
+
+ public void tapSelfProfileButton() {
+ getElement(predicateLeftButtonByLabel.apply(strOpenSelfProfileButton)).click();
+ }
+
+ public void tapMoreActionsButton() {
+ rightActionButton.click();
+ }
+
+ public boolean isUserProfilePopupVisible() {
+ return isElementVisible(userNameLabel) && xButton.isDisplayed();
+ }
+
+ public boolean isUserProfilePopupInvisible() {
+ return isElementInvisible(userNameLabel) && isElementInvisible(xButton);
+ }
+
+ public boolean isUserNameVisible(String value) {
+ return isLocatorDisplayed(predicateUsernameByValue.apply(value));
+ }
+
+ public boolean isUserNameInvisible(String value) { return isLocatorInvisible(predicateUsernameByValue.apply(value)); }
+
+ public boolean isUniqueUserNameVisible(String value) {
+ return isLocatorDisplayed(predicateUniqueUsernameByValue.apply(value));
+ }
+
+ public boolean isUniqueUserNameInvisible(String value) { return isLocatorInvisible(predicateUniqueUsernameByValue.apply(value)); }
+
+ public boolean isUserProfilePictureVisible() {
+ return isElementVisible(profilePicture);
+ }
+
+ public boolean isInformationLabelVisible() {
+ return informationLabel.isDisplayed();
+ }
+
+ public boolean isInformationLabelInvisible() {
+ return isElementInvisible(informationLabel);
+ }
+
+ public boolean isInformationKeyValuePairVisible(String key, String value, int index) {
+ final By cellKeyLocator = xpathInformationKeyByIndex.apply(index);
+ final By cellValueLocator = xpathInformationValueByIndex.apply(index);
+ final By keyID = MobileBy.iOSNsPredicateString(predicateStrInformationKeyValue.apply(key));
+ final By valueID = MobileBy.iOSNsPredicateString(predicateStrInformationKeyValue.apply(value));
+ return isLocatorDisplayed(getElement(cellKeyLocator), keyID) && isLocatorDisplayed(getElement(cellValueLocator), valueID);
+ }
+
+ public boolean isMoreActionsButtonVisible() {
+ return rightActionButton.isDisplayed();
+ }
+
+ public boolean isMoreActionsButtonInvisible() {
+ return isElementInvisible(rightActionButton);
+ }
+
+ public boolean isOpenConversationButtonVisible() {
+ return isLocatorDisplayed(predicateLeftButtonByLabel.apply(strOpenConversationButton));
+ }
+
+ public boolean isOpenConversationButtonInvisible() {
+ return isLocatorInvisible(predicateLeftButtonByLabel.apply(strOpenConversationButton));
+ }
+
+ public boolean isConnectButtonVisible() {
+ return isLocatorDisplayed(predicateLeftButtonByLabel.apply(strConnectButton));
+ }
+
+ public boolean isConnectButtonInvisible() {
+ return isLocatorInvisible(predicateLeftButtonByLabel.apply(strConnectButton));
+ }
+
+ public boolean isDevicesTabInvisible() {
+ return isElementInvisible(devicesTab);
+ }
+
+ public boolean isGuestLabelVisible() {
+ return guestLabel.isDisplayed();
+ }
+
+ public boolean isGuestLabelInvisible() {
+ return isElementInvisible(guestLabel);
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/external_app/FileChooseDialogPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/external_app/FileChooseDialogPage.java
new file mode 100644
index 00000000000..7133e90d491
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/external_app/FileChooseDialogPage.java
@@ -0,0 +1,81 @@
+package com.wearezeta.auto.ios.pages.external_app;
+
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.appium.java_client.MobileBy;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.interactions.Actions;
+
+public class FileChooseDialogPage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "DOC.itemCollectionMenuButton.Ellipsis")
+ private WebElement ellipsisButton;
+
+ @iOSXCUITFindBy(accessibility = "BackButton")
+ private WebElement iPadBrowserButton;
+
+ @iOSXCUITFindBy(accessibility = "DOC.sidebar.item.On My iPad")
+ private WebElement onMyIPadSelection;
+
+ @iOSXCUITFindBy(accessibility = "DOC.sortMenuButton.date")
+ private WebElement sortByDateEntry;
+
+ @iOSXCUITFindBy(accessibility = "DOC.sortMenuButton.date.descending")
+ private WebElement sortByDateEntrySelected;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "name == 'Browse' AND type == 'XCUIElementTypeButton'")
+ private WebElement browseFoldersButton;
+
+ @iOSXCUITFindBy(accessibility = "On My iPhone")
+ private WebElement onMyIPhoneSelection;
+
+ private static String predicateStringFileByName = "type == 'XCUIElementTypeCell' AND label CONTAINS '%s'";
+
+ public FileChooseDialogPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void tapOnEllipsisButton() {
+ ellipsisButton.click();
+ }
+
+ public void dismissEllipsisMenu() {
+ //dismiss by tapping to the right of the menu
+ Actions action = new Actions(getDriver());
+ action.click(sortByDateEntrySelected).moveByOffset(150, 0).click().build().perform();
+ }
+
+ public void tapOnMyIPhone() {
+ onMyIPhoneSelection.click();
+ }
+
+ public void tapOnMyIPad() {
+ onMyIPadSelection.click();
+ }
+
+ public void tapFileContaining(String name) {
+ By locator = MobileBy.iOSNsPredicateString(String.format(predicateStringFileByName, name));
+ isLocatorDisplayed(locator);
+ getDriver().findElement(locator).click();
+ }
+
+ public void tapBrowseFoldersButton() {
+ waitUntilElementVisible(browseFoldersButton);
+ browseFoldersButton.click();
+ }
+
+ public void tapBrowserFolderButtonOnIPad(){
+ waitUntilElementVisible(iPadBrowserButton);
+ iPadBrowserButton.click();
+ }
+
+ public boolean isSortByDateNotSelected() {
+ return isElementInvisible(sortByDateEntrySelected);
+ }
+
+ public void tapSortByDateEntry() {
+ sortByDateEntry.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/external_app/FileSavingPopupPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/external_app/FileSavingPopupPage.java
new file mode 100644
index 00000000000..5cb15771b8e
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/external_app/FileSavingPopupPage.java
@@ -0,0 +1,59 @@
+package com.wearezeta.auto.ios.pages.external_app;
+
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class FileSavingPopupPage extends IOSPage {
+
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeNavigationBar[`name == 'UIActivityContentView'`]/XCUIElementTypeOther/XCUIElementTypeOther")
+ private WebElement fileLabel;
+
+ @iOSXCUITFindBy(accessibility = "Close")
+ private WebElement closeButton;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "name == 'Save to Files'")
+ private WebElement saveToFilesButton;
+
+ @iOSXCUITFindBy(accessibility = "On My iPhone")
+ private WebElement onMyIPhoneChoice;
+
+ @iOSXCUITFindBy(accessibility = "On My iPad")
+ private WebElement onMyIPadChoice;
+
+ @iOSXCUITFindBy(accessibility = "Save")
+ private WebElement saveButton;
+
+ public FileSavingPopupPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public String getFileLabel() {
+ waitUntilElementVisible(fileLabel);
+ return fileLabel.getAttribute("name");
+ }
+
+ public void tapSaveToFilesButton() {
+ saveToFilesButton.click();
+ }
+
+ public void tapOnMyIPhone() {
+ waitUntilElementVisible(onMyIPhoneChoice);
+ onMyIPhoneChoice.click();
+ }
+
+ public void tapOnMyIPad() {
+ waitUntilElementVisible(onMyIPadChoice);
+ onMyIPadChoice.click();
+ }
+
+ public void tapSaveButton() {
+ waitUntilElementClickable(saveButton);
+ saveButton.click();
+ }
+
+ public void tapCloseButton() {
+ closeButton.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/keyboard/IOSKeyboard.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/keyboard/IOSKeyboard.java
new file mode 100644
index 00000000000..77af0a7045a
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/keyboard/IOSKeyboard.java
@@ -0,0 +1,62 @@
+package com.wearezeta.auto.ios.pages.keyboard;
+
+import com.wearezeta.auto.common.misc.Timedelta;
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+
+import org.openqa.selenium.WebDriver;
+
+public class IOSKeyboard extends IOSPage {
+ private static final By classKeyboard = By.className("XCUIElementTypeKeyboard");
+ private static final By predicateCommitButton = MobileBy.iOSNsPredicateString(
+ "name IN[c] {'Go', 'Send', 'Done', 'Return'}"
+ );
+
+ private static final By nameSpaceButton = MobileBy.AccessibilityId("space");
+ private static final By nameNextButton = MobileBy.AccessibilityId("Next:");
+
+ private static final By nameHideKeyboardButton = MobileBy.AccessibilityId("Hide keyboard");
+
+ private static final By nameKeyboardDeleteButton = MobileBy.AccessibilityId("delete");
+
+ private static final Timedelta DEFAULT_VISIBILITY_TIMEOUT = Timedelta.ofSeconds(5);
+
+ public IOSKeyboard(WebDriver driver) {
+ super(driver);
+ }
+
+ public boolean isVisible(Timedelta timeout) {
+ return isLocatorDisplayed(nameSpaceButton, timeout);
+ }
+
+ public boolean isInvisible(Timedelta timeout) {
+ return isLocatorInvisible(nameSpaceButton, timeout);
+ }
+
+ public boolean isVisible() {
+ return isVisible(DEFAULT_VISIBILITY_TIMEOUT);
+ }
+
+ public boolean isInvisible() {
+ return isInvisible(DEFAULT_VISIBILITY_TIMEOUT);
+ }
+
+ public void pressSpaceButton() {
+ getElement(nameSpaceButton).click();
+ }
+
+ public void pressNextButton() {
+ getElement(nameNextButton).click();
+ }
+
+ public void pressHideButton() {
+ getElement(nameHideKeyboardButton).click();
+ }
+
+ public void pressCommitButton() {
+ getElement(classKeyboard)
+ .findElement(predicateCommitButton)
+ .click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/linear_groupcreation/AddPeoplePage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/linear_groupcreation/AddPeoplePage.java
new file mode 100644
index 00000000000..7084abecb4a
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/linear_groupcreation/AddPeoplePage.java
@@ -0,0 +1,68 @@
+package com.wearezeta.auto.ios.pages.linear_groupcreation;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import com.wearezeta.auto.ios.pages.IOSPage;
+import com.wearezeta.auto.ios.pages.search.GroupParticipantsSearchList;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import java.util.function.Function;
+
+public class AddPeoplePage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "button.addpeople.create")
+ private WebElement createButton;
+
+ @iOSXCUITFindBy(accessibility = "button.addpeople.skip")
+ private WebElement skipButton;
+
+ @iOSXCUITFindBy(accessibility = "Back")
+ private WebElement backButton;
+
+ private static final Function nameAddPeopleCount = count ->
+ MobileBy.AccessibilityId(String.format("Add Participants (%s)", count));
+
+ private final GroupParticipantsSearchList participantsSearchList;
+
+ public AddPeoplePage(WebDriver driver) {
+ super(driver);
+ this.participantsSearchList = new GroupParticipantsSearchList(driver);
+ }
+
+ public void tapCreateButton(){
+ createButton.click();
+ }
+
+ public void tapSkipButton(){
+ skipButton.click();
+ }
+
+ public void tapBackButton(){
+ backButton.click();
+ }
+
+ public boolean isParticipantsCountEqualTo(int expectedCount) {
+ return isLocatorDisplayed(nameAddPeopleCount.apply(expectedCount));
+ }
+
+ public void typeSearchQuery(String query, boolean shouldClear) {
+ participantsSearchList.typeSearchQuery(query, shouldClear);
+ }
+
+ public void selectItem(String name) {
+ participantsSearchList.selectItem(name);
+ }
+
+ public boolean isItemVisible(String name) {
+ return participantsSearchList.isItemVisible(name);
+ }
+
+ public boolean isItemInvisible(String name) {
+ return participantsSearchList.isItemInvisible(name);
+ }
+
+ public boolean waitUntilResultsLabelIsVisible(String msg) {
+ return participantsSearchList.waitUntilResultsLabelIsVisible(msg);
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/linear_groupcreation/NewGroupPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/linear_groupcreation/NewGroupPage.java
new file mode 100644
index 00000000000..08f4ffee431
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/linear_groupcreation/NewGroupPage.java
@@ -0,0 +1,135 @@
+package com.wearezeta.auto.ios.pages.linear_groupcreation;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import java.util.function.Function;
+
+public class NewGroupPage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "NameField")
+ private WebElement groupNameTextfield;
+
+ @iOSXCUITFindBy(accessibility = "button.newgroup.next")
+ private WebElement nextButton;
+
+ @iOSXCUITFindBy(accessibility = "Go back to contact list")
+ private WebElement backButton;
+
+ @iOSXCUITFindBy(accessibility = "cell.groupdetails.options")
+ private WebElement groupOptions;
+
+ @iOSXCUITFindBy(accessibility = "toggle.newgroup.allowguests")
+ private WebElement toggleAllowGuests;
+
+ @iOSXCUITFindBy(accessibility = "toggle.newgroup.allowservices")
+ private WebElement toggleAllowServices;
+
+ @iOSXCUITFindBy(accessibility = "Protocol")
+ private WebElement protocolOption;
+
+ @iOSXCUITFindBy(accessibility = "Proteus (default)")
+ private WebElement protocolButton;
+
+ @iOSXCUITFindBy(accessibility = "MLS")
+ private WebElement mlsOption;
+
+ private static final String strToggleAllowGuests = "toggle.newgroup.allowguests";
+
+ private static final Function classChainToggle = toggleName -> MobileBy.iOSClassChain(
+ String.format("**/XCUIElementTypeCell[`name == '%s'`]/**/XCUIElementTypeSwitch", toggleName));
+
+ private static final Function classChainAllowGuestsByValue = value -> MobileBy.iOSClassChain(
+ String.format("**/XCUIElementTypeCell[`name == '%s'`]/**/XCUIElementTypeSwitch[`value == '%s'`]", strToggleAllowGuests, value));
+
+ private static final Function predicateStrMaxParticipantLimit = limit ->
+ String.format("name CONTAINS 'Up to %s participants can join a group conversation'", limit);
+
+ public NewGroupPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void enterGroupName(String groupName) {
+ groupNameTextfield.clear();
+ groupNameTextfield.sendKeys(groupName);
+ }
+
+ public void tapNextButton(){
+ nextButton.click();
+ }
+ public void tapBackButton(){
+ backButton.click();
+ }
+
+ public boolean isExpectedConversationOptionsVisible(String text) {
+ final By locator = MobileBy.AccessibilityId(text);
+ return isLocatorDisplayed(locator);
+ }
+
+ public boolean isAllowGuestsEqualsTo(String expectedValue) {
+ return isLocatorDisplayed(classChainAllowGuestsByValue.apply(expectedValue));
+ }
+
+ public void switchToggle() {
+ getElement(classChainToggle.apply(strToggleAllowGuests)).click();
+ }
+
+ public boolean isProtocolVisible() {
+ return protocolOption.isDisplayed();
+ }
+
+ public boolean isProtocolInvisible() {
+ return isElementInvisible(protocolOption);
+ }
+
+ public boolean isProteusValueVisible() {
+ return protocolButton.isDisplayed();
+ }
+
+ public boolean isProteusValueInvisible() {
+ return isElementInvisible(protocolButton);
+ }
+
+ public boolean isMlsValueVisible() {
+ return mlsOption.isDisplayed();
+ }
+
+ public boolean isMlsValueInvisible() {
+ return isElementInvisible(mlsOption);
+ }
+
+ public void tapProtocolOption() {
+ protocolButton.click();
+ }
+
+ public void tapMlsOption() {
+ mlsOption.click();
+ }
+
+ public void tapConversationOptions() {
+ groupOptions.click();
+ }
+
+ public boolean isMaxLimitEqualsTo(int expectedLimit) {
+ return isLocatorDisplayed(MobileBy.iOSNsPredicateString(predicateStrMaxParticipantLimit.apply(expectedLimit)));
+ }
+
+ public boolean isGuestOptionVisible() {
+ return toggleAllowGuests.isDisplayed();
+ }
+
+ public boolean isGuestOptionInvisible() {
+ return isElementInvisible(toggleAllowGuests);
+ }
+
+ public boolean isServiceOptionVisible() {
+ return toggleAllowServices.isDisplayed();
+ }
+
+ public boolean isServiceOptionInvisible() {
+ return isElementInvisible(toggleAllowServices);
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/search/BaseSearchableItemsList.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/search/BaseSearchableItemsList.java
new file mode 100644
index 00000000000..6d9d41beab3
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/search/BaseSearchableItemsList.java
@@ -0,0 +1,255 @@
+package com.wearezeta.auto.ios.pages.search;
+
+import org.openqa.selenium.WebDriver;
+import com.wearezeta.auto.common.misc.Timedelta;
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.WebDriverException;
+import org.openqa.selenium.WebElement;
+
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+public abstract class BaseSearchableItemsList extends IOSPage {
+ private String classChainStrViewRoot;
+
+ private static final String nameStrSearchInput = "textViewSearch";
+
+ private static final By predicateSearchInput = MobileBy.iOSNsPredicateString(isTablet() ?
+ String.format("type == 'XCUIElementTypeTextView' AND name == '%s' AND visible == 1", nameStrSearchInput) :
+ String.format("type == 'XCUIElementTypeTextView' AND name == '%s'", nameStrSearchInput));
+
+ private static final By nameNoResults = MobileBy.AccessibilityId("No results.");
+
+ private static final By predicateEveryoneIsHere =
+ MobileBy.iOSNsPredicateString("value BEGINSWITH 'Everyone' AND value ENDSWITH 'here.'");
+
+ private static final String nameStrItemName = "user_cell.name";
+ private static final String nameStrItemUsername = "user_cell.username";
+ static final String nameStrParticipantCell = "participants.section.participants.cell";
+ static final By nameParticipantCell = MobileBy.iOSNsPredicateString("name ENDSWITH 'participants.section.participants.cell'");
+ static final String nameStrServiceCellName = "participants.section.services.cell";
+ static final By nameServiceCellName = MobileBy.AccessibilityId("participants.section.services.cell");
+
+ private final BiFunction classChainStrItemCellByCellAndName = (name, cellName) ->
+ String.format("%s/XCUIElementTypeCell[`name ENDSWITH '%s'`][$name == '%s' AND value == '%s' OR name == '%s' AND value == '%s (You)'$]",
+ classChainStrViewRoot, cellName, nameStrItemName, name, nameStrItemName, name);
+
+ private final BiFunction classChainStrFederatedGroupDetailsItemCellByCellAndName = (name, cellName) ->
+ String.format("%s/XCUIElementTypeCell[`name ENDSWITH '%s'`][$name == '%s' AND value == '%s' OR name == '%s' AND value == '%s (You)'$]",
+ classChainStrViewRoot, cellName, nameStrItemUsername, name, nameStrItemUsername, name);
+
+ final BiFunction classChainItemCellByCellAndName = (name, cellName) ->
+ MobileBy.iOSClassChain(classChainStrItemCellByCellAndName.apply(name, cellName));
+
+ final BiFunction classChainFederatedGroupDetailsItemCellByCellAndName = (name, cellName) ->
+ MobileBy.iOSClassChain(classChainStrFederatedGroupDetailsItemCellByCellAndName.apply(name, cellName));
+
+ private final BiFunction classChainItemNameByCellAndName = (name, cellName) ->
+ MobileBy.iOSClassChain(String.format("%s/XCUIElementTypeCell[`name ENDSWITH '%s'`]/" +
+ "**/XCUIElementTypeStaticText[`name == '%s' AND value == '%s' OR name == '%s' AND value == '%s (You)'`]",
+ classChainStrViewRoot, cellName, nameStrItemName, name, nameStrItemName, name));
+
+ private final BiFunction classChainItemCellByCellWithUniqueUsername = (name, cellName) ->
+ MobileBy.iOSClassChain(String.format("%s[$name == '%s'$]",
+ classChainStrItemCellByCellAndName.apply(name, cellName), nameStrItemUsername));
+
+ private final BiFunction classChainGuestIconForParticipantByCellAndName = (name, cellName) ->
+ MobileBy.iOSClassChain(String.format("%s/**/XCUIElementTypeImage[`name == 'img.guest'`]",
+ classChainStrItemCellByCellAndName.apply(name, cellName)));
+
+ private final BiFunction classChainFederatedIconForParticipantByCellAndName = (name, cellName) ->
+ MobileBy.iOSClassChain(String.format("%s/**/XCUIElementTypeImage[`name == 'img.federated'`]",
+ classChainStrItemCellByCellAndName.apply(name, cellName)));
+
+ private final BiFunction classChainExternalIconForParticipantByCellAndName = (name, cellName) ->
+ MobileBy.iOSClassChain(String.format("%s/**/XCUIElementTypeImage[`name == 'img.external'`]",
+ classChainStrItemCellByCellAndName.apply(name, cellName)));
+
+ private final Function classChainStrItemCellByName = name ->
+ String.format("%s/XCUIElementTypeCell[$name == '%s' AND value == '%s'$]",
+ classChainStrViewRoot, nameStrItemName, name);
+
+ private final Function classChainStrFederatedItemCellByName = name ->
+ String.format("%s/XCUIElementTypeCell[$name == '%s' AND value == '%s'$]",
+ classChainStrViewRoot, nameStrItemUsername, name);
+
+ final Function classChainItemCellByName = name -> MobileBy.iOSClassChain(
+ classChainStrItemCellByName.apply(name));
+
+ final Function classChainFederatedItemCellByName = name -> MobileBy.iOSClassChain(
+ classChainStrFederatedItemCellByName.apply(name));
+
+ final Function classChainItemNameByName = name -> MobileBy.iOSClassChain(
+ String.format("%s/XCUIElementTypeCell/**/XCUIElementTypeStaticText[`name == '%s' AND value == '%s'`]",
+ classChainStrViewRoot, nameStrItemName, name));
+
+ private final Function classChainItemCellByNameWithUniqueUsername = name ->
+ MobileBy.iOSClassChain(String.format("%s[$name == '%s'$]", classChainStrItemCellByName.apply(name),
+ nameStrItemUsername));
+
+ private final Function classChainGuestIconForParticipantByName = name -> MobileBy.iOSClassChain(
+ String.format("%s/**/XCUIElementTypeImage[`name == 'img.guest'`]", classChainStrItemCellByName.apply(name)));
+
+ private final Function classChainExternalIconForParticipantByName = name -> MobileBy.iOSClassChain(
+ String.format("%s/**/XCUIElementTypeImage[`name == 'img.external'`]", classChainStrItemCellByName.apply(name)));
+
+ private final Function classChainVerifiedIconForParticipantByName = name -> MobileBy.iOSClassChain(
+ String.format("%s/**/XCUIElementTypeImage[`name == 'img.shield'`]", classChainStrItemCellByName.apply(name)));
+
+ public BaseSearchableItemsList(WebDriver driver, String classChainStrViewRoot) {
+ super(driver);
+ this.classChainStrViewRoot = classChainStrViewRoot;
+ }
+
+ public boolean isVisible() {
+ return isLocatorDisplayed(MobileBy.iOSClassChain(classChainStrViewRoot), Timedelta.ofSeconds(2));
+ }
+
+ public boolean isParticipantCellVisible() {
+ return isLocatorDisplayed(nameParticipantCell, Timedelta.ofSeconds(2));
+ }
+
+ boolean isUniqueUserNameLabelVisibleFor(String cellName, String name) {
+ return isLocatorDisplayed(classChainItemCellByCellWithUniqueUsername.apply(name, cellName));
+ }
+
+ boolean isUniqueUserNameLabelInvisibleFor(String cellName, String name) {
+ return isLocatorInvisible(classChainItemCellByCellWithUniqueUsername.apply(name, cellName));
+ }
+
+ void selectItem(String cellName, String name) {
+ getElement(classChainItemNameByCellAndName.apply(name, cellName)).click();
+ }
+
+ boolean isItemVisible(String cellName, String name) {
+ return isLocatorDisplayed(classChainItemCellByCellAndName.apply(name, cellName), Timedelta.ofSeconds(2));
+ }
+
+ boolean isItemInvisible(String cellName, String name) {
+ return isLocatorInvisible(classChainItemCellByCellAndName.apply(name, cellName), Timedelta.ofSeconds(1));
+ }
+
+ boolean isFederatedItemVisibleOnGroupDetails(String cellName, String name) {
+ return isLocatorDisplayed(classChainFederatedGroupDetailsItemCellByCellAndName.apply(name, cellName));
+ }
+
+ boolean isFederatedItemInvisibleOnGroupDetails(String cellName, String name) {
+ return isLocatorInvisible(classChainFederatedGroupDetailsItemCellByCellAndName.apply(name, cellName));
+ }
+
+ boolean isGuestLabelVisibleFor(String cellName, String name) {
+ return isLocatorExist(classChainGuestIconForParticipantByCellAndName.apply(name, cellName));
+ }
+
+ boolean isGuestLabelInvisibleFor(String cellName, String name) {
+ return isLocatorInvisible(classChainGuestIconForParticipantByCellAndName.apply(name, cellName));
+ }
+
+ boolean isExternalIconVisibleFor(String cellName, String name) {
+ return isLocatorExist(classChainExternalIconForParticipantByCellAndName.apply(name, cellName));
+ }
+
+ protected boolean isUniqueUserNameLabelVisibleFor(String name) {
+ return isLocatorDisplayed(classChainItemCellByNameWithUniqueUsername.apply(name));
+ }
+
+ protected boolean isUniqueUserNameLabelInvisibleFor(String name) {
+ return isLocatorInvisible(classChainItemCellByNameWithUniqueUsername.apply(name));
+ }
+
+ protected void selectItem(String name) {
+ getElement(classChainItemNameByName.apply(name)).click();
+ }
+
+ protected boolean isItemVisible(String name) {
+ return isLocatorDisplayed(classChainItemCellByName.apply(name));
+ }
+
+ protected boolean isItemInvisible(String name) {
+ return isLocatorInvisible(classChainItemCellByName.apply(name));
+ }
+
+ protected boolean isFederatedItemVisible(String name) {
+ return isLocatorDisplayed(classChainFederatedItemCellByName.apply(name));
+ }
+
+ protected boolean isFederatedItemInvisible(String name) {
+ return isLocatorInvisible(classChainFederatedItemCellByName.apply(name));
+ }
+
+ boolean isVerifiedLabelVisibleFor(String name) {
+ return isLocatorExist(classChainVerifiedIconForParticipantByName.apply(name));
+ }
+
+ protected boolean isGuestLabelVisibleFor(String name) {
+ return isLocatorExist(classChainGuestIconForParticipantByName.apply(name));
+ }
+
+ protected boolean isExternalLabelVisibleFor(String name) {
+ return isLocatorExist(classChainExternalIconForParticipantByName.apply(name));
+ }
+
+ protected boolean isExternalLabelInvisibleFor(String name) {
+ return isLocatorInvisible(classChainExternalIconForParticipantByName.apply(name));
+ }
+
+ protected boolean isGuestLabelInvisibleFor(String name) {
+ return isLocatorInvisible(classChainGuestIconForParticipantByName.apply(name));
+ }
+
+ private static By getLocatorByLabelName(String name) {
+ switch (name.toLowerCase()) {
+ case "no results":
+ return nameNoResults;
+ case "everyone is here":
+ return predicateEveryoneIsHere;
+ default:
+ throw new IllegalArgumentException(String.format("Unknown message label: '%s'", name));
+ }
+ }
+
+ public boolean waitUntilResultsLabelIsVisible(String label) {
+ final By locator = getLocatorByLabelName(label);
+ return isLocatorExist(locator);
+ }
+
+ protected void typeSearchQuery(String text) {
+ typeSearchQuery(text, false);
+ }
+
+ protected void typeSearchQuery(String text, boolean shouldClearFieldBeforeInput) {
+ final WebElement searchInput = getElement(predicateSearchInput);
+ if (shouldClearFieldBeforeInput) {
+ this.clearSearchQuery();
+ }
+ searchInput.sendKeys(text + " ");
+ }
+
+ public void clearSearchQuery() {
+ final WebElement searchInput = getElement(predicateSearchInput);
+ try {
+ this.tapAtTheCenterOfElement(searchInput);
+ searchInput.clear();
+ } catch (WebDriverException e) {
+ this.tapAtTheCenterOfElement(searchInput);
+ searchInput.clear();
+ }
+ }
+ protected void sendKeysToSearchInput(Keys... keys) {
+ for (Keys key : keys) {
+ getElement(predicateSearchInput).sendKeys(key);
+ }
+ }
+
+ protected String getCurrentSearchQuery() {
+ return getElement(predicateSearchInput).getText();
+ }
+
+ boolean isFederatedLabelVisibleFor(String cellName, String name) {
+ return isLocatorExist(classChainFederatedIconForParticipantByCellAndName.apply(name, cellName));
+ }
+}
+
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/search/GroupParticipantsList.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/search/GroupParticipantsList.java
new file mode 100644
index 00000000000..df4d8335003
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/search/GroupParticipantsList.java
@@ -0,0 +1,56 @@
+package com.wearezeta.auto.ios.pages.search;
+
+import org.openqa.selenium.WebDriver;
+import com.wearezeta.auto.common.misc.Timedelta;
+
+public class GroupParticipantsList extends BaseSearchableItemsList {
+ private static final String classChainStrViewRoot = "**/XCUIElementTypeCollectionView[`name == 'group_details.list'`]";
+
+ public GroupParticipantsList(WebDriver driver) {
+ super(driver, classChainStrViewRoot);
+ }
+
+ public void selectParticipant(String name) {
+ selectItem(nameStrParticipantCell, name);
+ }
+
+ public int getPeopleCount() {
+ return selectVisibleElements(nameParticipantCell, Timedelta.ofSeconds(3)).size();
+ }
+
+ public int getServicesCount() {
+ return selectVisibleElements(nameServiceCellName, Timedelta.ofSeconds(3)).size();
+ }
+
+ @Override
+ public boolean isUniqueUserNameLabelVisibleFor(String name) {
+ return super.isUniqueUserNameLabelVisibleFor(nameStrParticipantCell, name);
+ }
+
+ @Override
+ public boolean isUniqueUserNameLabelInvisibleFor(String name) {
+ return super.isUniqueUserNameLabelInvisibleFor(nameStrParticipantCell, name);
+ }
+
+ public boolean isParticipantVisible(String name) {
+ return super.isItemVisible(nameStrParticipantCell, name);
+ }
+
+ public boolean isParticipantInvisible(String name) {
+ return super.isItemInvisible(nameStrParticipantCell, name);
+ }
+
+ @Override
+ public boolean isGuestLabelVisibleFor(String name) {
+ return super.isGuestLabelVisibleFor(nameStrParticipantCell, name);
+ }
+
+ @Override
+ public boolean isGuestLabelInvisibleFor(String name) {
+ return super.isGuestLabelInvisibleFor(nameStrParticipantCell, name);
+ }
+
+ public boolean isExternalIconVisibleFor(String name) {
+ return super.isExternalIconVisibleFor(nameStrParticipantCell, name);
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/search/GroupParticipantsSearchList.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/search/GroupParticipantsSearchList.java
new file mode 100644
index 00000000000..51d92684fce
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/search/GroupParticipantsSearchList.java
@@ -0,0 +1,90 @@
+package com.wearezeta.auto.ios.pages.search;
+
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.*;
+
+import java.util.function.Function;
+
+public class GroupParticipantsSearchList extends IOSPage {
+ private static final String classChainStrViewRoot = "**/XCUIElementTypeCollectionView[`name == 'add_participants.list'`]";
+
+ public GroupParticipantsSearchList(WebDriver driver) {
+ super(driver);
+ }
+
+ private static final String nameStrSearchInput = "textViewSearch";
+
+ private static final By predicateSearchInput = MobileBy.iOSNsPredicateString(isTablet() ?
+ String.format("type == 'XCUIElementTypeTextView' AND name == '%s' AND visible == 1", nameStrSearchInput) :
+ String.format("type == 'XCUIElementTypeTextView' AND name == '%s'", nameStrSearchInput));
+
+ private static final By nameNoResults = MobileBy.AccessibilityId("button.searchui.open-services-no-results");
+
+ private static final By predicateEveryoneIsHere =
+ MobileBy.iOSNsPredicateString("value BEGINSWITH 'Everyone' AND value ENDSWITH 'here.'");
+
+ private static final String nameStrItemName = "user_cell.name";
+
+ private final Function classChainStrItemCellByName = name ->
+ String.format("%s/XCUIElementTypeCell[$name == '%s' AND value == '%s'$]",
+ classChainStrViewRoot, nameStrItemName, name);
+
+ final Function classChainItemCellByName = name -> MobileBy.iOSClassChain(
+ classChainStrItemCellByName.apply(name));
+
+ final Function classChainItemNameByName = name -> MobileBy.iOSClassChain(
+ String.format("%s/XCUIElementTypeCell/**/XCUIElementTypeStaticText[`name == '%s' AND value == '%s'`]",
+ classChainStrViewRoot, nameStrItemName, name));
+
+ public void selectItem(String name) {
+ getElement(classChainItemNameByName.apply(name)).click();
+ }
+
+ public boolean isItemVisible(String name) {
+ return isLocatorDisplayed(classChainItemCellByName.apply(name));
+ }
+
+ public boolean isItemInvisible(String name) {
+ return isLocatorInvisible(classChainItemCellByName.apply(name));
+ }
+
+ private static By getLocatorByLabelName(String name) {
+ switch (name.toLowerCase()) {
+ case "no results":
+ return nameNoResults;
+ case "everyone is here":
+ return predicateEveryoneIsHere;
+ default:
+ throw new IllegalArgumentException(String.format("Unknown message label: '%s'", name));
+ }
+ }
+
+ public boolean waitUntilResultsLabelIsVisible(String label) {
+ final By locator = getLocatorByLabelName(label);
+ return isLocatorExist(locator);
+ }
+
+ public void typeSearchQuery(String text) {
+ typeSearchQuery(text, false);
+ }
+
+ public void typeSearchQuery(String text, boolean shouldClearFieldBeforeInput) {
+ final WebElement searchInput = getElement(predicateSearchInput);
+ if (shouldClearFieldBeforeInput) {
+ this.clearSearchQuery();
+ }
+ searchInput.sendKeys(text + " ");
+ }
+
+ public void clearSearchQuery() {
+ final WebElement searchInput = getElement(predicateSearchInput);
+ try {
+ this.tapAtTheCenterOfElement(searchInput);
+ searchInput.clear();
+ } catch (WebDriverException e) {
+ this.tapAtTheCenterOfElement(searchInput);
+ searchInput.clear();
+ }
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/search/MentionSuggestionsList.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/search/MentionSuggestionsList.java
new file mode 100644
index 00000000000..b172343b9c1
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/search/MentionSuggestionsList.java
@@ -0,0 +1,53 @@
+package com.wearezeta.auto.ios.pages.search;
+
+import org.openqa.selenium.WebDriver;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+
+import java.util.function.Function;
+
+public class MentionSuggestionsList extends BaseSearchableItemsList {
+
+ private static final String classChainStrViewRoot = "**/XCUIElementTypeCollectionView[`name == 'mentions.list.collection'`]";
+
+ private static final Function classChainRecentMention = username -> MobileBy.iOSClassChain(
+ String.format("**/XCUIElementTypeTable/XCUIElementTypeCell[2]/**/XCUIElementTypeLink[`name == '@%s'`]", username));
+
+ private static final Function predicateSuggestionByName = name -> MobileBy.iOSNsPredicateString(
+ String.format("name == 'user_cell.name' AND value == '%s'", name));
+
+ public MentionSuggestionsList(WebDriver driver) {
+ super(driver, classChainStrViewRoot);
+ }
+
+ public void tapSuggestedMention(String username) {
+ final By locator = predicateSuggestionByName.apply(username);
+ getElement(locator).click();
+ }
+
+ public boolean isRecentMention(String username) {
+ final By locator = classChainRecentMention.apply(username);
+ return isLocatorDisplayed(locator);
+ }
+
+ public boolean isNotRecentMention(String username) {
+ final By locator = classChainRecentMention.apply(username);
+ return isLocatorInvisible(locator);
+ }
+
+ public boolean isGuestLabelVisibleFor(String name) {
+ return super.isGuestLabelVisibleFor(name);
+ }
+
+ public boolean isExternalLabelVisibleFor(String name) {
+ return super.isExternalLabelVisibleFor(name);
+ }
+
+ public boolean isVerifiedLabelVisibleFor(String name) {
+ return super.isVerifiedLabelVisibleFor(name);
+ }
+
+ public boolean isSuggestionsVisible() {
+ return isVisible();
+ }
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/search/SearchList.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/search/SearchList.java
new file mode 100644
index 00000000000..f368882812e
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/search/SearchList.java
@@ -0,0 +1,93 @@
+package com.wearezeta.auto.ios.pages.search;
+
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.Keys;
+
+public class SearchList extends BaseSearchableItemsList {
+ private static final String classChainStrViewRoot = "**/XCUIElementTypeCollectionView[`name == 'search.list'`]";
+
+ public SearchList(WebDriver driver) {
+ super(driver, classChainStrViewRoot);
+ }
+
+ public void tapInstantConnectButton(String name) {
+ this.tapByPercentOfElementSize(getElement(classChainItemCellByName.apply(name)), 94, 50);
+ }
+
+ public int getOccurrencesCount(String name) {
+ return selectVisibleElements(classChainItemNameByName.apply(name)).size();
+ }
+
+ public boolean isFederatedItemVisible(String name) {
+ return super.isFederatedItemVisible(name);
+ }
+
+ public boolean isFederatedItemInvisible(String name) {
+ return super.isFederatedItemInvisible(name);
+ }
+
+ @Override
+ public boolean isUniqueUserNameLabelVisibleFor(String name) {
+ return super.isUniqueUserNameLabelVisibleFor(name);
+ }
+
+ @Override
+ public boolean isUniqueUserNameLabelInvisibleFor(String name) {
+ return super.isUniqueUserNameLabelInvisibleFor(name);
+ }
+
+ @Override
+ public void selectItem(String name) {
+ super.selectItem(name);
+ }
+
+ @Override
+ public boolean isItemVisible(String name) {
+ return super.isItemVisible(name);
+ }
+
+ @Override
+ public boolean isItemInvisible(String name) {
+ return super.isItemInvisible(name);
+ }
+
+ @Override
+ public boolean isGuestLabelVisibleFor(String name) {
+ return super.isGuestLabelVisibleFor(name);
+ }
+
+ @Override
+ public boolean isExternalLabelVisibleFor(String name) {
+ return super.isExternalLabelVisibleFor(name);
+ }
+
+ @Override
+ public boolean isExternalLabelInvisibleFor(String name) {
+ return super.isExternalLabelInvisibleFor(name);
+ }
+
+ @Override
+ public boolean isGuestLabelInvisibleFor(String name) {
+ return super.isGuestLabelInvisibleFor(name);
+ }
+
+ @Override
+ public void typeSearchQuery(String text) {
+ super.typeSearchQuery(text);
+ }
+
+ @Override
+ public void typeSearchQuery(String text, boolean shouldClearFieldBeforeInput) {
+ super.typeSearchQuery(text, shouldClearFieldBeforeInput);
+ }
+
+ @Override
+ public void sendKeysToSearchInput(Keys... keys) {
+ super.sendKeysToSearchInput(keys);
+ }
+
+ @Override
+ public String getCurrentSearchQuery() {
+ return super.getCurrentSearchQuery();
+ }
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/team_creation/InvitePeoplePage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/team_creation/InvitePeoplePage.java
new file mode 100644
index 00000000000..03bfd7cfb47
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/team_creation/InvitePeoplePage.java
@@ -0,0 +1,27 @@
+package com.wearezeta.auto.ios.pages.team_creation;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import com.wearezeta.auto.ios.pages.IOSPage;
+import org.openqa.selenium.WebElement;
+
+public class InvitePeoplePage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "EmailInputField")
+ private WebElement enterEmailField;
+
+ @iOSXCUITFindBy(accessibility = "button.addpeople.create")
+ private WebElement doneButton;
+
+ public InvitePeoplePage(WebDriver driver) {
+ super(driver);
+ }
+
+ public boolean isVisible() {
+ return isElementVisible(enterEmailField);
+ }
+
+ public void tapDoneButton(){
+ doneButton.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/team_creation/TCVerificationCodePage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/team_creation/TCVerificationCodePage.java
new file mode 100644
index 00000000000..28e5eabf3c5
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/team_creation/TCVerificationCodePage.java
@@ -0,0 +1,35 @@
+package com.wearezeta.auto.ios.pages.team_creation;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import com.wearezeta.auto.ios.pages.IOSPage;
+import org.openqa.selenium.WebElement;
+
+public class TCVerificationCodePage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "VerificationCode")
+ private WebElement emailCodeField;
+
+ @iOSXCUITFindBy(accessibility = "resend_button")
+ private WebElement resendButton;
+
+ @iOSXCUITFindBy(accessibility = "Back")
+ private WebElement backButton;
+
+ public TCVerificationCodePage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void enterVerificationCode(String code) {
+ emailCodeField.clear();
+ emailCodeField.sendKeys(code);
+ }
+
+ public void tapResendCode(){
+ resendButton.click();
+ }
+
+ public void tapBack(){
+ backButton.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/webview/KeycloakWebViewPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/webview/KeycloakWebViewPage.java
new file mode 100644
index 00000000000..baf8519af4d
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/webview/KeycloakWebViewPage.java
@@ -0,0 +1,57 @@
+package com.wearezeta.auto.ios.pages.webview;
+
+import com.wearezeta.auto.common.misc.Timedelta;
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class KeycloakWebViewPage extends IOSPage {
+
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeOther[`name == \"Sign in to Keycloak\"`]/XCUIElementTypeOther[2]/XCUIElementTypeTextField")
+ private WebElement username;
+
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeOther[`name == \"Sign in to Keycloak\"`]/XCUIElementTypeOther[3]/XCUIElementTypeSecureTextField")
+ private WebElement password;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "name == \"Sign In\"")
+ private WebElement signInButton;
+
+ @iOSXCUITFindBy(accessibility = "Failed to retrieve certificate")
+ private WebElement certificateError;
+
+ @iOSXCUITFindBy(accessibility = "Ok")
+ private WebElement ok;
+
+ public KeycloakWebViewPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void setUsername(String login) {
+ username.click();
+ username.clear();
+ username.sendKeys(login);
+ }
+
+ public void setPassword(String password) {
+ this.password.click();
+ this.password.clear();
+ this.password.sendKeys(password);
+ }
+
+ public void tapSignInButton() {
+ signInButton.click();
+ }
+
+ public boolean isKeycloakWebPageVisible() {
+ return isElementVisible(username, Timedelta.ofSeconds(20));
+ }
+
+ public boolean isKeycloakWebPageInvisible() {
+ return isElementInvisible(username);
+ }
+
+ public boolean isCertificateErrorVisible() {
+ return isElementVisible(certificateError);
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/webview/OktaWebViewPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/webview/OktaWebViewPage.java
new file mode 100644
index 00000000000..9fff16f973a
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/webview/OktaWebViewPage.java
@@ -0,0 +1,47 @@
+package com.wearezeta.auto.ios.pages.webview;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import com.wearezeta.auto.common.misc.Timedelta;
+import com.wearezeta.auto.ios.pages.IOSPage;
+import org.openqa.selenium.WebElement;
+
+public class OktaWebViewPage extends IOSPage {
+
+ @iOSXCUITFindBy(iOSNsPredicate = "label == 'Username' AND name == 'Username' AND type == 'XCUIElementTypeOther'")
+ private WebElement usernameField;
+
+ @iOSXCUITFindBy(accessibility = "Password")
+ private WebElement passwordField;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeButton' AND name CONTAINS 'Sign In'")
+ private WebElement signInButton;
+
+ public OktaWebViewPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public boolean isOktaWebPageVisible() {
+ return isElementVisible(usernameField, Timedelta.ofSeconds(20));
+ }
+
+ public boolean isOktaWebPageInvisible() {
+ return isElementInvisible(usernameField);
+ }
+
+ public void setUsername(String login) {
+ usernameField.click();
+ usernameField.clear();
+ usernameField.sendKeys(login);
+ }
+
+ public void setPassword(String password) {
+ passwordField.click();
+ passwordField.clear();
+ passwordField.sendKeys(password);
+ }
+
+ public void tapSignInButton() {
+ signInButton.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/webview/WebViewPage.java b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/webview/WebViewPage.java
new file mode 100644
index 00000000000..9f0a8c83703
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/java/com/wearezeta/auto/ios/pages/webview/WebViewPage.java
@@ -0,0 +1,219 @@
+package com.wearezeta.auto.ios.pages.webview;
+
+import io.appium.java_client.AppiumBy;
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import com.wearezeta.auto.common.misc.Timedelta;
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.appium.java_client.MobileBy;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import java.util.function.Function;
+
+public class WebViewPage extends IOSPage {
+
+ @iOSXCUITFindBy(accessibility = "Done")
+ private WebElement nameDoneButton;
+
+ @iOSXCUITFindBy(accessibility = "Open")
+ private WebElement openButton;
+
+ @iOSXCUITFindBy(accessibility = "ShareButton")
+ private WebElement safariShareButtonID;
+
+ @iOSXCUITFindBy(accessibility = "More")
+ private WebElement moreButtonShareExt;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "label == 'Wire' AND name == 'Wire' AND value == 'Wire'")
+ private WebElement idWireShareExt;
+
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeCell[`name == \"Wire Bund\"`]/XCUIElementTypeOther/XCUIElementTypeImage")
+ private WebElement idWireColumnShareExt;
+
+ @iOSXCUITFindBy(accessibility = "Choose")
+ private WebElement idShareExtChoose;
+
+ @iOSXCUITFindBy(accessibility = "Send")
+ private WebElement idSendButton;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeSwitch' AND value == '0' AND name = 'Wire'")
+ private WebElement wireSwitchOff;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeStaticText' AND name == 'Change Password'")
+ private WebElement changePasswordButton;
+
+ @iOSXCUITFindBy(accessibility = "Join in App")
+ private WebElement joinInTheAppButton;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "label == 'Address' AND name == 'URL'")
+ private WebElement urlBar;
+
+ @iOSXCUITFindBy(accessibility = "OpenInSafariButton")
+ private WebElement openInSafariButton;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "label == 'Done' AND name == 'Done'")
+ private WebElement webViewDoneButton;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "label == 'Address'")
+ private WebElement tapURLLink;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "label == 'Address' AND name == 'URL' AND type == 'XCUIElementTypeOther'")
+ private WebElement tapURLLinkOnRealDevice;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "label == 'Paste and Go'")
+ private WebElement pasteURLLink;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "label == 'Open in App' AND name == 'Open in App' AND value == 'Open in App'")
+ private WebElement openiOSApp;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "label == 'Download Wire' AND name == 'Download Wire' AND value == 'Download Wire'")
+ private WebElement downloadApp;
+
+ private static final Function predicateText = value -> MobileBy.iOSNsPredicateString(
+ String.format("type == 'XCUIElementTypeStaticText' AND name CONTAINS '%s'", value));
+
+ private static final Function predicateStrAddressBarByUrlPart = urlPart ->
+ String.format("(label == 'Address') AND value CONTAINS '%s'", urlPart);
+
+ @iOSXCUITFindBy(accessibility = "unlock_screen.text_field.enter_passcode")
+ private WebElement passcodeField;
+
+ @iOSXCUITFindBy(accessibility = "unlock_screen.title.enter_passcode")
+ private WebElement title;
+
+ @iOSXCUITFindBy(accessibility = "unlock_screen.button.unlock")
+ private WebElement unlockButton;
+
+ private static final Timedelta VISIBILITY_TIMEOUT = Timedelta.ofSeconds(20);
+
+ public WebViewPage(WebDriver driver) {
+ super(driver);
+ }
+
+ public void closeWebView() {
+ nameDoneButton.click();
+ }
+
+ public boolean isWebPageVisible(String expectedUrl) {
+ return waitUntilElementVisible(getDriver().findElement(AppiumBy.iOSNsPredicateString(predicateStrAddressBarByUrlPart.apply(expectedUrl))));
+ }
+
+ public String getUrlFromUrlBar() {
+ return urlBar.getText();
+ }
+
+ public boolean isChangePasswordPageVisible() {
+ return isElementVisible(changePasswordButton, VISIBILITY_TIMEOUT);
+ }
+
+ public boolean isTextVisible(String expectedText) {
+ return isLocatorExist(predicateText.apply(expectedText), Timedelta.ofSeconds(15));
+ }
+
+ public void tapShareButtonSafari() {
+ safariShareButtonID.click();
+ }
+
+ public void tapMoreButonShareExt() {
+ moreButtonShareExt.click();
+ }
+
+ public void enableWireShareExt() {
+ if (isElementVisible(wireSwitchOff)){
+ wireSwitchOff.click();
+ }
+ }
+
+ public void tapOpenButton() {
+ openButton.click();
+ }
+
+ public void tapDoneOnShareExt() {
+ nameDoneButton.click();
+ }
+
+ public void tapWireInShareExt() {
+ tapAtTheCenterOfElement(idWireShareExt);
+ }
+
+ public void tapWireColumnInShareExt() {
+ idWireColumnShareExt.click();
+ }
+
+ public void tapChooseInShareExt() {
+ waitUntilElementClickable(idShareExtChoose);
+ idShareExtChoose.click();
+ }
+
+ public void selectConversationInShareExt(String name) {
+ getDriver().findElement(MobileBy.AccessibilityId(name)).click();
+ }
+
+ public void tapSendButtonShareExt() {
+ idSendButton.click();
+ }
+
+ public void inputPasscode(String passcode) {
+ passcodeField.clear();
+ passcodeField.sendKeys(passcode);
+ }
+ public boolean isUnlockWireInShareExtensionVisble() {
+ return waitUntilElementVisible(title);
+ }
+ public void tapUnlockButtonOnShareExtension() {
+ unlockButton.click();
+ }
+
+ public void tapJoinInTheAppButton() {
+ waitUntilElementClickable(joinInTheAppButton);
+ joinInTheAppButton.click();
+ }
+
+ public void iTapDoneOnWebView(){
+ webViewDoneButton.click();
+ }
+
+ public void tapURLLinkSafari(){
+ waitUntilElementVisible(tapURLLink);
+ tapURLLink.click();
+ longTapWithScript(tapURLLink);
+ }
+
+ public void tapURLLinkSafariRealDevice(){
+ waitUntilElementVisible(tapURLLinkOnRealDevice);
+ longTapWithScript(tapURLLinkOnRealDevice);
+ }
+
+ public void tapPasteURLLinkSafari(){
+ waitUntilElementVisible(pasteURLLink);
+ tapAtTheCenterOfElement(pasteURLLink);
+ }
+
+ public boolean goToiOSAppOnWebViewIsVisible() {
+ return openiOSApp.isEnabled();
+ }
+
+ public boolean goToiOSAppOnWebViewIsInvisible() {
+ return isElementInvisible(openiOSApp);
+ }
+
+ public void tapOnGoToiOSAppOnWireWebView() {
+ tapAtTheCenterOfElement(openiOSApp);
+ }
+
+ public boolean downloadAppOnWebViewVisible() {
+ return downloadApp.isEnabled();
+ }
+
+ public boolean downloadAppOnWebViewInvisible() {
+ return isElementInvisible(downloadApp);
+ }
+
+ public void tapOnDownloadAppOnWebView() {
+ tapAtTheCenterOfElement(downloadApp);
+ }
+
+ public void iTapOpenInSafarOnWebView(){
+ openInSafariButton.click();
+ }
+}
diff --git a/wire-ios-automation/ios/src/main/resources/Configuration.properties b/wire-ios-automation/ios/src/main/resources/Configuration.properties
new file mode 100644
index 00000000000..825628b0efe
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/resources/Configuration.properties
@@ -0,0 +1,32 @@
+#Configuration file for IOS tests
+#Configure before running tests
+appiumUrl=${Url}
+enableAppiumOutput=${enableAppiumOutput}
+iosApplicationPath=${appPath}
+oldAppPath=${oldAppPath}
+realBuildNumber=${realBuildNumber}
+bundleId=${bundleId}
+isSimulator=${isSimulator}
+UDID=${UDID}
+appName=${appName}
+platformVersion=${platformVersion}
+driverTimeoutSeconds=${driverTimeoutSeconds}
+projectBuildPath=${project.build.directory}
+iOSToolsRoot=${project.build.directory}/../../tools/ios/
+defaultImagesPath=${project.build.directory}/../../tools/img/
+defaultAudioPath=${project.build.directory}/../../tools/audio/
+defaultVideoPath=${project.build.directory}/../../tools/video/
+defaultMiscResourcesPath=${project.build.directory}/../../tools/ios/misc/
+defaultEmail=${defaultEmail}
+defaultEmailPassword=${defaultEmailPassword}
+specialEmail=${specialEmail}
+specialPassword=${specialPassword}
+deviceName=${deviceName}
+backendType=${backendType}
+perfReportPath=${perfReportPath}
+testrailUser=smoketester+ios@wire.com
+testrailToken=3ss3NdlIsbkVZ/qjARyZ-t42ldIdkAFdZzbPgC3o0
+isOnGrid=${isOnGrid}
+browserName=${browserName}
+accountPagesPath=${accountPagesPath}
+kubeConfigPath=${kubeConfigPath}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/main/resources/META-INF/services/javax.script.ScriptEngineFactory b/wire-ios-automation/ios/src/main/resources/META-INF/services/javax.script.ScriptEngineFactory
new file mode 100644
index 00000000000..7a84fb0a06a
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/resources/META-INF/services/javax.script.ScriptEngineFactory
@@ -0,0 +1 @@
+apple.applescript.AppleScriptEngineFactory
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/main/resources/mixpanel.cer b/wire-ios-automation/ios/src/main/resources/mixpanel.cer
new file mode 100644
index 00000000000..5d4106e68ea
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/resources/mixpanel.cer
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDoDCCAoigAwIBAgIJAMKSROeRdgaBMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNV
+BAYTAkRFMRMwEQYDVQQIDApTb21lLVN0YXRlMQ0wCwYDVQQKDARXaXJlMQswCQYD
+VQQLDAJRQTElMCMGA1UEAwwcUUEgQ2VydGlmaWNhdGUgQXV0aG9yaXR5KENBKTAe
+Fw0xODA3MTYwODM3MDNaFw0zODA3MTEwODM3MDNaMGUxCzAJBgNVBAYTAkRFMRMw
+EQYDVQQIDApTb21lLVN0YXRlMQ0wCwYDVQQKDARXaXJlMQswCQYDVQQLDAJRQTEl
+MCMGA1UEAwwcUUEgQ2VydGlmaWNhdGUgQXV0aG9yaXR5KENBKTCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAL2I7/bMuV158R+5dNioBAxbSyoQ9N08Nidm
+KDpEWDbRzmnXnM633kK0nNyBn+OlpSJjOcRtUWqItZEkIw8e9nxjz9nKmjrMvxY4
+dhfHhk9pmX1ztv15AvYSzZhJfGjM/+nCuFNbCACkrpaaAk2aCBHbIQGaZACcwzH7
+n2Jc818f/lHRCwsw1c+mNtdDvLo6eOzhC8iFpx7cSmEGFLMegnZK4eCWECaDmQx4
+kWhhy17eiJ8jELEhWy2EGfZumGQVsfNi9tSOqA//2wvEr4NvUKQtqcasyrlionMj
+gF4Phz1n8lhdGlIAm/0AdB+Vb8lH6BqMd1i/TEZnySL7ivAj0ZMCAwEAAaNTMFEw
+HQYDVR0OBBYEFGi7qqgK55VxtmSB7kObvy3NusaWMB8GA1UdIwQYMBaAFGi7qqgK
+55VxtmSB7kObvy3NusaWMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQAD
+ggEBAF5QbdvBT6fZbdmc7SSr7sbbmR9/K+4irn6XiPES96YM57z8Dpet7qDDjavY
+ZRSRHK6qJ+nOFkSeCAEbZhqlvZR6upujpiYorUDH3rsUsO2LrcrWNFLT+GUrvlMe
+WFFmvW2rCG5yRNux+Dt6027hr98+OWGMin+3EhRWg15dXqdf1ZIeKEG9L3bx4m4C
+9KASNIkAwpctX3qhvcNgNiITZooQXibaSJ4bNjjyLZDIAA4thJo957VAHFXUFHFU
+a9sEndA06YpJNZZpEBy3VR0pUOnjwKMzQLLu0fLR2rC0f/8lQbQ/NRFkYYzN/8Hl
+vOntbjYL44Ah0UZ1O+XwuTjpdCc=
+-----END CERTIFICATE-----
diff --git a/wire-ios-automation/ios/src/main/resources/scripts/export_trace_to_csv.txt b/wire-ios-automation/ios/src/main/resources/scripts/export_trace_to_csv.txt
new file mode 100644
index 00000000000..c7b148c4270
--- /dev/null
+++ b/wire-ios-automation/ios/src/main/resources/scripts/export_trace_to_csv.txt
@@ -0,0 +1,29 @@
+tell application "Instruments"
+ activate
+ close every window
+ delay 2
+ open "Users:jenkins:instrumentscli0.trace"
+end tell
+delay 2
+tell application "System Events" to tell process "Instruments"
+ set frontmost to true
+ delay 10
+ tell menu bar item "Instrument" of menu bar 1
+ click
+ click menu item "Export Track for 'Activity Monitor'..." of menu 1
+ end tell
+ delay 1
+ keystroke "g" using {command down, shift down}
+ delay 1
+ tell sheet 1 of sheet 1 of window 1
+ keystroke "/Project/iOS_Performance_Reports"
+ click button "Go"
+ end tell
+ delay 0.2
+ tell sheet 1 of window 1
+ click button "Save"
+ end tell
+end tell
+tell application "Instruments"
+ quit
+end tell
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/pages/AdvancedSettingsPage.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/pages/AdvancedSettingsPage.java
new file mode 100644
index 00000000000..b6c31c9ed15
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/pages/AdvancedSettingsPage.java
@@ -0,0 +1,26 @@
+package com.wearezeta.auto.ios.pages;
+
+import io.appium.java_client.pagefactory.iOSXCUITFindBy;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+public class AdvancedSettingsPage extends IOSPage {
+ @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeStaticText[`name == \"Version Technical Details\"`]")
+ private WebElement versionTechnicalDetailsMenu;
+
+ @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeStaticText' AND value CONTAINS \"core-crypto\"")
+ private WebElement versionDetails;
+
+ public AdvancedSettingsPage(WebDriver driver) {
+ super(driver);
+ }
+
+
+ public void openVersionTechnicalDetails() {
+ versionTechnicalDetailsMenu.click();
+ }
+
+ public boolean isVersionDetailsVisible() {
+ return versionDetails.isDisplayed();
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/AdvancedSettingsSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/AdvancedSettingsSteps.java
new file mode 100644
index 00000000000..7783f1df592
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/AdvancedSettingsSteps.java
@@ -0,0 +1,31 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.AdvancedSettingsPage;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.jcodec.common.Assert.assertTrue;
+
+public class AdvancedSettingsSteps {
+ IOSTestContext context;
+
+ public AdvancedSettingsSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ public AdvancedSettingsPage getAdvancedSettingsPage() {
+ return context.getPagesCollection().getPage(AdvancedSettingsPage.class);
+ }
+
+ @When("I open Version Technical Details")
+ public void iOpenVersionTechnicalDetails() {
+ getAdvancedSettingsPage().openVersionTechnicalDetails();
+ }
+
+ @Then("I see my version details")
+ public void iSeeMyVersionDetails() {
+ assertTrue("Version details are not displayed", getAdvancedSettingsPage().isVersionDetailsVisible());
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ArchivePageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ArchivePageSteps.java
new file mode 100644
index 00000000000..0790df6dfac
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ArchivePageSteps.java
@@ -0,0 +1,66 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.common.CommonUtils;
+import com.wearezeta.auto.common.misc.Timedelta;
+import com.wearezeta.auto.common.usrmgmt.ClientUsersManager;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.ArchivePage;
+import io.cucumber.java.en.Given;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.regex.Pattern;
+
+public class ArchivePageSteps {
+ IOSTestContext context;
+
+ public ArchivePageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private ArchivePage getArchivePage() {
+ return context.getPagesCollection()
+ .getPage(ArchivePage.class);
+ }
+
+ /**
+ * Tap close button on Archive page
+ */
+ @When("^I tap close Archive page button$")
+ public void IClickCloseArchivePageButton() {
+ getArchivePage().clickCloseArchivePageButton();
+ }
+
+ /**
+ * verifies the visibility of a specific item in the archived conversations list
+ *
+ * @param shouldNotSee equals to null if the item should be visible
+ * @param value conversation name/alias
+ */
+ @Then("^I (do not )?see conversation (.*) in archived conversations list$")
+ public void ISeeUserInContactList(String shouldNotSee, String value) {
+ value = context.getUsersManager()
+ .replaceAliasesOccurrences(value, ClientUsersManager.FindBy.NAME_ALIAS);
+ if (shouldNotSee == null) {
+ assertThat(String.format("The conversation '%s' is not visible in the conversation list",
+ value), getArchivePage().isConversationInList(value));
+ } else {
+ assertThat(
+ String.format("The conversation '%s' is visible in the conversation list, but should be hidden",
+ value), getArchivePage().isConversationNotInList(value));
+ }
+ }
+
+ /**
+ * Open the corresponding conversation by tapping its name in the conversations list
+ *
+ * @param name conversation name/alias
+ */
+ @Given("^I open archived (?:group |single |1:1 |\\s?)conversation \"(.*)\"")
+ public void IOpenArchivedConversation(String name) {
+ name = context.getUsersManager()
+ .replaceAliasesOccurrences(name, ClientUsersManager.FindBy.NAME_ALIAS);
+ getArchivePage().tapConversationsListItem(name);
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/BackupPasswordOverlaySteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/BackupPasswordOverlaySteps.java
new file mode 100644
index 00000000000..d2ebaeed31d
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/BackupPasswordOverlaySteps.java
@@ -0,0 +1,39 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.BackupPasswordOverlayPage;
+import io.cucumber.java.en.And;
+import io.cucumber.java.en.When;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class BackupPasswordOverlaySteps {
+
+ IOSTestContext context;
+
+ public BackupPasswordOverlaySteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private BackupPasswordOverlayPage getPage() {
+ return context.getPagesCollection().getPage(BackupPasswordOverlayPage.class);
+ }
+
+ /**
+ * Tap the corresponding button
+ *
+ */
+ @When("^I tap Next button on Backup password overlay$")
+ public void iTapNextButtonBackupOverlay() {
+ getPage().tapNextButton();
+ }
+
+ /**
+ * Type the given backup password
+ *
+ * @param password the actual password value
+ */
+ @And("^I type password \"(.*)\" on Backup password overlay$")
+ public void iTypeBackupPassword(String password) {
+ getPage().typePassword(password);
+ }
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/BiometricAuthSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/BiometricAuthSteps.java
new file mode 100644
index 00000000000..0148ce91d9d
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/BiometricAuthSteps.java
@@ -0,0 +1,39 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.common.Config;
+import com.wearezeta.auto.common.misc.Timedelta;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.IOSPage;
+import io.cucumber.java.en.When;
+
+public class BiometricAuthSteps {
+ IOSTestContext context;
+
+ public BiometricAuthSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private static void verifyCurrentDeviceIsSimulator() {
+ if (!Config.current().isSimulator(BiometricAuthSteps.class)) {
+ throw new IllegalStateException("The current device is expected to be an iOS Simulator");
+ }
+ }
+
+ /**
+ * Perform simulated touch ID on Simulator. It is mandatory that Touch ID feature
+ * is already enrolled
+ *
+ * @param type either 'successful' or 'failed)'
+ * @
+ */
+ @When("^I perform (successful|failed) Touch ID$")
+ public void IPerformTouchID(String type) {
+ verifyCurrentDeviceIsSimulator();
+ // Consistently needed to wait 2 seconds before sending the perform for it to be accepted
+ context.startPinging();
+ Timedelta.ofSeconds(2).sleep();
+ context.stopPinging();
+ context.getPagesCollection().getPage(IOSPage.class)
+ .performTouchID(type.equalsIgnoreCase("successful"));
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/BottomNavigationBarSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/BottomNavigationBarSteps.java
new file mode 100644
index 00000000000..737fd6e6e99
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/BottomNavigationBarSteps.java
@@ -0,0 +1,64 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.BottomNavigationBarPage;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class BottomNavigationBarSteps {
+ IOSTestContext context;
+
+ public BottomNavigationBarSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private BottomNavigationBarPage getBottomNavigationBarPage() {
+ return context.getPagesCollection()
+ .getPage(BottomNavigationBarPage.class);
+ }
+
+ /**
+ * Open the corresponding view by tapping a button
+ *
+ */
+
+ @Then("^I open archived conversations$")
+ public void IOpenArchivedConversations() {
+ getBottomNavigationBarPage().openArchivedConversations();
+ }
+
+ /**
+ * Verify whether Archive button is visible at the bottom of conversations list
+ *
+ * @param shouldNotSee equals to null if Archive button should be visible
+ */
+ @Then("^I (do not )?see Archive button at the bottom of conversations list$")
+ public void ISeeArchiveButton(String shouldNotSee) {
+ if (shouldNotSee == null) {
+ assertThat("Archive button should be visible, but it's hidden",
+ getBottomNavigationBarPage().isArchiveButtonVisible());
+ } else {
+ assertThat("Archive button should be invisible, but it's visible",
+ getBottomNavigationBarPage().isArchiveButtonInvisible());
+ }
+ }
+
+ /**
+ @Then("^I tap Folder button in bottom navigation bar$")
+ public void iTapFolderButton() {
+ getBottomNavigationBarPage().tapGroupedConversationsButton();
+ }
+*/
+ @Then("^I tap Conversations button in bottom navigation bar$")
+ public void iTapRecentConversationsButton() {
+ getBottomNavigationBarPage().tapRecentConversationsButton();
+ }
+
+ @When("^I open settings screen")
+ public void iTapSettingsButton() {
+ getBottomNavigationBarPage().tapSettingsButton();
+ }
+
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/CameraPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/CameraPageSteps.java
new file mode 100644
index 00000000000..92f87737e6e
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/CameraPageSteps.java
@@ -0,0 +1,53 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.CameraPage;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class CameraPageSteps {
+
+ IOSTestContext context;
+
+ public CameraPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private CameraPage getCameraPage() {
+ return context.getPagesCollection().getPage(CameraPage.class);
+ }
+
+ @When("^I tap Take Photo button on Camera page$")
+ public void iTapTakePhoto() {
+ getCameraPage().tapTakePhoto();
+ }
+
+ @When("^I tap Take Video button on Camera page$")
+ public void iTapTakeVideo() {
+ getCameraPage().tapTakeVideo();
+ }
+
+ @When("^I tap Use Video button on Camera page$")
+ public void iTapUseVideo() {
+ getCameraPage().tapUseVideo();
+ }
+
+ @Then("^I see Choose from library button on change profile pop up$")
+ public void iSeeChooseFromLibrary() {
+ assertThat("The Choose from library button is not visible on change profile pop up",
+ getCameraPage().isChooseFromLibraryVisible());
+ }
+
+ @Then("^I see Take Photo button on Camera page$")
+ public void iSeePhotoButton() {
+ assertThat("The take photo button is not visible on Camera screen",
+ getCameraPage().isTakePhotoButtonVisible());
+ }
+
+ @When("^I tap Choose from library button on change profile pop up$")
+ public void iTapChooseFromLibrary() {
+ getCameraPage().tapChooseFromLibrary();
+ }
+
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/CameraRollPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/CameraRollPageSteps.java
new file mode 100644
index 00000000000..3122a4544b4
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/CameraRollPageSteps.java
@@ -0,0 +1,28 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.CameraRollPage;
+import io.cucumber.java.en.When;
+
+public class CameraRollPageSteps {
+
+ IOSTestContext context;
+
+ public CameraRollPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private CameraRollPage getCameraRollPage() {
+ return context.getPagesCollection().getPage(CameraRollPage.class);
+ }
+
+ @When("^I select a picture from Camera Roll$")
+ public void ISelectPicture() {
+ getCameraRollPage().selectPicture();
+ }
+
+ @When("^I select first picture from Camera Roll$")
+ public void ISelectFirstPicture() {
+ getCameraRollPage().selectFirstPicture();
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/CollectionPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/CollectionPageSteps.java
new file mode 100644
index 00000000000..e9954521189
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/CollectionPageSteps.java
@@ -0,0 +1,37 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.CollectionPage;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class CollectionPageSteps {
+
+ IOSTestContext context;
+
+ public CollectionPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private CollectionPage getCollectionPage() {
+ return context.getPagesCollection()
+ .getPage(CollectionPage.class);
+ }
+
+ @When("^I (long )?tap the item number (\\d+) in collection category PICTURES$")
+ public void iTapPictureItemByIndex(String isLongTap, int index) {
+ getCollectionPage().tapPictureItemByIndex(index, isLongTap != null);
+ }
+
+ @Then("^I see full-screen image preview in collection view$")
+ public void iSeeFullScreenImagePreview() {
+ assertThat("Full-screen image preview is expected to be visible",
+ getCollectionPage().isFullScreenImagePreviewVisible());
+ }
+
+ @When("^I tap X button in collection view$")
+ public void iTapCloseButton() {
+ getCollectionPage().tapCloseButton();
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/CommonBackendSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/CommonBackendSteps.java
new file mode 100644
index 00000000000..24cd7fb131a
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/CommonBackendSteps.java
@@ -0,0 +1,460 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.common.CommonSteps;
+import com.wearezeta.auto.common.backend.Backend;
+import com.wearezeta.auto.common.backend.BackendConnections;
+import com.wearezeta.auto.common.backend.models.MuteState;
+import com.wearezeta.auto.common.misc.Timedelta;
+import com.wearezeta.auto.common.usrmgmt.ClientUser;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import io.cucumber.java.en.Given;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.io.ByteArrayInputStream;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import static org.hamcrest.Matchers.containsString;
+
+public class CommonBackendSteps {
+
+ IOSTestContext context;
+
+ public CommonBackendSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private CommonSteps getCommonSteps() {
+ return context.getCommonSteps();
+ }
+
+ private List userGetDeviceIds(String usernameAlias) {
+ return context.getCommonSteps().getDeviceIds(usernameAlias);
+ }
+
+ @When("^User (.*) blocks user (.*)$")
+ public void BlockContact(String blockAsUserNameAlias, String userToBlockNameAlias) {
+ getCommonSteps().blockContact(blockAsUserNameAlias, userToBlockNameAlias);
+ }
+
+ @Given("^(\\w+) waits? until (\\w+) exists in backend search results$")
+ public void UserWaitsUntilContactExistsInHisSearchResults(
+ String searchByNameAlias, String query) {
+ getCommonSteps().waitUntilContactIsFoundInSearch(searchByNameAlias, query);
+ }
+
+ @Given("^User (.*) removes their avatar picture$")
+ public void UserRemovesAvatarPicture(String nameAlias) {
+ getCommonSteps().userDeletesAvatarPicture(nameAlias);
+ }
+
+ @Given("^User (.*) removes all their registered OTR clients$")
+ public void UserRemovesAllRegisteredOtrClients(String userAs) {
+ getCommonSteps().userRemovesAllRegisteredOtrClients(userAs);
+ }
+
+ @When("^User (.*) cancels all outgoing connection requests$")
+ public void CancelAllOutgoingConnectRequest(String userToNameAlias) {
+ getCommonSteps().cancelAllOutgoingConnectRequests(userToNameAlias);
+ }
+
+ @Given("^User (.*) sent connection request to (.*)$")
+ public void GivenConnectionRequestIsSentTo(String userFromNameAlias, String usersToNameAliases) {
+ getCommonSteps().connectionRequestIsSentTo(userFromNameAlias, usersToNameAliases);
+ }
+
+ @Given("^User (.*) has group conversation (.*) with (.*)$")
+ public void UserHasGroupChatWithContacts(String chatOwnerNameAlias,
+ String chatName, String otherParticipantsNameAlises) {
+ getCommonSteps().userHasGroupChatWithContacts(chatOwnerNameAlias, chatName, otherParticipantsNameAlises);
+ }
+
+ @When("^User (.*) adds (.*) to group chat (.*)")
+ public void UserXDddsUserYToGroupChat(String chatOwnerNameAlias,
+ String userToAdd, String chatName) {
+ getCommonSteps().userXAddedContactsToGroupChat(chatOwnerNameAlias, userToAdd, chatName);
+ }
+
+ @Given("^User (.*) is connected to (.*)$")
+ public void UserIsConnectedTo(String userFromNameAlias, String usersToNameAliases) {
+ getCommonSteps().userIsConnectedTo(userFromNameAlias, usersToNameAliases);
+ }
+
+ @Given("^User (.*) accepts connection request from (.*)")
+ public void acceptConnectionRequestFrom(String asUserAlias, String userFromNameAliases) {
+ context.getCommonSteps().userAcceptsConnectionRequestFrom(asUserAlias, userFromNameAliases);
+ }
+
+ @Given("^User (.*) leaves group chat (.*)$")
+ public void UserLeavesGroupChat(String userName, String chatName) {
+ getCommonSteps().userXLeavesGroupChat(userName, chatName);
+ }
+
+ @Given("^User (.*) removes? user (.*) from group conversation (.*)$")
+ public void UserARemovesUserBFromGroupChat(String chatOwnerNameAlias, String userToRemove, String chatName) {
+ getCommonSteps().userXRemoveUserFromGroupConversation(chatOwnerNameAlias, userToRemove, chatName);
+ }
+
+ @When("^User (\\w+) changes? name to (.*)$")
+ public void IChangeName(String userNameAlias, String newName) {
+ getCommonSteps().userChangesName(userNameAlias, newName);
+ }
+
+ @When("^User (\\w+) changes? accent color to (.*)$")
+ public void IChangeAccentColor(String userNameAlias, String newColor) {
+ getCommonSteps().userChangesAccentColor(userNameAlias, newColor);
+ }
+
+ @Given("^There (?:is|are) personal account users? (.*)")
+ public void ThereAreUsers(String nameAliases) {
+ final List userNames = getCommonSteps().thereArePersonalUsers(nameAliases).stream()
+ .map(ClientUser::getName)
+ .collect(Collectors.toList());
+ getCommonSteps().usersSetUniqueUsername(String.join(",", userNames));
+ userNames.forEach(x -> {
+ try {
+ getCommonSteps().userChangesUserAvatarPicture(x);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ @Then("^I verify user's (.*) email on the backend is equal to (.*)")
+ public void IVerifyEmailOnBackend(String user, String expectedValue) {
+ getCommonSteps().userVerifiesEmail(user, expectedValue);
+ }
+
+ @Given("^User (.*) removes users? (.*) from team (.*)")
+ public void UserXRemovesUsersFromTeam(String userNameAlias, String userAliases, String teamName) {
+ getCommonSteps().userXRemovesUsersFromTeam(userNameAlias, userAliases, teamName);
+ }
+
+ @Given("^User (.*) has conversation (.*) with (.*) in team (.*)")
+ public void UserHasGroupChatWithContacts(String chatOwnerNameAlias,
+ String chatName, String otherParticipantsNameAliases, String teamName) {
+ getCommonSteps().userHasGroupConversationInTeam(chatOwnerNameAlias, chatName, otherParticipantsNameAliases, teamName);
+ }
+
+ @Given("^User (.*) has 1:1 conversation with (.*) in team (.*)")
+ public void UserHas1to1ChatInTeam(String chatOwnerNameAlias, String otherParticipantAlias, String teamName) {
+ getCommonSteps().userHasGroupConversationInTeam(chatOwnerNameAlias, null, otherParticipantAlias, teamName);
+ }
+
+
+ @Given("^There is a team owner \"(.*)\" with team \"(.*)\"( without unique username)?$")
+ public void thereIsATeamOwner(String userAlias, String teamName, String hasNoUniqueUsername) {
+ if (hasNoUniqueUsername == null) {
+ context.getCommonSteps().thereIsATeamOwner(userAlias, teamName, true);
+ } else {
+ context.getCommonSteps().thereIsATeamOwner(userAlias, teamName, false);
+ }
+ }
+
+ @Given("^There is a team owner \"(.*)\" who sets up team \"(.*)\" for E2EI on (.*) backend")
+ public void thereIsATeamOwnerWhoSetsUpTeamForE2EIOnBackend(String userAlias, String teamName, String backend) {
+ thereIsATeamOwnerOnCustomBackend(userAlias, teamName, backend);
+ getCommonSteps().userAddsKeycloakUserForE2EI(userAlias, userAlias);
+ context.getCommonSteps().configureMLSForBund(userAlias, teamName);
+ context.getCommonSteps().enableE2EIFeatureTeam(userAlias, teamName);
+ }
+
+ @Given("^There is a team owner \"(.*)\" with team \"(.*)\" on (.*) backend$")
+ public void thereIsATeamOwnerOnCustomBackend(String userAlias, String teamName, String backendName) {
+ Backend backend = BackendConnections.get(backendName);
+ context.getCommonSteps().thereIsATeamOwner(userAlias, teamName, backend);
+ }
+
+ /**
+ * Adds users to a team by creating them on the backend. Team owner user should already exist
+ *
+ * @param ownerNameAlias team owner name/alias
+ * @param userNameAliases team members aliases
+ * @param teamName the name of the team
+ * @param role one of available team roles
+ */
+ @Given("^User (.*) adds users? (.*) to team (.*) with role (Owner|Admin|Member|Partner)( and without unique usernames?)?$")
+ public void UserXAddsUsersToTeam(String ownerNameAlias, String userNameAliases, String teamName, String role,
+ String hasNoHandle) {
+ boolean hasHandle = hasNoHandle == null;
+ getCommonSteps().userXAddsUsersToTeam(ownerNameAlias, userNameAliases, teamName, role, hasHandle);
+ }
+
+ /**
+ * Creates team owner user with one SSO team for Okta
+ *
+ * @param userAlias team owner name/alias
+ * @param teamName the name of the team to be created
+ */
+ @Given("^There is a team owner \"(.*)\" with SSO team \"(.*)\" configured for okta$")
+ public void ThereIsASSOTeamOwnerForOkta(String userAlias, String teamName) {
+ getCommonSteps().thereIsASSOTeamOwnerForOkta(userAlias, teamName);
+ }
+
+ @Given("^User (.*) adds users? (.*) to keycloak for E2EI$")
+ public void userAddsKeycloakUserForE2EI(String ownerNameAlias, String userNameAliases) {
+ getCommonSteps().userAddsKeycloakUserForE2EI(ownerNameAlias, userNameAliases);
+ }
+
+ @When("^Admin user (.*) enables E2EI with ACME server for team \"(.*)\"$")
+ public void enableE2EIForTeam(String adminUserAlias, String teamName) {
+ context.getCommonSteps().enableE2EIFeatureTeam(adminUserAlias, teamName);
+ }
+
+ @When("^Admin user (.*) enables E2EI with insecure ACME server for team \"(.*)\"$")
+ public void enableE2EIForTeamWithInsecureACME(String adminUserAlias, String teamName) {
+ context.getCommonSteps().enableE2EIFeatureTeamWithInsecureACME(adminUserAlias, teamName);
+ }
+
+ @Given("^User (.*) adds users? (.*) to okta$")
+ public void userAddsOktaUsers(String ownerNameAlias, String userNameAliases) {
+ getCommonSteps().userAddsOktaUser(ownerNameAlias, userNameAliases);
+ }
+
+ @Given("^User (.*) adds users? (.*) to okta and SCIM$")
+ public void userAddsUserToOktaAndSCIM(String ownerNameAlias, String userNameAliases) {
+ getCommonSteps().userAddsUserToOktaAndSCIM(ownerNameAlias, userNameAliases);
+ }
+
+ @Given("^Team user (.*) invites wireless user (\\w+)(, which expires in \\d+ seconds?,)? to conversation (.*)$")
+ public void userInvitesWirelessUsers(String ownerNameAlias, String userNameAliases, String timeoutSeconds,
+ String conversationName) {
+ if (timeoutSeconds == null) {
+ getCommonSteps().userInvitesWirelessUsers(ownerNameAlias, userNameAliases, conversationName);
+ } else {
+ getCommonSteps().userInvitesWirelessUsers(ownerNameAlias, userNameAliases,
+ Duration.ofSeconds(Integer.parseInt(timeoutSeconds.replaceAll("\\D", ""))),
+ conversationName);
+ }
+ }
+
+ @Given("Team user (.*) allows guests in conversation (.*)$")
+ public void userInvitesWirelessUsers(String userNameAlias, String conversationName) {
+ getCommonSteps().userAllowsGuestsInConversation(userNameAlias, conversationName);
+ }
+
+ @Given("^User (.*) creates invite link for conversation (.*)")
+ public void userCreatesInviteLink(String userNameAlias, String conversationName) {
+ getCommonSteps().userCreatesInviteLink(userNameAlias, conversationName);
+ }
+
+ /*
+ * Creates a user with custom credentials. Useful to test with users we know exist (e.g.
+ * the user who is connected to 255 people)
+ */
+ @Given("^There is a known user (.*) with email (.*) and password (.*)$")
+ public void ThereIsAKnownUser(String name, String email, String password) {
+ getCommonSteps().thereIsAKnownUser(name, email, password, BackendConnections.getDefault());
+ }
+
+ @Given("^User (.*) (en|dis)ables (.*) services? for team (.*)$")
+ public void userWhitelistsService(String ownerOrAdminAlias, String action, String commaSeparatedServiceAliases,
+ String teamName) {
+ context.getCommonSteps().userSwitchesUsersServicesForTeam(ownerOrAdminAlias,
+ action.equals("en"), commaSeparatedServiceAliases, teamName);
+ }
+
+ @When("^User (.*) (mutes|unmutes|allows only mentions for) conversation (.*)")
+ public void muteConversationWithUser(String userToNameAlias, String action, String dstConvo) {
+ switch (action) {
+ case "mutes":
+ getCommonSteps().userSetsMuteStatusForConversation(userToNameAlias, dstConvo, MuteState.MUTE_ALL);
+ break;
+ case "unmutes":
+ getCommonSteps().userSetsMuteStatusForConversation(userToNameAlias, dstConvo, MuteState.NONE);
+ break;
+ case "allows only mentions for":
+ getCommonSteps().userSetsMuteStatusForConversation(userToNameAlias, dstConvo, MuteState.MENTIONS_ONLY);
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("Unknown action: %s", action));
+ }
+ }
+
+ @When("^User (.*) archives conversation (.*)")
+ public void archiveConversationWithUser(String userToNameAlias, String dstConvoName) {
+ getCommonSteps().userSetsArchivedStateForConversation(userToNameAlias, dstConvoName, true);
+ }
+
+ @When("^User (.*) (?:adds|updates) rich profile field \"(.*)\" with value \"(.*)\"$")
+ public void userUpdatesEnrichedProfileViaSCIM(String userNameAlias, String key, String value) {
+ getCommonSteps().userUpdatesRichProfile(userNameAlias, key, value);
+ }
+
+ @When("^User (.*) changes users? (.*) to role (.*) for conversation \"(.*)\"$")
+ public void userChangesRoleOtherInConversation(String userName, String subjectUsers, String conversationRole, String conversationName) {
+ getCommonSteps().userChangesRoleOtherInConversation(userName, subjectUsers, conversationRole, conversationName);
+ }
+
+ // region foma
+
+ /*
+ * The following 3 Methods can be used during the setup of a test when working with FOMA environment
+ * These methods will check for 60 seconds if the needed pods are available and if we can start the testcase.
+ */
+ @Given("^I wait until the (federator|brig|galley) pod on (.*) is available$")
+ public void waitUntilPodIsAvailable(String service, String backendName) throws Exception {
+ context.getCommonSteps().waitUntilPodIsAvailable(backendName, service);
+ }
+
+ /*
+ * This Method will turn the Federator for Federated environments on or off.
+ * Turning the federator off will disable federation for the selected environment.
+ */
+ @Given("^Federator for backend (.*) is turned (on|off)$")
+ public void turnFederatorforFederatedEnvironmentOnOrOff(String backendName, String status) throws Exception {
+ if (status.equals("on")) {
+ context.getCommonSteps().turnFederatorInBackendOn(backendName);
+ context.getCommonSteps().checkPodsStatusOn(backendName, "federator");
+ } else {
+ context.getCommonSteps().turnFederatorInBackendOff(backendName);
+ context.getCommonSteps().checkPodsStatusOff(backendName, "federator");
+ }
+ }
+
+ @Then("^The search policy is (.*) with no team level restriction from (.*) backend to (.*) backend$")
+ public void searchPolicyCheck(String searchPolicy, String fromBackend, String toBackend) {
+ String toDomain = BackendConnections.get(toBackend).getDomain();
+ assertThat("Search policy is not correct",
+ context.getCommonSteps().getSearchPolicy(fromBackend),
+ containsString("\"domain\":\"" + toDomain + "\","
+ + "\"restriction\":{\"tag\":\"allow_all\",\"value\":null},"
+ + "\"search_policy\":\"" + searchPolicy + "\""));
+ }
+
+ // endregion foma
+
+ @Given("^User (.*) changes their email to (.*)$")
+ public void userXchangesEmailToNewEmail(String userNameAliases, String email) {
+ getCommonSteps().userChangesEmail(userNameAliases, email);
+ }
+
+ @Given("^TeamOwner \"(.*)\" waits and enables conference calling feature for team (.*) via backdoor$")
+ public void TeamOwnerWaitsEnablesConferenceCallingViaBackdoor(String alias, String teamName) {
+ // Wait until stripe/ibis has set free account restrictions after team creation.
+ // This wait can be skipped if the driver was created because the creation usually
+ // takes more than 3 seconds and is happening before a call is started.
+ if (!context.isDriverCreated()) {
+ Timedelta.ofSeconds(10).sleep();
+ }
+ context.getCommonSteps().enableConferenceCallingFeatureViaBackdoorTeam(alias, teamName);
+ }
+
+ @Given("^TeamOwner \"(.*)\" enables conference calling feature for team (.*) via backdoor$")
+ public void TeamOwnerEnablesConferenceCallingViaBackdoor(String alias, String teamName) {
+ context.getCommonSteps().enableConferenceCallingFeatureViaBackdoorTeam(alias, teamName);
+ }
+
+ @Given("^Personal Users (.*) enables conference calling feature via backdoor$")
+ public void PersonalUserEnablesConferenceCalling(String alias) {
+ context.getCommonSteps().enableConferenceCallingFeatureViaBackdoorPersonalUser(alias);
+ }
+
+ @Given("^TeamOwner \"(.*)\" sets the search behaviour for SearchVisibilityInbound to SearchableByOwnTeam for team (.*)$")
+ public void TeamOwnerDisablesSearchInbound(String alias, String teamName) {
+ //SearchableByOwnTeam = disabled
+ context.getCommonSteps().disableSearchVisibilityInbound(alias, teamName);
+ }
+
+ @Given("^TeamOwner \"(.*)\" enables the search behaviour for TeamSearchVisibility for team (.*)$")
+ public void TeamOwnerEnablesSearchOutbound(String alias, String teamName) {
+ context.getCommonSteps().enableTeamSearchVisibilityOutbound(alias, teamName);
+ }
+
+ @Given("^TeamOwner \"(.*)\" sets the search behaviour for TeamSearchVisibility to SearchVisibilityNoNameOutsideTeam for team (.*)$")
+ public void TeamOwnerSetsSearchOutboundSearchVisibilityNoNameOutsideTeam(String alias, String teamName) {
+ context.getCommonSteps().setTeamSearchVisibilityOutboundNoNameOutsideTeam(alias, teamName);
+ }
+
+ @When("^Admin user (.*) enables 2 Factor Authentication for team (.*)$")
+ public void userOwnerEnablesGuestLinksForTeam(String adminUserAlias, String teamName) {
+ context.getCommonSteps().enable2FAuthentication(adminUserAlias, teamName);
+ }
+
+ @When("^Admin user (.*) disables 2 Factor Authentication for team (.*)$")
+ public void userOwnerDisablesGuestLinksForTeam(String adminUserAlias, String teamName) {
+ context.getCommonSteps().disable2FAuthentication(adminUserAlias, teamName);
+ }
+
+ @When("^Admin user (.*) unlocks 2F Authentication for team (.*)$")
+ public void userOwnerUnlocks2FAuthenticationForTeam(String adminUserAlias, String teamName) {
+ context.getCommonSteps().unlock2FAuthentication(adminUserAlias, teamName);
+ }
+
+ @Given("^User (.*) configures MLS for team \"(.*)\"$")
+ public void enableMLSForTeam(String adminUserAlias, String teamName) {
+ context.getCommonSteps().configureMLSForBund(adminUserAlias, teamName);
+ }
+
+ @When("^Admin user (.*) disables MLS for team (.*) via backdoor$")
+ public void disableMLSForTeam(String adminUserAlias, String teamName) {
+ context.getCommonSteps().disableMLSFeatureTeam(adminUserAlias, teamName);
+ }
+
+ @Given("^User (.*) has MLS conversation \"(.*)\" with (.*)$")
+ public void userHasMLSGroupChat(String chatOwnerNameAlias, String chatName, String participantAliases) {
+ context.getCommonSteps().userHasMLSGroupConversation(chatOwnerNameAlias, chatName, participantAliases);
+ }
+
+ @When("^User (.*) adds (\\d+) devices?$")
+ public void userAddsDevices(String userNameAlias, int amount) {
+ for (int i = 1; i <= amount; i++) {
+ context.getCommonSteps().addDevice(userNameAlias, null,
+ "Device" + i, Optional.of("Label" + i), true);
+ }
+ }
+
+ @Given("^There is a known user (.*) with email (.*) and password (.*) on (.*) backend$")
+ public void ThereIsAKnownUserOnBackend(String name, String email, String password, String backend) {
+ getCommonSteps().thereIsAKnownUser(name, email, password, BackendConnections.get(backend));
+ }
+
+ private static List extractEmails(String emails) {
+ return Arrays.stream(emails.split(","))
+ .map(String::trim)
+ .collect(Collectors.toList());
+ }
+
+ @Then("^User (.*) send invitations? to emails (.*) to join the (.*) team as a member$")
+ public void ISendTeamInvites(String ownerAlias, String emails, String dstTeam) {
+ for (String email : extractEmails(emails)) {
+ context.getCommonSteps().userXSendsInvitationMailToMember(ownerAlias, email, dstTeam, "member");
+ }
+ }
+ @Then("^I print all created users in the execution log$")
+ public void IPrintAllCreatedUsers() {
+ context.getCommonSteps().printAllCreatedUsers();
+ }
+
+
+ @When("^Admin of (.*) backend revokes remembered certificate on ACME server$")
+ public void revokeRememberedCertificate(String backendName) throws CertificateException {
+ String pem = context.getRememberedCertificate();
+ byte [] decoded = Base64.getDecoder().decode(pem
+ .replaceAll("-----BEGIN CERTIFICATE-----", "")
+ .replaceAll("-----END CERTIFICATE-----", "")
+ .replaceAll("\n", "")
+ .replaceAll(" ", "")
+ .strip());
+
+ CertificateFactory factory = CertificateFactory.getInstance("X.509");
+ X509Certificate certificate = (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(decoded));
+ String serialNumber = "0x" + certificate.getSerialNumber().toString(16);
+ context.getCommonSteps().revokeCertificate(backendName, serialNumber, certificate.getSerialNumber());
+ }
+
+ @When("^Users? (.*) claims? key packages$")
+ public void claimKeyPackages(String userAliases) {
+ context.getCommonSteps().claimKeyPackages(userAliases);
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/CommonIOSSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/CommonIOSSteps.java
new file mode 100644
index 00000000000..0fd5f267a25
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/CommonIOSSteps.java
@@ -0,0 +1,627 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.common.*;
+import com.wearezeta.auto.common.backend.Backend;
+import com.wearezeta.auto.common.backend.BackendConnections;
+import com.wearezeta.auto.common.misc.EphemeralTimeConverter;
+import com.wearezeta.auto.common.testservice.models.LegalHoldStatus;
+import com.wearezeta.auto.common.imagecomparator.QRCode;
+import com.wearezeta.auto.common.log.ZetaLogger;
+import com.wearezeta.auto.common.misc.Timedelta;
+import com.wearezeta.auto.common.usrmgmt.ClientUsersManager;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.common.Lifecycle;
+import com.wearezeta.auto.ios.pages.CustomBackendRedirectionPage;
+import com.wearezeta.auto.ios.pages.IOSPage;
+import com.wire.qa.picklejar.engine.exception.SkipException;
+import io.cucumber.java.en.And;
+import io.cucumber.java.en.Given;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+import java.nio.file.Path;
+import java.util.logging.Logger;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.openqa.selenium.ScreenOrientation;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.time.Duration;
+import java.text.ParseException;
+
+import static org.hamcrest.Matchers.*;
+
+public class CommonIOSSteps {
+
+ IOSTestContext context;
+
+ public CommonIOSSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private static final Logger log = ZetaLogger.getLog(IOSPage.class.getSimpleName());
+
+ private static final String SAFARI = "com.apple.mobilesafari";
+
+ static {
+ System.setProperty("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog");
+ System.setProperty("org.apache.commons.logging.simplelog.log.org.apache.http", "warn");
+ }
+
+ private CommonSteps getCommonSteps() {
+ return context.getCommonSteps();
+ }
+
+ private IOSPage getCommonPage() {
+ return context.getPagesCollection().getPage(IOSPage.class);
+ }
+
+ private CustomBackendRedirectionPage getCustomBackendRedirectionPage() {
+ return context.getPagesCollection().getPage(CustomBackendRedirectionPage.class);
+ }
+
+ /**
+ * Upgrade Wire to the recent version if the old one was previously installed
+ */
+ @Given("^I upgrade Wire to the recent version$")
+ public void IUpgradeWire() {
+ getCommonPage().installApp(new File(Lifecycle.getAppPath()));
+ getCommonPage().activateApp(Lifecycle.getBundleId());
+ }
+
+ @Given("^I install the old version of Wire$")
+ public void installOldWire() {
+ getCommonPage().installApp(new File(Lifecycle.getOldAppPath()));
+ getCommonPage().activateApp(Lifecycle.getBundleId());
+ }
+
+ @Given("^I point the app to production backend$")
+ public void IStartOnProductionBackend() {
+ context.startAppOnProductionBackend();
+ }
+
+ @Given("^All other versions of Wire are uninstalled$")
+ public void IUninstallAllVersionsWire() {
+ context.uninstallAllVersionsOfWire();
+ }
+
+ @Given("^I enroll the simulator for Touch ID$")
+ public void iEnrollSimulatorTouchID() {
+ context.enrollSimulatorTouchID();
+ }
+
+ /**
+ * Restarts currently executed Wire instance
+ */
+ @When("^I restart Wire$")
+ public void IRestartWire() {
+ final String wireBundleId = Lifecycle.getBundleId();
+ getCommonPage().terminateApp(wireBundleId);
+ getCommonPage().activateApp(wireBundleId);
+ }
+
+ @When("^I terminate Wire$")
+ public void ITerminateWire() {
+ getCommonPage().terminateApp(Lifecycle.getBundleId());
+ }
+
+ @Given("^The device is reset before and after the test$")
+ public void fullReset() {
+ context.doFullReset();
+ }
+
+ // region permissions
+
+ @Given("^I allow microphone access")
+ public void iAllowMicrophoneAccess() {
+ context.allowMicrophoneAccess();
+ }
+
+ @Given("^I allow camera access")
+ public void iAllowCameraAccess() {
+ context.allowCameraAccess();
+ }
+
+ @Given("^I allow access to all photos")
+ public void iAllowAccessToAllPhotos() {
+ context.allowAccessToAllPhotos();
+ }
+
+ // endregion permissions
+
+ @When("^I accept camera access alert on real device$")
+ public void iAcceptCameraAccessAlert() {
+ if (context.isRealDevice()) {
+ getCommonPage().acceptAlert();
+ }
+ }
+
+ @When("^I accept microphone access alert on real device$")
+ public void iAcceptMicrophoneAccessAlert() {
+ if (context.isRealDevice()) {
+ getCommonPage().acceptAlert();
+ }
+ }
+
+ @When("^I accept access to all photos on real device$")
+ public void iAcceptAccessToAllPhotos() {
+ if (context.isRealDevice()) {
+ getCommonPage().acceptAccessToAllPhotos();
+ }
+ }
+
+ @When("^I accept alert( if visible)?$")
+ public void IAcceptAlert(String mayIgnore) {
+ if (mayIgnore == null) {
+ getCommonPage().acceptAlert();
+ } else {
+ if (getCommonPage().acceptAlertIfVisible()) {
+ log.info("Unexpected alert was present on " + context.getScenario().getName());
+ } else {
+ log.info("Unexpected alert was not present on " + context.getScenario().getName());
+ }
+ }
+ }
+
+ @When("^I tap Not Now on save password alert$")
+ public void ITapNotNowPasswordSaveAlert() {
+ if (getCommonPage().isNotNowOnPasswordPromptVisible()) {
+ getCommonPage().tapNotNowOnPasswordPrompt();
+ }
+ }
+
+ @When("^I accept notification permission alert if visible$")
+ public void IAcceptNotificationAlert() {
+ getCommonPage().acceptNotificationAlertIfVisible();
+ }
+
+
+ /**
+ * Tap the corresponding on-screen keyboard button
+ *
+ * @param btnName button name
+ */
+ @When("^I tap (Hide|Space|Done|Next) keyboard button$")
+ public void ITapHideKeyboardBtn(String btnName) {
+ //sometimes the simulator doesn't show the keyboard up
+ if (getCommonPage().isKeyboardVisible()) {
+ switch (btnName.toLowerCase()) {
+ case "hide":
+ getCommonPage().tapHideKeyboardButton();
+ break;
+ case "space":
+ getCommonPage().tapSpaceKeyboardButton();
+ break;
+ case "done":
+ getCommonPage().tapKeyboardCommitButton();
+ break;
+ case "next":
+ getCommonPage().tapNextKeyboardButton();
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("Unknown button name: %s", btnName));
+ }
+ } else{
+ log.warning("keyboard should be visible but it's not");
+ }
+ }
+
+ /**
+ * Closes the app for a certain amount of time in seconds
+ *
+ * @param seconds time in seconds to close the app
+ */
+ @When("^I minimize Wire for (\\d+) seconds?$")
+ public void IMinimizeWire(int seconds) {
+ getCommonPage().putWireToBackgroundFor(Timedelta.ofSeconds(seconds));
+ }
+
+ /**
+ * Locks screen for a certain amount of time in seconds
+ *
+ * @param seconds time in seconds to lock screen
+ */
+ @When("^I lock screen for (\\d+) seconds?$")
+ public void ILockScreen(int seconds) {
+ getCommonPage().lockScreen(Timedelta.ofSeconds(seconds));
+ }
+
+ @Given("^There (?:is|are) (\\d+) users? where (.*) is me$")
+ public void thereAreNUsersWhereXIsMe(int count, String myNameAlias) throws Exception {
+ getCommonSteps().thereAreNPersonalUsersWhereXIsMe(count, myNameAlias);
+ getCommonSteps().userChangesUserAvatarPicture(myNameAlias);
+ getCommonSteps().usersSetUniqueUsername(myNameAlias);
+ }
+
+ /**
+ * Creates specified number of users and sets user with specified name as
+ * main user. The user is registered with a email only and has no phone
+ * number attached
+ */
+ @Given("^There (?:are|is) (\\d+) users? with email address only where (.*) is me$")
+ public void ThereAreNUsersWhereXIsMeWithoutPhone(int count, String myNameAlias) throws Exception {
+ throw new RuntimeException("Phone number support was removed");
+ }
+
+ @When("^I wait for (\\d+) seconds?$")
+ public void WaitForTime(int seconds) {
+ context.startPinging();
+ Timedelta.ofSeconds(seconds).sleep();
+ context.stopPinging();
+ }
+
+ @Deprecated // Please try not to use this step. Replace Myself with explicit placeholders
+ @Given("^User (.*) is [Mm]e$")
+ public void UserXIsMe(String nameAlias) {
+ getCommonSteps().userXIsMe(nameAlias);
+ }
+
+ @When("^I rotate UI to (landscape|portrait)$")
+ public void WhenIRotateUILandscape(String orientation) {
+ orientation = orientation.toUpperCase();
+ getCommonPage().rotateScreen(ScreenOrientation.valueOf(orientation));
+ Timedelta.ofSeconds(1).sleep();
+ }
+
+ /**
+ * Tap at the corresponding point of the visible viewport
+ *
+ * @param percentX 0 <= percentX <= 100
+ * @param percentY 0 <= percentY <= 100
+ */
+ @When("^I tap at (\\d+)%,(\\d+)% of the viewport size")
+ public void ITapAtPoint(int percentX, int percentY) {
+ getCommonPage().tapScreenByPercents(percentX, percentY);
+ }
+
+ @Deprecated // Please use alert title or alert description steps
+ @Then("^I (do not )?see alert contains text \"(.*)\"$")
+ public void ISeeAlertContains(String shouldNotBeVisible, String expectedText) {
+ if (shouldNotBeVisible == null) {
+ assertThat(String.format("There is no '%s' text on the alert", expectedText),
+ getCommonPage().isAlertContainsText(expectedText));
+ } else {
+ assertThat(String.format("There is '%s' text on the alert", expectedText),
+ getCommonPage().isAlertDoesNotContainsText(expectedText));
+ }
+ }
+
+ @Then("^I see alert title contains text \"(.*)\"$")
+ public void ISeeAlertTitleContains(String expectedText) {
+ assertThat("Wrong alert title", getCommonPage().getAlertTitle(), containsString(expectedText));
+ }
+
+ @Then("^I see alert description contains text \"(.*)\"$")
+ public void ISeeAlertDescriptionContains(String expectedText) {
+ assertThat("Wrong alert description", getCommonPage().getAlertDescription(),
+ containsString(expectedText));
+ }
+
+ @And("^I type \"(.*)\" text into the alert input field$")
+ public void iTypeInAlertInput(String text) {
+ // Wait until alert visible
+ text = context.getUsersManager()
+ .replaceAliasesOccurrences(text, ClientUsersManager.FindBy.PASSWORD_ALIAS);
+ getCommonPage().typeAlertText(text);
+ }
+
+ @And("^I tap (.*) button on the alert$")
+ public void iTapAlertButton(String caption) {
+ getCommonPage().tapAlertButton(caption);
+ }
+
+ @And("^I (do not )?see (.*) button on the alert$")
+ public void iSeeAlertButton(String shouldNotSee, String caption) {
+ if (shouldNotSee == null) {
+ assertThat(String.format("The '%s' button is not visible on the alert", caption), getCommonPage().isAlertButtonVisible(caption));
+ } else {
+ assertThat(String.format("The '%s' button is visible on the alert while it should not be", caption),
+ not(getCommonPage().isAlertButtonVisible(caption)));
+ }
+ }
+
+ /**
+ * Create random file in project.build.directory folder for further usage
+ *
+ * @param size file size. Can be float value. Example: 1MB, 2.00KB
+ * @param name file name without extension
+ * @param ext file extension
+ */
+ @Given("^I create temporary file (.*) in size with name \"(.*)\" and extension \"(.*)\"$")
+ public void ICreateTemporaryFile(String size, String name, String ext) {
+ final String tmpFilesRoot = Config.current().getBuildPath(getClass());
+ CommonUtils.createRandomAccessFile(String.format("%s%s%s.%s", tmpFilesRoot, File.separator, name, ext), size);
+ }
+
+ // Check ZIOS-6570 for more details
+ private static final String SIMULATOR_VIDEO_MESSAGE_PATH = "/var/tmp/video.mp4";
+
+ /**
+ * Prepares the existing video file to be uploaded by iOS simulator
+ *
+ * @param name the name of an existing file. The file should be located in tools/img folder
+ */
+ @Given("^I prepare (.*) to be uploaded as a video message$")
+ public void IPrepareVideoMessage(String name) {
+ final File srcVideo = new File(Config.current().getVideoPath(getClass()) + File.separator + name);
+ if (!srcVideo.exists()) {
+ throw new IllegalArgumentException(String.format("The file %s does not exist or is not accessible",
+ srcVideo.getAbsolutePath()));
+ }
+ final String path = srcVideo.toPath().toString();
+
+ getCommonPage().pushFile(name, path);
+ }
+
+ @Given("^I push image with QR code containing \"Image\" to camera roll$")
+ public void iPushImageWithQRCode() throws IOException {
+ final Path directory = Files.createTempDirectory("zautomation");
+ final String qrcode = "Image";
+ final String fileFormat = "png";
+ String fileName = String.format("%s.%s", qrcode, fileFormat);
+ final File tempFile = new File(directory.toAbsolutePath() + File.separator + fileName);
+ tempFile.deleteOnExit();
+ ImageIO.write(QRCode.generateCode(qrcode, Color.BLACK, Color.WHITE, 500, 4), fileFormat, tempFile);
+ if (!getCommonPage().doesFileExistOnDevice(fileName)) {
+ log.info(String.format(
+ "File named %s not found on device. Pushing %s...",
+ fileName,
+ tempFile.getAbsolutePath()));
+ getCommonPage().pushFile(fileName, tempFile.getAbsolutePath());
+ } else {
+ // FIXME: This does not work yet b/c appium does not find the file on the device via the name
+ log.info("File named %s already found on device. Not pushing another one.");
+ }
+ }
+
+ /**
+ * Clicks the send button on the keyboard
+ *
+ * @param canSkip equals to null if this step should throw an error if the button is not available for tapping
+ */
+ @When("^I tap (?:Commit|Return|Send|Enter) button on the keyboard( if visible)?$")
+ public void ITapCommitButtonOnKeyboard(String canSkip) {
+ try {
+ getCommonPage().tapKeyboardCommitButton();
+ } catch (IllegalStateException e) {
+ if (canSkip != null) {
+ return;
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * Minimizes/restores the App
+ *
+ * @param action either restore or minimize
+ * Restore Wire only to be used after app has been put in background
+ */
+ @Given("^I (minimize|restore) Wire$")
+ public void IMinimizeWire(String action) {
+ switch (action.toLowerCase()) {
+ case "minimize":
+ getCommonPage().pressHomeButton();
+ break;
+ case "restore":
+ getCommonPage().activateApp(Lifecycle.getBundleId());
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("Unknown action keyword: '%s'", action));
+ }
+ }
+
+ /**
+ * Verify visibility of default Map application
+ */
+ @Then("^I see map application is opened$")
+ public void VerifyMapDefaultApplicationVisibility() {
+ assertThat("The default map application is not visible",
+ getCommonPage().isDefaultMapApplicationVisible());
+ }
+
+ /**
+ * Set the content of clipboard content from an existing file or a string
+ *
+ * @param source either 'string' or 'file'
+ * @param data the name of existing text file located in tools/ios/misc/ folder
+ * The text in the file is expected to be encoded in UTF-8
+ * OR
+ * any non-empty string
+ */
+ @Given("^I load clipboard content from (file|string) \"(.*)\"$")
+ public void ISetClipboard(String source, String data) throws IOException {
+ String text = data;
+ if (source.equalsIgnoreCase("file")) {
+ final File srcPath = new File(String.format("%s/%s",
+ Config.current().getMiscResourcesPath(getClass()), data));
+ text = new String(Files.readAllBytes(srcPath.toPath()), StandardCharsets.UTF_8);
+ }
+ getCommonPage().setClipboard(text);
+ }
+
+ /**
+ * Set the content of clipboard content from an existing file or a string
+ *
+ * @param source 'okta|active directory|ldap'
+ */
+ @Given("^I load clipboard content with sso code from (okta)$")
+ public void ISetClipboardWithSSOCode(String source) {
+ String text;
+ if (source.equalsIgnoreCase("okta")) {
+ text = getCommonSteps().getSSOCode();
+ } else {
+ throw new IllegalArgumentException(String.format("Unknown source '%s'", source));
+ }
+ getCommonPage().setClipboard(text);
+ }
+
+ @When("^Group admin user (.*) deletes conversation (.*)$")
+ public void deleteConversation(String userToNameAlias, String dstConversationName) {
+ context.getCommonSteps().userXDeletesConversation(userToNameAlias, dstConversationName);
+ }
+
+ @Given("^I open Safari with url \"(.*)\"$")
+ public void IOpenSafariWithURL(String url){
+ getCommonPage().activateApp(SAFARI);
+ getCommonPage().openURL(url);
+ //getCommonPage().tapConfirmButtonIfVisible();
+ }
+
+ @When("^I wait up until (\\d+) seconds until alert is visible$")
+ public void IWaitForAlertToShow(int seconds){
+ assertThat(String.format("Alert has not showed up in '%s' seconds", seconds),
+ getCommonPage().waitUntilAlertIsVisible(seconds));
+ }
+
+ @When("^I open deep link for conversation (.*) that user (.*) has sent me in safari$")
+ public void iOpenDeepLinkForConversation(String conversationName, String nameAlias) {
+ String deeplink = getCommonSteps().getDeepLinkForConversation(conversationName, nameAlias);
+ getCommonPage().activateApp(SAFARI);
+ getCommonPage().openURL(deeplink);
+ }
+
+ @When("^I open deep link for profile of user (.*) in safari")
+ public void iOpenDeepLinkForConversation(String nameAlias) {
+ String deeplink = getCommonSteps().getDeepLinkForUserProfile(nameAlias);
+ getCommonPage().openDeepLink(deeplink, SAFARI);
+ }
+
+ // region legal hold
+
+ @When("^User (.*) (un)?registers legal hold service with team \"(.*)\"$")
+ public void legalHoldFeatureIsTurnedOnForTeam(String userAlias, String unregister, String teamName) {
+ if(unregister == null) {
+ context.getCommonSteps().registerLegalHoldService(userAlias, teamName);
+ } else {
+ context.getCommonSteps().unregisterLegalHoldService(userAlias, teamName);
+ }
+ }
+
+ @When("^Admin user (.*) sends Legal Hold request for user (.*)$")
+ public void adminUserXSendsLegalHoldRequest(String adminUserNameAlias, String userNameAlias) {
+ context.getCommonSteps().adminSendsLegalHoldRequestForUser(adminUserNameAlias, userNameAlias);
+ }
+
+ @When("^Admin user (.*) turns off Legal Hold for user (.*)$")
+ public void adminUserXTurnsOffLegalHold(String adminUserNameAlias, String userNameAlias) {
+ context.getCommonSteps().adminTurnsOffLegalHoldForUser(adminUserNameAlias, userNameAlias);
+ }
+
+ // end region legal hold
+
+ // region custom backend
+
+ //TODO: Once the app with the new protocol handler becomes old, this step can be removed
+ @When("^I open default backend via deep link with the old protocol in safari$")
+ public void iOpenDeepLinkWithOldProtocolForDefaultBackend() {
+ Backend backend = BackendConnections.getDefault();
+ String protocolHandler = "wire";
+
+ if (backend.getBackendName().contains("column-1")) {
+ protocolHandler = "wire-bk";
+ }
+
+ String deeplink = backend.getDeeplinkForiOS(protocolHandler);
+ log.fine("deeplink: " + deeplink);
+ getCommonPage().activateApp(SAFARI);
+ getCommonPage().openURL(deeplink);
+ }
+
+ @When("^I open default backend via deep link in safari$")
+ public void iOpenDeepLinkForDefaultBackend() {
+ getCommonPage().openDeepLinkForDefault();
+ }
+
+ @When("^I open a backend which has my build blacklisted via deep link in safari$")
+ public void iOpenDeepLinkForBlacklistedBackend() {
+ String deeplink = "wire://access/?config=https://wire-taco-test.s3.eu-west-1.amazonaws.com/blacklist-current.json";
+ getCommonPage().openDeepLink(deeplink, SAFARI);
+ }
+
+ @When("^I open a backend which has a higher minimum version via deep link in safari$")
+ public void iOpenDeepLinkForHigherMinimumBackend() {
+ String deeplink = "wire://access/?config=https://wire-taco-test.s3.eu-west-1.amazonaws.com/blacklist-minimum.json";
+ getCommonPage().openDeepLink(deeplink, SAFARI);
+ }
+
+ @When("^I open (.*) backend deep link in safari$")
+ public void iOpenDeepLinkForCustomBackend(String backendType) {
+ Backend backend = BackendConnections.getDefault();
+ String protocolHandler = "wire";
+
+ if (backend.getBackendName().contains("column")) {
+ protocolHandler = backend.getBackendName().contains("column-1") ? "wire-bk-test" : "wire-c3-test";
+ }
+
+ String deeplink = BackendConnections.get(backendType).getDeeplinkForiOS(protocolHandler);
+ log.info("deeplink: " + deeplink);
+ getCommonPage().activateApp(SAFARI);
+ getCommonPage().openURL(deeplink);
+ }
+
+ @When("^I open invite link url for conversation (.*) created by user (.*) in safari$")
+ public void iOpenInviteURL(String conversationName, String userName) {
+ String inviteLink = getCommonSteps().getInviteLinkOfConversation(userName, conversationName);
+ getCommonPage().activateApp(SAFARI);
+ getCommonPage().openURL(inviteLink);
+ }
+
+ // endregion custom backend
+
+ // region Folders
+
+ @When("^User (.*) adds conversation \"(.*)\" to Favorites$")
+ public void userXAddsConversationToFavorites(String userNameAlias, String conversationName) {
+ context.getCommonSteps().userXAddsConversationToFavorites(userNameAlias, conversationName);
+ }
+
+ @When("^User (.*) adds conversation \"(.*)\" to (.*) folder$")
+ public void userXAddsConversationToFolder(String userNameAlias, String conversationName, String folder) {
+ context.getCommonSteps().userXAddsConversationToFolder(userNameAlias, conversationName, folder);
+ }
+
+ // endregion Folders
+
+ // region Build feature flags
+
+ @Given("^SFT calling is enabled for backend$")
+ public void isSFTEnabled() {
+ if (!BackendConnections.getDefault().isFeatureSFTEnabled()) {
+ throw new SkipException("Skip test because backend has SFT disabled");
+ }
+ }
+
+ // endregion
+
+ @When("^User (.*) disables File Sharing for team (.*)$")
+ public void userOwnerDisablesFileSharingForTeam(String adminUserAlias, String teamName) {
+ context.getCommonSteps().disableFileSharingFeature(adminUserAlias, teamName);
+ }
+
+ @Given("^I enable Federation$")
+ public void iEnableFederation() {
+ context.enableFederation();
+ }
+
+ @Given("^I enable API versioning (\\d+)$")
+ public void iEnableApiVersion(int version) {
+ context.enableApiVersioning(version);
+ }
+
+ @Given("^I enable MLS support$")
+ public void iEnableMLSSupport() {
+ context.enableMLSSupport();
+ }
+
+ @When("I reset Wire")
+ public void iResetWire() {
+ context.getDriver().removeApp(Lifecycle.getBundleId());
+ context.getDriver().installApp(Lifecycle.getAppPath());
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ContactsUiPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ContactsUiPageSteps.java
new file mode 100644
index 00000000000..6222475399b
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ContactsUiPageSteps.java
@@ -0,0 +1,93 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+
+import com.wearezeta.auto.common.usrmgmt.ClientUsersManager;
+import com.wearezeta.auto.ios.pages.ContactsUiPage;
+
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class ContactsUiPageSteps {
+
+ IOSTestContext context;
+
+ public ContactsUiPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private ContactsUiPage getContactsUiPage() {
+ return context.getPagesCollection().getPage(ContactsUiPage.class);
+ }
+
+ /**
+ * Verify that ContactsUI page is shown (by verifying search input)
+ */
+ @When("^I see ContactsUI page$")
+ public void ISeeContactsUIPage() {
+ assertThat("Search on ContactsUI page is not shown",
+ getContactsUiPage().isSearchInputVisible());
+ assertThat("Invite on ContactsUI page is not shown",
+ getContactsUiPage().isInviteButtonVisible());
+ }
+
+ /**
+ * Input user name in search field
+ *
+ * @param contact username
+ */
+ @When("^I input user name (.*) in search on ContactsUI$")
+ public void IInputUserNameInSearchOnContactsUI(String contact) {
+ contact = context.getUsersManager()
+ .replaceAliasesOccurrences(contact, ClientUsersManager.FindBy.NAME_ALIAS);
+ getContactsUiPage().inputTextToSearch(contact);
+ }
+
+ /**
+ * Verify is user is presented in ContactsUI page
+ *
+ * @param shouldNotBeVisible equals to null if the contact should be visible
+ * @param contact user name
+ */
+ @Then("^I (do not )?see contact (.*) in ContactsUI page list$")
+ public void ISeeContactInContactsUIList(String shouldNotBeVisible, String contact) {
+ contact = context.getUsersManager()
+ .replaceAliasesOccurrences(contact, ClientUsersManager.FindBy.NAME_ALIAS);
+ if (shouldNotBeVisible == null) {
+ assertThat(
+ String.format("User '%s' should be visible in ContactsUI user list", contact),
+ getContactsUiPage().isContactVisible(contact)
+ );
+ } else {
+ assertThat(
+ String.format("User '%s' should not be visible in ContactsUI user list", contact),
+ getContactsUiPage().isContactInvisible(contact)
+ );
+ }
+ }
+
+ @When("^I tap Invite Others button on Contacts UI page$")
+ public void iTapInviteOthersButton() {
+ getContactsUiPage().tapInviteOthersButton();
+ }
+
+ @When("^I tap Back button on Contacts UI page$")
+ public void iTapBackButton() {
+ getContactsUiPage().tapBackButton();
+ }
+
+ /**
+ * Click on Open button on ContactsUI next to user name
+ *
+ * @param contact user name
+ */
+ @When("^I tap Open button next to user name (.*) on ContactsUI$")
+ public void IClickOpenButtonNextToUser(String contact) {
+ contact = context.getUsersManager()
+ .replaceAliasesOccurrences(contact, ClientUsersManager.FindBy.NAME_ALIAS);
+ getContactsUiPage().tapOpenButtonNextToUser(contact);
+ }
+
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ConversationActionsPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ConversationActionsPageSteps.java
new file mode 100644
index 00000000000..31084fce89f
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ConversationActionsPageSteps.java
@@ -0,0 +1,173 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.ActionsSheetPage;
+import io.cucumber.java.en.And;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class ConversationActionsPageSteps {
+ IOSTestContext context;
+
+ public ConversationActionsPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private ActionsSheetPage getPage() {
+ return context.getPagesCollection().getPage(ActionsSheetPage.class);
+ }
+
+ @And("^I (do not )?see (Clear Content…|Clear|Clear and leave) conversation action button$")
+ public void ISeeXButtonInActionMenuClear(String shouldNotSee, String buttonTitle) {
+ if (shouldNotSee == null) {
+ assertThat(String.format("Menu item '%s' should be visible", buttonTitle),
+ getPage().isItemVisible(buttonTitle));
+ } else {
+ assertThat(String.format("Menu item '%s' should not exist", buttonTitle),
+ getPage().isItemInvisible(buttonTitle));
+ }
+ }
+
+ @And("^I (do not )?see (Delete Group…|Delete Group) conversation action button$")
+ public void ISeeXButtonInActionMenuDelete(String shouldNotSee, String buttonTitle) {
+ if (shouldNotSee == null) {
+ assertThat(String.format("Menu item '%s' should be visible", buttonTitle),
+ getPage().isItemVisible(buttonTitle));
+ } else {
+ assertThat(String.format("Menu item '%s' should not exist", buttonTitle),
+ getPage().isItemInvisible(buttonTitle));
+ }
+ }
+
+ @And("^I (do not )?see (|Block…|Unblock…|Block|Unblock) conversation action button$")
+ public void ISeeXButtonInActionMenuBlock(String shouldNotSee, String buttonTitle) {
+ if (shouldNotSee == null) {
+ assertThat(String.format("Menu item '%s' should be visible", buttonTitle),
+ getPage().isItemVisible(buttonTitle));
+ } else {
+ assertThat(String.format("Menu item '%s' should not exist", buttonTitle),
+ getPage().isItemInvisible(buttonTitle));
+ }
+ }
+
+ @And("^I (do not )?see (Remove From Group…|Remove From Group) conversation action button$")
+ public void ISeeXButtonInActionMenuRemove(String shouldNotSee, String buttonTitle) {
+ if (shouldNotSee == null) {
+ assertThat(String.format("Menu item '%s' should be visible", buttonTitle),
+ getPage().isItemVisible(buttonTitle));
+ } else {
+ assertThat(String.format("Menu item '%s' should not exist", buttonTitle),
+ getPage().isItemInvisible(buttonTitle));
+ }
+ }
+
+ @And("^I (do not )?see (Leave Group…|Leave) conversation action button$")
+ public void ISeeXButtonInActionMenuLeave(String shouldNotSee, String buttonTitle) {
+ if (shouldNotSee == null) {
+ assertThat(String.format("Menu item '%s' should be visible", buttonTitle),
+ getPage().isItemVisible(buttonTitle));
+ } else {
+ assertThat(String.format("Menu item '%s' should not exist", buttonTitle),
+ getPage().isItemInvisible(buttonTitle));
+ }
+ }
+
+ @And("^I (do not )?see (Mute|Unmute) conversation action button$")
+ public void ISeeXButtonInActionMenuMute(String shouldNotSee, String buttonTitle) {
+ if (shouldNotSee == null) {
+ assertThat(String.format("Menu item '%s' should be visible", buttonTitle),
+ getPage().isItemVisible(buttonTitle));
+ } else {
+ assertThat(String.format("Menu item '%s' should not exist", buttonTitle),
+ getPage().isItemInvisible(buttonTitle));
+ }
+ }
+
+ @And("^I (do not )?see (Cancel|Connect|Archive|Unarchive|Cancel Request) conversation action button$")
+ public void ISeeXButtonInActionMenu(String shouldNotSee, String buttonTitle) {
+ if (shouldNotSee == null) {
+ assertThat(String.format("Menu item '%s' should be visible", buttonTitle),
+ getPage().isItemVisible(buttonTitle));
+ } else {
+ assertThat(String.format("Menu item '%s' should not exist", buttonTitle),
+ getPage().isItemInvisible(buttonTitle));
+ }
+ }
+
+ /**
+ * Tap the corresponding button to confirm/decline conversation action
+ *
+ * @param actionType either `confirm` or `decline`
+ */
+ @Then("^I (confirm|decline) conversation action$")
+ public void doAction(String actionType) {
+ if (actionType.equalsIgnoreCase("confirm")) {
+ getPage().confirm();
+ } else {
+ getPage().decline();
+ }
+ }
+
+ @When("^I tap (Clear Content…|Clear|Clear and leave) conversation action button$")
+ public void tapClearContentActionButton(String buttonTitle) { getPage().tapMenuItem(buttonTitle); }
+
+ @When("^I tap (Delete Group…|Delete Group) conversation action button$")
+ public void tapDeletegroupActionButton(String buttonTitle) { getPage().tapMenuItem(buttonTitle); }
+
+ @When("^I tap (Notifications…|Everything|Mentions and Replies|Nothing) conversation action button$")
+ public void tapNotificationsActionButton(String buttonTitle) {
+ getPage().tapMenuItem(buttonTitle);
+ }
+
+ @When("^I tap (Block…|Unblock…|Block|Unblock) conversation action button$")
+ public void tapBlockActionButton(String buttonTitle) {
+ getPage().tapMenuItem(buttonTitle);
+ }
+
+ @When("^I tap Move to… conversation action button$")
+ public void tapMoveToActionButton() {
+ getPage().tapMenuItem("Move to…");
+ }
+
+ @When("^I tap Remove from \"(.*)\" conversation action button$")
+ public void tapRemoveFromFolderActionButton(String buttonTitle) {
+ getPage().tapMenuItem(String.format("Remove from \"%s\"", buttonTitle));
+ }
+
+ @When("^I tap (Remove From Group…|Remove From Group|Remove) conversation action button$")
+ public void tapRemoveFromGroupActionButton(String buttonTitle) {
+ getPage().tapMenuItem(buttonTitle);
+ }
+
+ @When("^I tap (Leave Group…|Leave) conversation action button$")
+ public void tapLeaveActionButton(String buttonTitle) {
+ getPage().tapMenuItem(buttonTitle);
+ }
+
+ @When("^I tap (Mute|Unmute) conversation action button$")
+ public void tapMuteActionButton(String buttonTitle) {
+ getPage().tapMenuItem(buttonTitle);
+ }
+
+ @When("^I tap (Add to Favorites|Remove from Favorites) conversation action button$")
+ public void tapFavoritesActionButton(String buttonTitle) {
+ getPage().tapMenuItem(buttonTitle);
+ }
+
+ @When("^I tap (Cancel|Archive|Unarchive|Cancel Request|Revoke Link) conversation action button$")
+ public void tapCommonActionButton(String buttonTitle) {
+ getPage().tapMenuItem(buttonTitle);
+ }
+
+ @Then("^I (do not )?see action sheet contains text \"(.*)\"$")
+ public void ISeeAlertContains(String shouldNotBeVisible, String expectedText) {
+ if (shouldNotBeVisible == null) {
+ assertThat(String.format("There is no '%s' text on the alert", expectedText),
+ getPage().isActionSheetContainsText(expectedText));
+ } else {
+ assertThat(String.format("There is '%s' text on the alert", expectedText),
+ getPage().isActionSheetDoesNotContainsText(expectedText));
+ }
+ }
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ConversationViewPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ConversationViewPageSteps.java
new file mode 100644
index 00000000000..19e5ea06901
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ConversationViewPageSteps.java
@@ -0,0 +1,1147 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.common.ImageUtil;
+import com.wearezeta.auto.common.misc.Timedelta;
+import com.wearezeta.auto.common.usrmgmt.ClientUsersManager;
+import com.wearezeta.auto.common.usrmgmt.ClientUsersManager.FindBy;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.ConversationViewPage;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+
+import static com.wearezeta.auto.ios.common.Pinger.log;
+import static org.hamcrest.MatcherAssert.*;
+import static org.hamcrest.Matchers.*;
+
+import java.util.Optional;
+import java.awt.image.BufferedImage;
+
+import static com.wearezeta.auto.common.CommonSteps.DEFAULT_AUTOMATION_MESSAGE;
+
+public class ConversationViewPageSteps {
+
+ IOSTestContext context;
+
+ public ConversationViewPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private ConversationViewPage getConversationViewPage() {
+ return context.getPagesCollection().getPage(ConversationViewPage.class);
+ }
+
+ @When("^I see conversation view page$")
+ public void WhenISeePage() {
+ ISeeTextInput(null);
+ }
+
+ @When("^I tap on text input$")
+ public void iTapOnTextInput() {
+ getConversationViewPage().tapTextInput();
+ }
+
+ @When("^I long tap on text input$")
+ public void iLongTapOnTextInput() {
+ try {
+ getConversationViewPage().longTapTextInput();
+ } catch (InterruptedException e) {
+ log.severe(String.format("Caught Interrupted exception: %s", e.getMessage()));
+ }
+ }
+
+ @When("^I scroll to the (top|bottom) of the conversation$")
+ public void ScrollToThe(String where) {
+ if (where.equals("top")) {
+ getConversationViewPage().scrollToTheTop();
+ } else {
+ getConversationViewPage().scrollToTheBottom();
+ }
+ }
+
+ @When("^I (do not )?see text input in conversation view$")
+ public void ISeeTextInput(String shouldNotSee) {
+ if (shouldNotSee == null) {
+ assertThat("Cursor input is not visible", getConversationViewPage().waitForCursorInputVisible());
+ } else {
+ assertThat("Cursor input is visible, but should be hidden",
+ getConversationViewPage().waitForCursorInputInvisible());
+ }
+ }
+
+ @When("^I type the (default|.*) message$")
+ public void WhenITypeTheMessage(String msg) {
+ if (msg.equals("default")) {
+ getConversationViewPage().typeMessage(DEFAULT_AUTOMATION_MESSAGE);
+ } else {
+ msg = context.getUsersManager()
+ .replaceAliasesOccurrences(msg, FindBy.NAME_ALIAS);
+ msg = context.getUsersManager()
+ .replaceAliasesOccurrences(msg, FindBy.UNIQUE_USERNAME_ALIAS);
+ getConversationViewPage().typeMessage(msg.replaceAll("^\"|\"$", ""));
+ }
+ }
+
+ @When("^I type first (\\d+) letters? of name \"(.*)\" in conversation input$")
+ public void ITypeXLettersIntoSearchInput(int count, String name) {
+ name = context.getUsersManager()
+ .replaceAliasesOccurrences(name, FindBy.NAME_ALIAS);
+ name = context.getUsersManager()
+ .replaceAliasesOccurrences(name, FindBy.UNIQUE_USERNAME_ALIAS);
+
+ if (name.length() > count) {
+ getConversationViewPage().typeMessage(name.substring(0, count));
+ } else {
+ throw new IllegalArgumentException(String.format("Name is only %s chars length. Put in step a less value",
+ name.length()));
+ }
+ }
+
+ /**
+ * Verify whether the particular system message is visible in the conversation view
+ *
+ * @param expectedMsg the expected system message. may contyain user name aliases
+ * @param shouldNotSee equals to null if the message should be visible
+ */
+ @Then("^I (do not )?see \"(.*)\" system message in the conversation view$")
+ public void ISeeSystemMessage(String shouldNotSee, String expectedMsg) {
+ expectedMsg = context.getUsersManager()
+ .replaceAliasesOccurrences(expectedMsg, FindBy.NAME_ALIAS);
+ if (shouldNotSee == null) {
+ assertThat(String.format("The expected system message '%s' is not visible in the conversation",
+ expectedMsg), getConversationViewPage().isSystemMessageVisible(expectedMsg));
+ } else {
+ assertThat(String.format(
+ "The expected system message '%s' should not be visible in the conversation",
+ expectedMsg), getConversationViewPage().isSystemMessageInvisible(expectedMsg));
+ }
+ }
+
+ /**
+ * Verify whether the ping message is visible in the conversation view
+ *
+ * @param expectedMsg ping message containing user name or you
+ * @param shouldNotSee equals to null if the message should be visible
+ */
+ @Then("^I (do not )?see \"(.*)\" ping message in the conversation view$")
+ public void ISeePingMessage(String shouldNotSee, String expectedMsg) {
+ expectedMsg = context.getUsersManager()
+ .replaceAliasesOccurrences(expectedMsg, FindBy.NAME_ALIAS);
+ if (shouldNotSee == null) {
+ assertThat(String.format("The expected ping message '%s' is not visible in the conversation",
+ expectedMsg), getConversationViewPage().isPingMessageVisible(expectedMsg));
+ } else {
+ assertThat(String.format(
+ "The expected ping message '%s' should not be visible in the conversation",
+ expectedMsg), getConversationViewPage().isPingMessageInvisible(expectedMsg));
+ }
+ }
+
+ /**
+ * Type a default message or the given message in conversation view and send it
+ *
+ * @param msg the message that is going to be typed
+ */
+ @When("^I type the (default|\".*\") message and send it$")
+ public void ITypeTheMessageAndSendIt(String msg) {
+ if (msg.equals("default")) {
+ getConversationViewPage().typeMessage(DEFAULT_AUTOMATION_MESSAGE, true);
+ } else {
+ msg = context.getUsersManager()
+ .replaceAliasesOccurrences(msg, FindBy.NAME_ALIAS);
+ getConversationViewPage().typeMessage(msg.replaceAll("^\"|\"$", ""), true);
+ }
+ }
+
+ @When("^I tap Send Message button in conversation view$")
+ public void ITapSendMessageConvoButton() {
+ getConversationViewPage().tapSendMessageButton();
+ }
+
+ @When("^I tap Hourglass button in conversation view$")
+ public void ITapHourglassConvoButton() {
+ getConversationViewPage().tapHourglassButton();
+ }
+
+ @When("^I tap Collection button in conversation view$")
+ public void ITapCollectionConvoButton() {
+ getConversationViewPage().tapCollectionButton();
+ }
+
+ /**
+ * Click open conversation details button in 1:1 dialog
+ */
+ @When("^I open (?:group |\\s?)conversation details$")
+ public void IOpenConversationDetails() {
+ getConversationViewPage().openConversationDetails();
+ }
+
+ /**
+ * Wait until text messages are visible in the conversation
+ *
+ * @param expectedCount the expected count of messages. Should be equal or greater than zero
+ * @param isDefault equals to null if presence of any messages are supposed to be verified
+ */
+ @Then("^I see (\\d+) (default )?messages? in the conversation view$")
+ public void ThenISeeMessageInTheDialog(int expectedCount, String isDefault) {
+ final Optional expectedMsg = (isDefault == null) ?
+ Optional.empty() : Optional.of(DEFAULT_AUTOMATION_MESSAGE);
+ if (expectedCount == 0) {
+ if (expectedMsg.isPresent()) {
+ assertThat(
+ String.format("There are some '%s' messages in the conversation, but zero is expected",
+ expectedMsg.get()),
+ getConversationViewPage().waitUntilTextMessageIsNotVisible(expectedMsg.get()));
+ } else {
+ assertThat("There are some messages in the conversation, but zero is expected",
+ getConversationViewPage().waitUntilAllTextMessageAreNotVisible());
+ }
+ } else if (expectedCount >= 1) {
+ if (expectedMsg.isPresent()) {
+ assertThat("Unexpected number of specific messages",
+ getConversationViewPage().numberOfSpecificTextMessagesVisible(
+ expectedMsg.get(),
+ expectedCount),
+ equalTo(expectedCount));
+ } else {
+ assertThat("Unexpected number of messages",
+ getConversationViewPage().numberOfTextMessagesVisible(expectedCount),
+ equalTo(expectedCount));
+ }
+ }
+ }
+
+ @Then("^I see at least one message in the conversation view")
+ public void iSeeAtLeastMessage() {
+ assertThat("There seems to be no message in the current view",
+ getConversationViewPage().waitUntilMessageInConversation());
+ }
+
+ @Then("^I see last message in the conversation view is expected message (.*)")
+ public void iSeeLastMessageIs(String msg) {
+ msg = context.getUsersManager().replaceAliasesOccurrences(msg, FindBy.EMAIL_ALIAS);
+ msg = context.getUsersManager().replaceAliasesOccurrences(msg, FindBy.NAME_ALIAS);
+ assertThat("The last message in the conversation is different from the expected",
+ getConversationViewPage().getLastTextMessage(), equalTo(msg));
+ }
+
+ @Then("^I see last message in the conversation view contains expected message (.*)")
+ public void iSeeLastMessageContains(String msg) {
+ msg = context.getUsersManager().replaceAliasesOccurrences(msg, FindBy.EMAIL_ALIAS);
+ msg = context.getUsersManager().replaceAliasesOccurrences(msg, FindBy.NAME_ALIAS);
+ assertThat("The last message in the conversation does not contain the expected one",
+ getConversationViewPage().getLastTextMessage(), containsString(msg));
+ }
+
+ @When("^I tap Add Picture button from input tools$")
+ public void ITapAddPictureButtonByNameFromInputTools() {
+ getConversationViewPage().tapAddPictureButton();
+ }
+
+ @When("^I tap Mention button from input tools$")
+ public void ITapMentionButtonByNameFromInputTools() {
+ getConversationViewPage().tapMentionButton();
+ }
+
+ @When("^I tap Sketch button from input tools$")
+ public void ITapSketchButtonByNameFromInputTools() {
+ getConversationViewPage().tapSketchButton();
+ }
+
+ @When("^I tap ellipsis button from input tools$")
+ public void iTapEllipsisButtonByNameFromInputTools() {
+ getConversationViewPage().tapEllipsisButton();
+ }
+
+ @When("^I tap Ping button from input tools$")
+ public void ITapPingButtonByNameFromInputTools() {
+ getConversationViewPage().tapPingButton();
+ }
+
+ @When("^I tap File Transfer button from input tools$")
+ public void ITapFileTransferButtonByNameFromInputTools() {
+ getConversationViewPage().tapFileTransferButton();
+ }
+
+ @When("^I tap Share Location button from input tools$")
+ public void ITapShareLocationButtonByNameFromInputTools() {
+ getConversationViewPage().tapShareLocationButton();
+ }
+
+ @When("^I tap Video Message button from input tools$")
+ public void ITapVideoMessageButtonByNameFromInputTools() {
+ getConversationViewPage().tapVideoMessageButton();
+ }
+
+ @When("^I tap Audio Message button from input tools$")
+ public void ITapAudioMessageButtonByNameFromInputTools() {
+ getConversationViewPage().tapAudioMessageButton();
+ }
+
+ @When("^I long tap Audio Message button from input tools$")
+ public void ILongTapAudioMessageButtonByNameFromInputTools() {
+ getConversationViewPage().longTapAudioMessageButton();
+ }
+
+ @When("^I long tap Audio Message button for (\\d+) seconds from input tools$")
+ public void ILongTapForSecondsAudioMessageButtonByNameFromInputTools(int durationSeconds) {
+ getConversationViewPage().longTapAudioMessageButtonWithDuration(durationSeconds);
+ }
+
+ @When("^I tap GIF button from input tools$")
+ public void ITapGIFButtonByNameFromInputTools() {
+ getConversationViewPage().tapGIFButton();
+ }
+
+ @When("^I (do not )?see Audio Message button in input tools palette$")
+ public void iSeeAudioMessageButton(String shouldNot) {
+ if (shouldNot == null) {
+ assertThat("Audio Message button in input tools palette is not visible",
+ getConversationViewPage().isAudioMessageButtonVisible());
+ } else {
+ assertThat("Audio Message button in input tools palette is visible",
+ getConversationViewPage().isAudioMessageButtonInvisible());
+ }
+ }
+
+ @When("^I (do not )?see File Transfer button in input tools palette$")
+ public void iSeeFileTransferButton(String shouldNot) {
+ if (shouldNot == null) {
+ assertThat("File Transfer button in input tools palette is not visible",
+ getConversationViewPage().isFileTransferButtonVisible());
+ } else {
+ assertThat("File Transfer button in input tools palette is visible",
+ getConversationViewPage().isFileTransferButtonInvisible());
+ }
+ }
+
+ @When("^I (do not )?see Video Message button in input tools palette$")
+ public void iSeeVideoMessageButton(String shouldNot) {
+ if (shouldNot == null) {
+ assertThat("Video Message button in input tools palette is not visible",
+ getConversationViewPage().isVideoMessageButtonVisible());
+ } else {
+ assertThat("Video Message button in input tools palette is visible",
+ getConversationViewPage().isVideoMessageButtonInvisible());
+ }
+ }
+
+ @When("^I (do not )?see phone gallery button in a draw sketch view$")
+ public void iSeeGalleryon(String shouldNot) {
+ if (shouldNot == null) {
+ assertThat("Photo gallery button in a draw sketch view is not visible",
+ getConversationViewPage().isPhotoGalleryButtonVisible());
+ } else {
+ assertThat("Photo gallery button in a draw sketch view is visible",
+ getConversationViewPage().isPhotoGalleryButtonInvisible());
+ }
+ }
+
+ @When("^I (do not )?see Sketch button in input tools palette$")
+ public void iSeeSketchButton(String shouldNot) {
+ if (shouldNot == null) {
+ assertThat("Sketch button in input tools palette is not visible",
+ getConversationViewPage().isSketchButtonVisible());
+ } else {
+ assertThat("Sketch button in input tools palette is visible",
+ getConversationViewPage().isSketchButtonInvisible());
+ }
+ }
+
+ @When("^I (do not )?see Giphy button in input tools palette$")
+ public void iSeeGiphyButton(String shouldNot) {
+ if (shouldNot == null) {
+ assertThat("Giphy button in input tools palette is not visible",
+ getConversationViewPage().isGiphyButtonVisible());
+ } else {
+ assertThat("Giphy button in input tools palette is visible",
+ getConversationViewPage().isGiphyButtonInvisible());
+ }
+ }
+
+ @When("^I (do not )?see degradation alert contains text New Device$")
+ public void iSeeDegradationAlert(String shouldNot) {
+ if (shouldNot == null) {
+ assertThat("Degradation alert in a call is not visible",
+ getConversationViewPage().isDegradationAlertVisible());
+ } else {
+ assertThat("Degradation alert in a call is visible",
+ getConversationViewPage().isDegradationAlertInvisible());
+ }
+ }
+
+ @When("^I (do not )?see Add Picture button in input tools palette$")
+ public void iSeeAddPictureButton(String shouldNot) {
+ if (shouldNot == null) {
+ assertThat("Add Picture button in input tools palette is not visible",
+ getConversationViewPage().isAddPictureButtonVisible());
+ } else {
+ assertThat("Add Picture button in input tools palette is visible",
+ getConversationViewPage().isAddPictureButtonInvisible());
+ }
+ }
+
+ @When("^I (do not )?see Ping button in input tools palette$")
+ public void iSeePingButton(String shouldNot) {
+ if (shouldNot == null) {
+ assertThat("Ping button in input tools palette is not visible",
+ getConversationViewPage().isPingButtonVisible());
+ } else {
+ assertThat("Ping button in input tools palette is visible",
+ getConversationViewPage().isPingButtonInvisible());
+ }
+ }
+
+ @When("^I (do not )?see Mention button in input tools palette$")
+ public void iSeeMentionButton(String shouldNot) {
+ if (shouldNot == null) {
+ assertThat("Mention button in input tools palette is not visible",
+ getConversationViewPage().isMentionButtonVisible());
+ } else {
+ assertThat("Mention button in input tools palette is visible",
+ getConversationViewPage().isMentionButtonInvisible());
+ }
+ }
+
+ @When("^I (do not )?see Share Location button in input tools palette$")
+ public void iSeeShareLocationButton(String shouldNot) {
+ if (shouldNot == null) {
+ assertThat("Share Location button in input tools palette is not visible",
+ getConversationViewPage().isShareLocationButtonVisible());
+ } else {
+ assertThat("Share Location button in input tools palette is visible",
+ getConversationViewPage().isShareLocationButtonInvisible());
+ }
+ }
+
+ @When("^I tap Audio Call button$")
+ public void ITapAudioCallButton() {
+ getConversationViewPage().tapAudioButton();
+ }
+
+ @When(("I tap call button on start call alert"))
+ public void ITapCallButton() {
+ getConversationViewPage().tapStartCallButton();
+ }
+
+ @When("^I tap Video Call button$")
+ public void ITapVideoCallButton() {
+ getConversationViewPage().tapVideoCallButton();
+ }
+
+ @When("^I tap call anyway on degradation alert$")
+ public void ITapCallAnywayButton() {
+ getConversationViewPage().tapCallAnywayButton();
+ }
+
+ @When("^I tap cancel button on degradation alert$")
+ public void ITapCancelButton() {
+ getConversationViewPage().tapCancelButton();
+ }
+
+ @Then("^I see (\\d+) photos? in the conversation view$")
+ public void ISeeNewPhotoInTheDialog(int expectedCount) {
+ if (expectedCount == 0) {
+ assertThat("No images are expected to be visible in the conversations",
+ getConversationViewPage().areNoImagesVisible());
+ } else {
+ assertThat("Unexpected number of images",
+ getConversationViewPage().numberOfImagesVisible(expectedCount),
+ equalTo(expectedCount));
+ }
+ }
+
+ @Then("^I see (\\d+) video? files in the conversation view$")
+ public void ISeeNewVideoInTheDialog(int expectedCount) {
+ if (expectedCount == 0) {
+ assertThat("No Video files are expected to be visible in the conversations",
+ getConversationViewPage().areNoVideoFilesVisible());
+ } else {
+ assertThat("Unexpected number of video files",
+ getConversationViewPage().numberOfVideoFiles(expectedCount),
+ equalTo(expectedCount));
+ }
+ }
+
+ @Then("^I see (\\d+) file? transfer placeholder in the conversation view$")
+ public void ISeeNewFileInTheDialog(int expectedCount) {
+ if (expectedCount == 0) {
+ assertThat("File transfer placeholder is visible in the conversations",
+ getConversationViewPage().isFileTransferTopLabelInvisible());
+ } else {
+ assertThat("Unexpected number of file placeholders",
+ getConversationViewPage().areXFilesVisible(expectedCount),
+ equalTo(expectedCount));
+ }
+ }
+
+ @Then("^I see (\\d+) placeholder photos? in the conversation view$")
+ public void iSeePlaceholderPhoto(int expectedCount) {
+ if (expectedCount == 0) {
+ assertThat("No placeholder images are expected to be visible in the conversations",
+ getConversationViewPage().areNoPlaceholderImagesVisible());
+ } else {
+ assertThat("Unexpected number of placeholder images",
+ getConversationViewPage().numberOfPlaceholderImages(expectedCount),
+ equalTo(expectedCount));
+ }
+ }
+
+ @Then("^I (do not )?see a placeholder file in the conversation view$")
+ public void iSeePlaceholderFile(String doNot) {
+ if (doNot == null) {
+ assertThat(
+ "Placeholder File is expected to be visible in the conversations",
+ getConversationViewPage().isPlaceholderFileVisible());
+ } else {
+ assertThat(
+ "Placeholder File is expected to be invisible in the conversations",
+ getConversationViewPage().isPlaceholderFileInvisible());
+ }
+ }
+
+ @When("^I long tap on restricted file transfer placeholder in conversation view$")
+ public void iLongTapPlaceholderFile() {
+ getConversationViewPage().longTapPlaceholderFile();
+ }
+
+ @Then("^I see text (.*) displayed on placeholder photo$")
+ public void iSeeTextOnPlaceholderPhoto(String expectedText) {
+ assertThat(
+ String.format("Text %s is not displayed on the placeholder image", expectedText),
+ getConversationViewPage().getPlaceholderImageText(), equalTo(expectedText));
+ }
+
+ @When("^I navigate back to conversations list")
+ public void INavigateToConversationsList() {
+ getConversationViewPage().returnToConversationsList();
+ }
+
+ @Then("^I (do not )?see TYPE A MESSAGE input placeholder text$")
+ public void ISeeStandardInputPlaceholderText(String shouldNotBeVisible) {
+ boolean result;
+ if (shouldNotBeVisible == null) {
+ result = getConversationViewPage().isPlaceholderStandardTextVisible();
+ } else {
+ result = getConversationViewPage().isPlaceholderStandardTextInvisible();
+ }
+ assertThat(
+ String.format("TYPE A MESSAGE placeholder text should be %s", (shouldNotBeVisible == null) ? "visible" : "not visible"),
+ result
+ );
+ }
+
+ @When("^I see the conversation with (.*) is opened$")
+ public void ISeeConversationWith(String participantNameAlias) {
+ participantNameAlias = context.getUsersManager()
+ .replaceAliasesOccurrences(participantNameAlias, ClientUsersManager.FindBy.NAME_ALIAS);
+
+ assertThat(
+ String.format("User '%s' are not displayed on Upper Toolbar", participantNameAlias),
+ getConversationViewPage().isUpperToolbarContainNames(participantNameAlias)
+ );
+ }
+
+ /**
+ * Verify that all buttons in toolbar are visible or not
+ *
+ * @param shouldNotBeVisible equals to null if the toolbar should be visible
+ */
+ @When("^I (do not )?see conversation tools buttons$")
+ public void ISeeOnlyPeopleButtonRestNotShown(String shouldNotBeVisible) {
+ if (shouldNotBeVisible == null) {
+ assertThat("Some of input tools buttons are not visible",
+ getConversationViewPage().areInputToolsVisible());
+ } else {
+ assertThat("Some of input tools buttons are still visible",
+ getConversationViewPage().areInputToolsInvisible());
+ }
+ }
+
+ /**
+ * Verifies amount of messages in conversation
+ *
+ * @param expectedCount expected number of messages
+ */
+ @When("^I see (\\d+) conversation (?:entries|entry)$")
+ public void ISeeXConvoEntries(int expectedCount) {
+ assertThat("The expected count of conversation entries is not equal to the actual count",
+ getConversationViewPage().getNumberOfMessageEntries(), equalTo(expectedCount));
+ }
+
+ /**
+ * Select the corresponding item from the modal menu, which appears after Delete badge is tapped
+ *
+ * @param name one of possible item names
+ */
+ @When("^I select (Delete for Me|Delete for Everyone|Cancel) item from Delete menu$")
+ public void ISelectDeleteMenuItem(String name) {
+ getConversationViewPage().selectDeleteMenuItem(name);
+ }
+
+ /**
+ * Clear conversation text input
+ */
+ @When("^I clear conversation text input$")
+ public void IClearConversationTextInput() {
+ getConversationViewPage().clearTextInput();
+ }
+
+ /**
+ * Verify that conversation is scrolled to the end by verifying that plus
+ * button and text input is visible
+ */
+ @When("^I see conversation is scrolled to the end$")
+ public void ISeeConversationIsScrolledToEnd() {
+ assertThat("The input field state looks incorrect",
+ getConversationViewPage().waitForCursorInputVisible());
+ }
+
+ /**
+ * Verify whether shield icon is visible next to convo input field
+ *
+ * @param shouldNotSee equals to null if the shield should be visible
+ */
+ @Then("^I (do not )?see shield icon in the conversation view$")
+ public void ISeeShieldIcon(String shouldNotSee) {
+ if (shouldNotSee == null) {
+ assertThat("The shield icon is not visible",
+ getConversationViewPage().isShieldIconVisible());
+ } else {
+ assertThat("The shield icon is visible, but should be hidden",
+ getConversationViewPage().isShieldIconInvisible());
+ }
+ }
+
+ /**
+ * Verify that the "You called" notification is shown
+ */
+ @Then("^I see \"You called\" notification in conversation view$")
+ public void ISeeYouCalledNotification() {
+ assertThat("\"You called\" message is not shown", getConversationViewPage().
+ isYouCalledMessageVisible());
+ }
+
+ private static final double MAX_SIMILARITY_THRESHOLD = 0.985;
+
+ /**
+ * Verify whether the particular picture is animated
+ */
+ @Then("^I see the picture in the conversation view is animated$")
+ public void ISeePictureIsAnimated() {
+ final int maxFrames = 4;
+ final double avgThreshold = ImageUtil.getAnimationThreshold(
+ getConversationViewPage()::getRecentPictureScreenshot, maxFrames, Timedelta.ofMillis(0));
+ assertThat(String.format("The picture in the conversation view seems to be static (%.2f >= %.2f)",
+ avgThreshold, MAX_SIMILARITY_THRESHOLD), avgThreshold < MAX_SIMILARITY_THRESHOLD);
+ }
+
+ @When("^I tap file transfer option for 80 MB file")
+ public void iTapFileTransferOptionFor80MBFile() {
+ getConversationViewPage().tapFileTransferOptionFor80MBFile();
+ }
+
+ @When("^I tap file transfer option to send CountryCodes.plist file$")
+ public void iTapFileTransferOptionForCountryCodes() {
+ getConversationViewPage().tapFileTransferOptionForCountryCodesFile();
+ }
+
+ /**
+ * Verify file transfer placeholder visibility
+ *
+ * @param shouldNotBeVisible equals to null if the placeholder should be visible
+ */
+ @When("^I (do not )?see file transfer placeholder$")
+ public void ISeeFileTransferPlaceHolder(String shouldNotBeVisible) {
+ if (shouldNotBeVisible == null) {
+ assertThat("File transfer placeholder is not visible",
+ getConversationViewPage().isFileTransferTopLabelVisible() &&
+ getConversationViewPage().isFileTransferBottomLabelVisible());
+ } else {
+ assertThat("File transfer placeholder is visible, but should be hidden",
+ getConversationViewPage().isFileTransferTopLabelInvisible());
+ }
+ }
+
+ /**
+ * Tap on file transfer action button to download/preview file
+ */
+ @When("^I tap file transfer action button until File Actions menu is visible$")
+ public void ITapFileTransferActionButton() {
+ getConversationViewPage().tapFileTransferActionButton();
+ }
+
+ /**
+ * Verify whether File Transfer placeholder is visible in the conversation view
+ */
+ @Then("^I wait up to (\\d+) seconds? until the file (.*) with size (.*) is ready for download from conversation view$")
+ public void IWaitUntilDownloadFinished(int timeoutSeconds, String expectedFilename, String expectedSize) {
+ assertThat(String.format(
+ "Cannot detect the Download Finished placeholder for a file '%s' in the conversation view after %s seconds",
+ expectedFilename, timeoutSeconds),
+ getConversationViewPage().waitUntilDownloadReadyPlaceholderVisible(expectedFilename, expectedSize,
+ Timedelta.ofSeconds(timeoutSeconds)));
+ }
+
+ @When("^I tap default message in conversation view$")
+ public void iTapOnDefaultTextMessage() {
+ getConversationViewPage().tapMessageByText(DEFAULT_AUTOMATION_MESSAGE);
+ }
+
+ @When("^I long tap default message in conversation view$")
+ public void iLongTapOnDefaultTextMessage() {
+ getConversationViewPage().longTapMessageByText(DEFAULT_AUTOMATION_MESSAGE);
+ }
+
+ @When("^I long tap \"(.*)\" message in conversation view$")
+ public void iLongTapTextMessage(String msg) {
+ msg = context.getUsersManager().replaceAliasesOccurrences(msg, ClientUsersManager.FindBy.NAME_ALIAS);
+ getConversationViewPage().longTapMessageByText(msg);
+ }
+
+ /**
+ * Tap pointed control button
+ */
+ @When("^I tap Send record control button$")
+ public void ITapSendRecordControlButton() {
+ getConversationViewPage().tapSendRecordControlButton();
+ }
+
+ @When("^I tap on image in conversation view$")
+ public void iTapImageInConversationView() {
+ getConversationViewPage().tapImageInConversation();
+ }
+
+ @When("^I long tap on image in conversation view$")
+ public void iLongTapImageInConversationView() {
+ getConversationViewPage().longTapImageInConversation();
+ }
+
+ @When("^I long tap on file transfer placeholder in conversation view$")
+ public void ILongTapFileTransferPlaceholder() {
+ getConversationViewPage().longTapFileTransferPlaceholder();
+ }
+
+ @When("^I tap on file transfer placeholder in conversation view$")
+ public void ITapFileTransferPlaceholder() {
+ getConversationViewPage().tapFileTransferPlaceholder();
+ }
+
+ @When("^I tap Play audio message button$")
+ public void iTapAudioMessagePlayButton() {
+ getConversationViewPage().tapAudioMessagePlayButton();
+ }
+
+ @When("^I long tap on audio message placeholder in conversation view$")
+ public void ILongTapAudioMessagePlaceholder() {
+ getConversationViewPage().longTapPlayAudioMessageButton();
+ }
+
+ @When("^I tap on video message in conversation view$")
+ public void ITapVideoMessage() {
+ getConversationViewPage().tapVideoMessage();
+ }
+
+ @When("^I long tap on video message in conversation view$")
+ public void ILongTapVideoMessage() {
+ getConversationViewPage().longTapVideoMessage();
+ }
+
+ @When("^I tap on location map in conversation view$")
+ public void ITapLocationMessage() {
+ getConversationViewPage().tapLocationMap();
+ }
+
+ @When("^I long tap on link preview in conversation view$")
+ public void ILongTapLinkPreview() {
+ getConversationViewPage().longTapLinkPreview();
+ }
+
+ @When("^I tap on Youtube preview in conversation view$")
+ public void ITapYoutubePreviewLink() {
+ getConversationViewPage().singleTapYoutubePreview();
+ }
+
+ /**
+ * Verify play/pause button state in audio message placeholder
+ *
+ * @param buttonState play or pause
+ */
+ @Then("^I see state of button on audio message placeholder is (play|Play)$")
+ public void ISeeAudioMessageControlButtonStateIs(String buttonState) {
+ boolean isStateCorrect = getConversationViewPage().isPlaceholderAudioMessageButtonState(buttonState);
+ assertThat(String.format("Wrong button state. The expected state is '%s'", buttonState),
+ isStateCorrect);
+ }
+
+ @Then("^I see state of button on audio message placeholder is Pause$")
+ public void iSeeAudioMessageControlButtonStatePause() {
+ assertThat("Wrong button state",
+ getConversationViewPage().isAudioMessagePauseButtonVisible());
+ }
+
+ @Then("^I (do not )?see video message container in the conversation view$")
+ public void ISeeVideoMessageContainer(String shouldNotSee) {
+ if (shouldNotSee == null) {
+ assertThat("video message container is not visible",
+ getConversationViewPage().isVideoMessageVisible());
+ } else {
+ assertThat("video message container is visible, but should be hidden",
+ getConversationViewPage().isVideoMessageInvisible());
+ }
+ }
+
+ @Then("^I (do not )?see audio message container in the conversation view$")
+ public void ISeeAudioMessageContainer(String shouldNotSee) {
+ if (shouldNotSee == null) {
+ assertThat("audio message container is not visible",
+ getConversationViewPage().isAudioMessageVisible());
+ } else {
+ assertThat("audio message container is visible, but should be hidden",
+ getConversationViewPage().isAudioMessageInvisible());
+ }
+ }
+
+ @Then("^I (do not )?see location map container in the conversation view$")
+ public void ISeeLocationMapContainer(String shouldNotSee) {
+ if (shouldNotSee == null) {
+ assertThat("location map container is not visible",
+ getConversationViewPage().isLocationMapVisible());
+ } else {
+ assertThat("location map container is visible, but should be hidden",
+ getConversationViewPage().isLocationMapInvisible());
+ }
+ }
+
+ @Then("^I (do not )?see link preview container in the conversation view$")
+ public void ISeeLinkPreviewContainer(String shouldNotSee) {
+ if (shouldNotSee == null) {
+ assertThat("Link preview container is not visible",
+ getConversationViewPage().isLinkPreviewVisible());
+ } else {
+ assertThat("Link preview container is visible, but should be hidden",
+ getConversationViewPage().isLinkPreviewInvisible());
+ }
+ }
+
+ /**
+ * Verify link preview image visibility
+ *
+ * @param shouldNotBeVisible equals to null if the placeholder should be visible
+ */
+ @When("^I (do not )?see link preview image in the conversation view$")
+ public void ISeeLinkPreviewImage(String shouldNotBeVisible) {
+ if (shouldNotBeVisible == null) {
+ assertThat("Link preview image is not visible",
+ getConversationViewPage().isLinkPreviewImageVisible());
+ } else {
+ assertThat("Link preview image is visible, but should be hidden",
+ getConversationViewPage().isLinkPreviewImageInvisible());
+ }
+ }
+
+ @When("^I tap Confirm button on Edit control")
+ public void ITapConfirmEditControlButton() {
+ getConversationViewPage().tapConfirmEditControlButton();
+ }
+
+ @When("^I tap Cancel button on Edit control")
+ public void ITapCancelEditControlButton() {
+ getConversationViewPage().tapCancelEditControlButton();
+ }
+
+ private static final Timedelta LIKE_ICON_STATE_CHANGE_TIMEOUT = Timedelta.ofSeconds(7); //seconds
+ private static final double LIKE_ICON_MIN_SIMILARITY = 0.9999;
+
+ /**
+ * Store the current state of Like icon
+ */
+ @When("^I remember the state of (?:Like|Unlike) icon in the conversation$")
+ public void IRememberLikeIconState() throws Exception {
+ context.setLikeIconState(() -> getConversationViewPage().getLikeIconState());
+ }
+
+ /**
+ * Verify whether the current state of Like icon differs from the previously remembered one
+ *
+ * @param shouldNotChange equals to null if the state should be changed
+ */
+ @Then("^I see the state of (?:Like|Unlike) icon is (not )?changed in the conversation$")
+ public void IVerifyLikeIconState(String shouldNotChange) throws Exception {
+ boolean condition;
+ if (shouldNotChange == null) {
+ condition = context.getLikeIconState().isChanged(LIKE_ICON_STATE_CHANGE_TIMEOUT, LIKE_ICON_MIN_SIMILARITY);
+ } else {
+ condition = context.getLikeIconState().isNotChanged(LIKE_ICON_STATE_CHANGE_TIMEOUT, LIKE_ICON_MIN_SIMILARITY);
+ }
+ assertThat(String.format("Like icon state is expected %s in %s seconds",
+ (shouldNotChange == null) ? "to be changed" : "to be not changed", LIKE_ICON_STATE_CHANGE_TIMEOUT),
+ condition);
+ }
+
+ /**
+ * Tap Like/Unlike icon in the conversation
+ */
+ @When("^I tap (?:Like|Unlike) icon in the conversation$")
+ public void ITapLikeIcon() {
+ getConversationViewPage().tapLikeIcon();
+ }
+
+ /**
+ * Verify visibility of the Like/Unlike icon
+ *
+ * @param shouldNotSee eqauls to null if the icon should be visible
+ */
+ @Then("^I (do not )?see (?:Like|Unlike) icon in the conversation$")
+ public void ISeeLikeIcon(String shouldNotSee) {
+ boolean condition;
+ if (shouldNotSee == null) {
+ condition = getConversationViewPage().isLikeIconVisible();
+ } else {
+ condition = getConversationViewPage().isLikeIconInvisible();
+ }
+ assertThat(String.format("The Like/Unlike icon is expected to be %s",
+ (shouldNotSee == null) ? "visible" : "invisible"), condition);
+ }
+
+ /**
+ * Tap the toolbox of the recent message to open likers list
+ */
+ @When("^I tap toolbox of the recent message$")
+ public void ITapMessageToolbox() {
+ getConversationViewPage().tapRecentMessageToolbox();
+ }
+
+ /**
+ * Tap the recent media container to show/hide like icon
+ *
+ * @param pWidth destination cell X tap point (in percent 0-100)
+ * @param pHeight destination cell Y tap point (in percent 0-100)
+ */
+ @When("^I tap at (\\d+)% of width and (\\d+)% of height of the recent message$")
+ public void ITapAtContainerCorner(int pWidth, int pHeight) {
+ getConversationViewPage().tapAtRecentMessage(pWidth, pHeight);
+ }
+
+ @When("^I tap at deep link message$")
+ public void ITapAtDeepLinkMessage() {
+ getConversationViewPage().tapAtDeepLinkMessage();
+ }
+
+ /**
+ * Verify whether the particular text is present or not on message toolbox
+ *
+ * @param shouldNotSee equals to null if the text should be visible
+ * @param expectedText part of the text to verify for presence
+ */
+ @Then("^I (do not )?see \"(.*)\"( button)? on the message toolbox in conversation view$")
+ public void ISeeTextOnToolbox(String shouldNotSee, String expectedText, String button) {
+ expectedText = context.getUsersManager()
+ .replaceAliasesOccurrences(expectedText, FindBy.NAME_ALIAS);
+ if (shouldNotSee == null) {
+ if (button == null) {
+ assertThat(
+ String.format("The expected '%s' text is not visible on the message toolbox", expectedText),
+ getConversationViewPage().isMessageToolboxTextVisible(expectedText)
+ );
+ } else {
+ assertThat(
+ String.format("The expected '%s' text is not visible on the message toolbox button", expectedText),
+ getConversationViewPage().isMessageToolboxButtonVisible(expectedText));
+ }
+ } else {
+ if (button == null) {
+ assertThat(
+ String.format("The expected '%s' text should not be visible on the message toolbox", expectedText),
+ getConversationViewPage().isMessageToolboxTextInvisible(expectedText)
+ );
+ } else {
+ assertThat(
+ String.format("The expected '%s' text should not be visible on the message toolbox button", expectedText),
+ getConversationViewPage().isMessageToolboxButtonInvisible(expectedText));
+ }
+ }
+ }
+
+ /**
+ * Set ephemeral messages timer to a corresponding value
+ *
+ * @param value one of available timer values
+ */
+ @When("^I set self deleting message expiration timer to (Off|10 seconds|5 minutes) on conversation view$")
+ public void ISetExpirationTimer(String value) {
+ getConversationViewPage().setMessageExpirationTimer(value);
+ }
+
+ @Then("^I (do not )?see Has Guests banner in conversation view$")
+ public void ISeeBannerHasGuests(String shouldNotSee) {
+ String bannerType = "Has Guests";
+ if (shouldNotSee == null) {
+ assertThat("Has Guests banner is expected to be visible",
+ getConversationViewPage().isConversationHasGuestsVisible());
+ } else {
+ assertThat(String.format("'Has %s' banner is not expected to be visible", bannerType),
+ getConversationViewPage().isConversationHasGuestsInvisible());
+ }
+ }
+
+ /**
+ * Check if the last message in the conversation view contains a reply
+ *
+ * @param shouldNotSee there should be no reply visible if this string is not null
+ */
+ @Then("^I (do not )?see the last message in conversation view contains a reply$")
+ public void iSeeTheLastMessageInConversationViewContainsAReply(String shouldNotSee) {
+ if (shouldNotSee == null) {
+ assertThat("The conversation view does not contain a reply",
+ getConversationViewPage().isReplyVisible());
+ } else {
+ assertThat("The quote should not be visible in input field",
+ getConversationViewPage().isReplyInvisible());
+ }
+ }
+
+ @Then("^I (do not )?see the input field$")
+ public void IDoNotSeeTheInputField(String shouldNotSee) {
+ if (shouldNotSee == null) {
+ assertThat("The input field is not visible",
+ getConversationViewPage().isInputFieldVisible());
+ } else {
+ assertThat("The input field should not be visible",
+ getConversationViewPage().isInputFieldInvisible());
+ }
+ }
+
+ /**
+ * Check if the message that is being replied to is of a certain type
+ *
+ * @param expectedElement the expected type of the message that is quoted
+ */
+ @Then("^I see that I'm replying to an? (Image|Video|Audio|File) message$")
+ public void iSeePictureInQuoteAboveTextInputField(String expectedElement) {
+ assertThat(String.format("The input field quote does not contain an '%s' message",
+ expectedElement), getConversationViewPage().isInputFieldQuoteOfTypeVisible(expectedElement));
+ }
+
+ @Then("^I see reply to quoted Image in conversation view$")
+ public void iSeeImageReplyInConversationView() {
+ assertThat("Image in reply is NOT visible",
+ getConversationViewPage().isQuotedImageVisible());
+ }
+
+ /**
+ * Check if the message that has been quoted contains text
+ *
+ * @param expectedMessage the expected text of the quoted message
+ */
+ @Then("^I see the quoted message contains text \"(.*)\"$")
+ public void iSeeReplyTextInQuotedMessage(String expectedMessage) {
+ assertThat("The text '%s' is not present in the quoted message",
+ getConversationViewPage().isQuotedMessageVisible(expectedMessage));
+ }
+
+ /**
+ * Checks for the number of reply cells visible in conversation view
+ *
+ * @param numberOfReplies the number of replies that should be in conversation view
+ */
+ @Then("^I see (\\d+) (?:reply|replies) in the conversation view$")
+ public void iSeeTheNumberOfReplies(int numberOfReplies) {
+ assertThat(String.format("The number of replies in conversation view is not '%s'", numberOfReplies),
+ getConversationViewPage().isNumberOfReplyCellsVisible(numberOfReplies));
+ }
+
+ /**
+ * check if the recent message is seen by x amount of users
+ * NOTE: does not work when the time of reading is shown
+ *
+ * @param persons the amount of users
+ */
+ @Then("^I see that recent message is seen by (\\d+) persons?$")
+ public void iSeeThatRecentMessageIsSeenByPerson(int persons) {
+ assertThat(String.format("The message is not seen by '%s' persons", persons),
+ getConversationViewPage().isMessageDeliveryStatusTextVisible(Integer.toString(persons)));
+ }
+
+ /**
+ * checks if text is visible in the message details part of the message toolbox
+ *
+ * @param doNot wether or not it should be visible
+ * @param status the text that should or should not be visible in the details part of the message toolbox
+ */
+ @Then("^I (do not )?see message details (.*) in message toolbox$")
+ public void iDoNotSeeTheMessageDetailsInToolbox(String doNot, String status) {
+ if (doNot == null) {
+ assertThat(String.format("The delivery status does not contain '%s'", status), getConversationViewPage().isMessageToolboxDetailsTextVisible(status));
+ } else {
+ assertThat(String.format("The delivery status does contain '%s' while it should not", status), getConversationViewPage().isMessageToolboxDetailsTextInvisible(status));
+ }
+ }
+
+ /**
+ * checks is the delivery status is visible in the message toolbox
+ *
+ * @param doNot wether or not it should be visible
+ */
+ @Then("^I (do not )?see the delivery status in message toolbox$")
+ public void iDoNotSeeTheDeliveryStatusInMessageToolbox(String doNot) {
+ if (doNot == null) {
+ assertThat("The delivery status is not visible", getConversationViewPage().isMessageDeliveryStatusVisible());
+ } else {
+ assertThat("The delivery status is visible while it should not be", getConversationViewPage().isMessageDeliveryStatusInvisible());
+ }
+ }
+
+ /**
+ * I see the legal hold indicator next to self title in Conversation View
+ *
+ * @param shouldNotSee equals to null if the element should be visible
+ */
+ @Then("^I (do not )?see legal hold indicator next to conversation title in conversation view$")
+ public void iSeeLegalHoldIndicatorNextToConversationTitle(String shouldNotSee) {
+ if (shouldNotSee == null) {
+ assertThat("The legal hold indicator is not visible next to conversation title in conversation view",
+ getConversationViewPage().isLegalHoldIndicatorVisible());
+ } else {
+ assertThat("The legal hold indicator is visible next to conversation title in conversation view while it should not be",
+ getConversationViewPage().isLegalHoldIndicatorInvisible());
+ }
+ }
+
+ @When("^I see an image with QR code \"(.*)\" in the conversation view$")
+ public void iSeeImageInConversation(String qrCode) {
+ BufferedImage actualImage = getConversationViewPage().getRecentPictureScreenshot().get();
+ context.addAdditionalScreenshots(actualImage);
+
+ assertThat("Could not find correct QR code",
+ getConversationViewPage().getQRCodeFromPicture(),
+ hasItem(qrCode));
+ }
+
+ @Then("^I see (.*) reaction in the conversation view$")
+ public void ISeeReactionX(String reaction) {
+ assertThat(String.format("Reaction %s is not visible", reaction), getConversationViewPage().isReactionVisible(reaction));
+ }
+
+ @Then("^I do not see (.*) reaction in the conversation view$")
+ public void IDoNotSeeReactionX(String reaction) {
+ assertThat(String.format("Reaction %s is visible", reaction), getConversationViewPage().isReactionInvisible(reaction));
+ }
+
+
+ @Then("^I see User (.*) will get your message later in conversation view$")
+ public void iSeeUserWillGetYourMessageLater(String usernameAlias) {
+ String name = context.getUsersManager()
+ .replaceAliasesOccurrences(usernameAlias, FindBy.NAME_ALIAS);
+ assertThat(String.format("User %s will get your message later is not visible", name), getConversationViewPage().isUserWillGetYourMessageLaterVisible(name));
+ }
+
+ @Then("^I tap on Learn more link on delayed message in conversation view$")
+ public void ITapOnLearnMoreLinkOnDelayedMessage() {
+ getConversationViewPage().iTapOnLearnMoreLinkOnDelayedMessage();
+ }
+
+ @Then("^I do not see Enterprise Upgrade alert$")
+ public void IDoNotSeeEnterpriseUpgradeAlert() {
+ assertThat(getConversationViewPage().enterpriseUpgradeAlertPresent(), is(false));
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ConversationsListPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ConversationsListPageSteps.java
new file mode 100644
index 00000000000..33a667a719c
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ConversationsListPageSteps.java
@@ -0,0 +1,364 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.common.CommonUtils;
+import com.wearezeta.auto.common.misc.Timedelta;
+import com.wearezeta.auto.common.usrmgmt.ClientUsersManager;
+import com.wearezeta.auto.common.usrmgmt.ClientUsersManager.FindBy;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.ConversationsListPage;
+import io.cucumber.java.en.Given;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+import static org.hamcrest.MatcherAssert.assertThat;
+import java.util.regex.Pattern;
+
+public class ConversationsListPageSteps {
+
+ IOSTestContext context;
+
+ public ConversationsListPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private ConversationsListPage getConversationsListPage() {
+ return context.getPagesCollection()
+ .getPage(ConversationsListPage.class);
+ }
+
+ @Given("^I (do not )?see conversations list$")
+ public void GivenISeeConversationsList(String doNot) {
+ if (doNot == null) {
+ assertThat("Conversations list is not visible after the timeout",
+ getConversationsListPage().isVisible());
+ } else {
+ assertThat("Conversations list is visible while it should not be",
+ getConversationsListPage().isInvisible());
+ }
+ }
+
+ /**
+ * Open the corresponding conversation by tapping its name in the conversations list
+ *
+ * @param name conversation name/alias
+ */
+ @Given("^I open (?:group |single |1:1 |\\s?)conversation \"(.*)\" in conversation list")
+ public void IOpenRecentConversation(String name) {
+ name = context.getUsersManager()
+ .replaceAliasesOccurrences(name, FindBy.NAME_ALIAS);
+ getConversationsListPage().tapConversationItemRecentList(name);
+ }
+
+ @When("I long tap conversation '(.*)' in conversation list")
+ public void iLongTapConversationVInConversationList(String conversationName) {
+ getConversationsListPage().longTapConversationItemRecentList(conversationName);
+ }
+
+ @When("I long tap alias conversation '(.*)' in conversation list")
+ public void iLongTapAliasConversationVInConversationList(String alias) {
+ String name = context.getUsersManager().findUserByNameOrNameAlias(alias).getName();
+ getConversationsListPage().longTapConversationItemRecentList(name);
+ }
+
+ @When("I long tap 1:1 conversation '(.*)' in conversation list")
+ public void iLongTap11ConversationVInConversationList(String name) {
+ name = context.getUsersManager()
+ .replaceAliasesOccurrences(name, FindBy.NAME_ALIAS);
+ getConversationsListPage().longTapConversationItemRecentList(name);
+ }
+
+ private final static Timedelta CONVO_LIST_UPDATE_TIMEOUT = Timedelta.ofSeconds(10);
+
+ /**
+ * Verify whether the first items in conversations list is the given item
+ *
+ * @param convoName conversation name
+ */
+ @Then("^I see the name of the first conversation is (.*)")
+ public void ISeeUserNameFirstInContactList(String convoName) {
+ final String name = context.getUsersManager()
+ .replaceAliasesOccurrences(convoName, FindBy.NAME_ALIAS);
+ assertThat(String.format("The conversation '%s' is not the first conversation in the list after " +
+ "%s timeout", name, CONVO_LIST_UPDATE_TIMEOUT),
+ CommonUtils.waitUntilTrue(CONVO_LIST_UPDATE_TIMEOUT, Timedelta.ofSeconds(1),
+ () -> getConversationsListPage().isFirstConversationName(name))
+ );
+ }
+
+ @Then("^I see conversation (.*) in conversations list$")
+ public void iSeeUserInContactList(String value) {
+ value = context.getUsersManager().replaceAliasesOccurrences(value, FindBy.NAME_ALIAS);
+ assertThat(String.format("The conversation '%s' is not visible in the conversation list",
+ value), getConversationsListPage().isConversationInList(value));
+ }
+
+ @Then("^I do not see conversation (.*) in conversations list$")
+ public void iDoNotSeeUserInContactList(String value) {
+ value = context.getUsersManager().replaceAliasesOccurrences(value, FindBy.NAME_ALIAS);
+ assertThat(
+ String.format("The conversation '%s' is visible in the conversation list, but should be hidden",
+ value), getConversationsListPage().isConversationNotInList(value));
+ }
+
+ @When("^I swipe right on conversation (.*) in Conversations view")
+ public void ISwipeRightOnConversationInConvView(String name) {
+ name = context.getUsersManager()
+ .replaceAliasesOccurrences(name, FindBy.NAME_ALIAS);
+ getConversationsListPage().swipeRightOnConversation(name);
+ }
+
+ /**
+ * Verify visibility of EVERYTHING ARCHIVED placeholder message in conversation list
+ */
+ @Then("^I see EVERYTHING ARCHIVED placeholder in conversations list$")
+ public void ISeeNoConversationMessage() {
+ assertThat("'EVERYTHING ARCHIVED' placeholder is not visible",
+ getConversationsListPage().isConversationsListPlaceholderVisible());
+ }
+
+ @When("^I tap JOIN button in conversations list next to (.*)")
+ public void ITapButtonInContactListNextTo(String contact) {
+ final String name = context.getUsersManager().replaceAliasesOccurrences(contact, FindBy.NAME_ALIAS);
+ getConversationsListPage().tapJoinButtonNextTo(name);
+ }
+
+ @When("I tap Incoming Pending Requests item in conversations list")
+ public void ITapPendingRequestLinkContactList() {
+ getConversationsListPage().tapPendingRequest();
+ }
+
+ @When("I (do not )?see Pending request link in conversations list$")
+ public void ISeePendingRequestLinkInContacts(String shouldNotSee) {
+ if (shouldNotSee == null) {
+ assertThat("Pending request link is not in conversations list",
+ getConversationsListPage().isPendingRequestInContactList());
+ } else {
+ assertThat("Pending request link is shown in conversations list",
+ getConversationsListPage().pendingRequestInContactListIsNotShown());
+ }
+ }
+
+ /**
+ * Check value of status icon in conversation list
+ *
+ * @param status the number of unread messages in the status icon
+ * @param conversation the unread messages are in
+ */
+ @Then("^I see status of conversations list item (.*) is (not )?\"(.*)\"$")
+ public void ISeeConversationStatus(String conversation, String notChanged, String status) {
+ conversation = context.getUsersManager()
+ .replaceAliasesOccurrences(conversation, ClientUsersManager.FindBy.NAME_ALIAS);
+ status = context.getUsersManager().replaceAliasesOccurrences(status, FindBy.NAME_ALIAS);
+ if (notChanged == null) {
+ assertThat(String.format("The current status for conversation item %s is not equal to %s",
+ conversation, status),
+ getConversationsListPage().isConversationItemWithStatusVisible(status, conversation));
+ } else {
+ assertThat(String.format("The current status for conversation item %s is not expected to be equal " +
+ "to %s",
+ conversation, status),
+ getConversationsListPage().isConversationItemWithStatusInvisible(status, conversation));
+ }
+ }
+
+ /**
+ * Check the secondary line of the conversation item in conversation list
+ *
+ * @param secondaryLine the text of the secondary line
+ * @param conversation the secondary line belongs to
+ */
+ @Then("^I see the secondary line in conversations list item (.*) is \"(.*)\"$")
+ public void ISeeTheSecondaryIs(String conversation, String secondaryLine) {
+ conversation = context.getUsersManager()
+ .replaceAliasesOccurrences(conversation, ClientUsersManager.FindBy.NAME_ALIAS);
+ final Pattern usernamePattern = Pattern.compile(ClientUsersManager.
+ STR_UNIQUE_USERNAME_ALIAS_TEMPLATE.apply("\\d+"));
+ if (usernamePattern.matcher(secondaryLine).find()) {
+ secondaryLine = context.getUsersManager()
+ .replaceAliasesOccurrences(secondaryLine, FindBy.UNIQUE_USERNAME_ALIAS);
+ } else {
+ secondaryLine = context.getUsersManager()
+ .replaceAliasesOccurrences(secondaryLine, ClientUsersManager.FindBy.NAME_ALIAS);
+ }
+ assertThat(String.format("The current secondary line for conversation item %s is not equal to %s",
+ conversation, secondaryLine),
+ getConversationsListPage().isSecondaryLineVisible(conversation, secondaryLine));
+ }
+
+ /**
+ * Check that the conversation item in conversation list has no status set
+ *
+ * @param isNot equals to null if a status should be visible
+ * @param conversation list item to check status of
+ */
+ @Given("^I (do not )?see a status for conversations list item (.*)$")
+ public void IDoNotSeeAConversationStatusForContact(String isNot, String conversation) {
+ conversation = context.getUsersManager()
+ .replaceAliasesOccurrences(conversation, ClientUsersManager.FindBy.NAME_ALIAS);
+ if (isNot == null) {
+ assertThat(String.format("The status for conversation item %s is not set", conversation),
+ getConversationsListPage().isConversationItemStatusVisible(conversation));
+ } else {
+ assertThat(String.format("The status for conversation item %s is set", conversation),
+ getConversationsListPage().isConversationItemStatusInvisible(conversation));
+ }
+ }
+
+ @Then("^I (do not )?see classified domain icon on the outgoing connection page$")
+ public void iSeeClassifiedDomainLabelOutgoingConnection(String shouldBeVisible) {
+ if (shouldBeVisible == null) {
+ assertThat("The classified domain label should be visible on the outgoing connection page",
+ getConversationsListPage().isClassifiedLabelVisible());
+ } else {
+ assertThat("The classified domain label should be invisible on the outgoing connection page",
+ getConversationsListPage().isClassifiedLabelInvisible());
+ }
+ }
+
+ @Then("^I (do not )?see classified domain icon on the incoming connection page$")
+ public void iSeeClassifiedDomainLabelIncomingConnection(String shouldBeVisible) {
+ if (shouldBeVisible == null) {
+ assertThat("The classified domain label should be visible on the incoming connection page",
+ getConversationsListPage().isClassifiedLabelVisible());
+ } else {
+ assertThat("The classified domain label should be invisible on the incoming connection page",
+ getConversationsListPage().isClassifiedLabelInvisible());
+ }
+ }
+
+ @Then("^I (do not )?see unclassified domain icon on the incoming connection page$")
+ public void iSeeNotClassifiedDomainLabelIncomingConnection(String shouldBeVisible) {
+ if (shouldBeVisible == null) {
+ assertThat("The unclassified domain label should be visible on the incoming connection page",
+ getConversationsListPage().isNotClassifiedLabelVisible());
+ } else {
+ assertThat("The unclassified domain label should be invisible on the incoming connection page",
+ getConversationsListPage().isNotClassifiedLabelInvisible());
+ }
+ }
+
+ @Then("^I (do not )?see unclassified domain icon on the outgoing connection page$")
+ public void iSeeNotClassifiedDomainLabelOutgoingConnection(String shouldBeVisible) {
+ if (shouldBeVisible == null) {
+ assertThat("The unclassified domain label should be visible on the outgoing connection page",
+ getConversationsListPage().isNotClassifiedLabelVisible());
+ } else {
+ assertThat("The unclassified domain label should be invisible on the outgoing connection page",
+ getConversationsListPage().isNotClassifiedLabelInvisible());
+ }
+ }
+
+ @Then("^I (do not )?see classified domain label in the conversation$")
+ public void iSeeClassifiedDomainLabelConvo(String shouldBeVisible) {
+ if (shouldBeVisible == null) {
+ assertThat("The classified domain label should be visible",
+ getConversationsListPage().isClassifiedLabelVisibleConvo());
+ } else {
+ assertThat("The classified domain label should be invisible",
+ getConversationsListPage().isClassifiedLabelInvisibleConvo());
+ }
+ }
+
+ @Then("^I (do not )?see unclassified domain label in the conversation$")
+ public void iSeeNotClassifiedDomainLabelConvo(String shouldBeVisible) {
+ if (shouldBeVisible == null) {
+ assertThat("The unclassified domain label should be visible",
+ getConversationsListPage().isNotClassifiedLabelVisibleConvo());
+ } else {
+ assertThat("The unclassified domain label should be invisible",
+ getConversationsListPage().isNotClassifiedLabelInvisibleConvo());
+ }
+ }
+
+ @Then("^I (do not )?see classified domain label on Group participant user profile page$")
+ public void iSeeClassifiedDomainLabelUserProfile(String shouldBeVisible) {
+ if (shouldBeVisible == null) {
+ assertThat("The classified domain label should be visible",
+ getConversationsListPage().isClassifiedLabelVisibleUserProfile());
+ } else {
+ assertThat("The classified domain label should be invisible",
+ getConversationsListPage().isClassifiedLabelInvisibleUserProfile());
+ }
+ }
+
+ @Then("^I (do not )?see unclassified domain label on Group participant user profile page$")
+ public void iSeeNotClassifiedDomainLabelUserProfile(String shouldNot) {
+ if (shouldNot == null) {
+ assertThat("The unclassified domain label should be visible",
+ getConversationsListPage().isNotClassifiedLabelVisibleUserProfile());
+ } else {
+ assertThat("The unclassified domain label should be invisible",
+ getConversationsListPage().isNotClassifiedLabelInvisibleUserProfile());
+ }
+ }
+
+ @When("I choose Mark as Read from conversation list context menu")
+ public void iChooseMarkAsReadFromConversationListContextMenu() {
+ getConversationsListPage().tapMarkAsRead();
+ }
+
+ @When("I choose Notifications... from conversation list context menu")
+ public void iChooseNotificationsFromConversationListContextMenu() {
+ getConversationsListPage().tapNotificationsMenu();
+ }
+
+ @When("I choose Nothing from the Notifications submenu")
+ public void iChooseNothingFromTheNotificationsSubmenu() {
+ getConversationsListPage().tapNothing();
+ }
+
+ @When("I choose Archive from conversation list context menu")
+ public void iChooseArchiveAsReadFromConversationListContextMenu() {
+ getConversationsListPage().tapArchive();
+ }
+
+ @When("I choose Favorite from conversation list context menu")
+ public void iChooseFavoriteAsReadFromConversationListContextMenu() {
+ getConversationsListPage().tapFavorite();
+ }
+
+ @When("I choose Move To from conversation list context menu")
+ public void iChooseMoveToFromConversationListContextMenu() {
+ getConversationsListPage().tapMoveTo();
+ }
+
+ @When("I choose Clear Content from conversation list context menu")
+ public void iChooseClearContentFromConversationListContextMenu() {
+ getConversationsListPage().tapClearContent();
+ }
+
+ @When("I choose Block from conversation list context menu")
+ public void iChooseBlockFromConversationListContextMenu() {
+ getConversationsListPage().tapBlock();
+ }
+
+ @When("I choose Leave Group from conversation list context menu")
+ public void iChooseLeaveGroupFromConversationListContextMenu() {
+ getConversationsListPage().tapLeaveGroup();
+ }
+
+ @When("I remove conversation from favorites in conversation list context menu")
+ public void iRemoveConversationFromFavoritesInConversationListContextMenu() {
+ getConversationsListPage().tapRemoveFromFavorite();
+ }
+
+ @When("I choose clear from clear content menu")
+ public void iChooseClearFromClearContentMenu() {
+ getConversationsListPage().tapClearInClearContent();
+ }
+
+ @When("I confirm the block dialog")
+ public void iConfirmTheBlockDialog() {
+ getConversationsListPage().tapBlockConfirm();
+ }
+
+ @When("I choose Leave and Clear in the dialog")
+ public void iChooseLeaveAndClearInTheDialog() {
+ getConversationsListPage().tapLeaveAndClearConfirm();
+ }
+
+ @Then("I see that I am certified on Conversation List Page")
+ public void iSeeThatIAmCertifiedOnConversationListPage() {
+ assertThat("User is missing certified status", getConversationsListPage().isCertified());
+ }
+}
+
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/CreateFolderSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/CreateFolderSteps.java
new file mode 100644
index 00000000000..9821338ec40
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/CreateFolderSteps.java
@@ -0,0 +1,29 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.pages.CreateFolderPage;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import io.cucumber.java.en.When;
+
+public class CreateFolderSteps {
+ IOSTestContext context;
+
+ public CreateFolderSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private CreateFolderPage getNewFolderPage() {
+ return context.getPagesCollection().getPage(CreateFolderPage.class);
+ }
+
+ @When("^I enter Folder name \"(.*)\" on New Folder page$")
+ public void iEnterFoldernameOnNewFolderPage(String folderName) {
+ getNewFolderPage().enterFolderName(folderName);
+ }
+
+ @When("^I tap Create button on New Folder page$")
+ public void iTapCreateNewButton() {
+ getNewFolderPage().tapCreateButton();
+ }
+}
+
+
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/CustomBackendRedirectionPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/CustomBackendRedirectionPageSteps.java
new file mode 100644
index 00000000000..09293b5be89
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/CustomBackendRedirectionPageSteps.java
@@ -0,0 +1,54 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.common.backend.Backend;
+import com.wearezeta.auto.common.backend.BackendConnections;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.CustomBackendRedirectionPage;
+import io.cucumber.java.en.Given;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+import org.apache.commons.lang3.StringUtils;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class CustomBackendRedirectionPageSteps {
+
+ IOSTestContext context;
+
+ public CustomBackendRedirectionPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private CustomBackendRedirectionPage getCustomBackendRedirectionPage() {
+ return context.getPagesCollection().getPage(CustomBackendRedirectionPage.class);
+ }
+
+ @Given("^I tap Proceed button on backend redirection page$")
+ public void iTapProceedButtonOnBackendRedirectionPage() {
+ getCustomBackendRedirectionPage().tapProceedButton();
+ }
+
+ @When("^I see redirection title on backend redirection page$")
+ public void iSeeRedirectionTitleOnBackendRedirectionPage() {
+ assertThat("Redirection title on redirection page is not visible while it should be.", getCustomBackendRedirectionPage().isRedirectionTitleVisible());
+ }
+
+ @Then("^I see backend information of backend (.*)$")
+ public void iSeeBackendInformationOfBackend(String backendName) {
+ Backend backend;
+ if(backendName.equals("default")) {
+ backend = BackendConnections.getDefault();
+ } else {
+ backend = BackendConnections.get(backendName);
+ }
+ String domainName = backend.getBackendName() + ".wire.link";
+ assertThat("Wrong or missing domain name on redirection page",
+ getCustomBackendRedirectionPage().isTextVisible(domainName));
+
+ assertThat("Wrong or missing backend url on redirection page",
+ getCustomBackendRedirectionPage().isTextVisible(StringUtils.chop(backend.getBackendUrl())));
+
+ assertThat("Wrong or missing backend websocket url on redirection page",
+ getCustomBackendRedirectionPage().isTextVisible(backend.getBackendWebsocket().replace("wss://", "https://")));
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/CustomBackendWelcomePageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/CustomBackendWelcomePageSteps.java
new file mode 100644
index 00000000000..f7f5753d47a
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/CustomBackendWelcomePageSteps.java
@@ -0,0 +1,36 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.CustomBackendWelcomePage;
+import io.cucumber.java.en.Given;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class CustomBackendWelcomePageSteps {
+
+ IOSTestContext context;
+
+ public CustomBackendWelcomePageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private CustomBackendWelcomePage getCustomBackendWelcomePage() {
+ return context.getPagesCollection().getPage(CustomBackendWelcomePage.class);
+ }
+
+ @Then("^I see \"([^\"]*)\" label on Custom backend welcome page$")
+ public void iSeeLabelOnCustomBackendWelcomePage(String backendName) {
+ assertThat("Custom backend connection message is not visible.", getCustomBackendWelcomePage().isConnectionMessageVisible(backendName));
+ }
+
+ @When("^I tap Login with Email button on Custom backend welcome page$")
+ public void iTapLoginWithEmailButtonOnCustomBackendWelcomePage() {
+ getCustomBackendWelcomePage().tapOnLoginWithEmailButton();
+ }
+
+ @Given("^I see Custom backend welcome page for backend \"([^\"]*)\"$")
+ public void iSeeCustomBackendWelcomePageForBackend(String backendName) {
+ assertThat("Custom backend Welcome page is not visible", getCustomBackendWelcomePage().isTextVisible(backendName));
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/E2EIOverlayPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/E2EIOverlayPageSteps.java
new file mode 100644
index 00000000000..249a0d4730d
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/E2EIOverlayPageSteps.java
@@ -0,0 +1,28 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.E2EIPage;
+import io.cucumber.java.en.When;
+
+public class E2EIOverlayPageSteps {
+
+ IOSTestContext context;
+
+ public E2EIOverlayPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private E2EIPage getE2EIPage() {
+ return context.getPagesCollection().getPage(E2EIPage.class);
+ }
+
+ @When("^I tap Get Certificate button on Enrollment overlay$")
+ public void iTapGetCertificateButtonOnE2EIPage() {
+ getE2EIPage().tapGetCertificateButton();
+ }
+
+ @When("I click Ok on the Enrollment Success screen")
+ public void iClickOkOnTheEnrollmentSuccessScreen() {
+ getE2EIPage().tapOkButton();
+ }
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/EncryptionAtRestOverlaySteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/EncryptionAtRestOverlaySteps.java
new file mode 100644
index 00000000000..91eef5daa8b
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/EncryptionAtRestOverlaySteps.java
@@ -0,0 +1,52 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.common.backend.BackendConnections;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.EncryptionAtRestOverlay;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class EncryptionAtRestOverlaySteps {
+ IOSTestContext context;
+
+ public EncryptionAtRestOverlaySteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private EncryptionAtRestOverlay getPage() {
+ return context.getPagesCollection().getPage(EncryptionAtRestOverlay.class);
+ }
+
+ @Then("^I see Encryption At Rest overlay$")
+ public void iSeeOverlay() {
+ assertThat("Encryption at Rest overlay is not visible", getPage().isPasscodeOverlayVisible());
+ }
+
+ @Then("^I do not see Encryption At Rest overlay$")
+ public void iDontSeeOverlay() {
+ assertThat("Encryption At Rest overlay is shown", getPage().isPasscodeOverlayInvisible());
+ }
+
+ @When("^I confirm overlay if build has encryption at rest enabled$")
+ public void iConfirm() {
+ if (BackendConnections.getDefault().isFeatureEncryptionAtRestEnabled()) {
+ // Currently not possible as on iOS 17 it is not possible to interact with the passcode overlay
+ // getPage().waitUntilPasscodeOverlayIsVisible();
+ getPage().typePasscode("a");
+ getPage().pressEnter();
+ }
+ }
+
+ @When("^I type (.*) on the Encryption At Rest overlay input")
+ public void ITypeOnOverlay(String input) {
+ getPage().typePasscode(input);
+ }
+
+ @When("^I press enter on the Encryption At Rest overlay input")
+ public void IPressEnterOnOverlay() {
+ getPage().pressEnter();
+ }
+
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/EnterpriseLoginPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/EnterpriseLoginPageSteps.java
new file mode 100644
index 00000000000..66bb7a664ad
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/EnterpriseLoginPageSteps.java
@@ -0,0 +1,50 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.EnterpriseLoginPage;
+import io.cucumber.java.en.Then;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class EnterpriseLoginPageSteps {
+ IOSTestContext context;
+
+ public EnterpriseLoginPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private EnterpriseLoginPage getEnterpriseLoginPage() {
+ return context.getPagesCollection().getPage(EnterpriseLoginPage.class);
+ }
+
+ @Then("^I (do not )?see Enterprise Login popup$")
+ public void iSeeEnterpriseLoginDialogueBox(String shouldNotSee) {
+ if (shouldNotSee == null) {
+ assertThat("Enterprise Log In dialogue box is not visible.", getEnterpriseLoginPage().isEnterpriseLoginBoxVisible());
+ } else {
+ assertThat("Enterprise Log In dialogue box is visible.", getEnterpriseLoginPage().isEnterpriseLoginBoxInvisible());
+ }
+ }
+
+ @Then("^I see Enterprise Login popup contains text \"([^\"]*)\"$")
+ public void iSeeDialogueBoxContainsText(String alertText) {
+ assertThat("Enterprise login popup doesn't contain expected text.", getEnterpriseLoginPage().isAlertContainsText(alertText));
+ }
+
+ @Then("^I see Enterprise Login popup contains button Cancel$")
+ public void iSeeDialogueBoxContainsOptionCancel() {
+ assertThat("Cancel option is not present on the Enterprise Log In popup.", getEnterpriseLoginPage().isCancelOptionVisible());
+ }
+
+ @Then("^I type \"([^\"]*)\" into EmailSSO code field$")
+ public void iTypeCodeIntoSSOCodeField(String code) {
+ if (code.equals("default")) {
+ code = context.getCommonSteps().getSSOCode();
+ }
+ getEnterpriseLoginPage().typeCodeIntoEmailSSOField(code);
+ }
+
+ @Then("^I see error message \"([^\"]*)\" on Enterprise Login popup$")
+ public void iSeeTheErrorMessage(String expectedText) {
+ assertThat("Actual error message is not same as expected.", getEnterpriseLoginPage().isAlertContainsText(expectedText));
+ }
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/FileInspectionPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/FileInspectionPageSteps.java
new file mode 100644
index 00000000000..f091728e723
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/FileInspectionPageSteps.java
@@ -0,0 +1,27 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.FileInspectionPage;
+import io.cucumber.java.en.When;
+
+public class FileInspectionPageSteps {
+ IOSTestContext context;
+
+ public FileInspectionPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private FileInspectionPage getPage() {
+ return context.getPagesCollection().getPage(FileInspectionPage.class);
+ }
+
+ @When("^I tap Share button in file inspection page$")
+ public void iTapShareButtonInFileInspectionPage() {
+ getPage().tapShareButton();
+ }
+
+ @When("^I tap Done button in file inspection page$")
+ public void iTapDoneButtonInFileInspectionPage() {
+ getPage().tapDoneButton();
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/FirstTimeOverlaySteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/FirstTimeOverlaySteps.java
new file mode 100644
index 00000000000..8f75fb0cac3
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/FirstTimeOverlaySteps.java
@@ -0,0 +1,42 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.FirstTimeOverlay;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class FirstTimeOverlaySteps {
+
+ IOSTestContext context;
+
+ public FirstTimeOverlaySteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private FirstTimeOverlay getOverlay() {
+ return context.getPagesCollection().getPage(FirstTimeOverlay.class);
+ }
+
+ @Then("^I see First Time overlay$")
+ public void iSeeOverlay() {
+ assertThat("Restore button is not visible", getOverlay().waitUntilVisible());
+ assertThat("Overlay text wrong", getOverlay().isHeadingVisible());
+ }
+
+ @Then("^I do not see First Time overlay$")
+ public void iDoNotSeeOverlay() {
+ assertThat("Restore button is still visible", getOverlay().waitUntilInvisible());
+ }
+
+ @When("^I accept First Time overlay$")
+ public void iAccept() {
+ getOverlay().accept();
+ }
+
+ @When("^I tap Restore from backup button on First Time overlay$")
+ public void iTapRestoreButton() {
+ getOverlay().tapRestoreButton();
+ }
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/FolderViewSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/FolderViewSteps.java
new file mode 100644
index 00000000000..130352637e5
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/FolderViewSteps.java
@@ -0,0 +1,269 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.common.usrmgmt.ClientUsersManager;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.FolderViewPage;
+import io.cucumber.java.en.Given;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.*;
+
+import java.util.regex.Pattern;
+
+public class FolderViewSteps {
+ IOSTestContext context;
+
+ public FolderViewSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private FolderViewPage getFolderViewPage() {
+ return context.getPagesCollection()
+ .getPage(FolderViewPage.class);
+ }
+
+ @Given("^I see Folder view$")
+ public void ISeeFolderView() {
+ assertThat("Folder view is not visible after the timeout",
+ getFolderViewPage().isVisible());
+ }
+
+ /**
+ * Open the corresponding conversation by tapping its name in the conversations list
+ *
+ * @param name conversation name/alias
+ */
+ @Given("^I open (?:group |single |1:1 |\\s?)conversation \"(.*)\" in Folder view")
+ public void IOpenRecentConversation(String name) {
+ name = context.getUsersManager().replaceAliasesOccurrences(name, ClientUsersManager.FindBy.NAME_ALIAS);
+ getFolderViewPage().tapConversationItemGroupedList(name);
+ }
+
+ @Then("^I see People folder in Folder view$")
+ public void iSeePeopleFolder() {
+ assertThat("People folder is not visible", getFolderViewPage().isPeopleFolderVisible());
+ }
+
+ /**
+ * verifies the visibility of the Favorites folder in Folder view
+ *
+ * @param shouldNotSee equals to null if the item should be visible
+ */
+ @Then("^I (do not )?see Favorites folder in Folder view$")
+ public void ISeeFavoritesFolder(String shouldNotSee) {
+ if (shouldNotSee == null) {
+ assertThat("Favorites folder is not visible", getFolderViewPage().isFavoritesFolderVisible());
+ } else {
+ assertThat("Favorites folder is not visible", getFolderViewPage().isFavoritesFolderInvisible());
+ }
+ }
+
+ /**
+ * Taps the People folder to collapse the folder
+ */
+ @Then("^I (collapse|expand) People folder$")
+ public void ITapPeopleFolder() {
+ getFolderViewPage().tapPeopleFolder();
+ }
+
+ /**
+ * Taps the Favorites folder to collapse the folder
+ */
+ @Then("^I (collapse|expand) Favorites folder$")
+ public void ITapFavoritesFolder() {
+ getFolderViewPage().tapFavoritesFolder();
+ }
+
+ /**
+ * Taps the custom folder to collapse the folder
+ * @param folderName name of the custom folder
+ */
+ @Then("^I (collapse|expand) custom folder (.*)$")
+ public void ITapCustomFolder(String folderName) {
+ getFolderViewPage().tapCustomFolder(folderName);
+ }
+
+ /**
+ * Taps the People folder to collapse the folder
+ */
+ @Then("^I see People folder is (collapsed|expanded)$")
+ public void ISeePeopleFolderExpanded(String state) {
+ if (state.equals("collapsed")) {
+ getFolderViewPage().isFolderCollapsed("People");
+ } else {
+ getFolderViewPage().isFolderExpanded("People");
+ }
+ }
+
+ /**
+ * verifies the visibility of the Groups folder in grouped conversation list
+ *
+ * @param shouldNotSee equals to null if the item should be visible
+ */
+ @Then("^I (do not )?see Groups folder in Folder view$")
+ public void ISeeGroupsFolder(String shouldNotSee) {
+ if (shouldNotSee == null) {
+ assertThat("Group folder is not visible", getFolderViewPage().isGroupFolderVisible());
+ } else {
+ assertThat("Group folder is not visible", getFolderViewPage().isGroupFolderInvisible());
+ }
+ }
+
+ /**
+ * verifies the visibility of a custom folder in grouped conversation list
+ *
+ * @param shouldNotSee equals to null if the item should be visible
+ * @param folderName the name of the custom folder
+ */
+ @Then("^I (do not )?see custom folder (.*) in Folder view$")
+ public void ISeeGroupsFolder(String shouldNotSee, String folderName) {
+ if (shouldNotSee == null) {
+ assertThat("Group folder is not visible", getFolderViewPage().isCustomFolderVisible(folderName));
+ } else {
+ assertThat("Group folder is not visible", getFolderViewPage().isCustomFolderInvisible(folderName));
+ }
+ }
+
+ @Then("^I see conversation (.*) in People folder$")
+ public void iSeeUserInContactFolder(String value) {
+ value = context.getUsersManager()
+ .replaceAliasesOccurrences(value, ClientUsersManager.FindBy.NAME_ALIAS);
+ assertThat("The conversation is not visible in the People folder",
+ getFolderViewPage().getConversationOfPeopleFolder(), hasItem(value));
+ }
+
+ @Then("^I do not see conversation (.*) in People folder$")
+ public void iDoNotSeeUserInContactFolder(String value) {
+ value = context.getUsersManager()
+ .replaceAliasesOccurrences(value, ClientUsersManager.FindBy.NAME_ALIAS);
+ if (!getFolderViewPage().isPeopleFolderInvisible()) {
+ assertThat("The conversation is visible in the People folder, but should be hidden",
+ getFolderViewPage().getConversationOfPeopleFolder(), not(hasItem(value)));
+ }
+ }
+
+ /**
+ * verifies the visibility of a specific conversation in the People folder
+ *
+ * @param shouldNotSee equals to null if the item should be visible
+ * @param value conversation name/alias
+ */
+ @Then("^I (do not )?see conversation (.*) in Groups folder$")
+ public void ISeeUserInGroupFolder(String shouldNotSee, String value) {
+ if (shouldNotSee == null) {
+ assertThat(String.format("The conversation '%s' is not visible in the Groups list",
+ value), getFolderViewPage().isConversationInGroupsFolder(value));
+ } else {
+ assertThat(
+ String.format("The conversation '%s' is visible in the Groups list, but should be hidden",
+ value), getFolderViewPage().isConversationNotInGroupsFolder(value));
+ }
+ }
+
+ /**
+ * verifies the visibility of a specific conversation in the Favorites folder
+ *
+ * @param shouldNotSee equals to null if the item should be visible
+ * @param value conversation name/alias
+ */
+ @Then("^I (do not )?see conversation (.*) in Favorites folder$")
+ public void ISeeUserInFavoritesFolder(String shouldNotSee, String value) {
+ value = context.getUsersManager()
+ .replaceAliasesOccurrences(value, ClientUsersManager.FindBy.NAME_ALIAS);
+ if (shouldNotSee == null) {
+ assertThat(String.format("The conversation '%s' is not visible in the Favorites list",
+ value), getFolderViewPage().isConversationInFavoritesFolder(value));
+ } else {
+ assertThat(
+ String.format("The conversation '%s' is visible in the Favorites list, but should be hidden",
+ value), getFolderViewPage().isConversationNotInFavoritesFolder(value));
+ }
+ }
+
+ /**
+ * verifies the visibility of a specific conversation in a specific folder
+ *
+ * @param shouldNotSee equals to null if the item should be visible
+ * @param value conversation name/alias
+ * @param folderName the name of the folder that should contain the conversation
+ */
+ @Then("^I (do not )?see conversation (.*) in custom folder (.*)$")
+ public void ISeeUserInGroupFolder(String shouldNotSee, String value, String folderName) {
+ value = context.getUsersManager()
+ .replaceAliasesOccurrences(value, ClientUsersManager.FindBy.NAME_ALIAS);
+ if (shouldNotSee == null) {
+ assertThat("The conversation is not visible in custom folder",
+ getFolderViewPage().getConversationsInCustomFolder(folderName), hasItem(value));
+ } else {
+ assertThat(
+ "The conversation is visible in the custom folder, but should not",
+ getFolderViewPage().getConversationsInCustomFolder(folderName), not(hasItem(value)));
+ }
+ }
+
+ /**
+ * Check value of status icon in conversation list
+ *
+ * @param status the number of unread messages in the status icon
+ * @param conversation the unread messages are in
+ */
+ @Then("^I see status of Folder view conversation item (.*) is (not )?(\\d+|ping|Pinged|missed call|active call|Silenced|you are mentioned|You are mentioned)$")
+ public void ISeeConversationStatus(String conversation, String notChanged, String status) {
+ conversation = context.getUsersManager()
+ .replaceAliasesOccurrences(conversation, ClientUsersManager.FindBy.NAME_ALIAS);
+ if (notChanged == null) {
+ assertThat(String.format("The current status for conversation item %s is not equal to %s",
+ conversation, status),
+ getFolderViewPage().isConversationItemWithStatusVisible(status, conversation));
+ } else {
+ assertThat(String.format("The current status for conversation item %s is not expected to be equal " +
+ "to %s",
+ conversation, status),
+ getFolderViewPage().isConversationItemWithStatusInvisible(status, conversation));
+ }
+ }
+
+ @Then("^I (do not )?see the secondary line of Folder view conversation item (.*) is \"(.*)\"$")
+ public void ISeeTheSecondaryIs(String doNot, String conversation, String secondaryLine) {
+ conversation = context.getUsersManager()
+ .replaceAliasesOccurrences(conversation, ClientUsersManager.FindBy.NAME_ALIAS);
+ final Pattern usernamePattern = Pattern.compile(ClientUsersManager.
+ STR_UNIQUE_USERNAME_ALIAS_TEMPLATE.apply("\\d+"));
+ if (usernamePattern.matcher(secondaryLine).find()) {
+ secondaryLine = context.getUsersManager()
+ .replaceAliasesOccurrences(secondaryLine, ClientUsersManager.FindBy.UNIQUE_USERNAME_ALIAS);
+ } else {
+ secondaryLine = context.getUsersManager()
+ .replaceAliasesOccurrences(secondaryLine, ClientUsersManager.FindBy.NAME_ALIAS);
+ }
+ if(doNot == null) {
+ assertThat(String.format("The current secondary line for conversation item %s is not equal to %s",
+ conversation, secondaryLine),
+ getFolderViewPage().isSecondaryLineVisible(conversation, secondaryLine));
+ } else {
+ assertThat(String.format("The current secondary line for conversation item %s is equal to '%s' while it should not be",
+ conversation, secondaryLine),
+ getFolderViewPage().isSecondaryLineInvisible(conversation, secondaryLine));
+ }
+ }
+
+ @When("^I swipe right on conversation (.*) in Folder view")
+ public void ISwipeRightOnGroupedConversation(String name) {
+ name = context.getUsersManager()
+ .replaceAliasesOccurrences(name, ClientUsersManager.FindBy.NAME_ALIAS);
+ getFolderViewPage().swipeRightOnGroupedConversation(name);
+ }
+
+ @Then("^I see unread conversations badge is (\\d+) for folder \"(.*)\"$")
+ public void ISeeUnreadConversationsCounter(int number, String folderName) {
+ assertThat(String.format("The number of unread conversations is not %s for folder %s", number, folderName),
+ getFolderViewPage().isBadgeCountForFolder(folderName, number));
+ }
+
+ @Then("^I do not see unread conversations badge for folder \"(.*)\"$")
+ public void iDoNotSeeUnreadConversationsBadge(String folderName) {
+ assertThat(String.format("There is an unread conversations badge on folder %s", folderName),
+ getFolderViewPage().isBadgeCountInvisibleForFolder(folderName));
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ForwardPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ForwardPageSteps.java
new file mode 100644
index 00000000000..70567474646
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ForwardPageSteps.java
@@ -0,0 +1,97 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.common.usrmgmt.ClientUsersManager;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.ForwardPage;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class ForwardPageSteps {
+ IOSTestContext context;
+
+ public ForwardPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private ForwardPage getPage() {
+ return context.getPagesCollection().getPage(ForwardPage.class);
+ }
+
+ @When("^I tap Send button on Forward page$")
+ public void ITapSendButton() {
+ getPage().tapSendButton();
+ }
+
+ /**
+ * Select the corresponding conversation from the list on Forward page
+ *
+ * @param name conversation name/alias
+ */
+ @Then("^I select (.*) conversation on Forward page$")
+ public void ISelectConversation(String name) {
+ name = context.getUsersManager()
+ .replaceAliasesOccurrences(name, ClientUsersManager.FindBy.NAME_ALIAS);
+ getPage().selectConversation(name);
+ }
+
+ /**
+ * Verify whether a conversation is visible in the list of conversations available
+ * for message forwarding
+ *
+ * @param shouldNotSee equals to null if the conversation should be visible
+ * @param name conversation name/alias
+ */
+ @When("^I (do not )?see (.*) conversation on Forward page$")
+ public void ISeeConversation(String shouldNotSee, String name) {
+ name = context.getUsersManager()
+ .replaceAliasesOccurrences(name, ClientUsersManager.FindBy.NAME_ALIAS);
+ if (shouldNotSee == null) {
+ assertThat(String.format("The '%s' conversation is expected to be visible", name),
+ getPage().isConversationVisible(name));
+ } else {
+ assertThat(String.format("The '%s' conversation is expected to be invisible", name),
+ getPage().isConversationInvisible(name));
+ }
+ }
+
+ /**
+ * Verify whether a conversation with legal hold has the legal hold indicator on the message forward page
+ *
+ * @param shouldNotSee equals to null if the legal hold indicator should be visible
+ */
+ @When("^I (do not )?see (legal hold indicator|shield icon|guest icon|external icon) on Forward page$")
+ public void ISeeIconOnForwardPage(String shouldNotSee, String icon) {
+ if (shouldNotSee == null) {
+ if(icon.equals("shield icon")) {
+ // check if shield icon is visible
+ assertThat("Shield icon is not visible", getPage().isShieldIconVisible());
+ } else if (icon.equals("guest icon")) {
+ // check if guest icon is visible
+ assertThat("Guest icon is not visible", getPage().isGuestIconVisible());
+ } else if (icon.equals("external icon")) {
+ // check if guest icon is visible
+ assertThat("external icon is not visible", getPage().isExternalIconVisible());
+ } else {
+ //check if legal hold indicator is visible
+ assertThat("Legal Hold indicator icon is not visible", getPage().isLegalHoldIndicatorVisible());
+ }
+ } else {
+ // Should not be visible
+ if (icon.equals("shield icon")) {
+ // check if shield icon is not visible
+ assertThat("Shield icon is visible while it should not be", getPage().isShieldIconInvisible());
+ } else if (icon.equals("guest icon")) {
+ // check if guest icon is not visible
+ assertThat("Guest icon is visible while it should not be", getPage().isGuestIconInvisible());
+ } else if (icon.equals("external icon")) {
+ // check if external icon is not visible
+ assertThat("external icon is visible while it should not be", getPage().isExternalIconInvisible());
+ } else {
+ // check if Legal hold indicator is not visible
+ assertThat("Legal hold indicator is visible while it should not be", getPage().isLegalHoldIndicatorInvisible());
+ }
+ }
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/GiphyPreviewPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/GiphyPreviewPageSteps.java
new file mode 100644
index 00000000000..f4882917862
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/GiphyPreviewPageSteps.java
@@ -0,0 +1,42 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.wearezeta.auto.ios.pages.GiphyPreviewPage;
+
+import io.cucumber.java.en.When;
+
+public class GiphyPreviewPageSteps {
+ IOSTestContext context;
+
+ public GiphyPreviewPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private GiphyPreviewPage getGiphyPreviewPage() {
+ return context.getPagesCollection()
+ .getPage(GiphyPreviewPage.class);
+ }
+
+ @When("^I select the first item from Giphy grid$")
+ public void ISelectFirstItem() {
+ getGiphyPreviewPage().selectFirstItem();
+ }
+
+ @When("^I tap Send button on Giphy preview page$")
+ public void ITapSendButtonOnGiphyPreview() {
+ getGiphyPreviewPage().tapSendButton();
+ }
+
+ @When("^I see Giphy preview page$")
+ public void ISeeGiphyPreviewPage() {
+ assertThat("Giphy Send Button is not visible", getGiphyPreviewPage().isSendButtonVisible());
+ assertThat("Giphy Cancel Button is not visible", getGiphyPreviewPage().isCancelButtonVisible());
+ }
+
+ @When("^I see Giphy grid preview$")
+ public void ISeeGiphyGridPreview() {
+ assertThat("Giphy grid is not shown", getGiphyPreviewPage().isGridVisible());
+ }
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/HistoryBackupSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/HistoryBackupSteps.java
new file mode 100644
index 00000000000..2d215a63af3
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/HistoryBackupSteps.java
@@ -0,0 +1,47 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.common.usrmgmt.ClientUser;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.HistoryBackupPage;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+
+import static org.hamcrest.MatcherAssert.*;
+
+public class HistoryBackupSteps {
+
+ IOSTestContext context;
+
+ public HistoryBackupSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private HistoryBackupPage getPage() {
+ return context.getPagesCollection().getPage(HistoryBackupPage.class);
+ }
+
+ @When("^I initiate history backup from Settings$")
+ public void iInitiateHistoryBackupFromSettings() {
+ getPage().initiateHistoryBackup();
+ }
+
+ @Then("^I verify history backup for user (.*) from Settings is successfully completed$")
+ public void iVerifyHistoryBackupIsSuccessful(String userAlias) {
+ ClientUser user = context.getUsersManager().findUserByNameOrNameAlias(userAlias);
+ assertThat("Backup now button is not back", getPage().isBackupNowButtonShown());
+
+ // The files can be found under ~/Library/Developer/CoreSimulator/Devices/65061465-C2A1-4F3E-AEE6-E48C4E89D2B6/data/Containers/Shared/AppGroup/4806084A-DA44-4A49-A07F-9FC69E516F7D/File Provider Storage/Wire-5u4asoh3-Backup_20210204.ios_wbu
+ // But i was unable to find the correct app group hash to get them
+ // Appium is running "xcrun simctl get_app_container 65061465-C2A1-4F3E-AEE6-E48C4E89D2B6 com.wearezeta.zclient.ios-development groups"
+ // when using pullFile("@com.wearezeta.zclient.ios-development:groups/") but this returns a wrong app group hash.
+ // Neither com.apple.DocumentsApp nor com.apple.FileProvider.LocalStorage works
+ // See also https://stackoverflow.com/a/58299287
+ /*
+ final TimeZone timezone = TimeZone.getTimeZone("UTC");
+ DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
+ dateFormat.setTimeZone(timezone);
+ byte[] file = getPage().getBackupFile("com.wearezeta.zclient.ios-development", user.getUniqueUsername(), dateFormat.format(new Date()));
+ assertThat("Backup file has no content", file.length, greaterThan(0));
+ */
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ImageFullScreenPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ImageFullScreenPageSteps.java
new file mode 100644
index 00000000000..57583bcccad
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ImageFullScreenPageSteps.java
@@ -0,0 +1,44 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.common.ImageUtil;
+import com.wearezeta.auto.common.misc.Timedelta;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.ImageFullScreenPage;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class ImageFullScreenPageSteps {
+ IOSTestContext context;
+
+ public ImageFullScreenPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private ImageFullScreenPage getImageFullScreenPage() {
+ return context.getPagesCollection().getPage(ImageFullScreenPage.class);
+ }
+
+ @When("^I see Full Screen Page opened$")
+ public void ISeeFullScreenPage() {
+ assertThat("Image not in full screen",
+ getImageFullScreenPage().isImageFullScreenShown());
+ }
+
+ private static final double MAX_SIMILARITY_THRESHOLD = 0.995;
+
+ @Then("^I see the picture on image fullscreen page is animated$")
+ public void ISeePictureIsAnimated() {
+ final int maxFrames = 4;
+ final double avgThreshold = ImageUtil.getAnimationThreshold(getImageFullScreenPage()::getPreviewPictureScreenshot,
+ maxFrames, Timedelta.ofMillis(10));
+ assertThat(String.format("The picture in the image preview view seems to be static (%f >= %f)",
+ avgThreshold, MAX_SIMILARITY_THRESHOLD), avgThreshold < MAX_SIMILARITY_THRESHOLD);
+ }
+
+ @When("^I tap X button on fullscreen image$")
+ public void ITapXOnButtonFullscreen() {
+ getImageFullScreenPage().tapFullScreenCloseButton();
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/KeyboardGalleryPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/KeyboardGalleryPageSteps.java
new file mode 100644
index 00000000000..d7583aeb6fb
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/KeyboardGalleryPageSteps.java
@@ -0,0 +1,48 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.KeyboardGalleryPage;
+
+import io.cucumber.java.en.When;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class KeyboardGalleryPageSteps {
+ IOSTestContext context;
+
+ public KeyboardGalleryPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private KeyboardGalleryPage getKeyboardGalleryPage() {
+ return context.getPagesCollection().getPage(KeyboardGalleryPage.class);
+ }
+
+ /**
+ * Tap the first visible picture on Keyboard Gallery overlay
+ */
+ @When("^I select the first item from Keyboard Gallery$")
+ public void ISelectFirstPicture() {
+ getKeyboardGalleryPage().selectFirstPicture();
+ }
+
+ @When("^I tap Camera Roll button on Keyboard Gallery overlay$")
+ public void ITapCameraRollButton() {
+ getKeyboardGalleryPage().tapCameraRollButton();
+ }
+
+ @When("^I tap Fullscreen Camera button on Keyboard Gallery overlay$")
+ public void ITapFullscreenCameraButton() {
+ getKeyboardGalleryPage().tapFullScreenButton();
+ }
+
+ @When("^I (do not )?see first item from Keyboard Gallery$")
+ public void iSeeFirstItemGallery(String shouldNot) {
+ if (shouldNot == null) {
+ assertThat("Firt item from Keyboard Gallery is not visible",
+ getKeyboardGalleryPage().isFirstItemGalleryVisible());
+ } else {
+ assertThat("Firt item from Keyboard Gallery is visible",
+ getKeyboardGalleryPage().isFirstItemGalleryInvisible());
+ }
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/LegalHoldOverviewPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/LegalHoldOverviewPageSteps.java
new file mode 100644
index 00000000000..11765a52b38
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/LegalHoldOverviewPageSteps.java
@@ -0,0 +1,59 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.common.usrmgmt.ClientUsersManager;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.LegalHoldOverviewPage;
+import io.cucumber.java.en.Given;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.List;
+
+public class LegalHoldOverviewPageSteps {
+
+ IOSTestContext context;
+
+ public LegalHoldOverviewPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private LegalHoldOverviewPage getLegalHoldOverviewPage() {
+ return context.getPagesCollection().getPage(LegalHoldOverviewPage.class);
+ }
+
+ @Then("^I (do not )?see Myself as a legal hold subject on Legal hold overview page$")
+ public void ISeeMyselfAsLegalHoldSubject(String shouldNotSee) {
+ String name = context.getUsersManager().replaceAliasesOccurrences("Myself", ClientUsersManager.FindBy.NAME_ALIAS);
+ if (shouldNotSee == null) {
+ assertThat(String.format("Myself should be visible as legal hold subject"), getLegalHoldOverviewPage().isMyselfVisible(name));
+ } else {
+ assertThat(String.format("Myself should not be visible as legal hold subject"), getLegalHoldOverviewPage().isMyselfVisible(name));
+ }
+ }
+
+ @Then("^I (do not )?see legal hold subjects? (.*) on Legal hold overview page$")
+ public void ISeeLegalHoldSubject(String shouldNotSee, String subjects) {
+ final List aliases = context.getUsersManager().splitAliases(subjects);
+ for (final String alias : aliases) {
+ final String name = context.getUsersManager()
+ .replaceAliasesOccurrences(alias, ClientUsersManager.FindBy.NAME_ALIAS);
+ if (shouldNotSee == null) {
+ assertThat(String.format("User '%s' should be visible", name), getLegalHoldOverviewPage().isSubjectDisplayNameVisible(name));
+ } else {
+ assertThat(String.format("User '%s' should not be visible", name), getLegalHoldOverviewPage().isSubjectDisplayNameInvisible(name));
+ }
+ }
+ }
+
+ @When("^I tap legal hold subject (.*) on Legal hold overview page$")
+ public void iSelectSubject(String name) {
+ name = context.getUsersManager().replaceAliasesOccurrences(name, ClientUsersManager.FindBy.NAME_ALIAS);
+ getLegalHoldOverviewPage().tapOnSubject(name);
+ }
+
+ @When("^I tap close button legal hold overview page$")
+ public void iCloseLegalHoldPage() {
+ getLegalHoldOverviewPage().tapCloseButton();
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/LoginPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/LoginPageSteps.java
new file mode 100644
index 00000000000..c82fc563400
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/LoginPageSteps.java
@@ -0,0 +1,190 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.common.log.ZetaLogger;
+import com.wearezeta.auto.common.usrmgmt.NoSuchUserException;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.*;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.logging.Logger;
+
+import com.wearezeta.auto.common.usrmgmt.ClientUser;
+import com.wearezeta.auto.common.usrmgmt.ClientUsersManager;
+
+import io.cucumber.java.en.*;
+
+public class LoginPageSteps {
+
+ private static final Logger log = ZetaLogger.getLog(LoginPageSteps.class.getSimpleName());
+ private IOSTestContext context;
+
+ public LoginPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private LoginPage getLoginPage() {
+ return context.getPagesCollection().getPage(LoginPage.class);
+ }
+
+ /**
+ * Verifies whether sign in screen is the current screen
+ */
+ @Given("^I see sign in screen$")
+ public void ISeeSignInScreen() {
+ assertThat("Login page is not visible", getLoginPage().isVisible());
+ }
+
+ @When("^I accept Connect to server alert$")
+ public void iAcceptConnectToServerAlert() {
+ getLoginPage().acceptConnectToServerAlert();
+ }
+
+ @When("^I accept Open in Wire alert$")
+ public void iAcceptOpenInWireAlert() {
+ getLoginPage().acceptOpenInWireAlert();
+ }
+
+ /**
+ * Tap EMAIL tab caption on log in screen
+ */
+ @When("^I switch to Email Log In tab$")
+ public void ITapEmailButton() {
+ getLoginPage().switchToEmailLogin();
+ }
+
+ @Given("^I sign in user (.*) with email$")
+ public void iSignInUsingEmail(String nameAlias) {
+ ClientUser user = null;
+ try {
+ user = context.getUsersManager().findUserByNameOrNameAlias(nameAlias);
+ } catch (NoSuchUserException e) {
+ try {
+ // search for user by email aliases in case name is specified
+ user = context.getUsersManager().findUserByEmailOrEmailAlias(nameAlias);
+ } catch (NoSuchUserException ex) {
+ log.severe("Could not find user by name alias or email alias in users manager");
+ }
+ }
+ log.info("Login with email " + user.getEmail() + " and password " + user.getPassword());
+ LoginPage page = getLoginPage();
+ log.info("Enter credentials");
+ page.setLogin(user.getEmail());
+ page.setPassword(user.getPassword());
+ log.info("Tap login button");
+ page.tapLoginButton();
+ }
+
+ @Given("^I sign in user (.*) with fast login$")
+ public void iSignInUsingFastLogin(String nameAlias) {
+ ClientUser user = null;
+ try {
+ user = context.getUsersManager().findUserByNameOrNameAlias(nameAlias);
+ } catch (NoSuchUserException e) {
+ try {
+ // search for user by email aliases in case name is specified
+ user = context.getUsersManager().findUserByEmailOrEmailAlias(nameAlias);
+ } catch (NoSuchUserException ex) {
+ log.severe("Could not find user by name alias or email alias in users manager");
+ }
+ }
+ log.info("Fast login with email " + user.getEmail() + " and password " + user.getPassword());
+ if (context.isDriverCreated()) {
+ throw new RuntimeException("The fast login step can only be used before the driver is created! "
+ + "Make sure you do not use the 'I tap Login button on Welcome page' step before.");
+ }
+ if (context.getScenario().hasTag("fastlogin")) {
+ throw new RuntimeException("Please remove @fastlogin for tests using this step!");
+ }
+ context.setFastLoginUser(user);
+ context.getPagesCollection().getPage(WelcomePage.class).tapLoginButton();
+ }
+
+ /**
+ * Taps Login button on the corresponding screen
+ */
+ @When("^I tap Login button on Login page$")
+ public void ITapSignInButton() {
+ getLoginPage().tapLoginButton();
+ }
+
+ /**
+ * Taps Login button on the corresponding screen
+ */
+ @When("^I attempt to tap Login button$")
+ public void IAttemptToTapLoginButton() {
+ getLoginPage().tapLoginButton();
+ }
+
+ /**
+ * Assert that the login button is disabled.
+ */
+
+ @Then("^I don't see the Login button$")
+ public void IDontSeeTheLoginButton() {
+ assertThat("I see the the login button.", !getLoginPage().isLoginButtonInvisible());
+ }
+
+ /**
+ * Types login string into the corresponding input field on sign in page
+ *
+ * @param login login string (usually it is user email)
+ */
+ @When("^I enter login (.*) on Login page$")
+ public void IEnteredLogin(String login) {
+ login = context.getUsersManager()
+ .replaceAliasesOccurrences(login, ClientUsersManager.FindBy.EMAIL_ALIAS);
+ getLoginPage().setLogin(login);
+ }
+
+ @When("^I login as (.*)$")
+ public void ILogin(String email) {
+ ClientUser user = context.getUsersManager().findUserByEmailOrEmailAlias(email);
+ getLoginPage().setLogin(user.getEmail());
+ getLoginPage().setPassword(user.getPassword());
+ getLoginPage().tapLoginButton();
+ }
+
+ @When("I login")
+ public void iLogin() {
+ ClientUser me = context.getUsersManager().getSelfUser().get();
+ getLoginPage().setLogin(me.getEmail());
+ getLoginPage().setPassword(me.getPassword());
+ getLoginPage().tapLoginButton();
+ }
+
+ /**
+ * Types password string into the corresponding input field on sign in page
+ *
+ * @param password password string
+ */
+ @When("^I enter password (.*) on Login page$")
+ public void IEnterLoginPassword(String password) {
+ password = context.getUsersManager()
+ .replaceAliasesOccurrences(password, ClientUsersManager.FindBy.PASSWORD_ALIAS);
+ getLoginPage().setPassword(password);
+ }
+
+ @Then("^I (do not )?see Phone login tab on Login page$")
+ public void iSeePhoneLogin(String shouldNotSee) {
+ if (shouldNotSee == null) {
+ assertThat("Phone Login is not visible", getLoginPage().isPhoneLoginVisible());
+ } else {
+ assertThat("Phone Login is visible", getLoginPage().isPhoneLoginInvisible());
+ }
+ }
+
+ @Then("^I should not see Company Login button on Login page$")
+ public void iShouldNotSeeCompanyLoginButton() {
+ assertThat("Company Login button is visible.", getLoginPage().isCompanyLoginButtonInvisible());
+ }
+
+ @Then("^I am signed in properly$")
+ public void iAmSignedInProperly() {
+ assertThat("Can't find profile button. Are we logged in?", getLoginPage().waitForLoginProperly());
+ }
+
+ @Then("I see Login page")
+ public void iDoNotSeeLoginPage() {
+ getLoginPage().isVisible();
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/LoginSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/LoginSteps.java
new file mode 100644
index 00000000000..daf31670362
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/LoginSteps.java
@@ -0,0 +1,115 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.common.email.messages.VerificationMessage;
+import com.wearezeta.auto.common.misc.Timedelta;
+import com.wearezeta.auto.common.usrmgmt.ClientUser;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.*;
+import com.wearezeta.auto.ios.pages.team_creation.TCVerificationCodePage;
+import com.wearezeta.auto.ios.pages.webview.WebViewPage;
+import io.cucumber.java.en.When;
+
+public class LoginSteps {
+ IOSTestContext context;
+
+ public LoginSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private WelcomePage getWelcomePage() {
+ return context.getPagesCollection().getPage(WelcomePage.class);
+ }
+
+ private IOSPage getCommonPage() {
+ return context.getPagesCollection().getPage(IOSPage.class);
+ }
+
+ private CustomBackendRedirectionPage getCustomBackendRedirectionPage() {
+ return context.getPagesCollection().getPage(CustomBackendRedirectionPage.class);
+ }
+
+ private IOSPage getIOSPage() {
+ return context.getPagesCollection().getPage(IOSPage.class);
+ }
+
+ private LoginPage getLoginPage() {
+ return context.getPagesCollection().getPage(LoginPage.class);
+ }
+
+ private WebViewPage getWebViewPage() {
+ return context.getPagesCollection().getPage(WebViewPage.class);
+ }
+
+ private TCVerificationCodePage getVerificationCodePage() {
+ return context.getPagesCollection().getPage(TCVerificationCodePage.class);
+ }
+
+ private FirstTimeOverlay getFirstTimeOverlay() {
+ return context.getPagesCollection().getPage(FirstTimeOverlay.class);
+ }
+
+ @When("I login to Wire as (.*)")
+ public void iLogin(String name) {
+ ClientUser user = context.getUsersManager().findUserByNameOrNameAlias(name);
+ getCommonPage().openDeepLinkForDefault();
+ context.startPinging();
+ Timedelta.ofSeconds(3).sleep();
+ context.stopPinging();
+ getWebViewPage().tapOpenButton();
+ context.startPinging();
+ Timedelta.ofSeconds(3).sleep();
+ context.stopPinging();
+ getCustomBackendRedirectionPage().tapProceedButton();
+ getWelcomePage().tapLoginButton();
+ getLoginPage().loginAs(user.getEmail(), user.getPassword());
+ if (getCommonPage().isNotNowOnPasswordPromptVisible()) {
+ getCommonPage().tapNotNowOnPasswordPrompt();
+ }
+ // Accept first time overlay
+ getFirstTimeOverlay().accept();
+ }
+
+
+ @When("I login to the default email verified backend as (.*)")
+ public void iLoginToTheDefaultBackendAsName(String name) throws Exception {
+ ClientUser user = context.getUsersManager().findUserByNameOrNameAlias(name);
+
+ // Open default backend
+ getCommonPage().openDeepLinkForDefault();
+ getCustomBackendRedirectionPage().tapProceedButton();
+
+ // Enroll simulator in touch ID
+ context.enrollSimulatorTouchID();
+
+ // Start email inbox monitoring
+ getIOSPage().startVerificationEmailMonitoring(user, context);
+
+ // Login as user
+ getWelcomePage().tapLoginButton();
+ getIOSPage().startVerificationEmailMonitoring(user, context);
+ getLoginPage().loginAs(user.getEmail(), user.getPassword());
+
+ // Enter email verification code
+ VerificationMessage verificationInfo = new VerificationMessage(context.getVerificationMessage().get());
+ getVerificationCodePage().enterVerificationCode(verificationInfo.getXZetaCode());
+
+ // Dismiss password prompt
+ if (getCommonPage().isNotNowOnPasswordPromptVisible()) {
+ getCommonPage().tapNotNowOnPasswordPrompt();
+ }
+
+ // Verify biometric
+ context.getPagesCollection().getPage(IOSPage.class)
+ .performTouchID(true);
+
+ // Accept first time overlay
+ getFirstTimeOverlay().accept();
+
+ // Verify biometric
+ context.startPinging();
+ Timedelta.ofSeconds(2).sleep();
+ context.stopPinging();
+ context.getPagesCollection().getPage(IOSPage.class)
+ .performTouchID(true);
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ManageDeviceOverlaySteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ManageDeviceOverlaySteps.java
new file mode 100644
index 00000000000..78718170b6e
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ManageDeviceOverlaySteps.java
@@ -0,0 +1,54 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.ManageDevicesOverlay;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class ManageDeviceOverlaySteps {
+ IOSTestContext context;
+
+ public ManageDeviceOverlaySteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private ManageDevicesOverlay getManageDevicesOverlay() {
+ return context.getPagesCollection().getPage(ManageDevicesOverlay.class);
+ }
+
+ /**
+ * Verify whether Manage Devices overlay is visible
+ *
+ * @param shouldNotSee equals to null if the overlay should be visible
+ */
+ @Then("^I (do not )?see Manage Devices overlay$")
+ public void iSeeManageDevicesOverlay(String shouldNotSee) {
+ if (shouldNotSee == null) {
+ assertThat("Manage Devices overlay is not visible",
+ getManageDevicesOverlay().waitUntilVisible());
+ } else {
+ assertThat("Manage Devices overlay is visible",
+ getManageDevicesOverlay().waitUntilInvisible());
+ }
+ }
+
+ /**
+ * Tap Mange Devices button
+ *
+ */
+ @When("^I tap Manage Devices button on Devices Overlay$")
+ public void iTapMangeDevicesButton() {
+ getManageDevicesOverlay().tapMangeDevicesButton();
+ }
+
+ @Then("^I tap Delete for device (.*)$")
+ public void iTapDeleteForDevice(String deviceName) {
+ getManageDevicesOverlay().tapDeleteButtonForDevice(deviceName);
+ }
+
+ @Then("^I tap Delete button on Devices Overlay$")
+ public void iTapDeleteButtonOnDevicesOverlay() {
+ getManageDevicesOverlay().tapDeleteButton();
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/MapViewPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/MapViewPageSteps.java
new file mode 100644
index 00000000000..988fedf436e
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/MapViewPageSteps.java
@@ -0,0 +1,22 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.MapViewPage;
+import io.cucumber.java.en.When;
+
+public class MapViewPageSteps {
+ IOSTestContext context;
+
+ public MapViewPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private MapViewPage getMapViewPage() {
+ return context.getPagesCollection().getPage(MapViewPage.class);
+ }
+
+ @When("^I tap Send location button from map view$")
+ public void ITapSendLocationButtonFromMapView() {
+ getMapViewPage().clickSendLocationButton();
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/MentionSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/MentionSteps.java
new file mode 100644
index 00000000000..e9ad0fbad24
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/MentionSteps.java
@@ -0,0 +1,103 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.ConversationViewPage;
+import io.cucumber.java.en.And;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+import com.wearezeta.auto.ios.pages.search.MentionSuggestionsList;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.wearezeta.auto.common.usrmgmt.ClientUsersManager;
+
+public class MentionSteps {
+ IOSTestContext context;
+
+ public MentionSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private MentionSuggestionsList getSuggestedMentionsList() {
+ return context.getPagesCollection().getPage(MentionSuggestionsList.class);
+ }
+
+ private ConversationViewPage getConversationViewPage() {
+ return context.getPagesCollection().getPage(ConversationViewPage.class);
+ }
+
+ /**
+ * Tap on the suggested mention with name x
+ *
+ * @param name the name of the suggested mention that should be tapped
+ */
+ @When("^I tap (.*) in the suggested mentions list$")
+ public void ITapSuggestedMentions(String name) {
+ name = context.getUsersManager()
+ .replaceAliasesOccurrences(name, ClientUsersManager.FindBy.NAME_ALIAS);
+ getSuggestedMentionsList().tapSuggestedMention(name);
+ }
+
+ /**
+ * Checks if the last message contains mentions
+ *
+ * @param doesNot equals to null if message should contain mention
+ * @param names The names of the users that should be mentioned
+ */
+ @Then("^I see the last message in the conversation view (does not )?contains? mentions? (.*)$")
+ public void messageContainsMentions(String doesNot, String names) {
+ for (String name : context.getUsersManager().splitAliases(names)) {
+ name = context.getUsersManager()
+ .replaceAliasesOccurrences(name, ClientUsersManager.FindBy.NAME_ALIAS);
+ if(doesNot == null) {
+ assertThat(
+ String.format("The last message in the conversation does not contain the expected mention @%s",
+ name), getSuggestedMentionsList().isRecentMention(name));
+ } else {
+ assertThat(
+ String.format("The last message in the conversation contains the mention @'%s' while this is not expected",
+ name), getSuggestedMentionsList().isNotRecentMention(name));
+ }
+ }
+ }
+
+ /**
+ * Checks for icon in the suggestion list
+ *
+ * @param iconName the expected icon
+ * @param name the name of the user that has the icon
+ */
+ @Then("^I see the (verified|guest|external) icon in the suggestions list for user (.*)$")
+ public void iSeeIconInSuggestionsList(String iconName, String name) {
+ name = context.getUsersManager()
+ .replaceAliasesOccurrences(name, ClientUsersManager.FindBy.NAME_ALIAS);
+ switch (iconName.toLowerCase()) {
+ case "verified":
+ assertThat(String.format("The verified icon is not visible for user '%s' in the suggested mentions list",
+ name), getSuggestedMentionsList().isVerifiedLabelVisibleFor(name));
+ break;
+ case "guest":
+ assertThat(String.format("The guest icon is not visible for user '%s' in the suggested mentions list",
+ name), getSuggestedMentionsList().isGuestLabelVisibleFor(name));
+ break;
+ case "external":
+ assertThat(String.format("The external icon is not visible for user '%s' in the suggested mentions list",
+ name), getSuggestedMentionsList().isExternalLabelVisibleFor(name));
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("Unknown view type: %s", iconName));
+ }
+ }
+
+ /**
+ * Check if the suggestion list is visible
+ * @param doNotSee whether or not the suggestion list should be visible
+ */
+ @Then("^I (do not )?see the suggested mentions list$")
+ public void iSeeTheSuggestedMentionsList(String doNotSee) {
+ if (doNotSee == null) {
+ assertThat("Suggestion list is invisble while this is not expected", getSuggestedMentionsList().isSuggestionsVisible());
+ } else {
+ assertThat("Suggestion list is visbile while this is not expected", !getSuggestedMentionsList().isSuggestionsVisible());
+ }
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/MessageDetailsPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/MessageDetailsPageSteps.java
new file mode 100644
index 00000000000..066ff13a4a3
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/MessageDetailsPageSteps.java
@@ -0,0 +1,34 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.common.usrmgmt.ClientUsersManager;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.MessageDetailsPage;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class MessageDetailsPageSteps {
+ IOSTestContext context;
+
+ public MessageDetailsPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private MessageDetailsPage getMessageDetailsPage() {
+ return context.getPagesCollection().getPage(MessageDetailsPage.class);
+ }
+
+ @When("^I close the message details$")
+ public void ICloseTheMessageDetails() {
+ getMessageDetailsPage().tapCloseButton();
+ }
+
+ @Then("^I see user (.*) in the Seen list$")
+ public void ISeeUserInMessageDetailSeenPage(String name) {
+ name = context.getUsersManager()
+ .replaceAliasesOccurrences(name, ClientUsersManager.FindBy.NAME_ALIAS);
+ assertThat(String.format("User name '%s' is not visible in Message Detail Seen list", name),
+ getMessageDetailsPage().isContactVisibleInSeenTab(name));
+ }
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/MoveToCustomFolderSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/MoveToCustomFolderSteps.java
new file mode 100644
index 00000000000..8decdef8d6e
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/MoveToCustomFolderSteps.java
@@ -0,0 +1,41 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.pages.MoveToCustomFolderPage;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import io.cucumber.java.en.Given;
+import io.cucumber.java.en.When;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class MoveToCustomFolderSteps {
+ IOSTestContext context;
+
+ public MoveToCustomFolderSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private MoveToCustomFolderPage getMoveToCustomFolderPage() {
+ return context.getPagesCollection().getPage(MoveToCustomFolderPage.class);
+ }
+
+ @Given("^I (do not )?see Move to Custom Folder page$")
+ public void ISeeMoveToPage(String shouldNotSee) {
+ if (shouldNotSee == null) {
+ assertThat("Move to Custom Folder page is not visible after the timeout",
+ getMoveToCustomFolderPage().isVisible());
+ } else {
+ assertThat("Move to Custom Folder page is still visible after the timeout",
+ getMoveToCustomFolderPage().isInvisible());
+ }
+ }
+
+ @When("^I tap Create button on Custom Folder page$")
+ public void iTapCreateNewButton() {
+ getMoveToCustomFolderPage().tapCreateNewButton();
+ }
+
+ @When("^I tap folder \"(.*)\" on Custom Folder page$")
+ public void iTapACustomFolder(String folderName) {
+ getMoveToCustomFolderPage().tapOnACustomFolder(folderName);
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/PasteDialogSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/PasteDialogSteps.java
new file mode 100644
index 00000000000..7f07e42d0b7
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/PasteDialogSteps.java
@@ -0,0 +1,20 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.PasteDialog;
+import io.cucumber.java.en.When;
+
+public class PasteDialogSteps {
+
+ private IOSTestContext context;
+
+ public PasteDialogSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ @When("I tap OK button on paste dialog")
+ public void iTapOKButton() {
+ context.getPagesCollection().getPage(PasteDialog.class).tapOKButton();
+ }
+
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/PicturePreviewPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/PicturePreviewPageSteps.java
new file mode 100644
index 00000000000..eee7a8c7c85
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/PicturePreviewPageSteps.java
@@ -0,0 +1,37 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.PicturePreviewPage;
+import io.cucumber.java.en.When;
+
+public class PicturePreviewPageSteps {
+ IOSTestContext context;
+
+ public PicturePreviewPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private PicturePreviewPage getPicturePreviewPage() {
+ return context.getPagesCollection().getPage(PicturePreviewPage.class);
+ }
+
+ @When("^I tap Confirm button on Picture [Pp]review page$")
+ public void iTapConfirmButton() {
+ getPicturePreviewPage().tapOkButton();
+ }
+
+ @When("^I tap Sketch button on Picture [Pp]review page$")
+ public void ITapOnSketchButton() {
+ getPicturePreviewPage().tapSketchButton();
+ }
+
+ @When("^I tap Use Photo button on Picture [Pp]review page$")
+ public void ITapOnPhotoButton() {
+ getPicturePreviewPage().tapPhotoButton();
+ }
+
+ @When("^I tap OK button on Picture [Pp]review page on iPAD$")
+ public void ITapOnOkButtonOnIPad() {
+ getPicturePreviewPage().tapOkButtonOnIPad();
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/PollMessagesSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/PollMessagesSteps.java
new file mode 100644
index 00000000000..268cf73c12b
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/PollMessagesSteps.java
@@ -0,0 +1,64 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.PollMessagesPage;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class PollMessagesSteps {
+
+ IOSTestContext context;
+
+ public PollMessagesSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private PollMessagesPage getPollMessagesPage() {
+ return context.getPagesCollection().getPage(PollMessagesPage.class);
+ }
+
+ @Then("^I see the poll message contains text \"([^\"]*)\"$")
+ public void iSeeThePollMessageContainsText(String text) {
+ assertThat("Poll message text is incorrect.", getPollMessagesPage().isPollMessageTextContains(text));
+ }
+
+ @Then("^I see all the poll buttons are in unselected state$")
+ public void iSeeAllThePollButtonsAreInUnselectedState() {
+ assertThat("All poll buttons are not in unselected mode.", getPollMessagesPage().areAllPollButtonsUnselected());
+ }
+
+ @When("^I tap poll button with the text \"([^\"]*)\"$")
+ public void iTapPollButtonWithTheText(String text) {
+ getPollMessagesPage().tapPollButtonWithTheText(text);
+ }
+
+ @Then("^I see the poll button with the text \"([^\"]*)\" is Confirmed$")
+ public void iSeeThePollButtonWithTheTextIsSelected(String buttonText) {
+ assertThat("State of the poll button with text \"" + buttonText + "\" is not confirmed.", getPollMessagesPage().isPollButtonWithTextConfirmed(buttonText));
+ }
+
+ @Then("^I see the poll button with the text \"([^\"]*)\" is Unselected$")
+ public void iSeeThePollButtonWithTheTextIsUnselected(String buttonText) {
+ assertThat("State of the poll button with text \"" + buttonText + "\" is selected but it should not be.", getPollMessagesPage().isPollButtonWithTextUnselected(buttonText));
+
+ }
+
+ @Then("^I see the poll button with the text \"([^\"]*)\" is Selected$")
+ public void iSeeThePollButtonWithTheTextIsLoading(String buttonText) {
+ assertThat("State of the poll button with text \"" + buttonText + "\" is not selected.", getPollMessagesPage().isPollButtonWithTextSelected(buttonText));
+ }
+
+
+ @Then("^I (do not )?see error contains \"(.*)\" under the poll button$")
+ public void iSeeErrorUnderButtonInPollMessage(String shouldNotSee, String error) {
+ if (shouldNotSee == null) {
+ assertThat("Wrong error message is \" + error + \" + not visible",
+ getPollMessagesPage().isPollErrorMessageVisible(error));
+ } else {
+ assertThat("Wrong error message is \" + error + \" + not visible",
+ getPollMessagesPage().isPollErrorMessageInvisible(error));
+ }
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ReactionsEditMenuPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ReactionsEditMenuPageSteps.java
new file mode 100644
index 00000000000..d938ef33f89
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/ReactionsEditMenuPageSteps.java
@@ -0,0 +1,154 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.ReactionsEditMenuPage;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class ReactionsEditMenuPageSteps {
+
+ IOSTestContext context;
+
+ public ReactionsEditMenuPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ @Then("^I see menu with quick reactions and other items$")
+ public void iSeeMenu() {
+ assertThat("Menu with quick reactions and other items is not visible",
+ context.getPagesCollection().getPage(ReactionsEditMenuPage.class).isVisible());
+ }
+
+ @When("^I tap on (.*) reaction in quick reactions$")
+ public void iTapOnReaction(String reaction) {
+ context.getPagesCollection().getPage(ReactionsEditMenuPage.class).iTapQuickReaction(reaction);
+ }
+
+ @When("^I do not see forward button on edit menu$")
+ public void iDontSeeForward() {
+ assertThat("Forward button > still visible",
+ context.getPagesCollection().getPage(ReactionsEditMenuPage.class).isForwardButtonInvisible());
+ }
+
+ @When("^I tap on Copy on edit menu$")
+ public void iTapCopy() {
+ context.getPagesCollection().getPage(ReactionsEditMenuPage.class).tapCopy();
+ }
+
+ @Then("^I see Copy on edit menu$")
+ public void iSeeCopy() {
+ assertThat("Edit menu item not visible",
+ context.getPagesCollection().getPage(ReactionsEditMenuPage.class).isCopyVisible());
+ }
+
+ @Then("^I do not see Copy on edit menu$")
+ public void iDoNotSeeCopy() {
+ assertThat("Edit menu item still visible",
+ context.getPagesCollection().getPage(ReactionsEditMenuPage.class).isCopyInvisible());
+ }
+
+ @When("^I tap on Delete on edit menu$")
+ public void iTapDelete() {
+ context.getPagesCollection().getPage(ReactionsEditMenuPage.class).tapDelete();
+ }
+
+ @Then("^I see Delete on edit menu$")
+ public void iSeeDelete() {
+ assertThat("Edit menu item not visible",
+ context.getPagesCollection().getPage(ReactionsEditMenuPage.class).isDeleteVisible());
+ }
+
+ @When("^I tap on Details on edit menu$")
+ public void iTapDetails() {
+ context.getPagesCollection().getPage(ReactionsEditMenuPage.class).tapDetails();
+ }
+
+ @When("^I tap on Download on edit menu$")
+ public void iTapDownload() {
+ context.getPagesCollection().getPage(ReactionsEditMenuPage.class).tapDownload();
+ }
+
+ @Then("^I do not see Download on edit menu$")
+ public void iDoNotSeeDownload() {
+ assertThat("Edit menu item still visible",
+ context.getPagesCollection().getPage(ReactionsEditMenuPage.class).isDownloadInvisible());
+ }
+
+ @When("^I tap on Edit on edit menu$")
+ public void iTapEdit() {
+ context.getPagesCollection().getPage(ReactionsEditMenuPage.class).tapEdit();
+ }
+
+ @When("^I tap on Like on edit menu$")
+ public void iTapLike() {
+ context.getPagesCollection().getPage(ReactionsEditMenuPage.class).tapLike();
+ }
+
+ @When("^I tap on Paste on edit menu$")
+ public void iTapPaste() {
+ context.getPagesCollection().getPage(ReactionsEditMenuPage.class).tapPaste();
+ }
+
+ @Then("^I do not see Paste on edit menu$")
+ public void iDoNotSeePaste() {
+ assertThat("Edit menu item still visible",
+ context.getPagesCollection().getPage(ReactionsEditMenuPage.class).isPasteInvisible());
+ }
+
+ @When("^I tap on Reply on edit menu$")
+ public void iTapReply() {
+ context.getPagesCollection().getPage(ReactionsEditMenuPage.class).tapReply();
+ }
+
+ @Then("^I see Reply on edit menu$")
+ public void iSeeReply() {
+ assertThat("Edit menu item not visible",
+ context.getPagesCollection().getPage(ReactionsEditMenuPage.class).isReplyVisible());
+ }
+
+ @When("^I tap on Save on edit menu$")
+ public void iTapSave() {
+ context.getPagesCollection().getPage(ReactionsEditMenuPage.class).tapSave();
+ }
+
+ @Then("^I see Save on edit menu$")
+ public void iSeeSave() {
+ assertThat("Edit menu item not visible",
+ context.getPagesCollection().getPage(ReactionsEditMenuPage.class).isSaveVisible());
+ }
+
+ @Then("^I do not see Save on edit menu$")
+ public void iDoNotSeeSave() {
+ assertThat("Edit menu item still visible",
+ context.getPagesCollection().getPage(ReactionsEditMenuPage.class).isSaveInvisible());
+ }
+
+ @When("^I tap on Select All on edit menu$")
+ public void iTapSelectAll() {
+ context.getPagesCollection().getPage(ReactionsEditMenuPage.class).tapSelectAll();
+ }
+
+ @When("^I tap on Share on edit menu$")
+ public void iTapShare() {
+ context.getPagesCollection().getPage(ReactionsEditMenuPage.class).tapShare();
+ }
+
+ @Then("^I see Share on edit menu$")
+ public void iSeeShare() {
+ assertThat("Edit menu item not visible",
+ context.getPagesCollection().getPage(ReactionsEditMenuPage.class).isShareVisible());
+ }
+
+ @Then("^I do not see Share on edit menu$")
+ public void iDoNotSeeShare() {
+ assertThat("Edit menu item still visible",
+ context.getPagesCollection().getPage(ReactionsEditMenuPage.class).isShareInvisible());
+ }
+
+ @When("^I tap on Cancel on edit menu$")
+ public void iTapCancel() {
+ context.getPagesCollection().getPage(ReactionsEditMenuPage.class).tapCancel();
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/RegistrationPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/RegistrationPageSteps.java
new file mode 100644
index 00000000000..af1aa234c0c
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/RegistrationPageSteps.java
@@ -0,0 +1,207 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.common.backend.BackendConnections;
+import com.wearezeta.auto.common.email.messages.ActivationMessage;
+import com.wearezeta.auto.common.email.messages.VerificationMessage;
+import com.wearezeta.auto.common.email.messages.WireMessage;
+import com.wearezeta.auto.common.email.handlers.ISupportsMessagesPolling;
+import com.wearezeta.auto.common.email.MailboxProvider;
+import com.wearezeta.auto.common.misc.Timedelta;
+import com.wearezeta.auto.common.usrmgmt.ClientUser;
+import com.wearezeta.auto.common.usrmgmt.ClientUsersManager;
+import com.wearezeta.auto.common.usrmgmt.NoSuchUserException;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.IOSPage;
+import com.wearezeta.auto.ios.pages.RegistrationPage;
+import com.wearezeta.auto.ios.pages.team_creation.TCVerificationCodePage;
+import io.cucumber.java.en.Given;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class RegistrationPageSteps {
+ IOSTestContext context;
+
+ public RegistrationPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private RegistrationPage getRegistrationPage() {
+ return context.getPagesCollection().getPage(RegistrationPage.class);
+ }
+
+ private TCVerificationCodePage getVerificationCodePage() {
+ return context.getPagesCollection().getPage(TCVerificationCodePage.class);
+ }
+
+ private IOSPage getIOSPage() {
+ return context.getPagesCollection().getPage(IOSPage.class);
+ }
+
+ /**
+ * Verifies whether registration screen is the current screen
+ */
+ @Given("I see registration screen")
+ public void ISeeRegistrationScreen() {
+ assertThat("Registration screen is not visible", getRegistrationPage().isVisible());
+ }
+
+ @Then("I see password failure message")
+ public void ISeePasswordFailureMessage() {
+ assertThat("password failure message is not visible", getRegistrationPage().isPasswordFailureVisible());
+ }
+
+ @Given("I see password rules")
+ public void ISeePasswordRules() {
+ assertThat("password rules is not visible", getRegistrationPage().isPasswordRulesVisible());
+ }
+
+ /**
+ * Click on I AGREE button to accept terms of service
+ */
+ @When("^I accept terms of service$")
+ public void IAcceptTermsOfService() {
+ getRegistrationPage().clickAcceptTOCButton();
+ }
+
+ @When("^I enter activation code for the email address of (.*)")
+ public void iEnterActivationCodeForEmailString(String user) {
+ final ClientUser clientUser = context.getUsersManager().findUserByNameOrNameAlias(user);
+ getRegistrationPage().inputActivationCode(clientUser);
+ }
+
+ @When("^I enter registration name \"(.*)\"$")
+ public void IEnterName(String name) {
+ try {
+ context.setUserToRegister(context.getUsersManager().findUserByNameOrNameAlias(name));
+ getRegistrationPage().typeName(context.getUserToRegister().getName());
+ } catch (NoSuchUserException e) {
+ getRegistrationPage().typeName(name);
+ }
+ }
+
+ @When("^I input (custom )?name (.*) and commit it$")
+ public void IInputNameAndCommit(String isCustom, String name) {
+ if (isCustom == null) {
+ IEnterName(name);
+ } else {
+ getRegistrationPage().typeName(name);
+ }
+ getRegistrationPage().tapNameConfirmButton();
+ }
+
+ @When("^I set the username to (.*)$")
+ public void IEnterUsername(String name) {
+ context.setUserToRegister(context.getUsersManager().findUserByUniqueUsernameAlias(name));
+ getRegistrationPage().typeUsername(context.getUserToRegister().getUniqueUsername());
+ getRegistrationPage().tapUsernameConfirmButton();
+ }
+
+ @When("^I enter registration email \"(.*)\"$")
+ public void IEnterEmail(String email) {
+ try {
+ context.setUserToRegister(context.getUsersManager().findUserByEmailOrEmailAlias(email));
+ getRegistrationPage().typeEmail(context.getUserToRegister().getEmail());
+ } catch (NoSuchUserException e) {
+ getRegistrationPage().typeEmail(email);
+ }
+ getRegistrationPage().tapNameConfirmButton();
+ }
+
+ @When("^I set the password to \"(.*)\"$")
+ public void IEnterPassword(String password) {
+ try {
+ context.setUserToRegister(context.getUsersManager().findUserByPasswordAlias(password));
+ getRegistrationPage().typePassword(context.getUserToRegister().getPassword());
+ } catch (NoSuchUserException e) {
+ getRegistrationPage().typePassword(password);
+ }
+ getRegistrationPage().tapPasswordConfirmButton();
+ }
+
+ @When("^I clear password input$")
+ public void IClearPasswordInput() {
+ getRegistrationPage().clearPasswordInput();
+ }
+
+ /**
+ * Start monitoring thread for activation email for the particular mailbox
+ *
+ * @param mbox mailbox email address/an alias
+ */
+ @When("^I start activation email monitoring on mailbox (.*)")
+ public void IStartActivationEmailMonitoringOnMbox(String mbox) throws Exception {
+ ClientUser user = context.getUsersManager().findUserByEmailOrEmailAlias(mbox);
+
+ final Map expectedHeaders = new HashMap<>();
+ expectedHeaders.put(WireMessage.ZETA_PURPOSE_HEADER_NAME, ActivationMessage.MESSAGE_PURPOSE);
+ ISupportsMessagesPolling mailbox = MailboxProvider.getInstance(BackendConnections.get(user), user.getEmail());
+ context.setActivationMessage(mailbox.getMessage(expectedHeaders, ActivationMessage.ACTIVATION_TIMEOUT));
+ }
+
+ @When("^I start verification email monitoring on mailbox (.*)")
+ public void IStartVerificationEmailMonitoringOnMbox(String mbox) throws Exception {
+ ClientUser user = context.getUsersManager().findUserByEmailOrName(mbox);
+ getIOSPage().startVerificationEmailMonitoring(user, context);
+ }
+
+ @When("^I enter verification code from Email$")
+ public void ICheckVerificationCodeInSubjectAndBody() throws Exception {
+ VerificationMessage verificationInfo = new VerificationMessage(context.getVerificationMessage().get());
+ getVerificationCodePage().enterVerificationCode(verificationInfo.getXZetaCode());
+
+ }
+
+ @When("^I wait until (\\d) mails arrived for (.*)$")
+ public void IWaitUntilXMailsArrived(int count, String emailAlias) throws Exception {
+ ClientUser user = context.getUsersManager().findUserByEmailOrEmailAlias(emailAlias);
+ context.startPinging();
+ try {
+ ISupportsMessagesPolling mbox = MailboxProvider.getInstance(BackendConnections.get(user), user.getEmail());
+ mbox.waitUntilMessagesCountReaches(user.getEmail(), count, Timedelta.ofMillis(0));
+ } finally {
+ context.stopPinging();
+ }
+ }
+
+ /**
+ * Activate email address using activation keys as soon as the corresponding message is received.
+ * This steps expects mailbox monitoring to be already running
+ *
+ * @param address the expected email address for a user
+ * @param user user name/alias
+ */
+ @Then("^I verify email address (.*) for (.*)")
+ public void IVerifyEmail(String address, String user) throws Exception {
+ if (context.getActivationMessage() == null) {
+ throw new IllegalStateException("Activation email monitoring is expected to be running");
+ }
+ context.getCommonSteps().activateRegisteredUserByEmail(context.getActivationMessage());
+ address = context.getUsersManager()
+ .replaceAliasesOccurrences(address, ClientUsersManager.FindBy.EMAIL_ALIAS);
+ final ClientUser dstUser = context.getUsersManager()
+ .findUserByNameOrNameAlias(user);
+ dstUser.setEmail(address);
+ context.setActivationMessage(null);
+ }
+
+ /**
+ * Verifies that the email verification reminder on the login page is
+ * displayed
+ */
+ @Then("^I see email verification reminder$")
+ public void ISeeEmailVerificationReminder() {
+ assertThat("Prompt not visible", getRegistrationPage().isEmailVerificationPromptVisible());
+ }
+
+ /**
+ * Taps back button on registration screen
+ */
+ @When("^I tap Back button on Registration page$")
+ public void ITapBackButtonOnRegistrationPage() {
+ getRegistrationPage().tapBackButton();
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/SearchUIPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/SearchUIPageSteps.java
new file mode 100644
index 00000000000..35ee86cbe9c
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/SearchUIPageSteps.java
@@ -0,0 +1,243 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.common.backend.BackendConnections;
+import com.wearezeta.auto.common.usrmgmt.ClientUser;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import io.cucumber.java.en.Given;
+import com.wearezeta.auto.common.usrmgmt.ClientUsersManager;
+import com.wearezeta.auto.ios.pages.SearchUIPage;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.not;
+
+public class SearchUIPageSteps {
+ IOSTestContext context;
+
+ public SearchUIPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private SearchUIPage getSearchUIPage() {
+ return context.getPagesCollection()
+ .getPage(SearchUIPage.class);
+ }
+
+ /**
+ * Type in text in Search input field
+ *
+ * @param text text to input
+ * @param isUpper null if should be input as it is
+ * @param shouldClearBeforeInput equals to null if the field should not be cleared first
+ */
+ @When("^I type \"(.*)\" in (cleared )?Search UI input field( in upper case)?$")
+ public void ITypeInSearchInput(String text, String shouldClearBeforeInput, String isUpper) {
+ text = context.getUsersManager()
+ .replaceAliasesOccurrences(text, ClientUsersManager.FindBy.NAME_ALIAS,
+ ClientUsersManager.FindBy.EMAIL_ALIAS, ClientUsersManager.FindBy.UNIQUE_USERNAME_ALIAS);
+ getSearchUIPage().typeSearchQuery((isUpper == null) ? text : text.toUpperCase(),
+ shouldClearBeforeInput != null);
+ }
+
+ @When("^I search user (.*) by email in Search UI input field$")
+ public void iSearchByEmail(String userAlias) {
+ ClientUser user = context.getUsersManager().findUserByNameOrNameAlias(userAlias);
+ getSearchUIPage().typeSearchQuery(user.getEmail(), false);
+ }
+
+ @When("^I search user (.*) by handle and domain in Search UI input field$")
+ public void iSearchByHandleAndDomain(String userAlias) {
+ ClientUser user = context.getUsersManager().findUserByNameOrNameAlias(userAlias);
+ String domain = BackendConnections.get(user).getDomain();
+ getSearchUIPage().typeSearchQuery(user.getUniqueUsername() + "@" + domain, false);
+ }
+
+ @When("^I clear Search UI input field$")
+ public void iClearSearchInput() {
+ getSearchUIPage().clearSearchInput();
+ }
+
+ @When("^I enter unique username with backend domain of user (.*) in (cleared )?Search UI input field$")
+ public void ITypeUniqueUsernameAndDomainOfUser(String userAlias, String shouldClearBeforeInput) {
+ ClientUser user = context.getUsersManager().findUserByNameOrNameAlias(userAlias);
+ String backendName = BackendConnections.get(user).getDomain();
+
+ getSearchUIPage().typeSearchQuery(String.format("%s@%s", user.getUniqueUsername(), backendName), shouldClearBeforeInput != null);
+ }
+
+ /**
+ * Fills in search field pointed amount of letters from username/conversation starting from the first one
+ *
+ * @param count amount of letters to be input
+ * @param name user name
+ * @param shouldBeCleared equals to null oif the input field should not be cleaned before input
+ */
+ @When("^I type first (\\d+) letters? of (?:user|conversation) name \"(.*)\" into (cleared )?Search UI input field$")
+ public void ITypeXLettersIntoSearchInput(int count, String name, String shouldBeCleared) {
+ name = context.getUsersManager()
+ .replaceAliasesOccurrences(name, ClientUsersManager.FindBy.NAME_ALIAS,
+ ClientUsersManager.FindBy.UNIQUE_USERNAME_ALIAS);
+ if (name.length() > count) {
+ getSearchUIPage().typeSearchQuery(name.substring(0, count), shouldBeCleared != null);
+ } else {
+ throw new IllegalArgumentException(String.format("Name is only %s chars length. Put in step a less value",
+ name.length()));
+ }
+ }
+
+ /**
+ * Verify that conversation or service is presented in search results
+ *
+ * @param name conversation or service name to search
+ * @param shouldNotExist equals to null if the conversation should be visible
+ * @param times defines count of entities in search result if set
+ */
+ @When("^I see the (?:conversation|service) \"(.*)\" (does not )?exists? (\\d+ times? )?in Search results$")
+ public void ISeeConversationIsFoundInSearchResult(String name, String shouldNotExist,
+ String times) {
+ name = context.getUsersManager()
+ .replaceAliasesOccurrences(name, ClientUsersManager.FindBy.NAME_ALIAS);
+ int expectedCount = 1;
+ if (times != null) {
+ expectedCount = Integer.parseInt(times.replaceAll("[\\D]", ""));
+ }
+ if (expectedCount == 1) {
+ if (shouldNotExist == null) {
+ assertThat(String.format("The conversation '%s' does not exist in Search results", name),
+ getSearchUIPage().isElementFoundInSearch(name));
+ } else {
+ assertThat(
+ String.format("The conversation '%s' exists in Search results, but it should not", name),
+ getSearchUIPage().isElementNotFoundInSearch(name));
+ }
+ } else {
+ final int actualCount = getSearchUIPage().getOccurrencesCount(name);
+ if (shouldNotExist == null) {
+ assertThat(String.format("The conversation '%s' should occur %d times in Search result",
+ name, expectedCount), actualCount, equalTo(expectedCount));
+ } else {
+ assertThat(String.format("The conversation '%s' should not occur %d times in Search result",
+ name, expectedCount), actualCount, not(equalTo(expectedCount)));
+ }
+ }
+ }
+
+ /**
+ * (Un)Select pointed amount of avatars from top people in a row starting from the first one
+ *
+ * @param count amount of avatars that should be (un)selected
+ */
+ @Then("^I (?:unselect|select) (\\d+) avatars? from Top connections$")
+ public void ISelectTopConnectionAvatars(int count) {
+ getSearchUIPage().tapTopConnectionsAvatars(count);
+ }
+
+ /**
+ * Tap on top connection contact avatar by pointed id order
+ *
+ * @param position contact position in top people. Starts from 1
+ */
+ @When("^I tap the (\\d+)\\w+ avatar in Top connections$")
+ public void IClickOnTopConnectionByOrder(int position) {
+ getSearchUIPage().tapOnTopConnectionAvatarByOrder(position);
+ }
+
+ /**
+ * Click on conversation or service in search result with pointed name
+ *
+ * @param name conversation or service name
+ */
+ @When("^I tap on (?:conversation|service) (.*) in search result$")
+ public void ITapOnConversationFromSearch(String name) {
+ name = context.getUsersManager()
+ .replaceAliasesOccurrences(name, ClientUsersManager.FindBy.NAME_ALIAS);
+ getSearchUIPage().selectElementInSearchResults(name);
+ }
+
+ @When("^I tap Create Group button on Search UI page$")
+ public void ITapCreateGroupButton() {
+ getSearchUIPage().tapCreateGroupButton();
+ }
+
+ @When("^I tap X button on Search UI page$")
+ public void ITapCloseButton() {
+ getSearchUIPage().tapCloseButton();
+ }
+
+ @When("^I tap Send Invite button on Search UI page$")
+ public void ITapSendInviteButton() {
+ getSearchUIPage().tapSendInviteButton();
+ }
+
+ @When("^I tap Copy Invite button on Search UI page$")
+ public void ITapCopyInviteButton() {
+ getSearchUIPage().tapCopyInviteButton();
+ }
+
+ @Then("^I (do not )?see Create Group button on Search UI page$")
+ public void ISeeCreateGroupButton(String shouldNotSee) {
+ if (shouldNotSee == null) {
+ assertThat("The button Create Group is expected to be visible",
+ getSearchUIPage().isCreateGroupButtonVisible());
+ } else {
+ assertThat("The button Create Group is expected to be invisible",
+ getSearchUIPage().isCreateGroupButtonInvisible());
+ }
+ }
+
+ @Then("^I (do not )?see Create Guest Room button on Search UI page$")
+ public void ISeeCreateGuestRoomButton(String shouldNotSee) {
+ if (shouldNotSee == null) {
+ assertThat("The button Create Guest Room is expected to be visible",
+ getSearchUIPage().isCreateGuestRoomButtonVisible());
+ } else {
+ assertThat("The button Create Guest Room is expected to be invisible",
+ getSearchUIPage().isCreateGuestRoomButtonInvisible());
+ }
+ }
+
+ /**
+ * Presses the instant connect plus button
+ *
+ * @param nameAlias user name/aias
+ */
+ @When("^I tap the instant connect button next to (.*)")
+ public void ITapInstantConnectButton(String nameAlias) {
+ nameAlias = context.getUsersManager()
+ .replaceAliasesOccurrences(nameAlias, ClientUsersManager.FindBy.NAME_ALIAS);
+ getSearchUIPage().tapInstantConnectButton(nameAlias);
+ }
+
+ @Given("^I (do not )?see the service \"([^\"]*)\" exists in service search results$")
+ public void iSeeTheServiceExistsInServiceSearchResults(String shouldNotSee, String serviceName) {
+ if (shouldNotSee == null) {
+ assertThat("Service " + serviceName + " does not exist in search result.",
+ getSearchUIPage().isServiceVisibleInSearchResult(serviceName));
+ } else {
+ assertThat("Service \" + serviceName + \" exist in search result but it should not.",
+ getSearchUIPage().isServiceInVisibleInSearchResult(serviceName));
+ }
+ }
+
+ @Given("^I tap on service \"([^\"]*)\" in service search result$")
+ public void iTapOnServiceServiceNameInServiceSearchResult(String serviceName) {
+ getSearchUIPage().tapOnService(serviceName);
+ }
+
+ @Then ("^I open create group screen$")
+ public void iOpenCreateGroupScreen() {
+ getSearchUIPage().iOpenCreateGroupScreen();
+ }
+ @Then("^I (do not )?see contact (.*) in Search UI$")
+ public void iSeeContactInSearchUI(String shouldNotSee, String name) {
+ name = context.getUsersManager()
+ .replaceAliasesOccurrences(name, ClientUsersManager.FindBy.NAME_ALIAS);
+ if (shouldNotSee == null) {
+ assertThat(String.format("The contact '%s' is not visible", name), getSearchUIPage().isContactVisible(name));
+ } else {
+ assertThat(String.format("The contact '%s' is visible while it should not be", name), getSearchUIPage().isContactInvisible(name));
+ }
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/SketchPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/SketchPageSteps.java
new file mode 100644
index 00000000000..f3b125e7328
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/SketchPageSteps.java
@@ -0,0 +1,32 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.SketchPage;
+
+import io.cucumber.java.en.When;
+
+public class SketchPageSteps {
+ IOSTestContext context;
+
+ public SketchPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private SketchPage getSketchPage() {
+ return context.getPagesCollection()
+ .getPage(SketchPage.class);
+ }
+
+ /**
+ * randomly draws lines in sketch feature
+ */
+ @When("^I draw a random sketch$")
+ public void IDrawRandomSketches() {
+ getSketchPage().sketchRandomLines();
+ }
+
+ @When("^I tap Send button on Sketch page$")
+ public void ITapSendButton() {
+ getSketchPage().tapSendButton();
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/StatusActionSheetSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/StatusActionSheetSteps.java
new file mode 100644
index 00000000000..ae99a94df37
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/StatusActionSheetSteps.java
@@ -0,0 +1,28 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.StatusActionSheetPage;
+import io.cucumber.java.en.Then;
+
+public class StatusActionSheetSteps {
+ IOSTestContext context;
+
+ public StatusActionSheetSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private StatusActionSheetPage getStatusActionSheetPage() {
+ return context.getPagesCollection()
+ .getPage(StatusActionSheetPage.class);
+ }
+
+ /**
+ * I tap the status that I want to set my profile to
+ *
+ * @param statusName the name of the status that I want (None, Available, Busy or Away)
+ */
+ @Then("^I tap status (None|Available|Busy|Away)$")
+ public void iTapMyNameInConversationView(String statusName) {
+ getStatusActionSheetPage().tapStatusName(statusName);
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/TestServiceSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/TestServiceSteps.java
new file mode 100644
index 00000000000..abb96028b76
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/TestServiceSteps.java
@@ -0,0 +1,343 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.common.CommonSteps;
+import com.wearezeta.auto.common.CommonUtils;
+import com.wearezeta.auto.common.Config;
+import com.wearezeta.auto.common.backend.models.ReactionType;
+import com.wearezeta.auto.common.log.ZetaLogger;
+import com.wearezeta.auto.common.misc.EphemeralTimeConverter;
+import com.wearezeta.auto.common.testservice.models.LegalHoldStatus;
+import com.wearezeta.auto.common.imagecomparator.QRCode;
+import com.wearezeta.auto.common.misc.Timedelta;
+import com.wearezeta.auto.common.usrmgmt.ClientUsersManager;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import io.cucumber.java.en.Given;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.file.Files;
+import java.util.concurrent.*;
+import java.util.logging.Logger;
+
+import static com.wearezeta.auto.common.CommonSteps.*;
+
+public class TestServiceSteps {
+
+ private static final Logger log = ZetaLogger.getLog(TestServiceSteps.class.getSimpleName());
+ IOSTestContext context;
+
+ public TestServiceSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private CommonSteps getCommonSteps() {
+ return context.getCommonSteps();
+ }
+
+ private ClientUsersManager getUsersManager() {
+ return context.getUsersManager();
+ }
+
+ @When("^User (.*) deletes? (everywhere )?the recent message from (?:user|group conversation) (.*)$")
+ public void UserXDeleteLastMessage(String userNameAlias, String deleteEverywhere, String dstNameAlias) {
+ getCommonSteps().userDeletesLatestMessage(userNameAlias, dstNameAlias, null,
+ deleteEverywhere != null);
+ }
+
+ @When("^User (.*) shares? the default location to (?:user|group conversation) (.*) via device (.*)")
+ public void UserXSharesLocationTo(String senderAlias, String convoName, String deviceName) {
+ getCommonSteps().userSendsLocationToConversation(senderAlias, convoName, deviceName,
+ context.getSelfDeletingMessageTimeout(senderAlias, convoName),
+ 0, 0, "location", 1);
+ }
+
+ @When("^User (.*) sends (.*) sized file with MIME type (.*) and name (.*)(?: via device (.*))? to conversation (.*)$")
+ public void iXSizedSendFile(String contact, String size, String mimeType, String fileName, String deviceName,
+ String dstConvoName) throws Exception {
+ String path = Files.createTempDirectory("zautomation")
+ .toAbsolutePath().toString().replace("%40", "@");
+ RandomAccessFile f = new RandomAccessFile(path + "/" + fileName, "rws");
+ int fileSize = Integer.valueOf(size.replaceAll("\\D+", "").trim());
+ if (size.contains("MB")) {
+ f.setLength(fileSize * 1024 * 1024);
+ } else if (size.contains("KB")) {
+ f.setLength(fileSize * 1024);
+ } else {
+ f.setLength(fileSize);
+ }
+ f.close();
+ context.startPinging();
+ context.getCommonSteps().userSendsFileToConversation(contact, dstConvoName,
+ deviceName, context.getCommonSteps().getEphemeralTimeout(contact, dstConvoName),
+ path + "/" + fileName, mimeType);
+ context.stopPinging();
+ }
+
+ @Deprecated // Please use above step instead for sending "temporary" files
+ @When("^User (.*) sends? (temporary )?file (.*) having MIME type (.*) to (?:single user|group) conversation (.*) using " +
+ "device (.*)$")
+ public void UserSendsFile(String sender, String isTemporary, String fileName, String mimeType,
+ String convoName, String deviceName) {
+ String root;
+ if (isTemporary == null) {
+ if (mimeType.toLowerCase().contains("audio")) {
+ // send audio with metadata through ETS
+ root = Config.current().getAudioPath(getClass());
+ context.getCommonSteps().userSendsAudioToConversation(sender, convoName, deviceName,
+ context.getSelfDeletingMessageTimeout(sender, convoName), root + File.separator + fileName, mimeType, Timedelta.ofSeconds(15));
+ } else if (mimeType.toLowerCase().contains("video")) {
+ // Send video with metadata through ETS
+ root = Config.current().getVideoPath(getClass());
+ context.getCommonSteps().userSendsVideoToConversation(sender, convoName, deviceName,
+ context.getSelfDeletingMessageTimeout(sender, convoName), root + File.separator + fileName, mimeType, Timedelta.ofSeconds(15), new int[]{800,600});
+ } else {
+ //send file without ETS metadata
+ if (mimeType.toLowerCase().contains("image")) {
+ root = Config.current().getImagesPath(getClass());
+ } else {
+ root = Config.current().getAudioPath(getClass());
+ }
+ getCommonSteps().userSendsFileToConversation(sender, convoName, deviceName,
+ context.getSelfDeletingMessageTimeout(sender, convoName),root + File.separator + fileName,
+ mimeType);
+ }
+ } else {
+ // send temporary file
+ root = Config.current().getBuildPath(getClass());
+ getCommonSteps().userSendsFileToConversation(sender, convoName, deviceName,
+ context.getSelfDeletingMessageTimeout(sender, convoName), root + File.separator + fileName,
+ mimeType);
+ }
+ }
+
+ @Given("^Users? adds? the following devices?: (.*)")
+ public void UsersAddDevices(String mappingAsJson) {
+ getCommonSteps().usersAddDevices(mappingAsJson, false);
+ }
+
+ @Given("^Users? of team owned by (.*) adds? the following 2FA devices?: (.*)")
+ public void UsersAddDevices(String teamOwnerAlias, String mappingAsJson) {
+ getCommonSteps().usersAdd2FADevices(teamOwnerAlias, mappingAsJson);
+ }
+
+ @Given("^User (\\w+) pings conversation (.*)")
+ public void UserPingedConversation(String pingFromUserNameAlias, String dstConversationName) {
+ getCommonSteps().userPingsConversation(pingFromUserNameAlias, dstConversationName,
+ FIRST_AVAILABLE_DEVICE, context.getSelfDeletingMessageTimeout(pingFromUserNameAlias, dstConversationName));
+ }
+
+ @Given("^User (.*) sends? (\\d+) messages? using device (.*) to (?:user|group conversation) (.*)$")
+ public void userSendXMessagesToConversationUsingDevice(String msgFromUserNameAlias,
+ int msgsCount, String deviceName,
+ String conversationName) {
+ for (int i = 0; i < msgsCount; i++) {
+ getCommonSteps().userSendsGenericMessageToConversation(msgFromUserNameAlias, conversationName,
+ deviceName, context.getSelfDeletingMessageTimeout(msgFromUserNameAlias, conversationName),
+ DEFAULT_AUTOMATION_MESSAGE, LegalHoldStatus.DISABLED);
+ }
+ }
+
+ @Given("^User (.*) (sets|changes) the unique username( to \".*\")?$")
+ public void userSetsUniqueUsername(String userAs, String action, String uniqueUsername) {
+ switch (action.toLowerCase()) {
+ case "sets":
+ getCommonSteps().usersSetUniqueUsername(userAs);
+ break;
+ case "changes":
+ if (uniqueUsername == null) {
+ throw new IllegalArgumentException("Unique username is mandatory to set");
+ }
+ // Exclude quotes
+ uniqueUsername = uniqueUsername.substring(5, uniqueUsername.length() - 1);
+ uniqueUsername = getUsersManager().replaceAliasesOccurrences(uniqueUsername,
+ ClientUsersManager.FindBy.UNIQUE_USERNAME_ALIAS);
+ getCommonSteps().userChangesUniqueUsername(userAs, uniqueUsername);
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("Unknown action '%s'", action));
+
+ }
+ }
+
+ @When("^User (.*) (likes|unlikes|received) the recent message from (?:user|group conversation) (.*)$")
+ @Deprecated // Use step to toggle reaction instead
+ public void userReactLastMessage(String userNameAlias, String reactionType, String dstNameAlias) {
+ switch (reactionType.toLowerCase()) {
+ case "likes":
+ getCommonSteps().userReactsToLatestMessage(userNameAlias, dstNameAlias, null,
+ ReactionType.LIKE);
+ break;
+ case "unlikes":
+ getCommonSteps().userReactsToLatestMessage(userNameAlias, dstNameAlias, null,
+ ReactionType.UNLIKE);
+ break;
+ case "received":
+ getCommonSteps().userSendsDeliveryConfirmationForLastEphemeralMessage(userNameAlias, dstNameAlias, null);
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("Cannot identify the reaction type '%s'",
+ reactionType));
+ }
+ }
+
+ @Then("^User (.*) marks the recent message as read in conversation (.*) via device (.*)$")
+ public void userMarksMessageRead(String receiverAlias, String convoName, String deviceName) {
+ getCommonSteps().userSendsReadConfirmationForRecentMessage(receiverAlias, convoName, deviceName);
+ }
+
+ @Given("^User (.*) sends (\\d+) (image|video|audio|temporary) files? (.*) to conversation (.*)")
+ public void UserSendsMultiplePictures(String senderUserNameAlias, int count,
+ String fileType, String fileName,
+ String dstConversationName) {
+ getCommonSteps().userSendsMultipleMedias(senderUserNameAlias, dstConversationName,
+ context.getSelfDeletingMessageTimeout(senderUserNameAlias, dstConversationName),
+ count, fileType, fileName);
+ }
+
+ @Given("^User (.*) sends image with QR code containing \"(.*)\" to conversation (.*)")
+ public void WhenUserSendQRImageToConv(String senderUserNameAlias, String text, String dstConversationName) throws IOException {
+ File tempFile = File.createTempFile("zautomation", ".png");
+ tempFile.deleteOnExit();
+ ImageIO.write(QRCode.generateCode(text, Color.BLACK, Color.WHITE, 500, 1), "png", tempFile);
+ getCommonSteps().userSendsImageToConversationViaTestservice(senderUserNameAlias, dstConversationName, FIRST_AVAILABLE_DEVICE, tempFile.getPath(),
+ context.getSelfDeletingMessageTimeout(senderUserNameAlias, dstConversationName));
+ }
+
+ @Given("^User (.*) sends (\\d+) (default|long|\".*\") messages? to conversation (.*)")
+ public void UserSendsMultipleMessages(String senderUserNameAlias, int count,
+ String msg, String dstConversationName) {
+ context.startPinging();
+ try {
+ getCommonSteps().userSendsMultipleMessages(senderUserNameAlias, dstConversationName,
+ context.getSelfDeletingMessageTimeout(senderUserNameAlias, dstConversationName),
+ count, msg, DEFAULT_AUTOMATION_MESSAGE, LegalHoldStatus.DISABLED);
+ } finally {
+ context.stopPinging();
+ }
+ }
+
+ @Given("^User (.*) sends invite link for conversation (.*) message to conversation (.*)")
+ public void UserSendsInviteLinkForConversationMessageToConversation(String senderUserNameAlias, String conversationTitle, String dstConversationName) {
+ String msg = getCommonSteps().getInviteLinkOfConversation(senderUserNameAlias, conversationTitle);
+
+ context.startPinging();
+ try {
+ getCommonSteps().userSendsMultipleMessages(senderUserNameAlias, dstConversationName,
+ context.getSelfDeletingMessageTimeout(senderUserNameAlias, dstConversationName),
+ 1, msg, DEFAULT_AUTOMATION_MESSAGE, LegalHoldStatus.DISABLED);
+ } finally {
+ context.stopPinging();
+ }
+ }
+
+ @Given("^User (.*) sends link preview for \"(.*)\" to conversation (.*)")
+ public void userSendsLinkPreview(String senderUserNameAlias, String url, String dstConversationName)
+ throws IOException {
+ context.startPinging();
+ try {
+ File tempFile = File.createTempFile("zautomation", ".png");
+ try {
+ ImageIO.write(QRCode.generateCode(url, Color.BLACK, Color.WHITE, 64, 1),
+ "png", tempFile);
+ getCommonSteps().userSendsLinkPreview(senderUserNameAlias, dstConversationName, null,
+ Timedelta.ofMillis(0), url, "Link preview for " + url, tempFile.getAbsolutePath());
+ } finally {
+ tempFile.delete();
+ }
+ } finally {
+ context.stopPinging();
+ }
+ }
+
+ @Given("^User (.*) sends (\\d+) (default|long|\".*\") messages? under (legal hold|unknown state) to conversation (.*)")
+ public void UserSendsMultipleMessagesUnderLegalHold(String senderUserNameAlias, int count,
+ String msg, String status, String dstConversationName) {
+ if (status.equals("legal hold")) {
+ getCommonSteps().userSendsMultipleMessages(senderUserNameAlias, dstConversationName,
+ context.getSelfDeletingMessageTimeout(senderUserNameAlias, dstConversationName),
+ count, msg, DEFAULT_AUTOMATION_MESSAGE, LegalHoldStatus.ENABLED);
+ } else {
+ getCommonSteps().userSendsMultipleMessages(senderUserNameAlias, dstConversationName,
+ context.getSelfDeletingMessageTimeout(senderUserNameAlias, dstConversationName),
+ count, msg, DEFAULT_AUTOMATION_MESSAGE, LegalHoldStatus.UNKNOWN);
+ }
+ }
+
+ @When("^User (.*) sends delivery confirmation for the recent message in (.*) conversation")
+ public void userSendsDeliveryConfirmationForRecentMessage(String receiverAlias, String convoName) {
+ getCommonSteps().userSendsDeliveryConfirmationForRecentMessage(receiverAlias, convoName,
+ FIRST_AVAILABLE_DEVICE);
+ }
+
+ @When("^User (.*) sends poll message \"(.*)\" with title \"(.*)\" and buttons \"(.*)\"(?: via device (.*))? to conversation (.*)$")
+ public void userSendPollMessageToConversation(String msgFromUserNameAlias,
+ String msg, String title, String buttons, String deviceName, String convoName) {
+ context.startPinging();
+ // We timeout after 2 minutes because ETS sometimes crashes
+ final ExecutorService service = Executors.newSingleThreadExecutor();
+ try {
+ final Future f = service.submit(() -> {
+ context.getCommonSteps().userSendsPollMessageToConversation(msgFromUserNameAlias, convoName,
+ deviceName, NO_EXPIRATION, msg, title, buttons,
+ LegalHoldStatus.DISABLED);
+ });
+ f.get(2, TimeUnit.MINUTES);
+ } catch (final TimeoutException e) {
+ log.severe("Sending poll message via Testservice timed out: " + e.getMessage());
+ } catch (final Exception e) {
+ log.severe("Sending poll message via Testservice failed: " + e.getMessage());
+ } finally {
+ service.shutdown();
+ context.stopPinging();
+ }
+ }
+
+ @When("^User (.*) sends button action confirmation to user (.*) on the latest poll(?: via device (.*))? in conversation (.*) with button \"(.*)\"$")
+ public void userSendsButtonActionConfirmationOnPollMessage(String senderAlias, String receiverAlias, String deviceName, String dstConvoName, String buttonText) {
+ context.startPinging();
+ try {
+ context.getCommonSteps().userSendsButtonActionConfirmationToLatestPollMessage(senderAlias, receiverAlias, deviceName, dstConvoName, buttonText);
+ } finally {
+ context.stopPinging();
+ }
+ }
+
+ @Given("^User (.*) sends deep link for conversation (.*) to conversation (.*)")
+ public void UserSendsDeepLink(String senderUserNameAlias, String srcConversationName, String dstConversationName) {
+ String deeplink = getCommonSteps().getDeepLinkForConversation(srcConversationName, senderUserNameAlias);
+ deeplink = CommonUtils.formatMarkdownURL("deep link", deeplink);
+ getCommonSteps().userSendsMessageToConversation(senderUserNameAlias, dstConversationName,null,NO_EXPIRATION, deeplink, LegalHoldStatus.DISABLED);
+ }
+
+ @When("^User (.*) sends? ephemeral message \"?(.*?)\"?\\s? with timer (10 seconds|5 minutes|1 hour|1 day|1 week|4 weeks) (?:via device (.*)\\s)?to (user|group conversation) (.*)$")
+ public void userSendEphemeralMessageToConversation(String msgFromUserNameAlias,
+ String msg, String msgTimer, String deviceName, String convotype, String dstConvoName) {
+ long msgTimerInMs = EphemeralTimeConverter.asMillis(msgTimer);
+ getCommonSteps().userSendsMessageToConversation(msgFromUserNameAlias, dstConvoName, deviceName,
+ Timedelta.ofMillis(msgTimerInMs), msg, LegalHoldStatus.DISABLED);
+ }
+
+ @When("^User (.*) sends message \"(.*?)\" as reply to last message of conversation (.*) via device (.*)?$")
+ public void userRepliesToLatestMessage(String senderAlias, String message, String conversationName, String deviceName) {
+ getCommonSteps().userRepliesToLatestMessage(senderAlias, conversationName, deviceName, NO_EXPIRATION, message);
+ }
+
+ @When("^User (.*) sends message \"(.*?)\" as reply to the last message of conversation (.*)$")
+ public void userRepliesToLatestMessage(String senderAlias, String message, String conversationName) {
+ getCommonSteps().userRepliesToLatestMessage(senderAlias, conversationName, FIRST_AVAILABLE_DEVICE, NO_EXPIRATION, message);
+ }
+
+ @When("^User (.*) sends (\\d+) messages? \"(.*?)\" with mention to conversation (.*)$")
+ public void userSendsMessageWithMentions(String msgFromUserNameAlias, int numberOfMessages, String msg, String dstConvoName) {
+ msg = msg.replace("\\n", "\n");
+ for (int i = 0; i < numberOfMessages; i++) {
+ getCommonSteps().userSendsTextWithMentions(msgFromUserNameAlias, dstConvoName,
+ FIRST_AVAILABLE_DEVICE, NO_EXPIRATION, msg);
+ }
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/TopNavigationBarSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/TopNavigationBarSteps.java
new file mode 100644
index 00000000000..33ab346eb37
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/TopNavigationBarSteps.java
@@ -0,0 +1,69 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.common.misc.Timedelta;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.TopNavigationBarPage;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+import java.io.IOException;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class TopNavigationBarSteps {
+ IOSTestContext context;
+
+ public TopNavigationBarSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private TopNavigationBarPage getTopNavigationBarPage() {
+ return context.getPagesCollection()
+ .getPage(TopNavigationBarPage.class);
+ }
+
+ @When("^I open Self profile$")
+ public void IOpenView() {
+ getTopNavigationBarPage().tapProfileButton();
+ }
+
+ @When("^I (do not )?see Self profile button on Conversations list page$")
+ public void IOpenView(String doNot) {
+ if(doNot == null) {
+ assertThat("The Self profile button is expected to be visible but it's not",
+ getTopNavigationBarPage().isSelfProfileButtonVisible());
+ } else {
+ assertThat("The Self profile button is expected to be invisible but it's not",
+ getTopNavigationBarPage().isSelfProfileButtonInvisible());
+ }
+ }
+
+ @Then("^I tap on my profile photo in conversation list$")
+ public void iTapMyImageInConversationView() {
+ getTopNavigationBarPage().tapProfileImage();
+ }
+
+ @Then("^I (do not )?see legal hold indicator next to self title in Conversation list$")
+ public void iSeeLegalHoldIndicatorInConversationList(String shouldNotSee) {
+ if (shouldNotSee == null) {
+ assertThat("The legal hold indicator is not visible next to self title",
+ getTopNavigationBarPage().isLegalHoldIndicatorVisible());
+ } else {
+ assertThat("The legal hold indicator is visible next to self title while it should not be",
+ getTopNavigationBarPage().isLegalHoldIndicatorInvisible());
+ }
+ }
+
+ @Then("^I tap the legal hold indicator next to self title in Conversation list$")
+ public void iTapLegalHoldIndicatorInConversationList() {
+ getTopNavigationBarPage().iTapLegalHoldIndicator();
+ }
+
+ @When("^I open search screen")
+ public void iOpenSearchScreen() {
+ getTopNavigationBarPage().iOpenSearchScreen();
+ }
+ @When("^I opened the filters")
+ public void iTapFilterButton() {
+ getTopNavigationBarPage().iTapOnFilterButton();
+ }
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/UniqueUsernamePageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/UniqueUsernamePageSteps.java
new file mode 100644
index 00000000000..e3646c62ea2
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/UniqueUsernamePageSteps.java
@@ -0,0 +1,52 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.common.usrmgmt.ClientUsersManager;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.UniqueUsernamePage;
+import io.cucumber.java.en.When;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class UniqueUsernamePageSteps {
+ IOSTestContext context;
+
+ public UniqueUsernamePageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private UniqueUsernamePage getUniqueUsernamePage() {
+ return context.getPagesCollection().getPage(UniqueUsernamePage.class);
+ }
+
+ @When("^I tap Save button on Unique Username page$")
+ public void ITapButtonOnUniqueUsernamePage() {
+ getUniqueUsernamePage().tapSaveButton();
+ }
+
+ /**
+ * Fill in name input an string
+ *
+ * @param name string to be input
+ */
+ @When("^I enter \"(.*)\" name on Unique Username page$")
+ public void IFillInNameInInputOnUniqueUsernamePage(String name) {
+ name = context.getUsersManager()
+ .replaceAliasesOccurrences(name, ClientUsersManager.FindBy.UNIQUE_USERNAME_ALIAS);
+ getUniqueUsernamePage().inputStringInNameInput(name);
+ }
+
+ /**
+ * Verify Save button isEnable state
+ *
+ * @param expectedState Disabe/Enable
+ */
+ @When("^I see Save button state is (Disabled|Enabled) on Unique Username page$")
+ public void ISeeSaveButtonIsDisabled(String expectedState) {
+ boolean buttonState = getUniqueUsernamePage().isSaveButtonEnabled();
+ if (expectedState.equals("Disabled")) {
+ assertThat(String.format("Wrong Save button state. Should be %s.", expectedState), !buttonState);
+ } else {
+ assertThat(String.format("Wrong Save button state. Should be %s.", expectedState), buttonState);
+ }
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/UniqueUsernameTakeoverPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/UniqueUsernameTakeoverPageSteps.java
new file mode 100644
index 00000000000..4ded45fcb69
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/UniqueUsernameTakeoverPageSteps.java
@@ -0,0 +1,28 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.common.log.ZetaLogger;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.UniqueUsernameTakeoverPage;
+import io.cucumber.java.en.When;
+
+import java.util.logging.Logger;
+
+public class UniqueUsernameTakeoverPageSteps {
+
+ private static final Logger log = ZetaLogger.getLog(UniqueUsernameTakeoverPageSteps.class.getSimpleName());
+ IOSTestContext context;
+
+ public UniqueUsernameTakeoverPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private UniqueUsernameTakeoverPage getPage() {
+ return context.getPagesCollection()
+ .getPage(UniqueUsernameTakeoverPage.class);
+ }
+
+ @When("^I tap Keep This One button on Unique Username Takeover page$")
+ public void iTapKeepThis() {
+ getPage().tapKeepThisOneButton();
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/VideoPlayerPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/VideoPlayerPageSteps.java
new file mode 100644
index 00000000000..5d50828c04a
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/VideoPlayerPageSteps.java
@@ -0,0 +1,37 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import io.cucumber.java.en.Then;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.wearezeta.auto.ios.pages.VideoPlayerPage;
+
+import io.cucumber.java.en.When;
+
+public class VideoPlayerPageSteps {
+ IOSTestContext context;
+
+ public VideoPlayerPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private VideoPlayerPage getVideoPlayerPage() {
+ return context.getPagesCollection().getPage(VideoPlayerPage.class);
+ }
+
+ @When("I see the video player web page is opened")
+ public void ISeeVideoPlayerWebPage() {
+ assertThat("Video Player web page is not opened", getVideoPlayerPage().isVideoPlayerPageOpened());
+ }
+
+ @Then("^I see pause button on Video page$")
+ public void ISeePauseButton() {
+ assertThat("Pause button is not displayed", getVideoPlayerPage().
+ isPlayPauseButtonVisible());
+ }
+
+ @When("^I tap Done button on video message player page$")
+ public void ITapDoneButtonOnVideoMessagePlayer() {
+ getVideoPlayerPage().tapDoneButton();
+ }
+}
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/VoiceFiltersOverlaySteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/VoiceFiltersOverlaySteps.java
new file mode 100644
index 00000000000..f804d7598d2
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/VoiceFiltersOverlaySteps.java
@@ -0,0 +1,49 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.VoiceFiltersOverlay;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class VoiceFiltersOverlaySteps {
+ IOSTestContext context;
+
+ public VoiceFiltersOverlaySteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private VoiceFiltersOverlay getVoiceFiltersOverlay() {
+ return context.getPagesCollection().getPage(VoiceFiltersOverlay.class);
+ }
+
+ @When("^I tap Start Recording button on Voice Filters overlay$")
+ public void ITapOnStartButton() {
+ getVoiceFiltersOverlay().tapOnStartButton();
+ }
+
+ @When("^I tap Stop Recording button on Voice Filters overlay$")
+ public void ITapOnStopButton() {
+ getVoiceFiltersOverlay().tapOnStopButton();
+ }
+
+ @When("^I tap Confirm button on Voice Filters overlay$")
+ public void ITapOnConfirmButton() {
+ getVoiceFiltersOverlay().tapOnConfirmButton();
+ }
+
+ @When("^I tap (\\d+) random effect buttons? on Voice Filters overlay$")
+ public void ITapXRandomEffectButtons(int count) {
+ getVoiceFiltersOverlay().tapRandomEffectButtons(count);
+ }
+
+ @Then("^I (do not )?see Confirm button on Voice Filters overlay$")
+ public void ISeeConfirmButton(String shouldNotSee) {
+ if (shouldNotSee == null) {
+ assertThat(("The confirm recording button is not visible on Voice Filters overlay"), getVoiceFiltersOverlay().isConfirmButtonVisible());
+ } else {
+ assertThat(("The confirm recording button is visible on Voice Filters overlay, but should be hidden"), getVoiceFiltersOverlay().isConfirmButtonInVisible());
+ }
+ }
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/WelcomePageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/WelcomePageSteps.java
new file mode 100644
index 00000000000..aefeb565f40
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/WelcomePageSteps.java
@@ -0,0 +1,62 @@
+package com.wearezeta.auto.ios.steps;
+
+import com.wearezeta.auto.common.backend.BackendConnections;
+import com.wearezeta.auto.common.misc.URLTransformer;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.WelcomePage;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.hasItem;
+
+public class WelcomePageSteps {
+
+ IOSTestContext context;
+
+ public WelcomePageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private WelcomePage getWelcomePage() {
+ return context.getPagesCollection().getPage(WelcomePage.class);
+ }
+
+ @Then("^I (do not )?see Welcome page$")
+ public void iSeeWelcomePage(String doNot) {
+ if (doNot == null) {
+ assertThat("Wire Logo on Welcome page not visible.", getWelcomePage().isWireLogoVisible());
+ assertThat("Welcome message on page is not visible.", getWelcomePage().isWelcomeMessageVisible());
+ } else {
+ assertThat("Welcome page is visible while it should not be.", getWelcomePage().isWelcomePageInvisible());
+ }
+ }
+
+ @Then("I see domain name of backend on Welcome page")
+ public void iSeeDomainName() {
+ String domainName = URLTransformer.getHost(BackendConnections.getDefault().getBackendUrl());
+ assertThat("Wrong or missing domain name on welcome page",
+ getWelcomePage().getStaticTexts(), hasItem(containsString(domainName)));
+ }
+
+ @Then("^I see Enterprise Log In button on Welcome page$")
+ public void iSeeEnterpriseLogInButtonOnWelcomePage() {
+ assertThat("Enterprise Log In button is not visible.", getWelcomePage().isEnterpriseLogInButtonVisible());
+ }
+
+ @When("^I tap Login button on Welcome page$")
+ public void iTapLoginButtonOnWelcomePage() {
+ getWelcomePage().tapLoginButton();
+ }
+
+ @When("^I tap Create An Account button on Welcome page$")
+ public void iTapCreateAnAccountButtonOnWelcomePage() {
+ getWelcomePage().tapCreateAnAccountButton();
+ }
+
+ @When("^I tap Enterprise Login button on Welcome page$")
+ public void iTapEnterpriseLoginButtonOnWelcomePage() {
+ getWelcomePage().tapEnterpriseLoginButton();
+ }
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/calling/CallPageSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/calling/CallPageSteps.java
new file mode 100644
index 00000000000..78291e9d129
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/calling/CallPageSteps.java
@@ -0,0 +1,185 @@
+package com.wearezeta.auto.ios.steps.calling;
+
+import com.wearezeta.auto.common.CommonUtils;
+import com.wearezeta.auto.common.misc.Timedelta;
+import com.wearezeta.auto.common.usrmgmt.ClientUser;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import com.wearezeta.auto.ios.pages.calling.CallPage;
+import com.wearezeta.auto.ios.pages.calling.VideoCallingOverlayPage;
+import io.cucumber.java.en.And;
+
+import com.wearezeta.auto.common.usrmgmt.ClientUsersManager;
+
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.not;
+
+public class CallPageSteps {
+
+ IOSTestContext context;
+
+ public CallPageSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ private CallPage getCallPage() {
+ return context.getPagesCollection().getPage(CallPage.class);
+ }
+
+ private VideoCallingOverlayPage getPage() {
+ return context.getPagesCollection().getPage(VideoCallingOverlayPage.class);
+ }
+
+ /**
+ * Verify whether calling overlay is visible or not
+ *
+ * @param shouldNotBeVisible equals to null if the overlay should be visible
+ */
+ @Then("^I (do not )?see Calling overlay$")
+ public void ISeeCallingOverlay(String shouldNotBeVisible) {
+ if (shouldNotBeVisible == null) {
+ assertThat("Calling overlay is not visible", getPage().iSeeLeaveCallButton());
+ } else {
+ assertThat("Calling overlay is visible, but should be hidden", getPage().iDontSeeLeaveCallButton());
+ }
+ }
+
+ @When("^I tap Accept button on (?:the |\\s*)Calling overlay$")
+ public void ITapAcceptButton() {
+ getPage().tapAcceptButton();
+ }
+
+ @When("^I tap Leave button on Calling overlay$")
+ public void iTapLeaveButton(){
+ getPage().tapDeclineButton();
+ }
+
+ @When("^I tap Minimize button on Calling overlay$")
+ public void iTapMinimizeButton(){
+ getPage().iTapMinimize();
+ }
+
+ @When("^I swipe up to see the participants list$")
+ public void iSwipeUpParticipantsList(){
+ getPage().swipeUpParticipantsList();
+ }
+
+ @When("^I tap OK button on permission alert if visible$")
+ public void iTapOK(){
+ getPage().tapOKButton();
+ }
+
+ @Then("^I see End Call button on Calling overlay$")
+ public void ISeeButton() {
+ assertThat("End Call button is not visible", getPage().iSeeLeaveCallButton());
+ }
+
+ /**
+ * Verify that call status message contains the particular text
+ *
+ * @param text the message to verify. This can contain user names
+ */
+ @When("^I see call status message contains \"(.*)\"$")
+ public void ISeeCallStatusMessage(String text) {
+ text = context.getUsersManager()
+ .replaceAliasesOccurrences(text, ClientUsersManager.FindBy.NAME_ALIAS);
+ assertThat(String.format("Call status message containing '%s' is not visible", text),
+ getPage().isCallingMessageContainingVisible(text));
+ }
+
+ /**
+ * Verify CBR indicator is visible or invisible
+ */
+ @Then("^I (do not )?see call indicator CONSTANT BIT RATE")
+ public void ISeeCBRIndicator(String shouldNotBeVisible) {
+ if (shouldNotBeVisible == null) {
+ assertThat("Call indicator CONSTANT BIT RATE is not visible", getPage().isBitRateLabelVisible());
+ } else {
+ assertThat("Call indicator CONSTANT BIT RATE is visible", getPage().isBitRateLabelInvisible());
+ }
+ }
+
+ @Then("^I (do not )?see label call indicator CONSTANT BIT RATE")
+ public void ISeeLabelCBRIndicator(String shouldNotBeVisible) {
+ if (shouldNotBeVisible == null) {
+ assertThat("Call indicator label CONSTANT BIT RATE is not visible",
+ getCallPage().isCBRLabelVisible());
+ } else {
+ assertThat("Call indicator label CONSTANT BIT RATE is visible",
+ getCallPage().isCBRLabelInvisible());
+ }
+ }
+
+ /**
+ * Verify VBR indicator is visible or invisible
+ */
+ @Then("^I (do not )?see call indicator VARIABLE BIT RATE")
+ public void ISeeVBRIndicator(String shouldNotBeVisible) {
+ if (shouldNotBeVisible == null) {
+ assertThat("Call indicator VARIABLE BIT RATE is not visible", getPage().isBitRateLabelVisible());
+ } else {
+ assertThat("Call indicator VARIABLE BIT RATE is visible", getPage().isBitRateLabelInvisible());
+ }
+ }
+
+ private static final Timedelta CALL_PARTICIPANTS_VISIBILITY_TIMEOUT = Timedelta.ofSeconds(20);
+
+ /**
+ * Verifies a number of participants in the calling overlay
+ *
+ * @param expectedNumber the expected number of avatars
+ */
+ @Then("^I see (\\d+) participants? on the Calling overlay$")
+ public void ISeeXAvatars(int expectedNumber) {
+ assertThat(
+ String.format("The actual number of calling avatars is not equal to the expected number %s",
+ expectedNumber),
+ getPage().isCountOfParticipantsEqualTo(expectedNumber, CALL_PARTICIPANTS_VISIBILITY_TIMEOUT)
+ );
+ }
+
+ /**
+ * Tap the top bar in order to restore the previously minimized overlay
+ */
+ @And("^I restore Calling overlay$")
+ public void iRestoreOverlay() {
+ getPage().tapRestoreButton();
+ }
+
+ /**
+ * Verify whether minimized calling overlay bar is visible on the top of the screen
+ */
+ @Then("^I see that Calling overlay is minimized$")
+ public void iSeeMinimizedOverlay() {
+ assertThat("Calling overlay is expected to be minimized",
+ getPage().isRestoreButtonVisible());
+ }
+
+ @When("^I tap Ok Button on Enterprise alert$")
+ public void iTapOkButton() {
+ getPage().tapOKButton();
+ }
+
+ @Then("^I (do not )?see SECURITY LEVEL: UNCLASSIFIED label on calling overlay$")
+ public void ISeeUnclassifiedLabelOnCallingOverlay(String shouldNotSee) {
+ if (shouldNotSee == null) {
+ assertThat("Unclassified label is not visible", getPage().isUnclassifiedLabelVisibleOnCallingOverlay());
+ } else {
+ assertThat("Unclassified label is not visible", getPage().isUnclassifiedLabelInvisibleOnCallingOverlay());
+ }
+ }
+
+ @Then("^I (do not )?see SECURITY LEVEL: VS-NfD label on calling overlay$")
+ public void ISeeClassifiedLabelOnCallingOverlay(String shouldNotSee) {
+ if (shouldNotSee == null) {
+ assertThat("Classified label is not visible", getPage().isClassifiedLabelVisibleOnCallingOverlay());
+ } else {
+ assertThat("Classified label is not visible", getPage().isClassifiedLabelInvisibleOnCallingOverlay());
+ }
+ }
+}
\ No newline at end of file
diff --git a/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/calling/CallingSteps.java b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/calling/CallingSteps.java
new file mode 100644
index 00000000000..685cd7c5b85
--- /dev/null
+++ b/wire-ios-automation/ios/src/test/java/com/wearezeta/auto/ios/steps/calling/CallingSteps.java
@@ -0,0 +1,221 @@
+package com.wearezeta.auto.ios.steps.calling;
+
+import com.wearezeta.auto.common.CallingManager;
+import com.wearezeta.auto.common.backend.Backend;
+import com.wearezeta.auto.common.backend.BackendConnections;
+import com.wearezeta.auto.common.backend.models.Conversation;
+import com.wearezeta.auto.common.log.ZetaLogger;
+import com.wearezeta.auto.common.usrmgmt.ClientUser;
+import com.wearezeta.auto.ios.common.IOSTestContext;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+
+import java.util.logging.Logger;
+
+import javax.management.InstanceNotFoundException;
+import java.util.*;
+
+public class CallingSteps {
+ IOSTestContext context;
+
+ private static final Logger log = ZetaLogger.getLog(CallingSteps.class.getSimpleName());
+
+ public CallingSteps(IOSTestContext context) {
+ this.context = context;
+ }
+
+ /**
+ * Make call to a specific user. You may instantiate more than one incoming
+ * call from single caller by calling this step multiple times
+ *
+ * @param caller caller name/alias
+ * @param conversationName destination conversation name
+ */
+ @When("(\\w+) calls (\\w+)$")
+ public void UserXCallsToUserYUsingCallBackend(String caller, String conversationName) throws Exception {
+ context.getCallingManager().callToConversation(caller, conversationName);
+ }
+
+ /**
+ * Stop outgoing or incoming call (audio and video) to the other side
+ *
+ * @param instanceUsers comma separated list of usernames/aliases
+ * @param conversationName destination conversation name
+ */
+ @When("^(.*) stops? (incoming call from|outgoing call to) (.*)")
+ public void UserXStopsIncomingOutgoingCallsToUserY(String instanceUsers, String typeOfCall, String conversationName)
+ throws Exception {
+ if (typeOfCall.equals("incoming call from")) {
+ context.getCallingManager()
+ .stopIncomingCall(context.getUsersManager()
+ .splitAliases(instanceUsers));
+ } else {
+ context.getCallingManager()
+ .stopOutgoingCall(context.getUsersManager()
+ .splitAliases(instanceUsers), conversationName);
+ }
+ }
+
+
+ /**
+ * Verify whether call status is changed to one of the expected values after
+ * N seconds timeout
+ *
+ * @param caller caller name/alias
+ * @param conversationName destination conversation
+ * @param expectedStatuses comma-separated list of expected call statuses. See
+ * com.wearezeta.auto.common.calling2.v1.model.CallStatus for
+ * more details
+ * @param timeoutSeconds number of seconds to wait until call status is changed
+ */
+ @Then("(.*) verif(?:ies|y) that call status to (.*) is changed to (.*) in (\\d+) seconds?$")
+ public void UserXVerifiesCallStatusToUserY(String caller,
+ String conversationName, String expectedStatuses, int timeoutSeconds)
+ throws Exception {
+ context.getCallingManager()
+ .verifyCallingStatus(caller, conversationName, expectedStatuses, timeoutSeconds);
+ }
+
+ /**
+ * Verify whether waiting instance status is changed to one of the expected
+ * values after N seconds timeout
+ *
+ * @param callees comma separated list of callee names/aliases
+ * @param expectedStatuses comma-separated list of expected call statuses. See
+ * com.wearezeta.auto.common.calling2.v1.model.CallStatus for
+ * more details
+ * @param timeoutSeconds number of seconds to wait until call status is changed
+ */
+ @Then("(.*) verif(?:ies|y) that waiting instance status is changed to (.*) in (\\d+) seconds?$")
+ public void UserXVerifiesCallStatusToUserY(String callees,
+ String expectedStatuses, int timeoutSeconds) throws Exception {
+ context.getCallingManager().verifyAcceptingCallStatus(context.getUsersManager()
+ .splitAliases(callees), expectedStatuses, timeoutSeconds);
+ }
+
+ /**
+ * Verify that the instance has X active flows
+ *
+ * @param callees comma separated list of callee names/aliases
+ * @param numberOfFlows expected number of flows
+ */
+ @Then("^Users? (.*) verif(?:ies|y) to have (\\d+) peer connections?$")
+ public void UserXVerifesHavingXPeerConnections(String callees, int numberOfFlows) throws Exception {
+ context.getCallingManager().verifyPeerConnections(callees, numberOfFlows);
+ }
+
+ /**
+ * Verify that the instance has an established CBR connection
+ *
+ * @param callees comma separated list of callee names/aliases
+ */
+ @Then("^Users? (.*) verif(?:ies|y) to have CBR connection$")
+ public void UserXVerifesHavingCbrConnections(String callees) throws Exception {
+ context.getCallingManager().verifyCbrConnections(callees);
+ }
+
+ /**
+ * Verify that each flow of the instance had incoming and outgoing packets for audio
+ * running over the line
+ *
+ * @param callees comma separated list of callee names/aliases
+ */
+ @Then("^Users? (.*) verif(?:ies|y) to send and receive audio$")
+ public void UserXVerifesAudio(final String callees) throws Exception {
+ context.getCallingManager().verifySendAndReceiveAudio(callees);
+ }
+
+ @When("(.*) starts? instances? using (.*)$")
+ public void UserXStartsInstance(String callees, String callingServiceBackend) {
+ context.getCallingManager()
+ .startInstances(context.getUsersManager().splitAliases(callees),
+ callingServiceBackend, "iOS", context.getScenario().getName());
+ }
+
+ @When("(.*) starts? 2FA instances? using (.*)$")
+ public void UserXStarts2FAInstance(String callees, String callingServiceBackend) {
+ context.startPinging();
+ List