diff --git a/README.md b/README.md index e4bde661..50c61cd8 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,8 @@ SKIP_IOS=1 appium driver doctor flutter | Capability | Description | Example Values | | - | - | -| +| appium:retryBackoffTime | The interval to find the observetory url from logs. (default 3000ms)|500| +| appium:maxRetryCount | The count to find the observatory url. (default 10) | 20| | appium:observatoryWsUri | The URL to attach to the Dart VM. The Appium Flutter Driver finds the WebSocket URL from the device log by default. You can skip the finding the URL process by specifying this capability. Then, this driver attempt to establish a WebSocket connection against the given WebSocket URL. Note that this capability expects the URL is ready for access by outside an appium session. This flutter driver does not do port-forwarding with this capability. You may need to coordinate the port-forwarding as well. | 'ws://127.0.0.1:60992/aaaaaaaaaaa=/ws' | | appium:isolateId | The isolate id to attach to as the initial attempt. A session can change the isolate with `flutter:setIsolateId` command. The default behavior finds `main` isolate id and attaches it. | `isolates/2978358234363215`, `2978358234363215` | | appium:skipPortForward | Whether skip port forwarding from the flutter driver local to the device under test with `observatoryWsUri` capability. It helps you to manage the application under test, the observatory URL and the port forwarding configuration. The default is `true`. | true, false | diff --git a/driver/CHANGELOG.md b/driver/CHANGELOG.md index fb88aefa..37870a8d 100644 --- a/driver/CHANGELOG.md +++ b/driver/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog ## 2.9.1 -- Fix observatory url connection after the app activation command -- Bring `appium:maxRetryCount` and `appium:retryBackoffTime` back +- Fix observatory url finding after an app activation + - Bring `appium:maxRetryCount` and `appium:retryBackoffTime` back to use for the observaotry url findings. ## 2.9.0 - Tune syslog scanning to find the observatory url diff --git a/driver/lib/desired-caps.ts b/driver/lib/desired-caps.ts index 4c32525d..ae1bdb0c 100755 --- a/driver/lib/desired-caps.ts +++ b/driver/lib/desired-caps.ts @@ -5,6 +5,9 @@ export const desiredCapConstraints = { avd: { isString: true, }, + maxRetryCount: { + isNumber: true, + }, platformName: { inclusionCaseInsensitive: [ 'iOS', @@ -13,6 +16,9 @@ export const desiredCapConstraints = { isString: true, presence: true, }, + retryBackoffTime: { + isNumber: true, + }, udid: { isString: true, }, diff --git a/driver/lib/sessions/android.ts b/driver/lib/sessions/android.ts index 8504d352..b69a2152 100644 --- a/driver/lib/sessions/android.ts +++ b/driver/lib/sessions/android.ts @@ -42,9 +42,10 @@ export async function startAndroidSession( export async function connectAndroidSession ( this: FlutterDriver, androiddriver: AndroidUiautomator2Driver, - caps: Record + caps: Record, + clearLog: boolean = false ): Promise { - const observatoryWsUri = await getObservatoryWsUri.bind(this)(androiddriver, caps); + const observatoryWsUri = await getObservatoryWsUri.bind(this)(androiddriver, caps, clearLog); return await connectSocket.bind(this)(observatoryWsUri, caps); } @@ -52,7 +53,14 @@ export async function getObservatoryWsUri ( this: FlutterDriver, proxydriver: AndroidUiautomator2Driver, caps: StringRecord, + clearLog: boolean = false ): Promise { + if (clearLog) { + this._logmon?.clearlastMatch(); + this._logmon?.stop(); + this._logmon?.start(); + } + let urlObject: URL; if (caps.observatoryWsUri) { urlObject = new URL(caps.observatoryWsUri); @@ -69,14 +77,18 @@ export async function getObservatoryWsUri ( `Have you disabled it in capabilities?` ); } - if (!this._logmon.lastMatch) { + const lastMatch = await this._logmon.waitForLastMatchExist( + caps.maxRetryCount, + caps.retryBackoffTime + ); + if (!lastMatch) { throw new Error( `No observatory URL matching to '${OBSERVATORY_URL_PATTERN}' was found in the device log. ` + `Please make sure the application under test is configured properly according to ` + `https://github.com/appium/appium-flutter-driver#usage and that it does not crash on startup.` ); } - urlObject = extractObservatoryUrl(this._logmon.lastMatch) as URL; + urlObject = extractObservatoryUrl(lastMatch) as URL; } const remotePort = urlObject.port; this.portForwardLocalPort = caps.forwardingPort ?? remotePort; diff --git a/driver/lib/sessions/ios.ts b/driver/lib/sessions/ios.ts index 52f9c110..c9eb11a7 100644 --- a/driver/lib/sessions/ios.ts +++ b/driver/lib/sessions/ios.ts @@ -50,9 +50,10 @@ export async function startIOSSession( export async function connectIOSSession( this: FlutterDriver, iosdriver: XCUITestDriver, - caps: Record + caps: Record, + clearLog: boolean = false ): Promise { - const observatoryWsUri = await getObservatoryWsUri.bind(this)(iosdriver, caps); + const observatoryWsUri = await getObservatoryWsUri.bind(this)(iosdriver, caps, clearLog); return await connectSocket.bind(this)(observatoryWsUri, iosdriver, caps); } @@ -70,8 +71,15 @@ async function requireFreePort( export async function getObservatoryWsUri ( this: FlutterDriver, - proxydriver: XCUITestDriver, caps: Record + proxydriver: XCUITestDriver, caps: Record, + clearLog: boolean = false ): Promise { + if (clearLog) { + this._logmon?.clearlastMatch(); + this._logmon?.stop(); + this._logmon?.start(); + } + let urlObject; if (caps.observatoryWsUri) { urlObject = new URL(caps.observatoryWsUri); @@ -88,14 +96,18 @@ export async function getObservatoryWsUri ( `Have you disabled it in capabilities?` ); } - if (!this._logmon.lastMatch) { + const lastMatch = await this._logmon.waitForLastMatchExist( + caps.maxRetryCount, + caps.retryBackoffTime + ); + if (!lastMatch) { throw new Error( `No observatory URL matching to '${OBSERVATORY_URL_PATTERN}' was found in the device log. ` + `Please make sure the application under test is configured properly according to ` + `https://github.com/appium/appium-flutter-driver#usage and that it does not crash on startup.` ); } - urlObject = extractObservatoryUrl(this._logmon.lastMatch) as URL; + urlObject = extractObservatoryUrl(lastMatch) as URL; } if (!proxydriver.isRealDevice()) { this.log.info(`Running on iOS simulator`); diff --git a/driver/lib/sessions/log-monitor.ts b/driver/lib/sessions/log-monitor.ts index 2b4e6278..3d80fcd4 100644 --- a/driver/lib/sessions/log-monitor.ts +++ b/driver/lib/sessions/log-monitor.ts @@ -1,11 +1,14 @@ import type {EventEmitter} from 'node:events'; - +import { retryInterval } from 'asyncbox'; export interface LogEntry { timestamp: number; level: string, message: string; } +const DEFAULT_MAX_RETRY_COUNT = 10; +const DEFAULT_BACKOFF_TIME_MS = 3000; + export type Filter = (x: LogEntry) => Promise; export class LogMonitor { @@ -18,17 +21,40 @@ export class LogMonitor { this._logsEmitter = logsEmitter; this._outputListener = null; this._filter = filter; - this._lastMatch = null; } get started(): boolean { return Boolean(this._outputListener); } + clearlastMatch() { + this._lastMatch = null; + } + get lastMatch(): LogEntry | null { return this._lastMatch; } + async waitForLastMatchExist( + maxRetryCount: number = DEFAULT_MAX_RETRY_COUNT, + retryBackoffTime: number = DEFAULT_BACKOFF_TIME_MS, + ): Promise { + return await retryInterval( + maxRetryCount, + retryBackoffTime, + async () => { + if (this._lastMatch !== null) { + return this._lastMatch; + } + throw new Error( + `No matched log found with ${retryBackoffTime} ms interval ` + + `up to ${maxRetryCount} times. Increasing appium:retryBackoffTime ` + + `and appium:maxRetryCount would help.` + ); + }, + ); + }; + start(): this { if (this.started) { return this; diff --git a/driver/lib/sessions/session.ts b/driver/lib/sessions/session.ts index cde880ac..64d42759 100644 --- a/driver/lib/sessions/session.ts +++ b/driver/lib/sessions/session.ts @@ -17,10 +17,10 @@ export const reConnectFlutterDriver = async function(this: FlutterDriver, caps: switch (_.toLower(caps.platformName)) { case PLATFORM.IOS: - this.socket = await connectIOSSession.bind(this)(this.proxydriver, caps); + this.socket = await connectIOSSession.bind(this)(this.proxydriver, caps, true); break; case PLATFORM.ANDROID: - this.socket = await connectAndroidSession.bind(this)(this.proxydriver, caps); + this.socket = await connectAndroidSession.bind(this)(this.proxydriver, caps, true); break; default: this.log.errorAndThrow( diff --git a/driver/package.json b/driver/package.json index 250a5e7d..ca05821a 100644 --- a/driver/package.json +++ b/driver/package.json @@ -77,6 +77,7 @@ "appium-android-driver": "^9.8.0", "appium-uiautomator2-driver": "^3.7.3", "appium-xcuitest-driver": "^7.24.0", + "asyncbox": "^3.0.0", "bluebird": "^3.1.1", "lodash": "^4.0.0", "portscanner": "^2.2.0", diff --git a/example/ruby/example_sample2.rb b/example/ruby/example_sample2.rb index 828a555f..69c3bc54 100644 --- a/example/ruby/example_sample2.rb +++ b/example/ruby/example_sample2.rb @@ -12,7 +12,9 @@ class ExampleTests < Minitest::Test automationName: 'flutter', udid: 'emulator-5554', deviceName: 'Android', - app: "#{Dir.pwd}/example/sample2/app-debug.apk" + app: "#{Dir.pwd}/example/sample2/app-debug.apk", + maxRetryCount: 20, + retryBackoffTime: 5000, }, appium_lib: { export_session: true, @@ -59,5 +61,13 @@ def test_run_example_android element = @driver.wait_until { |d| d.find_element :id, 'dev.flutter.example.androidfullscreen:id/counter_label' } assert_equal 'Current count: 2', element.text + + @driver.context = 'FLUTTER' + @driver.terminate_app 'dev.flutter.example.androidfullscreen' + @driver.activate_app 'dev.flutter.example.androidfullscreen' + + text_finder = by_text 'Tap me!' + element = ::Appium::Flutter::Element.new(@driver, finder: text_finder) + assert_equal 'Tap me!', element.text end end diff --git a/example/ruby/example_sample2_ios.rb b/example/ruby/example_sample2_ios.rb index 592f3e8c..05a18f79 100644 --- a/example/ruby/example_sample2_ios.rb +++ b/example/ruby/example_sample2_ios.rb @@ -13,8 +13,10 @@ class ExampleTests < Minitest::Test platformVersion: '17.4', deviceName: 'iPhone 15 Plus', app: "#{Dir.pwd}/../sample2/iOSFullScreen.zip", - showIOSLog: true, - wdaLaunchTimeout: 600_000 + wdaLaunchTimeout: 600_000, + maxRetryCount: 20, + retryBackoffTime: 5000, + }, appium_lib: { export_session: true, @@ -59,5 +61,13 @@ def test_run_example_ios element = @driver.wait_until { |d| d.find_element :accessibility_id, 'currentCounter' } assert_equal 'Current counter: 2', element.text + + @driver.context = 'FLUTTER' + @driver.terminate_app 'samples.flutter.example.IOSFullScreen' + @driver.activate_app 'samples.flutter.example.IOSFullScreen' + + text_finder = by_text 'Tap me!' + element = ::Appium::Flutter::Element.new(@driver, finder: text_finder) + assert_equal 'Tap me!', element.text end end