From 4402c294333e6084c854d63b4a8387a3b3cbe9ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20L=C3=BCdeke?= <311702+aluedeke@users.noreply.github.com> Date: Tue, 25 Jun 2024 12:59:13 +0200 Subject: [PATCH 01/23] feat: take viewport screenshot using safari remote debugger (#2413) --- lib/commands/screenshots.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/commands/screenshots.js b/lib/commands/screenshots.js index 9d55009fa..19d8dd4a8 100644 --- a/lib/commands/screenshots.js +++ b/lib/commands/screenshots.js @@ -72,6 +72,10 @@ export default { * @this {XCUITestDriver} */ async getViewportScreenshot() { + if (this.isWebContext()) { + return await this.remote.captureScreenshot(); + } + let statusBarHeight = await this.getStatusBarHeight(); const screenshot = await this.getScreenshot(); From 2f796e922cbca9baf47dc2b31874240a067a2f73 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 25 Jun 2024 11:01:01 +0000 Subject: [PATCH 02/23] chore(release): 7.19.0 [skip ci] ## [7.19.0](https://github.com/appium/appium-xcuitest-driver/compare/v7.18.0...v7.19.0) (2024-06-25) ### Features * take viewport screenshot using safari remote debugger ([#2413](https://github.com/appium/appium-xcuitest-driver/issues/2413)) ([4402c29](https://github.com/appium/appium-xcuitest-driver/commit/4402c294333e6084c854d63b4a8387a3b3cbe9ff)) --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60b06991e..934c27fb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [7.19.0](https://github.com/appium/appium-xcuitest-driver/compare/v7.18.0...v7.19.0) (2024-06-25) + +### Features + +* take viewport screenshot using safari remote debugger ([#2413](https://github.com/appium/appium-xcuitest-driver/issues/2413)) ([4402c29](https://github.com/appium/appium-xcuitest-driver/commit/4402c294333e6084c854d63b4a8387a3b3cbe9ff)) + ## [7.18.0](https://github.com/appium/appium-xcuitest-driver/compare/v7.17.6...v7.18.0) (2024-06-20) ### Features diff --git a/package.json b/package.json index bd132b070..8d2b03df5 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "xcuitest", "xctest" ], - "version": "7.18.0", + "version": "7.19.0", "author": "Appium Contributors", "license": "Apache-2.0", "repository": { From c9d9d4475bcb8d394ae0ba5f3c0a80bea40d1eed Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Tue, 25 Jun 2024 19:41:39 +0200 Subject: [PATCH 03/23] feat: Introduce the `webScreenshotMode` setting (#2415) --- docs/reference/settings.md | 1 + lib/commands/screenshots.js | 21 +++++++++++++++++++++ lib/driver.js | 2 ++ lib/simulator-management.js | 2 ++ 4 files changed, 26 insertions(+) diff --git a/docs/reference/settings.md b/docs/reference/settings.md index 64ba0e6c5..e12b4de89 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -40,3 +40,4 @@ Along with the common settings, the following driver-specific settings are avail | `pageSourceExcludedAttributes` | `string` | One or more comma-separated attribute names to be excluded from the XML output. It might be sometimes helpful to exclude, for example, the `visible` attribute, to significantly speed-up page source retrieval. This does not affect the XML output when `useJSONSource` is enabled. Defaults to an empty string. Example: `"visible,accessible"` | | `maxTypingFrequency` | `int` | Maximum frequency of keystrokes for typing and clear. If your tests are failing because of typing errors, you may want to adjust this. Defaults to `60` keystrokes per minute. | | `respectSystemAlerts` | `boolean` | Currently we detect the app under test as active if XCTest returns XCUIApplicationStateRunningForeground state for it. In case the app under test is covered by a system alert from the Springboard app this approach might be confusing as we cannot interact with it unless an alert is properly handled. If this setting is set to true (by default it is false) then it forces WDA to verify the presence of alerts shown by Springboard and return the latter while performing the automated app detection. It affects the performance of active app detection, but might be more convenient for writing test scripts (e.g. eliminates the need of proactive switching between system and custom apps). Also, this behavior emulates the legacy active application detection logic before version 6 of the driver. | +| `webScreenshotMode` | `native` or `page` or `viewport` | Defines the screenshoting logic if the current context is set to a web one. The default value is `native`, which makes the driver to take screenshots from WDA, e.g. the whole device screen including status bars. The `page` mode tries to retrieve the screenshot of the whole active web page, while the `viewport` one only retrieves a shot of the visible viewport. | diff --git a/lib/commands/screenshots.js b/lib/commands/screenshots.js index 19d8dd4a8..69ec9a253 100644 --- a/lib/commands/screenshots.js +++ b/lib/commands/screenshots.js @@ -9,6 +9,27 @@ export default { * @returns {Promise} */ async getScreenshot() { + if (this.isWebContext()) { + const webScreenshotMode = (await this.settings.getSettings()).webScreenshotMode; + switch (_.toLower(webScreenshotMode)) { + case 'page': + case 'viewport': + return await this.remote.captureScreenshot({ + coordinateSystem: _.capitalize(webScreenshotMode), + }); + case 'native': + case undefined: + case null: + break; + default: + this.log.warn( + `The webScreenshotMode setting value '${webScreenshotMode}' is not known. ` + + `Supported values are: page, viewport and native. Falling back to the native mode.` + ); + break; + } + } + const getScreenshotFromWDA = async () => { this.log.debug(`Taking screenshot with WDA`); const data = await this.proxyCommand('/screenshot', 'GET'); diff --git a/lib/driver.js b/lib/driver.js index f2ce62049..ff1523f6c 100644 --- a/lib/driver.js +++ b/lib/driver.js @@ -84,6 +84,7 @@ const DEFAULT_SETTINGS = { nativeWebTap: false, nativeWebTapStrict: false, useJSONSource: false, + webScreenshotMode: 'native', shouldUseCompactResponses: true, elementResponseAttributes: 'type,label', // Read https://github.com/appium/WebDriverAgent/blob/master/WebDriverAgentLib/Utilities/FBConfiguration.m for following settings' values @@ -1119,6 +1120,7 @@ export class XCUITestDriver extends BaseDriver { try { const device = await getSimulator(this.opts.udid, { devicesSetPath: this.opts.simulatorDevicesSetPath, + // @ts-ignore This is ok logger: this.log, }); return {device, realDevice: false, udid: this.opts.udid}; diff --git a/lib/simulator-management.js b/lib/simulator-management.js index 1830d5602..f27a653ec 100644 --- a/lib/simulator-management.js +++ b/lib/simulator-management.js @@ -42,6 +42,7 @@ export async function createSim() { platform, checkExistence: false, devicesSetPath, + // @ts-ignore This is ok logger: this.log, }); } @@ -76,6 +77,7 @@ export async function getExistingSim() { platform, checkExistence: false, devicesSetPath, + // @ts-ignore This is ok logger: this.log, }); From 7217619880782c137fca0e5930542966ad26714e Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 25 Jun 2024 17:43:36 +0000 Subject: [PATCH 04/23] chore(release): 7.20.0 [skip ci] ## [7.20.0](https://github.com/appium/appium-xcuitest-driver/compare/v7.19.0...v7.20.0) (2024-06-25) ### Features * Introduce the `webScreenshotMode` setting ([#2415](https://github.com/appium/appium-xcuitest-driver/issues/2415)) ([c9d9d44](https://github.com/appium/appium-xcuitest-driver/commit/c9d9d4475bcb8d394ae0ba5f3c0a80bea40d1eed)) --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 934c27fb8..f4919e661 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [7.20.0](https://github.com/appium/appium-xcuitest-driver/compare/v7.19.0...v7.20.0) (2024-06-25) + +### Features + +* Introduce the `webScreenshotMode` setting ([#2415](https://github.com/appium/appium-xcuitest-driver/issues/2415)) ([c9d9d44](https://github.com/appium/appium-xcuitest-driver/commit/c9d9d4475bcb8d394ae0ba5f3c0a80bea40d1eed)) + ## [7.19.0](https://github.com/appium/appium-xcuitest-driver/compare/v7.18.0...v7.19.0) (2024-06-25) ### Features diff --git a/package.json b/package.json index 8d2b03df5..3e7e16c60 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "xcuitest", "xctest" ], - "version": "7.19.0", + "version": "7.20.0", "author": "Appium Contributors", "license": "Apache-2.0", "repository": { From 9a793b10a7cbbe317d6b2f85b25162e64a614dee Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Wed, 26 Jun 2024 20:45:59 +0200 Subject: [PATCH 05/23] fix: Apply the default exec timeout if not provided explicitly (#2416) --- lib/devicectl.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/devicectl.js b/lib/devicectl.js index 4187d0602..cd1e3daaf 100644 --- a/lib/devicectl.js +++ b/lib/devicectl.js @@ -136,7 +136,11 @@ export class Devicectl { // @ts-ignore TS does not understand it return result; } - const result = await exec(XCRUN, finalArgs, {timeout}); + const result = await exec( + XCRUN, + finalArgs, + ...(_.isNumber(timeout) ? [{timeout}] : []), + ); if (logStdout) { this.log.debug(`Command output: ${result.stdout}`); } From f2d80da102b8fb3333b97a768bafe463553704cc Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Wed, 26 Jun 2024 20:46:16 +0200 Subject: [PATCH 06/23] fix: Respect the remote port capability for real devices (#2417) --- lib/commands/web.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/commands/web.js b/lib/commands/web.js index d3577d054..8606cb3a5 100644 --- a/lib/commands/web.js +++ b/lib/commands/web.js @@ -869,9 +869,9 @@ const extensions = { */ getWdaLocalhostRoot() { const remotePort = - (this.opts.wdaRemotePort - ?? this.wda?.url?.port - ?? this.opts.wdaLocalPort) + ((this.isRealDevice() ? this.opts.wdaRemotePort : null) + ?? this.wda?.url?.port + ?? this.opts.wdaLocalPort) || 8100; return `http://127.0.0.1:${remotePort}`; }, From 345604a994bc197271cf9b439c6bacc54071edd3 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 26 Jun 2024 18:48:15 +0000 Subject: [PATCH 07/23] chore(release): 7.20.1 [skip ci] ## [7.20.1](https://github.com/appium/appium-xcuitest-driver/compare/v7.20.0...v7.20.1) (2024-06-26) ### Bug Fixes * Apply the default exec timeout if not provided explicitly ([#2416](https://github.com/appium/appium-xcuitest-driver/issues/2416)) ([9a793b1](https://github.com/appium/appium-xcuitest-driver/commit/9a793b10a7cbbe317d6b2f85b25162e64a614dee)) * Respect the remote port capability for real devices ([#2417](https://github.com/appium/appium-xcuitest-driver/issues/2417)) ([f2d80da](https://github.com/appium/appium-xcuitest-driver/commit/f2d80da102b8fb3333b97a768bafe463553704cc)) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4919e661..7da6eeeab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [7.20.1](https://github.com/appium/appium-xcuitest-driver/compare/v7.20.0...v7.20.1) (2024-06-26) + +### Bug Fixes + +* Apply the default exec timeout if not provided explicitly ([#2416](https://github.com/appium/appium-xcuitest-driver/issues/2416)) ([9a793b1](https://github.com/appium/appium-xcuitest-driver/commit/9a793b10a7cbbe317d6b2f85b25162e64a614dee)) +* Respect the remote port capability for real devices ([#2417](https://github.com/appium/appium-xcuitest-driver/issues/2417)) ([f2d80da](https://github.com/appium/appium-xcuitest-driver/commit/f2d80da102b8fb3333b97a768bafe463553704cc)) + ## [7.20.0](https://github.com/appium/appium-xcuitest-driver/compare/v7.19.0...v7.20.0) (2024-06-25) ### Features diff --git a/package.json b/package.json index 3e7e16c60..389455aa2 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "xcuitest", "xctest" ], - "version": "7.20.0", + "version": "7.20.1", "author": "Appium Contributors", "license": "Apache-2.0", "repository": { From 6ba1b5e4ba192da6b8d7a0370cd3fa79947c540e Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Thu, 27 Jun 2024 09:27:44 -0700 Subject: [PATCH 08/23] chore: Bump chai and chai-as-promised (#2414) * bump chai * ignoer ts lint * update tests and remove unnecessary dependencies --- package.json | 10 +- test/functional/basic/alert-e2e-specs.js | 12 +- test/functional/basic/basic-e2e-specs.js | 18 +-- test/functional/basic/element-e2e-specs.js | 12 +- test/functional/basic/face-id-e2e-specs.js | 16 ++- test/functional/basic/find-e2e-specs.js | 13 +- test/functional/basic/gesture-e2e-specs.js | 13 +- test/functional/basic/touch-id-e2e-specs.js | 17 ++- .../device/accessibility-e2e-specs.js | 14 ++- .../device/file-movement-e2e-specs.js | 12 +- test/functional/device/otherApps-e2e-specs.js | 14 ++- test/functional/device/passwords-e2e-specs.js | 14 ++- .../device/performance-e2e-specs.js | 13 +- test/functional/device/xctest-e2e-specs.js | 12 +- test/functional/driver/driver-e2e-specs.js | 12 +- .../long/typing-stress-e2e-specs.js | 14 ++- test/functional/tv/tvos-e2e-specs.js | 12 +- .../functional/web/safari-alerts-e2e-specs.js | 12 +- test/functional/web/safari-basic-e2e-specs.js | 17 ++- .../web/safari-execute-e2e-specs.js | 18 +-- .../web/safari-nativewebtap-e2e-specs.js | 15 ++- test/functional/web/safari-ssl-e2e-specs.js | 12 +- .../functional/web/safari-window-e2e-specs.js | 14 ++- test/unit/app-infos-cache-specs.js | 15 ++- test/unit/app-utils-specs.js | 14 ++- test/unit/commands/activeAppInfo-specs.js | 17 +-- test/unit/commands/alert-specs.js | 17 ++- test/unit/commands/context-specs.js | 17 ++- test/unit/commands/deviceinfo-specs.js | 19 +-- test/unit/commands/element-specs.js | 73 +++++------ test/unit/commands/file-movement-specs.js | 22 ++-- test/unit/commands/find-specs.js | 22 ++-- test/unit/commands/general-specs.js | 54 ++++---- test/unit/commands/gesture-specs.js | 116 +++++++----------- test/unit/commands/pasteboard-specs.js | 8 +- test/unit/commands/proxy-helper-specs.js | 51 ++++---- test/unit/commands/screenshots-specs.js | 1 - test/unit/commands/session-specs.js | 20 ++- test/unit/commands/ssl-certificate-specs.js | 9 +- test/unit/commands/xctest-specs.js | 14 ++- test/unit/css-converter-specs.js | 15 ++- test/unit/device-connections-factory-specs.js | 13 +- test/unit/driver-specs.js | 63 +++++----- test/unit/language-specs.js | 35 ++++-- test/unit/processargs-specs.js | 26 +++- test/unit/real-device-management-specs.js | 46 +++---- test/unit/simulator-management-specs.js | 12 +- test/unit/utils-specs.js | 24 ++-- tsconfig.json | 5 +- 49 files changed, 620 insertions(+), 424 deletions(-) diff --git a/package.json b/package.json index 389455aa2..602ebe556 100644 --- a/package.json +++ b/package.json @@ -143,19 +143,14 @@ "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@types/bluebird": "^3.5.38", - "@types/chai": "^4.3.5", - "@types/chai-as-promised": "^7.1.5", "@types/lodash": "^4.14.196", "@types/mocha": "^10.0.1", "@types/node": "^20.4.7", "@types/portscanner": "^2.1.1", - "@types/sinon": "^17.0.0", - "@types/sinon-chai": "^3.2.9", "@types/teen_process": "^2.0.1", "axios": "^1.4.0", - "chai": "^4.3.7", - "chai-as-promised": "^7.1.1", - "chai-subset": "^1.6.0", + "chai": "^5.1.1", + "chai-as-promised": "^8.0.0", "conventional-changelog-conventionalcommits": "^8.0.0", "mocha": "^10.2.0", "mocha-junit-reporter": "^2.2.1", @@ -166,7 +161,6 @@ "semantic-release": "^24.0.0", "sharp": "^0.x", "sinon": "^18.0.0", - "sinon-chai": "^3.7.0", "ts-node": "^10.9.1", "type-fest": "^4.1.0", "typescript": "^5.4.2", diff --git a/test/functional/basic/alert-e2e-specs.js b/test/functional/basic/alert-e2e-specs.js index d7aa8d5d7..a1b25f99a 100644 --- a/test/functional/basic/alert-e2e-specs.js +++ b/test/functional/basic/alert-e2e-specs.js @@ -1,18 +1,22 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import B from 'bluebird'; import {retryInterval} from 'asyncbox'; import {amendCapabilities, UICATALOG_CAPS} from '../desired'; import {initSession, deleteSession, hasDefaultPrebuiltWDA, MOCHA_TIMEOUT} from '../helpers/session'; -chai.should(); -chai.use(chaiAsPromised); describe('XCUITestDriver - alerts -', function () { this.timeout(MOCHA_TIMEOUT); let driver; + let chai; + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + const caps = amendCapabilities(UICATALOG_CAPS, { 'appium:usePrebuiltWDA': hasDefaultPrebuiltWDA(), }); diff --git a/test/functional/basic/basic-e2e-specs.js b/test/functional/basic/basic-e2e-specs.js index cf6911604..8570dedef 100644 --- a/test/functional/basic/basic-e2e-specs.js +++ b/test/functional/basic/basic-e2e-specs.js @@ -1,6 +1,3 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -import chaiSubset from 'chai-subset'; import B from 'bluebird'; import util from 'util'; import {retryInterval} from 'asyncbox'; @@ -9,15 +6,20 @@ import {initSession, deleteSession, hasDefaultPrebuiltWDA, MOCHA_TIMEOUT} from ' import {GUINEA_PIG_PAGE} from '../web/helpers'; import sharp from 'sharp'; -chai.should(); -chai.use(chaiAsPromised); -chai.use(chaiSubset); describe('XCUITestDriver - basics -', function () { this.timeout(MOCHA_TIMEOUT); let driver; + let chai; + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + const caps = amendCapabilities(UICATALOG_CAPS, { 'appium:usePrebuiltWDA': hasDefaultPrebuiltWDA(), }); @@ -144,7 +146,9 @@ describe('XCUITestDriver - basics -', function () { it('should get the list of available logs', async function () { const expectedTypes = ['syslog', 'crashlog', 'performance', 'server', 'safariConsole']; const actualTypes = await driver.getLogTypes(); - actualTypes.should.containSubset(expectedTypes); + for (const actualType of actualTypes) { + expectedTypes.includes(actualType).should.be.true; + } }); }); diff --git a/test/functional/basic/element-e2e-specs.js b/test/functional/basic/element-e2e-specs.js index 894dfef88..fe1b3c852 100644 --- a/test/functional/basic/element-e2e-specs.js +++ b/test/functional/basic/element-e2e-specs.js @@ -1,5 +1,3 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import _ from 'lodash'; import B from 'bluebird'; import {retryInterval} from 'asyncbox'; @@ -7,14 +5,20 @@ import {extractCapabilityValue, amendCapabilities, UICATALOG_CAPS} from '../desi import {initSession, deleteSession, hasDefaultPrebuiltWDA, MOCHA_TIMEOUT} from '../helpers/session'; import {util} from 'appium/support'; -chai.should(); -chai.use(chaiAsPromised); describe('XCUITestDriver - elements -', function () { this.timeout(MOCHA_TIMEOUT); let driver; + let chai; + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + const caps = amendCapabilities(UICATALOG_CAPS, { 'appium:usePrebuiltWDA': hasDefaultPrebuiltWDA(), }); diff --git a/test/functional/basic/face-id-e2e-specs.js b/test/functional/basic/face-id-e2e-specs.js index eb563f0b1..85d60690d 100644 --- a/test/functional/basic/face-id-e2e-specs.js +++ b/test/functional/basic/face-id-e2e-specs.js @@ -1,5 +1,3 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import {amendCapabilities, FACEIDAPP_CAPS} from '../desired'; import {initSession, deleteSession, hasDefaultPrebuiltWDA, MOCHA_TIMEOUT} from '../helpers/session'; import B from 'bluebird'; @@ -7,9 +5,6 @@ import {killAllSimulators} from '../helpers/simulator'; import {CLASS_CHAIN_SEARCH} from '../helpers/element'; import {waitForCondition} from 'asyncbox'; -chai.should(); -chai.use(chaiAsPromised); -const expect = chai.expect; const DEFAULT_IMPLICIT_TIMEOUT_MS = 1000; const FACE_ID_SELECTOR = '**/XCUIElementTypeStaticText[`label == "Face ID"`]'; @@ -26,6 +21,17 @@ if (!process.env.CI) { this.timeout(MOCHA_TIMEOUT * 2); this.retries(MOCHA_RETRIES); let driver; + let chai; + let expect; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + expect = chai.expect; + }); beforeEach(async function () { await killAllSimulators(); diff --git a/test/functional/basic/find-e2e-specs.js b/test/functional/basic/find-e2e-specs.js index 644318dea..3b0e19d76 100644 --- a/test/functional/basic/find-e2e-specs.js +++ b/test/functional/basic/find-e2e-specs.js @@ -1,5 +1,3 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import B from 'bluebird'; import _ from 'lodash'; import {retryInterval} from 'asyncbox'; @@ -13,8 +11,6 @@ import {PREDICATE_SEARCH, CLASS_CHAIN_SEARCH} from '../helpers/element'; import {initSession, deleteSession, hasDefaultPrebuiltWDA, MOCHA_TIMEOUT} from '../helpers/session'; import {util} from 'appium/support'; -chai.should(); -chai.use(chaiAsPromised); const TEST_PAUSE_DURATION = 500; @@ -28,7 +24,16 @@ describe('XCUITestDriver - find -', function () { this.timeout(MOCHA_TIMEOUT); let driver; + let chai; + before(async function () { + + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + const caps = amendCapabilities(UICATALOG_CAPS, { 'appium:usePrebuiltWDA': hasDefaultPrebuiltWDA(), }); diff --git a/test/functional/basic/gesture-e2e-specs.js b/test/functional/basic/gesture-e2e-specs.js index ec60740b3..6a568b3bd 100644 --- a/test/functional/basic/gesture-e2e-specs.js +++ b/test/functional/basic/gesture-e2e-specs.js @@ -1,5 +1,3 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import B from 'bluebird'; import {retryInterval} from 'asyncbox'; import {UICATALOG_CAPS, amendCapabilities} from '../desired'; @@ -7,8 +5,6 @@ import {PREDICATE_SEARCH} from '../helpers/element'; import {initSession, deleteSession, hasDefaultPrebuiltWDA, MOCHA_TIMEOUT} from '../helpers/session'; import {APPIUM_IMAGE} from '../web/helpers'; -chai.should(); -chai.use(chaiAsPromised); const BTN_OK_CNCL = 'Okay / Cancel'; @@ -16,6 +12,15 @@ describe('XCUITestDriver - gestures', function () { this.timeout(MOCHA_TIMEOUT); let driver; + let chai; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + }); describe('dynamic gestures', function () { before(async function () { diff --git a/test/functional/basic/touch-id-e2e-specs.js b/test/functional/basic/touch-id-e2e-specs.js index c18b89699..66e00f213 100644 --- a/test/functional/basic/touch-id-e2e-specs.js +++ b/test/functional/basic/touch-id-e2e-specs.js @@ -1,5 +1,3 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import {amendCapabilities, TOUCHIDAPP_CAPS} from '../desired'; import {initSession, deleteSession, hasDefaultPrebuiltWDA, MOCHA_TIMEOUT} from '../helpers/session'; import B from 'bluebird'; @@ -7,9 +5,6 @@ import {killAllSimulators} from '../helpers/simulator'; import {CLASS_CHAIN_SEARCH} from '../helpers/element'; import {waitForCondition} from 'asyncbox'; -chai.should(); -chai.use(chaiAsPromised); -const expect = chai.expect; const DEFAULT_IMPLICIT_TIMEOUT_MS = 1000; const TOUCH_ID_SELECTOR = '**/XCUIElementTypeStaticText[`label == "Touch ID for β€œbiometric”"`]'; @@ -23,6 +18,18 @@ if (!process.env.CI) { this.timeout(MOCHA_TIMEOUT * 2); this.retries(MOCHA_RETRIES); let driver; + let chai; + let expect; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + + expect = chai.expect; + }); beforeEach(async function () { await killAllSimulators(); diff --git a/test/functional/device/accessibility-e2e-specs.js b/test/functional/device/accessibility-e2e-specs.js index eafe9ba4a..141fd0f30 100644 --- a/test/functional/device/accessibility-e2e-specs.js +++ b/test/functional/device/accessibility-e2e-specs.js @@ -1,17 +1,23 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import {PREDICATE_SEARCH} from '../helpers/element'; import {MOCHA_TIMEOUT, initSession, deleteSession, hasDefaultPrebuiltWDA} from '../helpers/session'; import {SETTINGS_CAPS, amendCapabilities} from '../desired'; -chai.should(); -chai.use(chaiAsPromised); describe('Accessibility', function () { this.timeout(MOCHA_TIMEOUT); let driver, caps; + let chai; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + }); + beforeEach(function () { caps = amendCapabilities(SETTINGS_CAPS, { 'appium:usePrebuiltWDA': hasDefaultPrebuiltWDA(), diff --git a/test/functional/device/file-movement-e2e-specs.js b/test/functional/device/file-movement-e2e-specs.js index 83fb7fb47..190f282fc 100644 --- a/test/functional/device/file-movement-e2e-specs.js +++ b/test/functional/device/file-movement-e2e-specs.js @@ -1,12 +1,8 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import {amendCapabilities, UICATALOG_CAPS} from '../desired'; import {initSession, deleteSession, hasDefaultPrebuiltWDA, MOCHA_TIMEOUT} from '../helpers/session'; import {fs, tempDir, zip} from 'appium/support'; import path from 'path'; -chai.should(); -chai.use(chaiAsPromised); const UICAT_CONTAINER = `@com.example.apple-samplecode.UICatalog`; @@ -19,7 +15,15 @@ describe('XCUITestDriver - file movement', function () { this.timeout(MOCHA_TIMEOUT); let driver; + let chai; + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + const caps = amendCapabilities(UICATALOG_CAPS, { 'appium:usePrebuiltWDA': hasDefaultPrebuiltWDA(), }); diff --git a/test/functional/device/otherApps-e2e-specs.js b/test/functional/device/otherApps-e2e-specs.js index f87497820..c151a02a4 100644 --- a/test/functional/device/otherApps-e2e-specs.js +++ b/test/functional/device/otherApps-e2e-specs.js @@ -1,10 +1,6 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import {MOCHA_TIMEOUT, initSession, deleteSession, hasDefaultPrebuiltWDA} from '../helpers/session'; import {MULTIPLE_APPS, amendCapabilities} from '../desired'; -chai.should(); -chai.use(chaiAsPromised); describe('OtherApps', function () { this.timeout(MOCHA_TIMEOUT); @@ -12,7 +8,15 @@ describe('OtherApps', function () { let caps; let driver; - before(function () { + let chai; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + caps = amendCapabilities(MULTIPLE_APPS, { 'appium:usePrebuiltWDA': hasDefaultPrebuiltWDA(), 'appium:wdaStartupRetries': 0, diff --git a/test/functional/device/passwords-e2e-specs.js b/test/functional/device/passwords-e2e-specs.js index 9c0eb9a24..bd7b07e9d 100644 --- a/test/functional/device/passwords-e2e-specs.js +++ b/test/functional/device/passwords-e2e-specs.js @@ -1,17 +1,23 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import {MOCHA_TIMEOUT, initSession, deleteSession, hasDefaultPrebuiltWDA} from '../helpers/session'; import {UICATALOG_CAPS, amendCapabilities, extractCapabilityValue} from '../desired'; import {util} from 'appium/support'; -chai.should(); -chai.use(chaiAsPromised); describe('Passwords', function () { this.timeout(MOCHA_TIMEOUT); let driver, caps; + let chai; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + }); + beforeEach(function () { caps = amendCapabilities(UICATALOG_CAPS, { 'appium:usePrebuiltWDA': hasDefaultPrebuiltWDA(), diff --git a/test/functional/device/performance-e2e-specs.js b/test/functional/device/performance-e2e-specs.js index ff427ec05..77c0a8118 100644 --- a/test/functional/device/performance-e2e-specs.js +++ b/test/functional/device/performance-e2e-specs.js @@ -1,17 +1,22 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import B from 'bluebird'; import {amendCapabilities, UICATALOG_CAPS} from '../desired'; import {initSession, deleteSession, hasDefaultPrebuiltWDA, MOCHA_TIMEOUT} from '../helpers/session'; -chai.should(); -chai.use(chaiAsPromised); describe('XCUITestDriver - performance', function () { this.timeout(MOCHA_TIMEOUT); const profileName = 'Time Profiler'; let driver; + let chai; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + }); describe('record performance metrics', function () { before(async function () { diff --git a/test/functional/device/xctest-e2e-specs.js b/test/functional/device/xctest-e2e-specs.js index 3d1237fd2..79140ec5e 100644 --- a/test/functional/device/xctest-e2e-specs.js +++ b/test/functional/device/xctest-e2e-specs.js @@ -1,6 +1,4 @@ -import chai from 'chai'; import path from 'path'; -import chaiAsPromised from 'chai-as-promised'; import {MOCHA_TIMEOUT, initSession, deleteSession, hasDefaultPrebuiltWDA} from '../helpers/session'; import {GENERIC_CAPS, amendCapabilities} from '../desired'; import xcode from 'appium-xcode'; @@ -15,15 +13,21 @@ const TEST_BUNDLE_PATH = path.resolve( ); const XCTEST_BUNDLE_PATH = path.join(TEST_BUNDLE_PATH, 'PlugIns', 'XCTesterAppUITests.xctest'); -chai.should(); -chai.use(chaiAsPromised); if (process.env.LAUNCH_WITH_IDB) { describe('XCTest', function () { this.timeout(MOCHA_TIMEOUT); let driver; + let chai; + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + // idb_companion doesn't work with xcode 13 or lower due to concurrency lib issue. if ( /** @type {import('appium-xcode').XcodeVersion} */ (await xcode.getVersion(true)).major < 14 diff --git a/test/functional/driver/driver-e2e-specs.js b/test/functional/driver/driver-e2e-specs.js index 9f5dad08d..90e28ce8b 100644 --- a/test/functional/driver/driver-e2e-specs.js +++ b/test/functional/driver/driver-e2e-specs.js @@ -1,5 +1,3 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import {retryInterval} from 'asyncbox'; import {getSimulator} from 'appium-ios-simulator'; import {killAllSimulators, deleteDeviceWithRetry, cleanupSimulator} from '../helpers/simulator'; @@ -18,9 +16,6 @@ import axios from 'axios'; const SIM_DEVICE_NAME = 'xcuitestDriverTest'; -chai.should(); -chai.use(chaiAsPromised); - const simctl = new Simctl(); async function createDevice() { @@ -41,8 +36,15 @@ describe('XCUITestDriver', function () { let baseCaps; let caps; let driver; + let chai; before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + const udid = await createDevice(); baseCaps = amendCapabilities(UICATALOG_SIM_CAPS, {'appium:udid': udid}); caps = amendCapabilities(baseCaps, { diff --git a/test/functional/long/typing-stress-e2e-specs.js b/test/functional/long/typing-stress-e2e-specs.js index 5d148e758..220dc3e08 100644 --- a/test/functional/long/typing-stress-e2e-specs.js +++ b/test/functional/long/typing-stress-e2e-specs.js @@ -1,14 +1,8 @@ -// @ts-check - -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import {UICATALOG_CAPS, amendCapabilities} from '../desired'; import {PREDICATE_SEARCH} from '../helpers/element'; import {initSession, deleteSession} from '../helpers/session'; import {retryInterval} from 'asyncbox'; -chai.should(); -chai.use(chaiAsPromised); // leave the long test to Travis const TYPING_TRIES = process.env.CI ? 100 : 10; @@ -17,7 +11,15 @@ describe('XCUITestDriver - long tests', function () { this.timeout(0); let driver; + let chai; + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + const caps = amendCapabilities(UICATALOG_CAPS, {'appium:maxTypingFrequency': 20}); driver = await initSession(caps); }); diff --git a/test/functional/tv/tvos-e2e-specs.js b/test/functional/tv/tvos-e2e-specs.js index 340ca19a4..2ae850c00 100644 --- a/test/functional/tv/tvos-e2e-specs.js +++ b/test/functional/tv/tvos-e2e-specs.js @@ -1,5 +1,3 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import {getSimulator} from 'appium-ios-simulator'; import {cleanupSimulator} from '../helpers/simulator'; import Simctl from 'node-simctl'; @@ -8,8 +6,6 @@ import {TVOS_CAPS} from '../desired'; const SIM_DEVICE_NAME = 'xcuitestDriverTest'; -chai.should(); -chai.use(chaiAsPromised); const simctl = new Simctl(); @@ -18,7 +14,15 @@ describe('tvOS', function () { let baseCaps; let udid; + let chai; + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + udid = await simctl.createDevice( SIM_DEVICE_NAME, TVOS_CAPS.deviceName, diff --git a/test/functional/web/safari-alerts-e2e-specs.js b/test/functional/web/safari-alerts-e2e-specs.js index ca3b9fd47..892be4205 100644 --- a/test/functional/web/safari-alerts-e2e-specs.js +++ b/test/functional/web/safari-alerts-e2e-specs.js @@ -1,18 +1,22 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import {retryInterval} from 'asyncbox'; import {SAFARI_CAPS, amendCapabilities} from '../desired'; import {initSession, deleteSession, hasDefaultPrebuiltWDA, MOCHA_TIMEOUT} from '../helpers/session'; import {GUINEA_PIG_PAGE} from './helpers'; -chai.should(); -chai.use(chaiAsPromised); describe('safari - alerts', function () { this.timeout(MOCHA_TIMEOUT); let driver; + let chai; + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + const caps = amendCapabilities(SAFARI_CAPS, { 'appium:safariInitialUrl': GUINEA_PIG_PAGE, 'appium:safariAllowPopups': true, diff --git a/test/functional/web/safari-basic-e2e-specs.js b/test/functional/web/safari-basic-e2e-specs.js index 251dda70a..04e81fcb7 100644 --- a/test/functional/web/safari-basic-e2e-specs.js +++ b/test/functional/web/safari-basic-e2e-specs.js @@ -1,5 +1,3 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import B from 'bluebird'; import {MOCHA_TIMEOUT, initSession, deleteSession, hasDefaultPrebuiltWDA} from '../helpers/session'; import {SAFARI_CAPS, amendCapabilities} from '../desired'; @@ -22,9 +20,6 @@ import { import {util} from 'appium/support'; import {retryInterval} from 'asyncbox'; -chai.should(); -chai.use(chaiAsPromised); -const expect = chai.expect; const DEFAULT_CAPS = amendCapabilities(SAFARI_CAPS, { 'appium:safariInitialUrl': GUINEA_PIG_PAGE, @@ -37,6 +32,18 @@ describe('Safari - basics -', function () { this.timeout(MOCHA_TIMEOUT); let driver; + let chai; + let expect; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + + expect = chai.expect; + }); describe('init', function () { afterEach(async function () { diff --git a/test/functional/web/safari-execute-e2e-specs.js b/test/functional/web/safari-execute-e2e-specs.js index 872659b2a..bc4715e5b 100644 --- a/test/functional/web/safari-execute-e2e-specs.js +++ b/test/functional/web/safari-execute-e2e-specs.js @@ -1,15 +1,7 @@ -// import _ from 'lodash'; -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -// import http from 'http'; import {SAFARI_CAPS, amendCapabilities} from '../desired'; import {initSession, deleteSession, hasDefaultPrebuiltWDA, MOCHA_TIMEOUT} from '../helpers/session'; import {openPage, GUINEA_PIG_PAGE} from './helpers'; -chai.should(); -chai.use(chaiAsPromised); -const expect = chai.expect; - const SCROLL_INTO_VIEW = `return arguments[0].scrollIntoView(true);`; const GET_RIGHT_INNERHTML = `return document.body.innerHTML.indexOf('I am some page content') > 0`; const GET_WRONG_INNERHTML = `return document.body.innerHTML.indexOf('I am not some page content') > 0`; @@ -19,7 +11,17 @@ describe('safari - execute -', function () { this.timeout(MOCHA_TIMEOUT); let driver; + let chai; + let expect; + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + expect = chai.expect; + const caps = amendCapabilities(SAFARI_CAPS, { 'appium:safariInitialUrl': GUINEA_PIG_PAGE, 'appium:showSafariConsoleLog': true, diff --git a/test/functional/web/safari-nativewebtap-e2e-specs.js b/test/functional/web/safari-nativewebtap-e2e-specs.js index ce6bd4991..ce8c36e71 100644 --- a/test/functional/web/safari-nativewebtap-e2e-specs.js +++ b/test/functional/web/safari-nativewebtap-e2e-specs.js @@ -1,5 +1,3 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import _ from 'lodash'; import {util} from 'appium/support'; import {initSession, deleteSession, hasDefaultPrebuiltWDA, MOCHA_TIMEOUT} from '../helpers/session'; @@ -23,8 +21,6 @@ import {retryInterval} from 'asyncbox'; import B from 'bluebird'; import {CLASS_CHAIN_SEARCH} from '../helpers/element'; -chai.should(); -chai.use(chaiAsPromised); const caps = amendCapabilities(SAFARI_CAPS, { 'appium:safariInitialUrl': GUINEA_PIG_PAGE, @@ -42,7 +38,16 @@ describe('Safari - coordinate conversion -', function () { this.timeout(MOCHA_TIMEOUT * 2); const devices = [DEVICE_NAME, DEVICE_NAME_FOR_SAFARI_IPAD]; - before(function () { + + let chai; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + if (process.env.CI) { return this.skip(); } diff --git a/test/functional/web/safari-ssl-e2e-specs.js b/test/functional/web/safari-ssl-e2e-specs.js index 34b90ea57..08e73b334 100644 --- a/test/functional/web/safari-ssl-e2e-specs.js +++ b/test/functional/web/safari-ssl-e2e-specs.js @@ -1,6 +1,4 @@ import B from 'bluebird'; -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import https from 'https'; import {getFreePort} from '../helpers/ports'; import os from 'os'; @@ -11,8 +9,6 @@ import {doesIncludeCookie, doesNotIncludeCookie, newCookie, oldCookie1} from './ const pem = B.promisifyAll(_pem); -chai.should(); -chai.use(chaiAsPromised); let caps; let pemCertificate; @@ -25,7 +21,15 @@ describe('Safari SSL', function () { let driver; /** @type {string} */ let localHttpsUrl; + let chai; + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + // Create a random pem certificate const privateKey = await pem.createPrivateKeyAsync(); const keys = await pem.createCertificateAsync({ diff --git a/test/functional/web/safari-window-e2e-specs.js b/test/functional/web/safari-window-e2e-specs.js index 645361bae..817a69e10 100644 --- a/test/functional/web/safari-window-e2e-specs.js +++ b/test/functional/web/safari-window-e2e-specs.js @@ -1,6 +1,4 @@ -import chai from 'chai'; import _ from 'lodash'; -import chaiAsPromised from 'chai-as-promised'; import {SAFARI_CAPS, amendCapabilities} from '../desired'; import {initSession, deleteSession, hasDefaultPrebuiltWDA, MOCHA_TIMEOUT} from '../helpers/session'; import { @@ -12,8 +10,6 @@ import { } from './helpers'; import {waitForCondition} from 'asyncbox'; -chai.should(); -chai.use(chaiAsPromised); const GET_ELEM_SYNC = `return document.getElementsByTagName('h1')[0].innerHTML;`; const GET_ELEM_ASYNC = `arguments[arguments.length - 1](document.getElementsByTagName('h1')[0].innerHTML);`; @@ -26,6 +22,16 @@ const SUB_FRAME_3_TITLE = 'Sub frame 3'; const DEFAULT_IMPLICIT_TIMEOUT_MS = 1000; describe('safari - windows and frames', function () { + let chai; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + }); + describe('without safariAllowPopups', function () { this.timeout(MOCHA_TIMEOUT); diff --git a/test/unit/app-infos-cache-specs.js b/test/unit/app-infos-cache-specs.js index efc306f6f..a7eb57908 100644 --- a/test/unit/app-infos-cache-specs.js +++ b/test/unit/app-infos-cache-specs.js @@ -1,19 +1,24 @@ import { AppInfosCache, } from '../../lib/app-infos-cache'; -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import { fs, tempDir, zip } from 'appium/support'; import path from 'node:path'; import log from '../../lib/logger.js'; -chai.should(); -chai.use(chaiAsPromised); - const BIOMETRIC_BUNDLE_ID = 'com.mwakizaka.biometric'; describe('AppInfosCache', function () { + let chai; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + }); + describe('retrives info from different types of apps', function () { let ipaPath; const appPath = path.resolve(__dirname, '..', 'assets', 'biometric.app'); diff --git a/test/unit/app-utils-specs.js b/test/unit/app-utils-specs.js index fbbe49338..ddd5cc504 100644 --- a/test/unit/app-utils-specs.js +++ b/test/unit/app-utils-specs.js @@ -2,15 +2,21 @@ import { unzipStream, unzipFile, } from '../../lib/app-utils'; -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import { fs, tempDir, zip } from 'appium/support'; import path from 'node:path'; -chai.should(); -chai.use(chaiAsPromised); describe('app-utils', function () { + let chai; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + }); + describe('unzipStream', function () { it('should unzip from stream', async function () { try { diff --git a/test/unit/commands/activeAppInfo-specs.js b/test/unit/commands/activeAppInfo-specs.js index ededab475..18e978428 100644 --- a/test/unit/commands/activeAppInfo-specs.js +++ b/test/unit/commands/activeAppInfo-specs.js @@ -1,19 +1,22 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import sinon from 'sinon'; import XCUITestDriver from '../../../lib/driver'; -chai.should(); -chai.use(chaiAsPromised); describe('get activeapp commands', function () { const driver = new XCUITestDriver(); - // @ts-ignore give the driver a spy-able proxy object driver.wda = {jwproxy: {command: () => {}}}; let proxyStub; - this.beforeEach(function () { - // @ts-ignore ok for tests + let chai; + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + }); + + beforeEach(function () { proxyStub = sinon.stub(driver.wda.jwproxy, 'command'); }); diff --git a/test/unit/commands/alert-specs.js b/test/unit/commands/alert-specs.js index d3801c68f..7aa2b1d19 100644 --- a/test/unit/commands/alert-specs.js +++ b/test/unit/commands/alert-specs.js @@ -1,14 +1,19 @@ import sinon from 'sinon'; import XCUITestDriver from '../../../lib/driver'; -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -chai.should(); -chai.use(chaiAsPromised); describe('alert commands', function () { let driver = new XCUITestDriver(); let proxySpy = sinon.stub(driver, 'proxyCommand'); + let chai; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + }); afterEach(function () { proxySpy.reset(); @@ -25,7 +30,7 @@ describe('alert commands', function () { describe('setAlertText', function () { it('should send translated POST request to WDA', async function () { await driver.setAlertText('some text'); - proxySpy.should.have.been.calledOnceWith('/alert/text', 'POST', {value: 'some text'}); + proxySpy.calledOnceWith('/alert/text', 'POST', {value: 'some text'}).should.be.true; }); }); describe('postAcceptAlert', function () { @@ -57,7 +62,7 @@ describe('alert commands', function () { it('should send accept alert request to WDA with encoded button label', async function () { const buttonLabel = 'some label'; await driver.execute(`mobile: ${commandName}`, {action: 'accept', buttonLabel}); - proxySpy.should.have.been.calledOnceWith('/alert/accept', 'POST', {name: buttonLabel}); + proxySpy.calledOnceWith('/alert/accept', 'POST', {name: buttonLabel}).should.be.true; }); it('should send dimsiss alert request to WDA if button label is not provided', async function () { diff --git a/test/unit/commands/context-specs.js b/test/unit/commands/context-specs.js index f22e0f691..64b3e245c 100644 --- a/test/unit/commands/context-specs.js +++ b/test/unit/commands/context-specs.js @@ -1,12 +1,19 @@ import XCUITestDriver from '../../../lib/driver'; -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -chai.should(); -chai.use(chaiAsPromised); -const expect = chai.expect; describe('context', function () { + let chai; + let expect; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + expect = chai.expect; + }); + describe('onPageChange', function () { const pageChangeNotification = { appIdKey: '5191', diff --git a/test/unit/commands/deviceinfo-specs.js b/test/unit/commands/deviceinfo-specs.js index 4d06ae6b5..fe393acc5 100644 --- a/test/unit/commands/deviceinfo-specs.js +++ b/test/unit/commands/deviceinfo-specs.js @@ -1,19 +1,23 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import sinon from 'sinon'; import XCUITestDriver from '../../../lib/driver'; -chai.should(); -chai.use(chaiAsPromised); describe('get deviceinfo commands', function () { const driver = new XCUITestDriver(); - // @ts-ignore give the driver a spy-able proxy object driver.wda = {jwproxy: {command: () => {}}}; let proxyStub; - this.beforeEach(function () { - // @ts-ignore ok for tests + let chai; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + }); + + beforeEach(function () { proxyStub = sinon.stub(driver.wda.jwproxy, 'command'); }); @@ -27,7 +31,6 @@ describe('get deviceinfo commands', function () { currentLocale: 'ja_EN', }; proxyStub.returns(opts); - await driver.mobileGetDeviceInfo().should.eventually.eql(opts); }); diff --git a/test/unit/commands/element-specs.js b/test/unit/commands/element-specs.js index 2b8115160..5399752d7 100644 --- a/test/unit/commands/element-specs.js +++ b/test/unit/commands/element-specs.js @@ -1,25 +1,26 @@ -// @ts-check - -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -import {createSandbox} from 'sinon'; -import sinonChai from 'sinon-chai'; +// eslint-disable-next-line +import sinon, {createSandbox} from 'sinon'; import XCUITestDriver from '../../../lib/driver'; -chai.should(); -chai.use(chaiAsPromised).use(sinonChai); describe('element commands', function () { - /** @type {sinon.SinonSandbox} */ let sandbox; /** @type {XCUITestDriver} */ let driver; + let chai; + /** @type {sinon.SinonStubbedMember} */ let proxyStub; - before(function () { + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + driver = new XCUITestDriver(); }); @@ -39,8 +40,8 @@ describe('element commands', function () { it('should call setValue', async function () { await driver.setValueImmediate('hello', '2'); - driver.setValue.should.have.been.calledOnceWithExactly('hello', '2'); - driver.setValue.should.have.returned(undefined); + driver.setValue.calledOnceWithExactly('hello', '2').should.be.true; + driver.setValue.returned(undefined).should.be.true; }); }); @@ -49,7 +50,7 @@ describe('element commands', function () { const attribute = 'enabled'; afterEach(function () { - proxyStub.should.have.been.calledOnce; + proxyStub.calledOnce.should.be.true; }); it('should properly parse boolean true attribute presented as integer', async function () { @@ -88,7 +89,7 @@ describe('element commands', function () { const property = 'enabled'; afterEach(function () { - proxyStub.should.have.been.calledOnce; + proxyStub.calledOnce.should.be.true; }); it('should properly parse boolean true attribute presented as integer', async function () { @@ -127,8 +128,8 @@ describe('element commands', function () { const getContentSizeStub = sandbox.stub(driver, 'getContentSize'); getContentSizeStub.resolves('foo'); (await driver.getAttribute('contentSize', 2)).should.eql('foo'); - proxyStub.should.not.have.been.called; - getContentSizeStub.should.have.been.calledOnce; + proxyStub.called.should.be.false; + getContentSizeStub.calledOnce.should.be.true; }); }); @@ -167,7 +168,7 @@ describe('element commands', function () { driver.curContext = oldContext; }); it('should throw when in a web context', async function () { - await driver.getContentSize(el).should.be.rejectedWith(/not yet implemented/); + await driver.getContentSize(el).should.be.rejectedWith(/not yet implemented/); }); }); @@ -190,7 +191,7 @@ describe('element commands', function () { left: 0, scrollableOffset: 100, }); - getRectStub.should.have.been.calledOnce; + getRectStub.calledOnce.should.be.true; }); it('should get simple difference in element positions of a table', async function () { @@ -210,7 +211,7 @@ describe('element commands', function () { left: 0, scrollableOffset: 170, }); - getRectStub.should.have.been.calledTwice; + getRectStub.calledTwice.should.be.true; }); it('should be sensitive to row items in the case of a collection view', async function () { @@ -239,7 +240,7 @@ describe('element commands', function () { left: 0, scrollableOffset, }); - getRectStub.should.have.been.calledThrice; + getRectStub.calledThrice.should.be.true; }); }); @@ -252,33 +253,33 @@ describe('element commands', function () { describe('success', function () { it('should proxy string as array of characters', async function () { await driver.setValue('hello\uE006', elementId); - proxyStub.should.have.been.calledOnceWith(expectedEndpoint, expectedMethod, { + proxyStub.calledOnceWith(expectedEndpoint, expectedMethod, { value: ['h', 'e', 'l', 'l', 'o', '\n'], - }); + }).should.be.true; }); it('should proxy string with smileys as array of characters', async function () { await driver.setValue('helloπŸ˜€πŸ˜Ž', elementId); - proxyStub.should.have.been.calledOnceWith(expectedEndpoint, expectedMethod, { + proxyStub.calledOnceWith(expectedEndpoint, expectedMethod, { value: ['h', 'e', 'l', 'l', 'o', 'πŸ˜€', '😎'], - }); + }).should.be.true; }); it('should proxy number as array of characters', async function () { await driver.setValue(1234.56, elementId); - proxyStub.should.have.been.calledOnceWith(expectedEndpoint, expectedMethod, { + proxyStub.calledOnceWith(expectedEndpoint, expectedMethod, { value: ['1', '2', '3', '4', '.', '5', '6'], - }); + }).should.be.true; }); it('should proxy string array as array of characters', async function () { await driver.setValue(['hel', 'lo'], elementId); - proxyStub.should.have.been.calledOnceWith(expectedEndpoint, expectedMethod, { + proxyStub.calledOnceWith(expectedEndpoint, expectedMethod, { value: ['h', 'e', 'l', 'l', 'o'], - }); + }).should.be.true; }); it('should proxy integer array as array of characters', async function () { await driver.setValue([1234], elementId); - proxyStub.should.have.been.calledOnceWith(expectedEndpoint, expectedMethod, { + proxyStub.calledOnceWith(expectedEndpoint, expectedMethod, { value: ['1', '2', '3', '4'], - }); + }).should.be.true; }); }); @@ -318,19 +319,19 @@ describe('element commands', function () { it('with default', async function () { driver.opts.sendKeyStrategy = undefined; await driver.setValue('hello\uE006πŸ˜€', elementId); - atomElement.should.have.been.calledOnce; - executeAtom.should.have.been.calledOnce; - setValueWithWebAtom.should.have.been.calledOnceWith( + atomElement.calledOnce.should.be.true; + executeAtom.calledOnce.should.be.true; + setValueWithWebAtom.calledOnceWith( webEl, 'hello\uE006πŸ˜€' - ); + ).should.be.true; }); it('with oneByOne', async function () { driver.opts.sendKeyStrategy = 'oneByOne'; await driver.setValue('hello\uE006πŸ˜€', elementId); - atomElement.should.have.been.calledOnce; - executeAtom.should.have.been.calledOnce; + atomElement.calledOnce.should.be.true; + executeAtom.calledOnce.should.be.true; setValueWithWebAtom.getCall(0).args.should.eql([webEl, 'h']); setValueWithWebAtom.getCall(1).args.should.eql([webEl, 'e']); setValueWithWebAtom.getCall(2).args.should.eql([webEl, 'l']); diff --git a/test/unit/commands/file-movement-specs.js b/test/unit/commands/file-movement-specs.js index 5d2e840ee..3bd48d3f1 100644 --- a/test/unit/commands/file-movement-specs.js +++ b/test/unit/commands/file-movement-specs.js @@ -1,12 +1,20 @@ import {parseContainerPath} from '../../../lib/commands/file-movement'; -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import {tempDir} from 'appium/support'; -const should = chai.should(); -chai.use(chaiAsPromised); describe('file-movement', function () { + let chai; + let expect; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + expect = chai.expect; + }); + describe('parseContainerPath', function () { it('should parse with container', async function () { const mntRoot = await tempDir.openDir(); @@ -17,7 +25,7 @@ describe('file-movement', function () { bundleId.should.eql('io.appium.example'); pathInContainer.should.eql(`${mntRoot}/Documents/file.txt`); - /** @type {string} */ (containerType).should.eql('app'); + containerType.should.eql('app'); }); it('should parse with container root', async function () { const mntRoot = await tempDir.openDir(); @@ -28,7 +36,7 @@ describe('file-movement', function () { bundleId.should.eql('io.appium.example'); pathInContainer.should.eql(mntRoot); - /** @type {string} */ (containerType).should.eql('documents'); + containerType.should.eql('documents'); }); it('should parse without container', async function () { const mntRoot = await tempDir.openDir(); @@ -39,7 +47,7 @@ describe('file-movement', function () { bundleId.should.eql('io.appium.example'); pathInContainer.should.eql(`${mntRoot}/Documents/file.txt`); - should.equal(containerType, null); + expect(containerType).equal(null); }); it('should raise an error if no container path', async function () { const mntRoot = await tempDir.openDir(); diff --git a/test/unit/commands/find-specs.js b/test/unit/commands/find-specs.js index bf0c8597e..25b8a8453 100644 --- a/test/unit/commands/find-specs.js +++ b/test/unit/commands/find-specs.js @@ -5,6 +5,12 @@ describe('general commands', function () { const driver = new XCUITestDriver(); const proxySpy = sinon.stub(driver, 'proxyCommand'); + let chai; + + before(async function () { + chai = await import('chai'); + chai.should(); + }); afterEach(function () { proxySpy.reset(); }); @@ -22,10 +28,10 @@ describe('general commands', function () { try { await driver.findNativeElementOrElements(strategy, selector, mult); } catch (ign) {} - proxySpy.should.have.been.calledOnceWith(`/element${mult ? 's' : ''}`, 'POST', { + proxySpy.calledOnceWith(`/element${mult ? 's' : ''}`, 'POST', { using: modStrategy || strategy, value: modSelector, - }); + }).should.be.true; proxySpy.reset(); } @@ -100,16 +106,16 @@ describe('general commands', function () { let el = await driver.findNativeElementOrElements('xpath', variant, false, { ELEMENT: 'ctx', }); - proxySpy.should.have.been.calledTwice; - proxySpy.should.have.been.calledWith('/element/ctx/element', 'POST', { + proxySpy.calledTwice.should.be.true; + proxySpy.calledWith('/element/ctx/element', 'POST', { using: 'class chain', value: '*[1]', - }); - proxySpy.should.have.been.calledWith('/element/ctx/element', 'POST', { + }).should.be.true; + proxySpy.calledWith('/element/ctx/element', 'POST', { using: 'class chain', value: '*[2]', - }); - attribSpy.should.have.been.calledTwice; + }).should.be.true; + attribSpy.calledTwice.should.be.true; el.should.eql({ELEMENT: 2}); proxySpy.reset(); attribSpy.reset(); diff --git a/test/unit/commands/general-specs.js b/test/unit/commands/general-specs.js index 0dae3bf9b..7740ed782 100644 --- a/test/unit/commands/general-specs.js +++ b/test/unit/commands/general-specs.js @@ -1,39 +1,41 @@ import sinon from 'sinon'; import _ from 'lodash'; import XCUITestDriver from '../../../lib/driver'; -import chai from 'chai'; -import sinonChai from 'sinon-chai'; -chai.use(sinonChai); describe('general commands', function () { const driver = new XCUITestDriver(); - const proxyStub = sinon.stub(driver, 'proxyCommand'); + + let chai; + let mockDriver; + + before(async function () { + chai = await import('chai'); + chai.should(); + }); + + beforeEach(function () { + mockDriver = sinon.mock(driver); + }); afterEach(function () { - proxyStub.reset(); + mockDriver.verify(); }); describe('background', function () { it('should deactivate app for the given time if seconds is zero or greater', async function () { + mockDriver.expects('proxyCommand').once().withExactArgs('/wda/deactivateApp', 'POST', { duration: 0.5 }, true); await driver.background(0.5); - proxyStub.calledOnce.should.be.true; - proxyStub.firstCall.args[0].should.eql('/wda/deactivateApp'); - proxyStub.firstCall.args[1].should.eql('POST'); }); it('should switch to home screen if seconds less than zero', async function () { + mockDriver.expects('proxyCommand').once().withExactArgs('/wda/homescreen', 'POST', {}, false); await driver.background(-1); - proxyStub.calledOnce.should.be.true; - proxyStub.firstCall.args[0].should.eql('/wda/homescreen'); - proxyStub.firstCall.args[1].should.eql('POST'); }); it('should switch to home screen if seconds is null', async function () { + mockDriver.expects('proxyCommand').once().withExactArgs('/wda/homescreen', 'POST', {}, false); await driver.background(); - proxyStub.calledOnce.should.be.true; - proxyStub.firstCall.args[0].should.eql('/wda/homescreen'); - proxyStub.firstCall.args[1].should.eql('POST'); }); }); @@ -50,7 +52,6 @@ describe('general commands', function () { simctl: true, sendBiometricMatch: sandbox.stub(), }; - // @ts-ignore driver._device = device; }); @@ -60,20 +61,20 @@ describe('general commands', function () { it('should send default request to Simulator', async function () { await driver.touchId(); - device.sendBiometricMatch.should.have.been.calledOnceWith(true, 'touchId'); + device.sendBiometricMatch.calledOnceWith(true, 'touchId').should.be.true; }); it('should send request to Simulator with false', async function () { await driver.touchId(false); - device.sendBiometricMatch.should.have.been.calledOnceWith(false, 'touchId'); + device.sendBiometricMatch.calledOnceWith(false, 'touchId').should.be.true; }); it('should not be called on a real device', async function () { delete device.simctl; device.devicectl = true; await driver.touchId().should.be.rejected; - device.sendBiometricMatch.should.not.have.been.called; - // sendBiometricMatchSpy.notCalled.should.be.true; + + device.sendBiometricMatch.called.should.be.false; }); }); @@ -90,7 +91,6 @@ describe('general commands', function () { simctl: true, enrollBiometric: sandbox.stub(), }; - // @ts-ignore driver._device = device; }); @@ -102,7 +102,7 @@ describe('general commands', function () { // @ts-expect-error random stuff on opts again driver.opts.allowTouchIdEnroll = true; await driver.toggleEnrollTouchId(); - device.enrollBiometric.should.have.been.calledOnce; + device.enrollBiometric.calledOnce.should.be.true; }); it('should not be called on a real device', async function () { @@ -111,18 +111,14 @@ describe('general commands', function () { // @ts-expect-error random stuff on opts again driver.opts.allowTouchIdEnroll = true; await driver.toggleEnrollTouchId().should.be.rejected; - device.enrollBiometric.should.not.have.been.called; + device.enrollBiometric.called.should.be.false; }); }); describe('window size', function () { it('should be able to get the current window size with Rect', async function () { - proxyStub.withArgs('/window/size', 'GET').resolves({width: 100, height: 20}); - + mockDriver.expects('proxyCommand').once().withExactArgs('/window/size', 'GET').returns({width: 100, height: 20}); await driver.getWindowRect(); - proxyStub.calledOnce.should.be.true; - proxyStub.firstCall.args[0].should.eql('/window/size'); - proxyStub.firstCall.args[1].should.eql('GET'); }); }); @@ -178,8 +174,8 @@ describe('general commands', function () { }); describe('getDevicePixelRatio and getStatusBarHeight', function () { - beforeEach(function () { - proxyStub.withArgs('/wda/screen', 'GET').resolves({ + before(function () { + mockDriver.expects('proxyCommand').withExactArgs('/wda/screen', 'GET').returns({ statusBarSize: { width: 100, height: 20, diff --git a/test/unit/commands/gesture-specs.js b/test/unit/commands/gesture-specs.js index 1340b8a7e..7dc24d48d 100644 --- a/test/unit/commands/gesture-specs.js +++ b/test/unit/commands/gesture-specs.js @@ -1,18 +1,25 @@ import sinon from 'sinon'; import XCUITestDriver from '../../../lib/driver'; import {gesturesChainToString} from '../../../lib/commands/gesture'; -import _ from 'lodash'; -import sinonChai from 'sinon-chai'; -import chai from 'chai'; -chai.use(sinonChai); describe('gesture commands', function () { const driver = new XCUITestDriver(); - const proxySpy = sinon.stub(driver, 'proxyCommand'); + + let chai; + let mockDriver; + + before(async function () { + chai = await import('chai'); + chai.should(); + }); + + beforeEach(function () { + mockDriver = sinon.mock(driver); + }); afterEach(function () { - proxySpy.reset(); + mockDriver.verify(); }); describe('gesturesChainToString', function () { @@ -55,42 +62,32 @@ describe('gesture commands', function () { .should.be.rejectedWith(/Mobile scroll supports the following strategies/); }); it('should pass through bare element', async function () { + mockDriver.expects('proxyCommand').once().withExactArgs('/wda/element/4/scroll', 'POST', { direction: 'down' }); await driver.execute('mobile: scroll', {element: 4, direction: 'down'}); - proxySpy.calledOnce.should.be.true; - proxySpy.firstCall.args[0].should.eql('/wda/element/4/scroll'); - proxySpy.firstCall.args[1].should.eql('POST'); }); it('should unpack element object', async function () { + mockDriver.expects('proxyCommand').once().withExactArgs('/wda/element/4/scroll', 'POST', { direction: 'down' }); await driver.execute('mobile: scroll', {element: {ELEMENT: 4}, direction: 'down'}); - proxySpy.calledOnce.should.be.true; - proxySpy.firstCall.args[0].should.eql('/wda/element/4/scroll'); - proxySpy.firstCall.args[1].should.eql('POST'); }); it('should pass name strategy exclusively', async function () { + mockDriver.expects('proxyCommand').once().withExactArgs('/wda/element/4/scroll', 'POST', { name: 'something' }); await driver.execute('mobile: scroll', {element: 4, direction: 'down', name: 'something'}); - proxySpy.should.have.been.calledOnceWith('/wda/element/4/scroll', 'POST', { - name: 'something', - }); }); it('should pass direction strategy exclusively', async function () { + mockDriver.expects('proxyCommand').once().withExactArgs('/wda/element/4/scroll', 'POST', { direction: 'down' }); await driver.execute('mobile: scroll', { element: 4, direction: 'down', predicateString: 'something', }); - proxySpy.should.have.been.calledOnceWith('/wda/element/4/scroll', 'POST', { - direction: 'down', - }); }); it('should pass predicateString strategy exclusively', async function () { + mockDriver.expects('proxyCommand').once().withExactArgs('/wda/element/4/scroll', 'POST', { predicateString: 'something' }); await driver.execute('mobile: scroll', { element: 4, toVisible: true, predicateString: 'something', }); - proxySpy.should.have.been.calledOnceWith('/wda/element/4/scroll', 'POST', { - predicateString: 'something', - }); }); }); @@ -107,8 +104,8 @@ describe('gesture commands', function () { }); it('should proxy a swipe up request through to WDA', async function () { + mockDriver.expects('proxyCommand').once().withExactArgs('/wda/element/4/swipe', 'POST', { direction: 'up' }); await driver.execute(`mobile: ${commandName}`, {element: 4, direction: 'up'}); - proxySpy.should.have.been.calledOnceWith('/wda/element/4/swipe', 'POST', {direction: 'up'}); }); }); @@ -132,11 +129,12 @@ describe('gesture commands', function () { it('should proxy a pinch request through to WDA', async function () { const opts = {element: 4, scale: 1, velocity: '1'}; - await driver.execute(`mobile: ${commandName}`, opts); - proxySpy.should.have.been.calledOnceWith('/wda/element/4/pinch', 'POST', { + + mockDriver.expects('proxyCommand').once().withExactArgs('/wda/element/4/pinch', 'POST', { scale: opts.scale, velocity: parseInt(opts.velocity, 10), }); + await driver.execute(`mobile: ${commandName}`, opts); }); }); @@ -144,23 +142,19 @@ describe('gesture commands', function () { const commandName = 'doubleTap'; it('should proxy a doubleTap request without element through to WDA', async function () { + mockDriver.expects('proxyCommand').once().withExactArgs('/wda/doubleTap', 'POST', { x: undefined, y: undefined }); await driver.execute(`mobile: ${commandName}`); - proxySpy.calledOnce.should.be.true; - proxySpy.firstCall.args[0].should.eql('/wda/doubleTap'); - proxySpy.firstCall.args[1].should.eql('POST'); }); it('should proxy a doubleTap request for an element through to WDA', async function () { + mockDriver.expects('proxyCommand').once().withExactArgs('/wda/element/4/doubleTap', 'POST', { x: undefined, y: undefined }); await driver.execute(`mobile: ${commandName}`, {element: 4}); - proxySpy.calledOnce.should.be.true; - proxySpy.firstCall.args[0].should.eql('/wda/element/4/doubleTap'); - proxySpy.firstCall.args[1].should.eql('POST'); }); it('should proxy a doubleTap request for a coordinate point through to WDA', async function () { const opts = {x: 100, y: 100}; + mockDriver.expects('proxyCommand').once().withExactArgs('/wda/doubleTap', 'POST', opts); await driver.execute(`mobile: ${commandName}`, opts); - proxySpy.should.have.been.calledOnceWith('/wda/doubleTap', 'POST', opts); }); }); @@ -168,10 +162,8 @@ describe('gesture commands', function () { const commandName = 'twoFingerTap'; it('should proxy a twoFingerTap request for an element through to WDA', async function () { + mockDriver.expects('proxyCommand').once().withExactArgs('/wda/element/4/twoFingerTap', 'POST'); await driver.execute(`mobile: ${commandName}`, {element: 4}); - proxySpy.calledOnce.should.be.true; - proxySpy.firstCall.args[0].should.eql('/wda/element/4/twoFingerTap'); - proxySpy.firstCall.args[1].should.eql('POST'); }); }); @@ -190,36 +182,26 @@ describe('gesture commands', function () { it('should proxy a touchAndHold request without element through to WDA', async function () { const opts = {duration: 100}; + + mockDriver.expects('proxyCommand').once().withExactArgs('/wda/touchAndHold', 'POST', { + ...opts, + x: undefined, + y: undefined, + }); + await driver.execute(`mobile: ${commandName}`, opts); - proxySpy.should.have.been.calledOnceWith( - '/wda/touchAndHold', - 'POST', - { - ...opts, - x: undefined, - y: undefined, - }, - ); }); it('should proxy a touchAndHold request for an element through to WDA', async function () { const opts = {elementId: 4, duration: 100}; + mockDriver.expects('proxyCommand').once().withExactArgs('/wda/element/4/touchAndHold', 'POST', { duration: 100, x: undefined, y: undefined }); await driver.execute(`mobile: ${commandName}`, opts); - proxySpy.should.have.been.calledOnceWith( - '/wda/element/4/touchAndHold', - 'POST', - { - ..._.omit(opts, 'elementId'), - x: undefined, - y: undefined, - } - ); }); it('should proxy a touchAndHold request for a coordinate point through to WDA', async function () { const opts = {duration: 100, x: 100, y: 100}; + mockDriver.expects('proxyCommand').once().withExactArgs('/wda/touchAndHold', 'POST', opts); await driver.execute('mobile: touchAndHold', opts); - proxySpy.should.have.been.calledOnceWith('/wda/touchAndHold', 'POST', opts); }); }); @@ -228,14 +210,14 @@ describe('gesture commands', function () { it('should proxy a tap request for an element through to WDA', async function () { const opts = {elementId: 4, x: 100, y: 100}; + mockDriver.expects('proxyCommand').once().withExactArgs('/wda/element/4/tap', 'POST', { x: 100, y: 100 }); await driver.execute(`mobile: ${commandName}`, opts); - proxySpy.should.have.been.calledOnceWith('/wda/element/4/tap', 'POST', _.omit(opts, 'elementId')); }); it('should proxy a tap request for a coordinate point through to WDA', async function () { const opts = {x: 100, y: 100}; + mockDriver.expects('proxyCommand').once().withExactArgs('/wda/tap', 'POST', { x: 100, y: 100 }); await driver.execute(`mobile: ${commandName}`, opts); - proxySpy.should.have.been.calledOnceWith('/wda/tap', 'POST', opts); }); }); @@ -262,12 +244,8 @@ describe('gesture commands', function () { it('should proxy a selectPickerWheel request for an element through to WDA', async function () { const opts = {elementId: 4, order: 'next', offset: 0.3}; + mockDriver.expects('proxyCommand').once().withExactArgs('/wda/pickerwheel/4/select', 'POST', { order: 'next', offset: 0.3 }); await driver.execute(`mobile: ${commandName}`, opts); - proxySpy.should.have.been.calledOnceWith( - '/wda/pickerwheel/4/select', - 'POST', - _.omit(opts, 'elementId'), - ); }); }); @@ -347,28 +325,28 @@ describe('gesture commands', function () { it('should proxy a dragFromToForDuration request for an element through to WDA', async function () { const opts = {element: 4, duration: 100, fromX: 1, fromY: 1, toX: 100, toY: 100}; - await driver.execute(`mobile: ${commandName}`, { - element: 4, + mockDriver.expects('proxyCommand').once().withExactArgs('/wda/element/4/dragfromtoforduration', 'POST', { duration: 100, fromX: 1, fromY: 1, toX: 100, toY: 100, }); - proxySpy.should.have.been.calledOnceWith( - '/wda/element/4/dragfromtoforduration', - 'POST', - _.omit(opts, 'element'), - ); + await driver.execute(`mobile: ${commandName}`, opts); }); it('should proxy a dragFromToForDuration request for a coordinate point through to WDA', async function () { const opts = {duration: 100, fromX: 1, fromY: 1, toX: 100, toY: 100}; + mockDriver.expects('proxyCommand').once().withExactArgs('/wda/dragfromtoforduration', 'POST', { + duration: 100, + fromX: 1, + fromY: 1, + toX: 100, + toY: 100, + }); await driver.execute(`mobile: ${commandName}`, opts); - proxySpy.should.have.been.calledOnceWith('/wda/dragfromtoforduration', 'POST', opts); }); }); - }); }); diff --git a/test/unit/commands/pasteboard-specs.js b/test/unit/commands/pasteboard-specs.js index 44d7716a9..3092d7a46 100644 --- a/test/unit/commands/pasteboard-specs.js +++ b/test/unit/commands/pasteboard-specs.js @@ -6,11 +6,17 @@ describe('pasteboard commands', function () { const driver = new XCUITestDriver(); let isSimulatorStub, setPasteboardStub, getPasteboardStub; + let chai; + + before(async function () { + chai = await import('chai'); + chai.should(); + }); + beforeEach(function () { const simctl = new Simctl(); setPasteboardStub = sinon.stub(simctl, 'setPasteboard'); getPasteboardStub = sinon.stub(simctl, 'getPasteboard'); - // @ts-ignore driver._device = { simctl }; isSimulatorStub = sinon.stub(driver, 'isSimulator'); }); diff --git a/test/unit/commands/proxy-helper-specs.js b/test/unit/commands/proxy-helper-specs.js index 34aff6edc..c3d3fc340 100644 --- a/test/unit/commands/proxy-helper-specs.js +++ b/test/unit/commands/proxy-helper-specs.js @@ -1,52 +1,55 @@ import {errors} from 'appium/driver'; import sinon from 'sinon'; -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import XCUITestDriver from '../../../lib/driver'; -chai.should(); -chai.use(chaiAsPromised); describe('proxy commands', function () { let driver = new XCUITestDriver(); - // @ts-ignore give the driver a spy-able proxy object driver.wda = {jwproxy: {command: () => {}}}; - // @ts-ignore ok for tests - const proxyStub = sinon.stub(driver.wda.jwproxy, 'command'); + + let chai; + let mockJwproxy; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + }); + + beforeEach(function () { + mockJwproxy = sinon.mock(driver.wda.jwproxy); + }); afterEach(function () { - if (proxyStub) { - proxyStub.reset(); - } + mockJwproxy.verify(); }); describe('proxyCommand', function () { it('should send command through WDA', async function () { - proxyStub.returns({status: 0}); - + mockJwproxy.expects('command').once().withExactArgs( + '/some/endpoint', + 'POST', + { some: 'stuff' } + ); await driver.proxyCommand('/some/endpoint', 'POST', {some: 'stuff'}); - proxyStub.calledOnce.should.be.true; - // @ts-ignore - proxyStub.firstCall.args[0].should.eql('/some/endpoint'); - // @ts-ignore - proxyStub.firstCall.args[1].should.eql('POST'); - // @ts-ignore - proxyStub.firstCall.args[2].some.should.eql('stuff'); }); + it('should throw an error if no endpoint is given', async function () { + mockJwproxy.expects('command').never().called; // @ts-expect-error incorrect usage await driver.proxyCommand(null, 'POST', {some: 'stuff'}).should.be.rejectedWith(/endpoint/); - proxyStub.callCount.should.eql(0); }); it('should throw an error if no method is given', async function () { + mockJwproxy.expects('command').never().called; await driver // @ts-expect-error incorrect usage .proxyCommand('/some/endpoint', null, {some: 'stuff'}) .should.be.rejectedWith(/GET, POST/); - proxyStub.callCount.should.eql(0); }); it('should throw an error if wda returns an error (even if http status is 200)', async function () { - proxyStub.returns({status: 13, value: 'WDA error occurred'}); + mockJwproxy.expects('command').once().returns({status: 13, value: 'WDA error occurred'}); try { await driver.proxyCommand('/some/endpoint', 'POST', {some: 'stuff'}); } catch (err) { @@ -54,12 +57,10 @@ describe('proxy commands', function () { err.message.should.include('WDA error occurred'); err.should.be.an.instanceof(errors.UnknownError); } - proxyStub.calledOnce.should.be.true; }); it('should not throw an error if no status is returned', async function () { - proxyStub.returns({value: 'WDA error occurred'}); + mockJwproxy.expects('command').once().returns({value: 'WDA error occurred'}); await driver.proxyCommand('/some/endpoint', 'POST', {some: 'stuff'}); - proxyStub.calledOnce.should.be.true; }); }); }); diff --git a/test/unit/commands/screenshots-specs.js b/test/unit/commands/screenshots-specs.js index 2c95962ef..8b414d557 100644 --- a/test/unit/commands/screenshots-specs.js +++ b/test/unit/commands/screenshots-specs.js @@ -13,7 +13,6 @@ describe('screenshots commands', function () { beforeEach(function () { driver = new XCUITestDriver(); simctl = new Simctl(); - // @ts-ignore driver._device = { simctl }; proxyStub = sinon.stub(driver, 'proxyCommand'); }); diff --git a/test/unit/commands/session-specs.js b/test/unit/commands/session-specs.js index 7b9ceea56..3e6edd75c 100644 --- a/test/unit/commands/session-specs.js +++ b/test/unit/commands/session-specs.js @@ -1,11 +1,25 @@ import sinon from 'sinon'; import XCUITestDriver from '../../../lib/driver'; -import chai from 'chai'; - -chai.should(); describe('session commands', function () { let driver = new XCUITestDriver(); + + let chai; + let mockDriver; + + before(async function () { + chai = await import('chai'); + chai.should(); + }); + + beforeEach(function () { + mockDriver = sinon.mock(driver); + }); + + afterEach(function () { + mockDriver.verify(); + }); + driver.opts.udid = 'cecinestpasuneudid'; let proxySpy = sinon.stub(driver, 'proxyCommand').callsFake(async (endpoint, method) => { if (endpoint === '/' && method === 'GET') { diff --git a/test/unit/commands/ssl-certificate-specs.js b/test/unit/commands/ssl-certificate-specs.js index 6e397e3da..37a51e441 100644 --- a/test/unit/commands/ssl-certificate-specs.js +++ b/test/unit/commands/ssl-certificate-specs.js @@ -1,7 +1,5 @@ -import chai from 'chai'; import {parseCommonName} from '../../../lib/commands/certificate'; -chai.should(); describe('ssl certificate parser command', function () { const sslOutputLibreSSL = 'subject= /C=US/ST=California/L=San Francisco/O=BadSSL/CN=*.badssl.com'; @@ -9,6 +7,13 @@ describe('ssl certificate parser command', function () { 'subject=C = US, ST = California, L = San Francisco, O = BadSSL, CN = *.badssl.com'; const expectedString = '*.badssl.com'; + let chai; + + before(async function () { + chai = await import('chai'); + chai.should(); + }); + it('try to parse LibreSSL command output', function () { parseCommonName(sslOutputLibreSSL).should.eql(expectedString); }); diff --git a/test/unit/commands/xctest-specs.js b/test/unit/commands/xctest-specs.js index 3e7609d75..d347a64a2 100644 --- a/test/unit/commands/xctest-specs.js +++ b/test/unit/commands/xctest-specs.js @@ -1,12 +1,10 @@ -import chai from 'chai'; import {parseXCTestStdout} from '../../../lib/commands/xctest'; -chai.should(); describe('session commands', function () { const xctestLogs1Success = `XCTesterAppUITests - XCTesterAppUITests.XCTesterAppUITests/testExample | Passed: True | Crashed: False | Duration: 1.485 | Failure message: | Location :0 - XCTesterAppUITests - XCTesterAppUITests.XCTesterAppUITests/testLaunchPerformance | Passed: True | Crashed: False | Duration: 14.297 | Failure message: | Location :0 + XCTesterAppUITests - XCTesterAppUITests.XCTesterAppUITests/testLaunchPerformance | Passed: True | Crashed: False | Duration: 14.297 | Failure message: | Location :0 `.trim(); const xctestLogs2Success = ` XCTesterAppUITests - XCTesterAppUITests.XCTesterAppUITests/testExample | Status: passed | Duration: 2.2897069454193115 @@ -17,6 +15,16 @@ describe('session commands', function () { XCTesterAppUITests - XCTesterAppUITests.XCTesterAppUITests/testLaunchPerformance | Status: failed | Duration: 0.033468008041381836 | Failure message: XCTAssertTrue failed - error message here | Location /path/to/XCTesterAppUITests/XCTesterAppUITests.swift:36 `.trim(); + let chai; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + }); + describe('xctest', function () { it('should parse successful test logs - old version', function () { const results = parseXCTestStdout(xctestLogs1Success); diff --git a/test/unit/css-converter-specs.js b/test/unit/css-converter-specs.js index 34d400cb3..ff7555192 100644 --- a/test/unit/css-converter-specs.js +++ b/test/unit/css-converter-specs.js @@ -1,11 +1,18 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import CssConverter from '../../lib/css-converter'; -chai.should(); -chai.use(chaiAsPromised); describe('css-converter.js', function () { + + let chai; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + }); + describe('simple cases', function () { const simpleCases = [ ['XCUIElementTypeWindow:nth-child(2)', '**/XCUIElementTypeWindow[2]'], diff --git a/test/unit/device-connections-factory-specs.js b/test/unit/device-connections-factory-specs.js index 424e6f1c5..dffaa0f4d 100644 --- a/test/unit/device-connections-factory-specs.js +++ b/test/unit/device-connections-factory-specs.js @@ -1,13 +1,18 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import _ from 'lodash'; import {DeviceConnectionsFactory} from '../../lib/device-connections-factory'; -chai.should(); -chai.use(chaiAsPromised); describe('DeviceConnectionsFactory', function () { let devConFactory; + let chai; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + }); beforeEach(function () { devConFactory = new DeviceConnectionsFactory(); diff --git a/test/unit/driver-specs.js b/test/unit/driver-specs.js index 26973c18c..f8d36bacc 100644 --- a/test/unit/driver-specs.js +++ b/test/unit/driver-specs.js @@ -1,20 +1,13 @@ import xcode from 'appium-xcode'; import {JWProxy} from 'appium/driver'; -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import _ from 'lodash'; import {createSandbox} from 'sinon'; -import sinonChai from 'sinon-chai'; import cmds from '../../lib/commands'; import XCUITestDriver from '../../lib/driver'; import * as utils from '../../lib/utils'; import {MOCHA_LONG_TIMEOUT} from './helpers'; import RealDevice from '../../lib/real-device'; -chai.should(); -chai.use(sinonChai).use(chaiAsPromised); - -const expect = chai.expect; const caps = { fistMatch: [{}], @@ -27,8 +20,20 @@ const caps = { }; describe('XCUITestDriver', function () { - /** @type {sinon.SinonSandbox} */ let sandbox; + let chai; + let expect; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + + expect = chai.expect; + }); + beforeEach(function () { sandbox = createSandbox(); }); @@ -75,7 +80,6 @@ describe('XCUITestDriver', function () { // fake the proxy to WDA const jwproxy = new JWProxy(); jwproxyCommandSpy = sandbox.stub(jwproxy, 'command').resolves({some: 'thing'}); - // @ts-ignore ok for tests driver.wda = { jwproxy, }; @@ -249,7 +253,6 @@ describe('XCUITestDriver', function () { driver = new XCUITestDriver(); const jwproxy = new JWProxy(); sandbox.stub(jwproxy, 'command').resolves(deviceInfoResponse); - // @ts-ignore ok for tests driver.wda = { jwproxy, }; @@ -290,13 +293,13 @@ describe('XCUITestDriver', function () { driver.opts.device = 'some-device'; driver.lifecycleData = {createSim: false}; await driver.installOtherApps('/path/to/iosApp.app'); - expect(driver.isRealDevice).to.have.been.calledOnce; - expect(driver.helpers.configureApp).to.have.been.calledOnce; - expect(RealDeviceManagementModule.installToRealDevice).to.have.been.calledOnceWith( + (driver.isRealDevice).calledOnce.should.be.true; + (driver.helpers.configureApp).calledOnce.should.be.true; + (RealDeviceManagementModule.installToRealDevice).calledOnceWith( '/path/to/iosApp.app', 'bundle-id', {skipUninstall: true, timeout: undefined}, - ); + ).should.be.true; }); it('should install multiple apps from otherApps as JSON array on on real devices', async function () { @@ -313,18 +316,18 @@ describe('XCUITestDriver', function () { driver.opts.device = 'some-device'; driver.lifecycleData = {createSim: false}; await driver.installOtherApps('["/path/to/iosApp1.app","/path/to/iosApp2.app"]'); - expect(driver.isRealDevice).to.have.been.calledTwice; - expect(driver.helpers.configureApp).to.have.been.calledTwice; - expect(RealDeviceManagementModule.installToRealDevice).to.have.been.calledWith( + (driver.isRealDevice).calledTwice.should.be.true; + (driver.helpers.configureApp).calledTwice.should.be.true; + (RealDeviceManagementModule.installToRealDevice).calledWith( '/path/to/iosApp1.app', 'bundle-id', {skipUninstall: true, timeout: undefined}, - ); - expect(RealDeviceManagementModule.installToRealDevice).to.have.been.calledWith( + ).should.be.true; + (RealDeviceManagementModule.installToRealDevice).calledWith( '/path/to/iosApp2.app', 'bundle-id2', {skipUninstall: true, timeout: undefined}, - ); + ).should.be.true; }); it('should install multiple apps from otherApps as string on simulators', async function () { @@ -339,13 +342,13 @@ describe('XCUITestDriver', function () { driver.opts.device = 'some-device'; driver.lifecycleData = {createSim: false}; await driver.installOtherApps('/path/to/iosApp.app'); - expect(driver.isRealDevice).to.have.been.calledOnce; - expect(driver.helpers.configureApp).to.have.been.calledOnce; - expect(SimulatorManagementModule.installToSimulator).to.have.been.calledOnceWith( + (driver.isRealDevice).calledOnce.should.be.true; + (driver.helpers.configureApp).calledOnce.should.be.true; + (SimulatorManagementModule.installToSimulator).calledOnceWith( '/path/to/iosApp.app', 'bundle-id', {newSimulator: false}, - ); + ).should.be.true; }); it('should install multiple apps from otherApps as JSON array on simulators', async function () { @@ -361,18 +364,18 @@ describe('XCUITestDriver', function () { driver.opts.noReset = false; driver.lifecycleData = {createSim: false}; await driver.installOtherApps('["/path/to/iosApp1.app","/path/to/iosApp2.app"]'); - expect(driver.isRealDevice).to.have.been.calledTwice; - expect(driver.helpers.configureApp).to.have.been.calledTwice; - expect(SimulatorManagementModule.installToSimulator).to.have.been.calledWith( + (driver.isRealDevice).calledTwice.should.be.true; + (driver.helpers.configureApp).calledTwice.should.be.true; + (SimulatorManagementModule.installToSimulator).calledWith( '/path/to/iosApp1.app', 'bundle-id', {newSimulator: false}, - ); - expect(SimulatorManagementModule.installToSimulator).to.have.been.calledWith( + ).should.be.true; + (SimulatorManagementModule.installToSimulator).calledWith( '/path/to/iosApp2.app', 'bundle-id2', {newSimulator: false}, - ); + ).should.be.true; }); }); diff --git a/test/unit/language-specs.js b/test/unit/language-specs.js index d141e7b50..94e3abaa7 100644 --- a/test/unit/language-specs.js +++ b/test/unit/language-specs.js @@ -1,9 +1,7 @@ import sinon from 'sinon'; -import chai from 'chai'; import _ from 'lodash'; import XCUITestDriver from '../../lib/driver'; -chai.should(); describe('language and locale', function () { const LANGUAGE = 'en'; @@ -27,6 +25,24 @@ describe('language and locale', function () { environment: {}, }; + + let mockDriver; + let chai; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + }); + + afterEach(function () { + if (mockDriver) { + mockDriver.verify(); + } + }); + describe('send only language and locale', function () { it('should send translated POST /session request with valid desired caps to WDA', async function () { const expectedWDACapabilities = { @@ -59,13 +75,15 @@ describe('language and locale', function () { }); let driver = new XCUITestDriver(desiredCapabilities); - let proxySpy = sinon.stub(driver, 'proxyCommand'); + + mockDriver = sinon.mock(driver); + mockDriver.expects('proxyCommand').once().withExactArgs('/session', 'POST', expectedWDACapabilities); + driver.validateDesiredCaps(desiredCapabilities); await driver.startWdaSession( desiredCapabilities.bundleId, desiredCapabilities.processArguments, ); - proxySpy.should.have.been.calledOnceWith('/session', 'POST', expectedWDACapabilities); }); }); @@ -115,17 +133,16 @@ describe('language and locale', function () { processArguments, }); let driver = new XCUITestDriver(desiredCapabilities); - let proxySpy = sinon.stub(driver, 'proxyCommand'); + + mockDriver = sinon.mock(driver); + mockDriver.expects('proxyCommand').once().withExactArgs('/session', 'POST', expectedWDACapabilities); + driver.validateDesiredCaps(desiredCapabilities); await driver.startWdaSession( desiredCapabilities.bundleId, desiredCapabilities.processArguments, ); - proxySpy.calledOnce.should.be.true; - proxySpy.firstCall.args[0].should.eql('/session'); - proxySpy.firstCall.args[1].should.eql('POST'); desiredCapabilities.processArguments.should.eql(expectedProcessArguments); - /** @type {any} */ (proxySpy.firstCall.args[2]).should.eql(expectedWDACapabilities); }); }); }); diff --git a/test/unit/processargs-specs.js b/test/unit/processargs-specs.js index 566d6fb15..6698f19ad 100644 --- a/test/unit/processargs-specs.js +++ b/test/unit/processargs-specs.js @@ -1,14 +1,12 @@ import sinon from 'sinon'; -import chai from 'chai'; import XCUITestDriver from '../../lib/driver'; -chai.should(); describe('process args', function () { const BUNDLE_ID = 'com.test.app'; let driver = new XCUITestDriver(); driver.opts.platformVersion = '10.3'; - let proxySpy = sinon.stub(driver, 'proxyCommand'); + let mockDriver; const DEFAULT_CAPS = { elementResponseFields: undefined, disableAutomaticScreenshots: undefined, @@ -46,12 +44,28 @@ describe('process args', function () { }, }; + let chai; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + }); + + beforeEach(function () { + mockDriver = sinon.mock(driver); + }); + afterEach(function () { - proxySpy.reset(); + mockDriver.verify(); }); describe('send process args as object', function () { it('should send translated POST /session request with valid desired caps to WDA', async function () { + mockDriver.expects('proxyCommand').once().withExactArgs('/session', 'POST', desired); + let desiredWithProArgsObject = { platformName: 'iOS', platformVersion: '10.3', @@ -65,12 +79,13 @@ describe('process args', function () { desiredWithProArgsObject.bundleId, desiredWithProArgsObject.processArguments, ); - proxySpy.should.have.been.calledOnceWith('/session', 'POST', desired); }); }); describe('send process args json string', function () { it('should send translated POST /session request with valid desired caps to WDA', async function () { + mockDriver.expects('proxyCommand').once().withExactArgs('/session', 'POST', desired); + let desiredWithProArgsString = { platformName: 'iOS', platformVersion: '10.3', @@ -84,7 +99,6 @@ describe('process args', function () { desiredWithProArgsString.bundleId, desiredWithProArgsString.processArguments, ); - proxySpy.should.have.been.calledOnceWith('/session', 'POST', desired); }); }); }); diff --git a/test/unit/real-device-management-specs.js b/test/unit/real-device-management-specs.js index 5510b1c40..de8173fda 100644 --- a/test/unit/real-device-management-specs.js +++ b/test/unit/real-device-management-specs.js @@ -1,24 +1,26 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import {createSandbox} from 'sinon'; -import sinonChai from 'sinon-chai'; import { installToRealDevice } from '../../lib/real-device-management'; import RealDevice from '../../lib/real-device'; import XCUITestDriver from '../../lib/driver'; -chai.should(); -chai.use(sinonChai).use(chaiAsPromised); - -const expect = chai.expect; describe('installToRealDevice', function () { const udid = 'test-udid'; const app = '/path/to.app'; const bundleId = 'test.bundle.id'; - /** @type {sinon.SinonSandbox} */ let sandbox; let driver; + let chai; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + }); + beforeEach(function () { sandbox = createSandbox(); driver = new XCUITestDriver(); @@ -36,8 +38,8 @@ describe('installToRealDevice', function () { driver._device = realDevice; await installToRealDevice.bind(driver)(undefined, bundleId, {}); - expect(realDevice.remove).to.not.have.been.called; - expect(realDevice.install).to.not.have.been.called; + (realDevice.remove).called.should.be.false; + (realDevice.install).called.should.be.false; }); it('nothing happen without bundle id', async function () { @@ -48,8 +50,8 @@ describe('installToRealDevice', function () { driver.opts = {udid}; await installToRealDevice.bind(driver)(app, undefined, {}); - expect(realDevice.remove).to.not.have.been.called; - expect(realDevice.install).to.not.have.been.called; + (realDevice.remove).called.should.be.false; + (realDevice.install).called.should.be.false; }); it('should install without remove', async function () { @@ -64,8 +66,8 @@ describe('installToRealDevice', function () { await installToRealDevice.bind(driver)(app, bundleId, opts); - expect(realDevice.remove).to.not.have.been.called; - expect(realDevice.install).to.have.been.calledOnce; + (realDevice.remove).called.should.be.false; + (realDevice.install).calledOnce.should.be.true; }); it('should install after remove', async function () { @@ -80,8 +82,8 @@ describe('installToRealDevice', function () { await installToRealDevice.bind(driver)(app, bundleId, opts); - expect(realDevice.remove).to.have.been.calledOnce; - expect(realDevice.install).to.have.been.calledOnce; + (realDevice.remove).calledOnce.should.be.true; + (realDevice.install).calledOnce.should.be.true; }); it('should raise an error for invalid verification error after uninstall', async function () { @@ -96,8 +98,8 @@ describe('installToRealDevice', function () { driver.opts = {udid}; await installToRealDevice.bind(driver)(app, bundleId, opts).should.be.rejectedWith('ApplicationVerificationFailed'); - expect(realDevice.remove).to.have.been.calledOnce; - expect(realDevice.install).to.have.been.calledOnce; + (realDevice.remove).calledOnce.should.be.true; + (realDevice.install).calledOnce.should.be.true; }); it('should install after removal once because of MismatchedApplicationIdentifierEntitlement error', async function () { @@ -116,8 +118,8 @@ describe('installToRealDevice', function () { await installToRealDevice.bind(driver)(app, bundleId, opts); - expect(realDevice.remove).to.have.been.calledOnce; - expect(realDevice.install).to.have.been.calledTwice; + (realDevice.remove).calledOnce.should.be.true; + (realDevice.install).calledTwice.should.be.true; }); it('should raise an error in the install ApplicationVerificationFailed error because it is not recoverable', async function () { @@ -133,7 +135,7 @@ describe('installToRealDevice', function () { driver.opts = {udid}; await installToRealDevice.bind(driver)(app, bundleId, opts).should.be.rejectedWith('ApplicationVerificationFailed'); - expect(realDevice.remove).to.not.have.been.called; - expect(realDevice.install).to.have.been.calledOnce; + (realDevice.remove).called.should.be.false; + (realDevice.install).calledOnce.should.be.true; }); }); diff --git a/test/unit/simulator-management-specs.js b/test/unit/simulator-management-specs.js index 1d09816d4..a2d3dd729 100644 --- a/test/unit/simulator-management-specs.js +++ b/test/unit/simulator-management-specs.js @@ -1,10 +1,16 @@ import {runSimulatorReset} from '../../lib/simulator-management.js'; -import chai from 'chai'; import XCUITestDriver from '../../lib/driver'; -const should = chai.should(); - describe('simulator management', function () { + + let chai; + let should; + + before(async function () { + chai = await import('chai'); + should = chai.should(); + }); + describe('runSimulatorReset', function () { let result; let driver; diff --git a/test/unit/utils-specs.js b/test/unit/utils-specs.js index 48082e9fd..059c0f067 100644 --- a/test/unit/utils-specs.js +++ b/test/unit/utils-specs.js @@ -4,18 +4,25 @@ import { markSystemFilesForCleanup, isLocalHost, } from '../../lib/utils'; -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import {withMocks} from '@appium/test-support'; import {fs} from 'appium/support'; import * as iosUtils from '../../lib/utils'; -chai.should(); -chai.use(chaiAsPromised); const DERIVED_DATA_ROOT = '/path/to/DerivedData/WebDriverAgent-eoyoecqmiqfeodgstkwbxkfyagll'; describe('utils', function () { + + let chai; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + }); + describe( 'clearSystemFiles', withMocks({iosUtils, fs}, function (mocks) { @@ -28,13 +35,9 @@ describe('utils', function () { return DERIVED_DATA_ROOT; }, }; - // @ts-ignore withMocks is wonky mocks.fs.expects('glob').once().returns([]); - // @ts-ignore withMocks is wonky mocks.fs.expects('walkDir').once().returns(); - // @ts-ignore withMocks is wonky mocks.fs.expects('exists').atLeast(1).returns(true); - // @ts-ignore withMocks is wonky mocks.iosUtils .expects('clearLogs') .once() @@ -49,13 +52,9 @@ describe('utils', function () { return DERIVED_DATA_ROOT; }, }; - // @ts-ignore withMocks is wonky mocks.fs.expects('glob').once().returns([]); - // @ts-ignore withMocks is wonky mocks.fs.expects('walkDir').once().returns(); - // @ts-ignore withMocks is wonky mocks.fs.expects('exists').atLeast(1).returns(true); - // @ts-ignore withMocks is wonky mocks.iosUtils .expects('clearLogs') .once() @@ -72,7 +71,6 @@ describe('utils', function () { return null; }, }; - // @ts-ignore withMocks is wonky mocks.iosUtils.expects('clearLogs').never(); await clearSystemFiles(wda); }); diff --git a/tsconfig.json b/tsconfig.json index f7ccca9a9..15a8c09c7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,12 +3,11 @@ "extends": "@appium/tsconfig/tsconfig.json", "compilerOptions": { "outDir": "build", - "types": ["node", "mocha", "chai", "chai-as-promised", "sinon-chai", "sinon"], + "types": ["node", "mocha"], "checkJs": true }, "include": [ "index.js", - "lib", - "test" + "lib" ] } From 76e53b7331f51d74efd60861ceb828beef3758d5 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 27 Jun 2024 16:29:28 +0000 Subject: [PATCH 09/23] chore(release): 7.20.2 [skip ci] ## [7.20.2](https://github.com/appium/appium-xcuitest-driver/compare/v7.20.1...v7.20.2) (2024-06-27) ### Miscellaneous Chores * Bump chai and chai-as-promised ([#2414](https://github.com/appium/appium-xcuitest-driver/issues/2414)) ([6ba1b5e](https://github.com/appium/appium-xcuitest-driver/commit/6ba1b5e4ba192da6b8d7a0370cd3fa79947c540e)) --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7da6eeeab..18751e402 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [7.20.2](https://github.com/appium/appium-xcuitest-driver/compare/v7.20.1...v7.20.2) (2024-06-27) + +### Miscellaneous Chores + +* Bump chai and chai-as-promised ([#2414](https://github.com/appium/appium-xcuitest-driver/issues/2414)) ([6ba1b5e](https://github.com/appium/appium-xcuitest-driver/commit/6ba1b5e4ba192da6b8d7a0370cd3fa79947c540e)) + ## [7.20.1](https://github.com/appium/appium-xcuitest-driver/compare/v7.20.0...v7.20.1) (2024-06-26) ### Bug Fixes diff --git a/package.json b/package.json index 602ebe556..1605a4ba1 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "xcuitest", "xctest" ], - "version": "7.20.1", + "version": "7.20.2", "author": "Appium Contributors", "license": "Apache-2.0", "repository": { From 3b41576b5cb51f6b4c296e48c799c069cae50f63 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Thu, 27 Jun 2024 21:20:17 +0200 Subject: [PATCH 10/23] feat: Add mobile: wrappers for the clipboard API (#2418) --- docs/reference/execute-methods.md | 26 ++++++++++++++++++++++++++ lib/execute-method-map.ts | 13 +++++++++++++ 2 files changed, 39 insertions(+) diff --git a/docs/reference/execute-methods.md b/docs/reference/execute-methods.md index 1eae5f7be..b4abde47e 100644 --- a/docs/reference/execute-methods.md +++ b/docs/reference/execute-methods.md @@ -637,6 +637,32 @@ Name | Type | Required | Description | Example --- | --- | --- | --- | --- style | string | yes | Either `light` or `dark` | dark +### mobile: getClipboard + +Gets the content of the primary clipboard on the device under test. + +#### Arguments + +Name | Type | Required | Description | Example +--- | --- | --- | --- | --- +contentType | string | no | `plaintext` (default), `image` or `url` | image + +#### Returned Result + +The actual clipboard content encoded into base64 string. +An empty string is returned if the clipboard contains no data. + +### mobile: setClipboard + +Sets the primary clipboard's content on the device under test. + +#### Arguments + +Name | Type | Required | Description | Example +--- | --- | --- | --- | --- +content| string | yes | The content to be set as base64-encoded string. | QXBwaXVt +contentType | string | no | `plaintext` (default), `image` or `url` | image + ### mobile: siriCommand Presents the Siri UI, if it is not currently active, and accepts a string which is then processed as if it were recognized speech. Check the documentation on [activateWithVoiceRecognitionText](https://developer.apple.com/documentation/xctest/xcuisiriservice/2852140-activatewithvoicerecognitiontext?language=objc) XCTest method for more details. diff --git a/lib/execute-method-map.ts b/lib/execute-method-map.ts index 11c28bd7c..f40fcb39c 100644 --- a/lib/execute-method-map.ts +++ b/lib/execute-method-map.ts @@ -333,6 +333,19 @@ export const executeMethodMap = { required: ['style'], }, }, + 'mobile: getClipboard': { + command: 'getClipboard', + params: { + optional: ['contentType'], + }, + }, + 'mobile: setClipboard': { + command: 'setClipboard', + params: { + required: ['content'], + optional: ['contentType'], + }, + }, 'mobile: siriCommand': { command: 'mobileSiriCommand', params: { From e35fd20a1c37a90f4e7a37641a26215c8999c061 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 27 Jun 2024 19:21:57 +0000 Subject: [PATCH 11/23] chore(release): 7.21.0 [skip ci] ## [7.21.0](https://github.com/appium/appium-xcuitest-driver/compare/v7.20.2...v7.21.0) (2024-06-27) ### Features * Add mobile: wrappers for the clipboard API ([#2418](https://github.com/appium/appium-xcuitest-driver/issues/2418)) ([3b41576](https://github.com/appium/appium-xcuitest-driver/commit/3b41576b5cb51f6b4c296e48c799c069cae50f63)) --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18751e402..bf323b2fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [7.21.0](https://github.com/appium/appium-xcuitest-driver/compare/v7.20.2...v7.21.0) (2024-06-27) + +### Features + +* Add mobile: wrappers for the clipboard API ([#2418](https://github.com/appium/appium-xcuitest-driver/issues/2418)) ([3b41576](https://github.com/appium/appium-xcuitest-driver/commit/3b41576b5cb51f6b4c296e48c799c069cae50f63)) + ## [7.20.2](https://github.com/appium/appium-xcuitest-driver/compare/v7.20.1...v7.20.2) (2024-06-27) ### Miscellaneous Chores diff --git a/package.json b/package.json index 1605a4ba1..f7ceeeb8c 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "xcuitest", "xctest" ], - "version": "7.20.2", + "version": "7.21.0", "author": "Appium Contributors", "license": "Apache-2.0", "repository": { From d469237304d507feb1f59b07fd6a76d51f63fe19 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sun, 30 Jun 2024 11:06:08 +0200 Subject: [PATCH 12/23] chore: Streamline logging helpers (#2419) --- lib/commands/log.js | 20 ++--- lib/commands/types.ts | 6 ++ lib/device-log/helpers.ts | 9 +++ lib/device-log/ios-crash-log.js | 15 +--- lib/device-log/ios-device-log.js | 6 +- lib/device-log/ios-log.js | 105 +++++++++++++++++++------- lib/device-log/ios-performance-log.js | 69 ++++++++++------- lib/device-log/ios-simulator-log.js | 94 +++++++++++------------ package.json | 2 +- 9 files changed, 193 insertions(+), 133 deletions(-) create mode 100644 lib/device-log/helpers.ts diff --git a/lib/commands/log.js b/lib/commands/log.js index 6ced42692..d5532a715 100644 --- a/lib/commands/log.js +++ b/lib/commands/log.js @@ -8,6 +8,7 @@ import log from '../logger'; import WebSocket from 'ws'; import SafariConsoleLog from '../device-log/safari-console-log'; import SafariNetworkLog from '../device-log/safari-network-log'; +import { toLogEntry } from '../device-log/helpers'; /** * Determines the websocket endpoint based on the `sessionId` @@ -47,27 +48,18 @@ const SUPPORTED_LOG_TYPES = { server: { description: 'Appium server logs', /** - * @returns {AppiumServerLogEntry[]} + * @returns {import('./types').LogEntry[]} */ getter: (self) => { self.assertFeatureEnabled(GET_SERVER_LOGS_FEATURE); - return log.unwrap().record.map((x) => ({ - timestamp: /** @type {any} */ (x).timestamp ?? Date.now(), - level: 'ALL', - message: _.isEmpty(x.prefix) ? x.message : `[${x.prefix}] ${x.message}`, - })); + return log.unwrap().record.map((x) => toLogEntry( + _.isEmpty(x.prefix) ? x.message : `[${x.prefix}] ${x.message}`, + /** @type {any} */ (x).timestamp ?? Date.now() + )); }, }, }; -/** - * Log entry in the array returned by `getLogs('server')` - * @typedef AppiumServerLogEntry - * @property {number} timestamp - * @property {'ALL'} level - * @property {string} message - */ - export default { supportedLogTypes: SUPPORTED_LOG_TYPES, /** diff --git a/lib/commands/types.ts b/lib/commands/types.ts index d9f1e861d..2b7ec7544 100644 --- a/lib/commands/types.ts +++ b/lib/commands/types.ts @@ -566,3 +566,9 @@ export interface KeyboardKey { */ modifierFlags?: number; } + +export interface LogEntry { + timestamp: number; + level: string, + message: string; +} diff --git a/lib/device-log/helpers.ts b/lib/device-log/helpers.ts new file mode 100644 index 000000000..00724ad40 --- /dev/null +++ b/lib/device-log/helpers.ts @@ -0,0 +1,9 @@ +import type { LogEntry } from '../commands/types'; + +export function toLogEntry(message: string, timestamp: number): LogEntry { + return { + timestamp, + level: 'ALL', + message, + }; +} diff --git a/lib/device-log/ios-crash-log.js b/lib/device-log/ios-crash-log.js index 5c0a4a4c3..e060f6dee 100644 --- a/lib/device-log/ios-crash-log.js +++ b/lib/device-log/ios-crash-log.js @@ -11,13 +11,6 @@ const MAGIC_SEP = '/'; // The file format has been changed from '.crash' to '.ips' since Monterey. const CRASH_REPORTS_GLOB_PATTERN = '**/*.@(crash|ips)'; -/** - * @typedef {Object} LogRecord - * @property {number} timestamp - * @property {string} level - * @property {string} message - */ - class IOSCrashLog { constructor(opts = {}) { this.udid = opts.udid; @@ -103,7 +96,7 @@ class IOSCrashLog { } /** - * @returns {Promise} + * @returns {Promise} */ async getLogs() { let crashFiles = await this.getCrashes(); @@ -113,7 +106,7 @@ class IOSCrashLog { } /** - * @returns {Promise} + * @returns {Promise} */ async getAllLogs() { let crashFiles = await this.getCrashes(); @@ -123,12 +116,12 @@ class IOSCrashLog { /** * @param {string[]} paths - * @returns {Promise} + * @returns {Promise} */ async filesToJSON(paths) { const tmpRoot = await tempDir.openDir(); try { - return /** @type {LogRecord[]} */ (( + return /** @type {import('../commands/types').LogEntry[]} */ (( await B.map(paths, async (fullPath) => { if (_.includes(fullPath, REAL_DEVICE_MAGIC)) { const fileName = /** @type {string} */ (_.last(fullPath.split(MAGIC_SEP))); diff --git a/lib/device-log/ios-device-log.js b/lib/device-log/ios-device-log.js index 10d641def..50a29e91b 100644 --- a/lib/device-log/ios-device-log.js +++ b/lib/device-log/ios-device-log.js @@ -4,7 +4,7 @@ import {services} from 'appium-ios-device'; const log = logger.getLogger('IOSDeviceLog'); -class IOSDeviceLog extends IOSLog { +export class IOSDeviceLog extends IOSLog { constructor(opts) { super(); this.udid = opts.udid; @@ -22,6 +22,9 @@ class IOSDeviceLog extends IOSLog { this.service.start(this.onLog.bind(this)); } + /** + * @param {string} logLine + */ onLog(logLine) { this.broadcast(logLine); if (this.showLogs) { @@ -50,5 +53,4 @@ class IOSDeviceLog extends IOSLog { } } -export {IOSDeviceLog}; export default IOSDeviceLog; diff --git a/lib/device-log/ios-log.js b/lib/device-log/ios-log.js index 03c65e813..19e79f760 100644 --- a/lib/device-log/ios-log.js +++ b/lib/device-log/ios-log.js @@ -1,14 +1,22 @@ import {EventEmitter} from 'events'; +import { LRUCache } from 'lru-cache'; +import { toLogEntry } from './helpers'; // We keep only the most recent log entries to avoid out of memory error const MAX_LOG_ENTRIES_COUNT = 10000; -class IOSLog extends EventEmitter { - constructor() { +// TODO: Rewrite this class to typescript for better generic typing + +export class IOSLog extends EventEmitter { + constructor(maxBufferSize = MAX_LOG_ENTRIES_COUNT) { super(); - this.logs = []; - this.logIdxSinceLastRequest = -1; - this.maxBufferSize = MAX_LOG_ENTRIES_COUNT; + this.maxBufferSize = maxBufferSize; + /** @type {LRUCache} */ + this.logs = new LRUCache({ + max: this.maxBufferSize, + }); + /** @type {number?} */ + this.logIndexSinceLastRequest = null; } /** @returns {Promise} */ @@ -28,38 +36,81 @@ class IOSLog extends EventEmitter { throw new Error(`Sub-classes need to implement a 'isCapturing' function`); } - broadcast(logLine) { - const logObj = { - timestamp: Date.now(), - level: 'ALL', - message: logLine, - }; - this.logs.push(logObj); - this.emit('output', logObj); - if (this.logs.length > this.maxBufferSize) { - this.logs.shift(); - if (this.logIdxSinceLastRequest > 0) { - --this.logIdxSinceLastRequest; - } + /** + * + * @param {any} entry + * @returns {void} + */ + broadcast(entry) { + let recentIndex = -1; + for (const key of this.logs.rkeys()) { + recentIndex = key; + break; + } + const serializedEntry = this._serializeEntry(entry); + this.logs.set(++recentIndex, serializedEntry); + if (this.listenerCount('output')) { + this.emit('output', this._deserializeEntry(serializedEntry)); } } + /** + * + * @returns {import('../commands/types').LogEntry[]} + */ getLogs() { - if (this.logs.length && this.logIdxSinceLastRequest < this.logs.length) { - let result = this.logs; - if (this.logIdxSinceLastRequest > 0) { - result = result.slice(this.logIdxSinceLastRequest); + /** @type {import('../commands/types').LogEntry[]} */ + const result = []; + /** @type {number?} */ + let recentLogIndex = null; + for (const [index, value] of this.logs.entries()) { + if (this.logIndexSinceLastRequest && index > this.logIndexSinceLastRequest + || !this.logIndexSinceLastRequest) { + recentLogIndex = index; + result.push(this._deserializeEntry(value)); } - this.logIdxSinceLastRequest = this.logs.length; - return result; } - return []; + if (recentLogIndex !== null) { + this.logIndexSinceLastRequest = recentLogIndex; + } + return result; } + /** + * + * @returns {import('../commands/types').LogEntry[]} + */ getAllLogs() { - return this.logs; + /** @type {import('../commands/types').LogEntry[]} */ + const result = []; + for (const value of this.logs.values()) { + result.push(this._deserializeEntry(value)); + } + return result; + } + + /** + * + * @param {any} value + * @returns {any} + */ + _serializeEntry(value) { + return [value, Date.now()]; + } + + /** + * + * @param {any} value + * @returns {any} + */ + _deserializeEntry(value) { + const [message, timestamp] = value; + return toLogEntry(message, timestamp); + } + + _clearEntries() { + this.logs.clear(); } } -export {IOSLog}; export default IOSLog; diff --git a/lib/device-log/ios-performance-log.js b/lib/device-log/ios-performance-log.js index 3d7678fe7..5fb75aa7a 100644 --- a/lib/device-log/ios-performance-log.js +++ b/lib/device-log/ios-performance-log.js @@ -1,57 +1,68 @@ import {logger} from 'appium/support'; import _ from 'lodash'; +import { IOSLog } from './ios-log'; const log = logger.getLogger('IOSPerformanceLog'); const MAX_EVENTS = 5000; -class IOSPerformanceLog { +export class IOSPerformanceLog extends IOSLog { constructor(remoteDebugger, maxEvents = MAX_EVENTS) { + super(maxEvents); this.remoteDebugger = remoteDebugger; this.maxEvents = parseInt(String(maxEvents), 10); - - this.timelineEvents = []; + this._started = false; } + /** + * @override + */ async startCapture() { log.debug('Starting performance (Timeline) log capture'); - this.timelineEvents = []; - return await this.remoteDebugger.startTimeline(this.onTimelineEvent.bind(this)); + this._clearEntries(); + const result = await this.remoteDebugger.startTimeline(this.onTimelineEvent.bind(this)); + this._started = true; + return result; } + /** + * @override + */ async stopCapture() { log.debug('Stopping performance (Timeline) log capture'); - return await this.remoteDebugger.stopTimeline(); + const result = await this.remoteDebugger.stopTimeline(); + this._started = false; + return result; } - onTimelineEvent(event) { - log.debug(`Received Timeline event: ${_.truncate(JSON.stringify(event))}`); - this.timelineEvents.push(event); - - // if we have too many, get rid of the oldest log line - if (this.timelineEvents.length > this.maxEvents) { - let removedEvent = this.timelineEvents.shift(); - log.warn( - `Too many Timeline events, removing earliest: ${_.truncate(JSON.stringify(removedEvent))}`, - ); - } + /** + * @override + */ + get isCapturing() { + return this._started; } - // eslint-disable-next-line require-await - async getLogs() { - let events = this.timelineEvents; - - // flush events - log.debug('Flushing Timeline events'); - this.timelineEvents = []; + /** + * @override + */ + _serializeEntry(value) { + return value; + } - return events; + /** + * @override + */ + _deserializeEntry(value) { + return value; } - // eslint-disable-next-line require-await - async getAllLogs() { - return this.getLogs(); + /** + * + * @param {import('../commands/types').LogEntry} event + */ + onTimelineEvent(event) { + log.debug(`Received Timeline event: ${_.truncate(JSON.stringify(event))}`); + this.broadcast(event); } } -export {IOSPerformanceLog}; export default IOSPerformanceLog; diff --git a/lib/device-log/ios-simulator-log.js b/lib/device-log/ios-simulator-log.js index 1ac75acec..b708e9135 100644 --- a/lib/device-log/ios-simulator-log.js +++ b/lib/device-log/ios-simulator-log.js @@ -4,10 +4,11 @@ import {logger} from 'appium/support'; import {exec} from 'teen_process'; const log = logger.getLogger('IOSSimulatorLog'); +const EXECVP_ERROR_PATTERN = /execvp\(\)/; const START_TIMEOUT = 10000; -class IOSSimulatorLog extends IOSLog { +export class IOSSimulatorLog extends IOSLog { constructor({sim, showLogs, xcodeVersion, iosSimulatorLogsPredicate}) { super(); this.sim = sim; @@ -17,6 +18,9 @@ class IOSSimulatorLog extends IOSLog { this.proc = null; } + /** + * @override + */ async startCapture() { if (_.isUndefined(this.sim.udid)) { throw new Error(`Log capture requires a sim udid`); @@ -44,41 +48,9 @@ class IOSSimulatorLog extends IOSLog { } } - async finishStartingLogCapture() { - if (!this.proc) { - log.errorAndThrow('Could not capture simulator log'); - } - let firstLine = true; - let logRow = ''; - this.proc.on('output', (stdout, stderr) => { - if (stdout) { - if (firstLine) { - if (stdout.endsWith('\n')) { - // don't store the first line of the log because it came before the sim was launched - firstLine = false; - } - } else { - logRow += stdout; - if (stdout.endsWith('\n')) { - this.onOutput(logRow); - logRow = ''; - } - } - } - if (stderr) { - this.onOutput(logRow, 'STDERR'); - } - }); - - let sd = (stdout, stderr) => { - if (/execvp\(\)/.test(stderr)) { - throw new Error('iOS log capture process failed to start'); - } - return stdout || stderr; - }; - await this.proc.start(sd, START_TIMEOUT); - } - + /** + * @override + */ async stopCapture() { if (!this.proc) { return; @@ -87,6 +59,25 @@ class IOSSimulatorLog extends IOSLog { this.proc = null; } + /** + * @override + */ + get isCapturing() { + return this.proc && this.proc.isRunning; + } + + /** + * @param {string} logRow + * @param {string} [prefix=''] + */ + onOutput(logRow, prefix = '') { + this.broadcast(logRow); + if (this.showLogs) { + const space = prefix.length > 0 ? ' ' : ''; + log.info(`[IOS_SYSLOG_ROW${space}${prefix}] ${logRow}`); + } + } + async killLogSubProcess() { if (!this.proc.isRunning) { return; @@ -103,22 +94,27 @@ class IOSSimulatorLog extends IOSLog { } } - get isCapturing() { - return this.proc && this.proc.isRunning; - } + async finishStartingLogCapture() { + if (!this.proc) { + log.errorAndThrow('Could not capture simulator log'); + } - onOutput(logRow, prefix = '') { - const logs = _.cloneDeep(logRow.split('\n')); - for (const logLine of logs) { - if (!logLine) continue; // eslint-disable-line curly - this.broadcast(logLine); - if (this.showLogs) { - const space = prefix.length > 0 ? ' ' : ''; - log.info(`[IOS_SYSLOG_ROW${space}${prefix}] ${logLine}`); - } + for (const streamName of ['stdout', 'stderr']) { + this.proc.on(`lines-${streamName}`, (/** @type {string[]} */ lines) => { + for (const line of lines) { + this.onOutput(line, ...(streamName === 'stderr' ? ['STDERR'] : [])); + } + }); } + + const startDetector = (/** @type {string} */ stdout, /** @type {string} */ stderr) => { + if (EXECVP_ERROR_PATTERN.test(stderr)) { + throw new Error('iOS log capture process failed to start'); + } + return stdout || stderr; + }; + await this.proc.start(startDetector, START_TIMEOUT); } } -export {IOSSimulatorLog}; export default IOSSimulatorLog; diff --git a/package.json b/package.json index f7ceeeb8c..998ae47c2 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "portscanner": "^2.2.0", "semver": "^7.5.4", "source-map-support": "^0.x", - "teen_process": "^2.0.60", + "teen_process": "^2.1.10", "ws": "^8.13.0" }, "scripts": { From 4bd3b6917a1a78b1b5be485f4e1a219cb69de552 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 30 Jun 2024 09:07:54 +0000 Subject: [PATCH 13/23] chore(release): 7.21.1 [skip ci] ## [7.21.1](https://github.com/appium/appium-xcuitest-driver/compare/v7.21.0...v7.21.1) (2024-06-30) ### Miscellaneous Chores * Streamline logging helpers ([#2419](https://github.com/appium/appium-xcuitest-driver/issues/2419)) ([d469237](https://github.com/appium/appium-xcuitest-driver/commit/d469237304d507feb1f59b07fd6a76d51f63fe19)) --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf323b2fe..0de1513e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [7.21.1](https://github.com/appium/appium-xcuitest-driver/compare/v7.21.0...v7.21.1) (2024-06-30) + +### Miscellaneous Chores + +* Streamline logging helpers ([#2419](https://github.com/appium/appium-xcuitest-driver/issues/2419)) ([d469237](https://github.com/appium/appium-xcuitest-driver/commit/d469237304d507feb1f59b07fd6a76d51f63fe19)) + ## [7.21.0](https://github.com/appium/appium-xcuitest-driver/compare/v7.20.2...v7.21.0) (2024-06-27) ### Features diff --git a/package.json b/package.json index 998ae47c2..9188dbf11 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "xcuitest", "xctest" ], - "version": "7.21.0", + "version": "7.21.1", "author": "Appium Contributors", "license": "Apache-2.0", "repository": { From 97895755c41a3a729a8f4fd972c0f900a41f383a Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Mon, 1 Jul 2024 07:51:17 +0200 Subject: [PATCH 14/23] chore: Rewrite logging-related classes to typescript (#2420) --- lib/commands/context.js | 5 +- lib/commands/log.js | 5 +- lib/device-log/ios-device-log.js | 56 --------- lib/device-log/ios-device-log.ts | 52 ++++++++ lib/device-log/ios-log.js | 116 ------------------ lib/device-log/ios-log.ts | 87 +++++++++++++ lib/device-log/ios-performance-log.js | 68 ---------- lib/device-log/ios-performance-log.ts | 61 +++++++++ ...-simulator-log.js => ios-simulator-log.ts} | 73 ++++++----- lib/device-log/line-consuming-log.ts | 16 +++ 10 files changed, 259 insertions(+), 280 deletions(-) delete mode 100644 lib/device-log/ios-device-log.js create mode 100644 lib/device-log/ios-device-log.ts delete mode 100644 lib/device-log/ios-log.js create mode 100644 lib/device-log/ios-log.ts delete mode 100644 lib/device-log/ios-performance-log.js create mode 100644 lib/device-log/ios-performance-log.ts rename lib/device-log/{ios-simulator-log.js => ios-simulator-log.ts} (60%) create mode 100644 lib/device-log/line-consuming-log.ts diff --git a/lib/commands/context.js b/lib/commands/context.js index 8d64ebf7d..80a02f491 100644 --- a/lib/commands/context.js +++ b/lib/commands/context.js @@ -558,7 +558,10 @@ const commands = { // attempt to start performance logging, if requested if (this.opts.enablePerformanceLogging && this.remote) { this.log.debug(`Starting performance log on '${this.curContext}'`); - this.logs.performance = new IOSPerformanceLog(this.remote); + this.logs.performance = new IOSPerformanceLog({ + remoteDebugger: this.remote, + log: this.log, + }); await this.logs.performance.startCapture(); } diff --git a/lib/commands/log.js b/lib/commands/log.js index d5532a715..c5d4e6947 100644 --- a/lib/commands/log.js +++ b/lib/commands/log.js @@ -103,13 +103,14 @@ export default { this.logs.syslog = new IOSDeviceLog({ udid: this.opts.udid, showLogs: this.opts.showIOSLog, + log: this.log, }); } else { this.logs.syslog = new IOSSimulatorLog({ - sim: this.device, + sim: /** @type {import('appium-ios-simulator').Simulator} */ (this.device), showLogs: this.opts.showIOSLog, - xcodeVersion: this.xcodeVersion, iosSimulatorLogsPredicate: this.opts.iosSimulatorLogsPredicate, + log: this.log, }); } this.logs.safariConsole = new SafariConsoleLog(!!this.opts.showSafariConsoleLog); diff --git a/lib/device-log/ios-device-log.js b/lib/device-log/ios-device-log.js deleted file mode 100644 index 50a29e91b..000000000 --- a/lib/device-log/ios-device-log.js +++ /dev/null @@ -1,56 +0,0 @@ -import {logger} from 'appium/support'; -import {IOSLog} from './ios-log'; -import {services} from 'appium-ios-device'; - -const log = logger.getLogger('IOSDeviceLog'); - -export class IOSDeviceLog extends IOSLog { - constructor(opts) { - super(); - this.udid = opts.udid; - this.showLogs = !!opts.showLogs; - this.service = null; - } - /** - * @override - */ - async startCapture() { - if (this.service) { - return; - } - this.service = await services.startSyslogService(this.udid); - this.service.start(this.onLog.bind(this)); - } - - /** - * @param {string} logLine - */ - onLog(logLine) { - this.broadcast(logLine); - if (this.showLogs) { - log.info(logLine); - } - } - - /** - * @override - */ - get isCapturing() { - return !!this.service; - } - - /** - * @override - */ - // XXX: superclass is async, but this is not - // eslint-disable-next-line require-await - async stopCapture() { - if (!this.service) { - return; - } - this.service.close(); - this.service = null; - } -} - -export default IOSDeviceLog; diff --git a/lib/device-log/ios-device-log.ts b/lib/device-log/ios-device-log.ts new file mode 100644 index 000000000..c60144d6d --- /dev/null +++ b/lib/device-log/ios-device-log.ts @@ -0,0 +1,52 @@ +import {services} from 'appium-ios-device'; +import { LineConsumingLog } from './line-consuming-log'; +import type { AppiumLogger } from '@appium/types'; + +export interface IOSDeviceLogOpts { + udid: string; + showLogs?: boolean; + log?: AppiumLogger; +} + +export class IOSDeviceLog extends LineConsumingLog { + private udid: string; + private showLogs: boolean; + private service: any | null; + + constructor(opts: IOSDeviceLogOpts) { + super({log: opts.log}); + this.udid = opts.udid; + this.showLogs = !!opts.showLogs; + this.service = null; + } + + override async startCapture(): Promise { + if (this.service) { + return; + } + this.service = await services.startSyslogService(this.udid); + this.service.start(this.onLog.bind(this)); + } + + override get isCapturing(): boolean { + return !!this.service; + } + + // eslint-disable-next-line require-await + override async stopCapture(): Promise { + if (!this.service) { + return; + } + this.service.close(); + this.service = null; + } + + private onLog(logLine: string): void { + this.broadcast(logLine); + if (this.showLogs) { + this.log.info(`[IOS_SYSLOG_ROW] ${logLine}`); + } + } +} + +export default IOSDeviceLog; diff --git a/lib/device-log/ios-log.js b/lib/device-log/ios-log.js deleted file mode 100644 index 19e79f760..000000000 --- a/lib/device-log/ios-log.js +++ /dev/null @@ -1,116 +0,0 @@ -import {EventEmitter} from 'events'; -import { LRUCache } from 'lru-cache'; -import { toLogEntry } from './helpers'; - -// We keep only the most recent log entries to avoid out of memory error -const MAX_LOG_ENTRIES_COUNT = 10000; - -// TODO: Rewrite this class to typescript for better generic typing - -export class IOSLog extends EventEmitter { - constructor(maxBufferSize = MAX_LOG_ENTRIES_COUNT) { - super(); - this.maxBufferSize = maxBufferSize; - /** @type {LRUCache} */ - this.logs = new LRUCache({ - max: this.maxBufferSize, - }); - /** @type {number?} */ - this.logIndexSinceLastRequest = null; - } - - /** @returns {Promise} */ - // eslint-disable-next-line require-await - async startCapture() { - throw new Error(`Sub-classes need to implement a 'startCapture' function`); - } - - /** @returns {Promise} */ - // eslint-disable-next-line require-await - async stopCapture() { - throw new Error(`Sub-classes need to implement a 'stopCapture' function`); - } - - /** @returns {boolean} */ - get isCapturing() { - throw new Error(`Sub-classes need to implement a 'isCapturing' function`); - } - - /** - * - * @param {any} entry - * @returns {void} - */ - broadcast(entry) { - let recentIndex = -1; - for (const key of this.logs.rkeys()) { - recentIndex = key; - break; - } - const serializedEntry = this._serializeEntry(entry); - this.logs.set(++recentIndex, serializedEntry); - if (this.listenerCount('output')) { - this.emit('output', this._deserializeEntry(serializedEntry)); - } - } - - /** - * - * @returns {import('../commands/types').LogEntry[]} - */ - getLogs() { - /** @type {import('../commands/types').LogEntry[]} */ - const result = []; - /** @type {number?} */ - let recentLogIndex = null; - for (const [index, value] of this.logs.entries()) { - if (this.logIndexSinceLastRequest && index > this.logIndexSinceLastRequest - || !this.logIndexSinceLastRequest) { - recentLogIndex = index; - result.push(this._deserializeEntry(value)); - } - } - if (recentLogIndex !== null) { - this.logIndexSinceLastRequest = recentLogIndex; - } - return result; - } - - /** - * - * @returns {import('../commands/types').LogEntry[]} - */ - getAllLogs() { - /** @type {import('../commands/types').LogEntry[]} */ - const result = []; - for (const value of this.logs.values()) { - result.push(this._deserializeEntry(value)); - } - return result; - } - - /** - * - * @param {any} value - * @returns {any} - */ - _serializeEntry(value) { - return [value, Date.now()]; - } - - /** - * - * @param {any} value - * @returns {any} - */ - _deserializeEntry(value) { - const [message, timestamp] = value; - return toLogEntry(message, timestamp); - } - - _clearEntries() { - this.logs.clear(); - } -} - -export default IOSLog; diff --git a/lib/device-log/ios-log.ts b/lib/device-log/ios-log.ts new file mode 100644 index 000000000..3709e5fbe --- /dev/null +++ b/lib/device-log/ios-log.ts @@ -0,0 +1,87 @@ +import {EventEmitter} from 'events'; +import { LRUCache } from 'lru-cache'; +import type { LogEntry } from '../commands/types'; +import type { AppiumLogger } from '@appium/types'; +import {logger} from 'appium/support'; + +// We keep only the most recent log entries to avoid out of memory error +const MAX_LOG_ENTRIES_COUNT = 10000; + +export interface IOSLogOptions { + maxBufferSize?: number; + log?: AppiumLogger; +} + +export abstract class IOSLog< + TRawEntry, + TSerializedEntry extends object +> extends EventEmitter { + private maxBufferSize: number; + private logs: LRUCache; + private logIndexSinceLastRequest: number | null; + private _log: AppiumLogger; + + constructor(opts: IOSLogOptions = {}) { + super(); + this.maxBufferSize = opts.maxBufferSize ?? MAX_LOG_ENTRIES_COUNT; + this.logs = new LRUCache({ + max: this.maxBufferSize, + }); + this.logIndexSinceLastRequest = null; + this._log = opts.log ?? logger.getLogger(this.constructor.name); + } + + abstract startCapture(): Promise; + abstract stopCapture(): Promise; + abstract get isCapturing(): boolean; + + get log(): AppiumLogger { + return this._log; + } + + broadcast(entry: TRawEntry): void { + let recentIndex = -1; + for (const key of this.logs.rkeys()) { + recentIndex = key; + break; + } + const serializedEntry = this._serializeEntry(entry); + this.logs.set(++recentIndex, serializedEntry); + if (this.listenerCount('output')) { + this.emit('output', this._deserializeEntry(serializedEntry)); + } + } + + getLogs(): LogEntry[] { + const result: LogEntry[] = []; + let recentLogIndex: number | null = null; + for (const [index, value] of this.logs.entries()) { + if (this.logIndexSinceLastRequest && index > this.logIndexSinceLastRequest + || !this.logIndexSinceLastRequest) { + recentLogIndex = index; + result.push(this._deserializeEntry(value)); + } + } + if (recentLogIndex !== null) { + this.logIndexSinceLastRequest = recentLogIndex; + } + return result; + } + + getAllLogs(): LogEntry[] { + const result: LogEntry[] = []; + for (const value of this.logs.values()) { + result.push(this._deserializeEntry(value)); + } + return result; + } + + protected abstract _serializeEntry(value: TRawEntry): TSerializedEntry; + protected abstract _deserializeEntry(value: TSerializedEntry): LogEntry; + + protected _clearEntries() { + this.logs.clear(); + } +} + +export default IOSLog; diff --git a/lib/device-log/ios-performance-log.js b/lib/device-log/ios-performance-log.js deleted file mode 100644 index 5fb75aa7a..000000000 --- a/lib/device-log/ios-performance-log.js +++ /dev/null @@ -1,68 +0,0 @@ -import {logger} from 'appium/support'; -import _ from 'lodash'; -import { IOSLog } from './ios-log'; - -const log = logger.getLogger('IOSPerformanceLog'); -const MAX_EVENTS = 5000; - -export class IOSPerformanceLog extends IOSLog { - constructor(remoteDebugger, maxEvents = MAX_EVENTS) { - super(maxEvents); - this.remoteDebugger = remoteDebugger; - this.maxEvents = parseInt(String(maxEvents), 10); - this._started = false; - } - - /** - * @override - */ - async startCapture() { - log.debug('Starting performance (Timeline) log capture'); - this._clearEntries(); - const result = await this.remoteDebugger.startTimeline(this.onTimelineEvent.bind(this)); - this._started = true; - return result; - } - - /** - * @override - */ - async stopCapture() { - log.debug('Stopping performance (Timeline) log capture'); - const result = await this.remoteDebugger.stopTimeline(); - this._started = false; - return result; - } - - /** - * @override - */ - get isCapturing() { - return this._started; - } - - /** - * @override - */ - _serializeEntry(value) { - return value; - } - - /** - * @override - */ - _deserializeEntry(value) { - return value; - } - - /** - * - * @param {import('../commands/types').LogEntry} event - */ - onTimelineEvent(event) { - log.debug(`Received Timeline event: ${_.truncate(JSON.stringify(event))}`); - this.broadcast(event); - } -} - -export default IOSPerformanceLog; diff --git a/lib/device-log/ios-performance-log.ts b/lib/device-log/ios-performance-log.ts new file mode 100644 index 000000000..e5fb5e564 --- /dev/null +++ b/lib/device-log/ios-performance-log.ts @@ -0,0 +1,61 @@ +import _ from 'lodash'; +import { IOSLog } from './ios-log'; +import type { LogEntry } from '../commands/types'; +import type { AppiumLogger } from '@appium/types'; + +const MAX_EVENTS = 5000; + +type PerformanceLogEntry = object; +export interface IOSPerformanceLogOptions { + remoteDebugger: any; + maxEvents?: number; + log?: AppiumLogger; +} + +export class IOSPerformanceLog extends IOSLog { + private remoteDebugger: any; + private _started: boolean; + + constructor(opts: IOSPerformanceLogOptions) { + super({ + maxBufferSize: opts.maxEvents ?? MAX_EVENTS, + log: opts.log, + }); + this.remoteDebugger = opts.remoteDebugger; + this._started = false; + } + + override async startCapture(): Promise { + this.log.debug('Starting performance (Timeline) log capture'); + this._clearEntries(); + const result = await this.remoteDebugger.startTimeline(this.onTimelineEvent.bind(this)); + this._started = true; + return result; + } + + override async stopCapture(): Promise { + this.log.debug('Stopping performance (Timeline) log capture'); + const result = await this.remoteDebugger.stopTimeline(); + this._started = false; + return result; + } + + override get isCapturing(): boolean { + return this._started; + } + + protected override _serializeEntry(value: PerformanceLogEntry): PerformanceLogEntry { + return value; + } + + protected override _deserializeEntry(value: PerformanceLogEntry): LogEntry { + return value as LogEntry; + } + + private onTimelineEvent(event: PerformanceLogEntry): void { + this.log.debug(`Received Timeline event: ${_.truncate(JSON.stringify(event))}`); + this.broadcast(event); + } +} + +export default IOSPerformanceLog; diff --git a/lib/device-log/ios-simulator-log.js b/lib/device-log/ios-simulator-log.ts similarity index 60% rename from lib/device-log/ios-simulator-log.js rename to lib/device-log/ios-simulator-log.ts index b708e9135..f39eafb33 100644 --- a/lib/device-log/ios-simulator-log.js +++ b/lib/device-log/ios-simulator-log.ts @@ -1,27 +1,35 @@ import _ from 'lodash'; -import {IOSLog} from './ios-log'; -import {logger} from 'appium/support'; -import {exec} from 'teen_process'; +import {SubProcess, exec} from 'teen_process'; +import { LineConsumingLog } from './line-consuming-log'; +import type { Simulator } from 'appium-ios-simulator'; +import type { AppiumLogger } from '@appium/types'; -const log = logger.getLogger('IOSSimulatorLog'); const EXECVP_ERROR_PATTERN = /execvp\(\)/; const START_TIMEOUT = 10000; -export class IOSSimulatorLog extends IOSLog { - constructor({sim, showLogs, xcodeVersion, iosSimulatorLogsPredicate}) { - super(); - this.sim = sim; - this.showLogs = !!showLogs; - this.xcodeVersion = xcodeVersion; - this.predicate = iosSimulatorLogsPredicate; +export interface IOSSimulatorLogOptions { + sim: Simulator; + showLogs?: boolean; + iosSimulatorLogsPredicate?: string; + log?: AppiumLogger; +} + +export class IOSSimulatorLog extends LineConsumingLog { + private sim: Simulator; + private showLogs: boolean; + private predicate?: string; + private proc: SubProcess | null; + + constructor(opts: IOSSimulatorLogOptions) { + super({log: opts.log}); + this.sim = opts.sim; + this.showLogs = !!opts.showLogs; + this.predicate = opts.iosSimulatorLogsPredicate; this.proc = null; } - /** - * @override - */ - async startCapture() { + override async startCapture(): Promise { if (_.isUndefined(this.sim.udid)) { throw new Error(`Log capture requires a sim udid`); } @@ -33,7 +41,7 @@ export class IOSSimulatorLog extends IOSLog { if (this.predicate) { spawnArgs.push('--predicate', this.predicate); } - log.debug( + this.log.debug( `Starting log capture for iOS Simulator with udid '${this.sim.udid}' ` + `using simctl`, ); try { @@ -48,10 +56,7 @@ export class IOSSimulatorLog extends IOSLog { } } - /** - * @override - */ - async stopCapture() { + override async stopCapture(): Promise { if (!this.proc) { return; } @@ -59,44 +64,38 @@ export class IOSSimulatorLog extends IOSLog { this.proc = null; } - /** - * @override - */ - get isCapturing() { - return this.proc && this.proc.isRunning; + override get isCapturing(): boolean { + return Boolean(this.proc && this.proc.isRunning); } - /** - * @param {string} logRow - * @param {string} [prefix=''] - */ - onOutput(logRow, prefix = '') { + private onOutput(logRow: string, prefix: string = ''): void { this.broadcast(logRow); if (this.showLogs) { const space = prefix.length > 0 ? ' ' : ''; - log.info(`[IOS_SYSLOG_ROW${space}${prefix}] ${logRow}`); + this.log.info(`[IOS_SYSLOG_ROW${space}${prefix}] ${logRow}`); } } - async killLogSubProcess() { - if (!this.proc.isRunning) { + private async killLogSubProcess(): Promise { + if (!this.proc?.isRunning) { return; } - log.debug('Stopping iOS log capture'); + + this.log.debug('Stopping iOS log capture'); try { await this.proc.stop('SIGTERM', 1000); } catch (e) { if (!this.proc.isRunning) { return; } - log.warn('Cannot stop log capture process. Sending SIGKILL'); + this.log.warn('Cannot stop log capture process. Sending SIGKILL'); await this.proc.stop('SIGKILL'); } } - async finishStartingLogCapture() { + private async finishStartingLogCapture() { if (!this.proc) { - log.errorAndThrow('Could not capture simulator log'); + throw this.log.errorWithException('Could not capture simulator log'); } for (const streamName of ['stdout', 'stderr']) { diff --git a/lib/device-log/line-consuming-log.ts b/lib/device-log/line-consuming-log.ts new file mode 100644 index 000000000..9cacf6b7d --- /dev/null +++ b/lib/device-log/line-consuming-log.ts @@ -0,0 +1,16 @@ +import {IOSLog} from './ios-log'; +import { toLogEntry } from './helpers'; +import type { LogEntry } from '../commands/types'; + +type TSerializedEntry = [string, number]; + +export abstract class LineConsumingLog extends IOSLog { + protected override _serializeEntry(value: string): TSerializedEntry { + return [value, Date.now()]; + } + + protected override _deserializeEntry(value: TSerializedEntry): LogEntry { + const [message, timestamp] = value; + return toLogEntry(message, timestamp); + } +} From 0f45dd568ddb55917a94abf647bb0fd27e5761b8 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 1 Jul 2024 05:52:58 +0000 Subject: [PATCH 15/23] chore(release): 7.21.2 [skip ci] ## [7.21.2](https://github.com/appium/appium-xcuitest-driver/compare/v7.21.1...v7.21.2) (2024-07-01) ### Miscellaneous Chores * Rewrite logging-related classes to typescript ([#2420](https://github.com/appium/appium-xcuitest-driver/issues/2420)) ([9789575](https://github.com/appium/appium-xcuitest-driver/commit/97895755c41a3a729a8f4fd972c0f900a41f383a)) --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0de1513e9..f017163e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [7.21.2](https://github.com/appium/appium-xcuitest-driver/compare/v7.21.1...v7.21.2) (2024-07-01) + +### Miscellaneous Chores + +* Rewrite logging-related classes to typescript ([#2420](https://github.com/appium/appium-xcuitest-driver/issues/2420)) ([9789575](https://github.com/appium/appium-xcuitest-driver/commit/97895755c41a3a729a8f4fd972c0f900a41f383a)) + ## [7.21.1](https://github.com/appium/appium-xcuitest-driver/compare/v7.21.0...v7.21.1) (2024-06-30) ### Miscellaneous Chores diff --git a/package.json b/package.json index 9188dbf11..18c3be373 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "xcuitest", "xctest" ], - "version": "7.21.1", + "version": "7.21.2", "author": "Appium Contributors", "license": "Apache-2.0", "repository": { From 3c727219577c51d941d6fab68feda62eaf7bf774 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Wed, 3 Jul 2024 00:12:33 +0200 Subject: [PATCH 16/23] feat: Update console and network log handlers (#2421) --- docs/reference/capabilities.md | 2 + lib/commands/context.js | 4 +- lib/commands/log.js | 46 ++++-- lib/device-log/helpers.ts | 8 +- lib/device-log/ios-crash-log.js | 9 -- lib/device-log/ios-device-log.ts | 6 +- lib/device-log/ios-log.ts | 49 +++---- lib/device-log/ios-performance-log.ts | 33 ++--- lib/device-log/ios-simulator-log.ts | 20 ++- lib/device-log/rotating-log.js | 65 --------- lib/device-log/safari-console-log.js | 96 ------------- lib/device-log/safari-console-log.ts | 112 +++++++++++++++ lib/device-log/safari-network-log.js | 193 -------------------------- lib/device-log/safari-network-log.ts | 80 +++++++++++ package.json | 2 +- 15 files changed, 274 insertions(+), 451 deletions(-) delete mode 100644 lib/device-log/rotating-log.js delete mode 100644 lib/device-log/safari-console-log.js create mode 100644 lib/device-log/safari-console-log.ts delete mode 100644 lib/device-log/safari-network-log.js create mode 100644 lib/device-log/safari-network-log.ts diff --git a/docs/reference/capabilities.md b/docs/reference/capabilities.md index 61d79a29b..44f2217af 100644 --- a/docs/reference/capabilities.md +++ b/docs/reference/capabilities.md @@ -147,6 +147,8 @@ about capabilities, refer to the [Appium documentation](https://appium.io/docs/e |`appium:autoWebview`| Move directly into Webview context if available. Default `false`|`true`, `false`| |`appium:skipTriggerInputEventAfterSendkeys`| If this capability is set to `true`, then whenever you call the Send Keys method in a web context, the driver will not fire an additional `input` event on the input field used for the call. This event, turned on by default, helps in situations where JS frameworks (like React) do not respond to the input events that occur by default when the underlying Selenium atom is executed. Default `false`|`true`, `false`| |`appium:sendKeyStrategy`| If this capability is set to `oneByOne`, then whenever you call the Send Keys method in a web context, the driver will type each character the given string consists of in serial order to the element. This strategy helps in situations where JS frameworks (like React) update the view for each input. If `appium:skipTriggerInputEventAfterSendkeys` capability is `true`, it will affect every type. For example, when you are going to type the word `appium` with `oneByOne` strategy and `appium:skipTriggerInputEventAfterSendkeys` is enabled, the `appium:skipTriggerInputEventAfterSendkeys` option affects each typing action: `a`, `p`, `p`,`i`, `u` and `m`. Suppose any other value or no value has been provided to the `appium:sendKeyStrategy` capability. In that case, the driver types the given string in the destination input element. `appium` Send Keys input types `appium` if `oneByOne` was not set. |`oneByOne`| +|`appium:showSafariConsoleLog`| Adds Safari JavaScript console events to Appium server logs (`true`) and writes fully serialized events into the `safariConsole` logs bucket (both `true` and `false`). If unset then no console events are being collected, which helps to save CPU and memory resources. Before the driver version 7.22 the default behavior was to always collect console logs if the capability is not set. Setting the value to `false` mimics that legacy behavior. |`true`, `false`| +|`appium:showSafariNetworkLog`| Adds Safari network events to Appium server logs (`true`) and writes fully serialized events into the `safariNetwork` logs bucket (both `true` and `false`). If unset then no network events are being collected, which helps to save CPU and memory resources. Before the driver version 7.22 the default behavior was to always collect network logs if the capability is not set. Setting the value to `false` mimics that legacy behavior. |`true`, `false`| ### Other diff --git a/lib/commands/context.js b/lib/commands/context.js index 80a02f491..3bd707eb5 100644 --- a/lib/commands/context.js +++ b/lib/commands/context.js @@ -569,12 +569,12 @@ const commands = { if (name && name !== NATIVE_WIN && this.logs) { if (this.logs.safariConsole) { await this.remote.startConsole( - this.logs.safariConsole.addLogLine.bind(this.logs.safariConsole), + this.logs.safariConsole.onConsoleLogEvent.bind(this.logs.safariConsole), ); } if (this.logs.safariNetwork) { await this.remote.startNetwork( - this.logs.safariNetwork.addLogLine.bind(this.logs.safariNetwork), + this.logs.safariNetwork.onNetworkEvent.bind(this.logs.safariNetwork), ); } } diff --git a/lib/commands/log.js b/lib/commands/log.js index c5d4e6947..8d05fb1d0 100644 --- a/lib/commands/log.js +++ b/lib/commands/log.js @@ -60,6 +60,12 @@ const SUPPORTED_LOG_TYPES = { }, }; +const LOG_NAMES_TO_CAPABILITY_NAMES_MAP = { + safariConsole: 'showSafariConsoleLog', + safariNetwork: 'showSafariNetworkLog', + enablePerformanceLogging: 'enablePerformanceLogging', +}; + export default { supportedLogTypes: SUPPORTED_LOG_TYPES, /** @@ -77,11 +83,18 @@ export default { // If logs captured successfully send response with data, else send error const logObject = logsContainer[logType]; - const logs = logObject ? await logObject.getLogs() : null; - if (logs) { - return logs; + if (logObject) { + return await logObject.getLogs(); + } + if (logType in LOG_NAMES_TO_CAPABILITY_NAMES_MAP) { + throw new Error( + `${logType} logs are not enabled. Make sure you've set a proper value ` + + `to the 'appium:${LOG_NAMES_TO_CAPABILITY_NAMES_MAP[logType]}' capability.` + ); } - throw new Error(`No logs of type '${String(logType)}' found.`); + throw new Error( + `No logs of type '${logType}' found. Supported log types are: ${_.keys(SUPPORTED_LOG_TYPES)}.` + ); }, /** @@ -98,23 +111,30 @@ export default { sim: this.device, udid: this.isRealDevice() ? this.opts.udid : undefined, }); - - if (this.isRealDevice()) { - this.logs.syslog = new IOSDeviceLog({ + this.logs.syslog = this.isRealDevice() + ? new IOSDeviceLog({ udid: this.opts.udid, showLogs: this.opts.showIOSLog, log: this.log, - }); - } else { - this.logs.syslog = new IOSSimulatorLog({ + }) + : new IOSSimulatorLog({ sim: /** @type {import('appium-ios-simulator').Simulator} */ (this.device), showLogs: this.opts.showIOSLog, iosSimulatorLogsPredicate: this.opts.iosSimulatorLogsPredicate, log: this.log, }); + if (_.isBoolean(this.opts.showSafariConsoleLog)) { + this.logs.safariConsole = new SafariConsoleLog({ + showLogs: this.opts.showSafariConsoleLog, + log: this.log, + }); + } + if (_.isBoolean(this.opts.showSafariNetworkLog)) { + this.logs.safariNetwork = new SafariNetworkLog({ + showLogs: this.opts.showSafariNetworkLog, + log: this.log, + }); } - this.logs.safariConsole = new SafariConsoleLog(!!this.opts.showSafariConsoleLog); - this.logs.safariNetwork = new SafariNetworkLog(!!this.opts.showSafariNetworkLog); } let didStartSyslog = false; @@ -130,8 +150,6 @@ export default { } })(), this.logs.crashlog.startCapture(), - this.logs.safariConsole.startCapture(), - this.logs.safariNetwork.startCapture(), ]; await B.all(promises); diff --git a/lib/device-log/helpers.ts b/lib/device-log/helpers.ts index 00724ad40..73903e23b 100644 --- a/lib/device-log/helpers.ts +++ b/lib/device-log/helpers.ts @@ -1,9 +1,13 @@ import type { LogEntry } from '../commands/types'; -export function toLogEntry(message: string, timestamp: number): LogEntry { +export const DEFAULT_LOG_LEVEL = 'ALL'; +export const MAX_JSON_LOG_LENGTH = 200; +export const MAX_BUFFERED_EVENTS_COUNT = 5000; + +export function toLogEntry(message: string, timestamp: number, level: string = DEFAULT_LOG_LEVEL): LogEntry { return { timestamp, - level: 'ALL', + level, message, }; } diff --git a/lib/device-log/ios-crash-log.js b/lib/device-log/ios-crash-log.js index e060f6dee..b5d3898a7 100644 --- a/lib/device-log/ios-crash-log.js +++ b/lib/device-log/ios-crash-log.js @@ -105,15 +105,6 @@ class IOSCrashLog { return await this.filesToJSON(diff); } - /** - * @returns {Promise} - */ - async getAllLogs() { - let crashFiles = await this.getCrashes(); - let logFiles = _.difference(crashFiles, this.prevLogs); - return await this.filesToJSON(logFiles); - } - /** * @param {string[]} paths * @returns {Promise} diff --git a/lib/device-log/ios-device-log.ts b/lib/device-log/ios-device-log.ts index c60144d6d..d53bf3981 100644 --- a/lib/device-log/ios-device-log.ts +++ b/lib/device-log/ios-device-log.ts @@ -5,12 +5,12 @@ import type { AppiumLogger } from '@appium/types'; export interface IOSDeviceLogOpts { udid: string; showLogs?: boolean; - log?: AppiumLogger; + log: AppiumLogger; } export class IOSDeviceLog extends LineConsumingLog { - private udid: string; - private showLogs: boolean; + private readonly udid: string; + private readonly showLogs: boolean; private service: any | null; constructor(opts: IOSDeviceLogOpts) { diff --git a/lib/device-log/ios-log.ts b/lib/device-log/ios-log.ts index 3709e5fbe..41910c118 100644 --- a/lib/device-log/ios-log.ts +++ b/lib/device-log/ios-log.ts @@ -18,7 +18,6 @@ export abstract class IOSLog< > extends EventEmitter { private maxBufferSize: number; private logs: LRUCache; - private logIndexSinceLastRequest: number | null; private _log: AppiumLogger; constructor(opts: IOSLogOptions = {}) { @@ -27,7 +26,6 @@ export abstract class IOSLog< this.logs = new LRUCache({ max: this.maxBufferSize, }); - this.logIndexSinceLastRequest = null; this._log = opts.log ?? logger.getLogger(this.constructor.name); } @@ -39,40 +37,12 @@ export abstract class IOSLog< return this._log; } - broadcast(entry: TRawEntry): void { - let recentIndex = -1; - for (const key of this.logs.rkeys()) { - recentIndex = key; - break; - } - const serializedEntry = this._serializeEntry(entry); - this.logs.set(++recentIndex, serializedEntry); - if (this.listenerCount('output')) { - this.emit('output', this._deserializeEntry(serializedEntry)); - } - } - getLogs(): LogEntry[] { const result: LogEntry[] = []; - let recentLogIndex: number | null = null; - for (const [index, value] of this.logs.entries()) { - if (this.logIndexSinceLastRequest && index > this.logIndexSinceLastRequest - || !this.logIndexSinceLastRequest) { - recentLogIndex = index; - result.push(this._deserializeEntry(value)); - } - } - if (recentLogIndex !== null) { - this.logIndexSinceLastRequest = recentLogIndex; - } - return result; - } - - getAllLogs(): LogEntry[] { - const result: LogEntry[] = []; - for (const value of this.logs.values()) { - result.push(this._deserializeEntry(value)); + for (const value of this.logs.rvalues()) { + result.push(this._deserializeEntry(value as TSerializedEntry)); } + this._clearEntries(); return result; } @@ -82,6 +52,19 @@ export abstract class IOSLog< protected _clearEntries() { this.logs.clear(); } + + protected broadcast(entry: TRawEntry): void { + let recentIndex = -1; + for (const key of this.logs.keys()) { + recentIndex = key; + break; + } + const serializedEntry = this._serializeEntry(entry); + this.logs.set(++recentIndex, serializedEntry); + if (this.listenerCount('output')) { + this.emit('output', this._deserializeEntry(serializedEntry)); + } + } } export default IOSLog; diff --git a/lib/device-log/ios-performance-log.ts b/lib/device-log/ios-performance-log.ts index e5fb5e564..97cadfab5 100644 --- a/lib/device-log/ios-performance-log.ts +++ b/lib/device-log/ios-performance-log.ts @@ -1,24 +1,22 @@ import _ from 'lodash'; -import { IOSLog } from './ios-log'; -import type { LogEntry } from '../commands/types'; import type { AppiumLogger } from '@appium/types'; - -const MAX_EVENTS = 5000; +import { MAX_JSON_LOG_LENGTH, MAX_BUFFERED_EVENTS_COUNT } from './helpers'; +import { LineConsumingLog } from './line-consuming-log'; type PerformanceLogEntry = object; export interface IOSPerformanceLogOptions { remoteDebugger: any; maxEvents?: number; - log?: AppiumLogger; + log: AppiumLogger; } -export class IOSPerformanceLog extends IOSLog { - private remoteDebugger: any; +export class IOSPerformanceLog extends LineConsumingLog { + private readonly remoteDebugger: any; private _started: boolean; constructor(opts: IOSPerformanceLogOptions) { super({ - maxBufferSize: opts.maxEvents ?? MAX_EVENTS, + maxBufferSize: opts.maxEvents ?? MAX_BUFFERED_EVENTS_COUNT, log: opts.log, }); this.remoteDebugger = opts.remoteDebugger; @@ -28,33 +26,24 @@ export class IOSPerformanceLog extends IOSLog { this.log.debug('Starting performance (Timeline) log capture'); this._clearEntries(); - const result = await this.remoteDebugger.startTimeline(this.onTimelineEvent.bind(this)); + await this.remoteDebugger.startTimeline(this.onTimelineEvent.bind(this)); this._started = true; - return result; } override async stopCapture(): Promise { this.log.debug('Stopping performance (Timeline) log capture'); - const result = await this.remoteDebugger.stopTimeline(); + await this.remoteDebugger.stopTimeline(); this._started = false; - return result; } override get isCapturing(): boolean { return this._started; } - protected override _serializeEntry(value: PerformanceLogEntry): PerformanceLogEntry { - return value; - } - - protected override _deserializeEntry(value: PerformanceLogEntry): LogEntry { - return value as LogEntry; - } - private onTimelineEvent(event: PerformanceLogEntry): void { - this.log.debug(`Received Timeline event: ${_.truncate(JSON.stringify(event))}`); - this.broadcast(event); + const serializedEntry = JSON.stringify(event); + this.broadcast(serializedEntry); + this.log.debug(`Received Timeline event: ${_.truncate(serializedEntry, {length: MAX_JSON_LOG_LENGTH})}`); } } diff --git a/lib/device-log/ios-simulator-log.ts b/lib/device-log/ios-simulator-log.ts index f39eafb33..be4d2ec9b 100644 --- a/lib/device-log/ios-simulator-log.ts +++ b/lib/device-log/ios-simulator-log.ts @@ -12,13 +12,13 @@ export interface IOSSimulatorLogOptions { sim: Simulator; showLogs?: boolean; iosSimulatorLogsPredicate?: string; - log?: AppiumLogger; + log: AppiumLogger; } export class IOSSimulatorLog extends LineConsumingLog { - private sim: Simulator; - private showLogs: boolean; - private predicate?: string; + private readonly sim: Simulator; + private readonly showLogs: boolean; + private readonly predicate?: string; private proc: SubProcess | null; constructor(opts: IOSSimulatorLogOptions) { @@ -93,24 +93,22 @@ export class IOSSimulatorLog extends LineConsumingLog { } } - private async finishStartingLogCapture() { + private async finishStartingLogCapture(): Promise { if (!this.proc) { throw this.log.errorWithException('Could not capture simulator log'); } for (const streamName of ['stdout', 'stderr']) { - this.proc.on(`lines-${streamName}`, (/** @type {string[]} */ lines) => { - for (const line of lines) { - this.onOutput(line, ...(streamName === 'stderr' ? ['STDERR'] : [])); - } + this.proc.on(`line-${streamName}`, (line: string) => { + this.onOutput(line, ...(streamName === 'stderr' ? ['STDERR'] : [])); }); } - const startDetector = (/** @type {string} */ stdout, /** @type {string} */ stderr) => { + const startDetector = (stdout: string, stderr: string) => { if (EXECVP_ERROR_PATTERN.test(stderr)) { throw new Error('iOS log capture process failed to start'); } - return stdout || stderr; + return Boolean(stdout || stderr); }; await this.proc.start(startDetector, START_TIMEOUT); } diff --git a/lib/device-log/rotating-log.js b/lib/device-log/rotating-log.js deleted file mode 100644 index 5f84e0b1e..000000000 --- a/lib/device-log/rotating-log.js +++ /dev/null @@ -1,65 +0,0 @@ -import _ from 'lodash'; -import {logger} from 'appium/support'; - -const MAX_LOG_ENTRIES_COUNT = 10000; - -class RotatingLog { - constructor(showLogs = false, label = 'Log Label') { - this.log = logger.getLogger(label); - - this.showLogs = showLogs; - this.logs = []; - this.logIdxSinceLastRequest = 0; - - this.isCapturing = false; - } - - // eslint-disable-next-line require-await - async startCapture() { - this.isCapturing = true; - } - - // eslint-disable-next-line require-await - async stopCapture() { - this.isCapturing = false; - } - - /** - * @privateRemarks Subclasses must implement this. - */ - addLogLine() { - throw new Error('Not implemented'); - } - - // eslint-disable-next-line require-await - async getLogs() { - if (this.logs.length && this.logIdxSinceLastRequest < this.logs.length) { - let result = this.logs; - if (this.logIdxSinceLastRequest > 0) { - result = result.slice(this.logIdxSinceLastRequest); - } - this.logIdxSinceLastRequest = this.logs.length; - return result; - } - return []; - } - - // eslint-disable-next-line require-await - async getAllLogs() { - return _.clone(this.logs); - } - - get logs() { - if (!this._logs) { - this.logs = []; - } - return this._logs; - } - - set logs(logs) { - this._logs = logs; - } -} - -export {RotatingLog, MAX_LOG_ENTRIES_COUNT}; -export default RotatingLog; diff --git a/lib/device-log/safari-console-log.js b/lib/device-log/safari-console-log.js deleted file mode 100644 index f5848ff33..000000000 --- a/lib/device-log/safari-console-log.js +++ /dev/null @@ -1,96 +0,0 @@ -import {RotatingLog, MAX_LOG_ENTRIES_COUNT} from './rotating-log'; -import _ from 'lodash'; -import {util} from 'appium/support'; - -class SafariConsoleLog extends RotatingLog { - constructor(showLogs) { - super(showLogs, 'SafariConsole'); - - // js console has `warning` level, so map to `warn` - this.log = new Proxy(this.log, { - get(target, prop, receiver) { - return Reflect.get(target, prop === 'warning' ? 'warn' : prop, receiver); - }, - }); - } - - addLogLine(err, out) { - if (this.isCapturing) { - this.logs = this.logs || []; - while (this.logs.length >= MAX_LOG_ENTRIES_COUNT) { - this.logs.shift(); - if (this.logIdxSinceLastRequest > 0) { - this.logIdxSinceLastRequest--; - } - } - - /* - * The output will be like: - * { - * "source": "javascript", - * "level":"error", - * "text":"ReferenceError: Can't find variable: s_account", - * "type":"log", - * "line":2, - * "column":21, - * "url":"https://assets.adobedtm.com/b46e318d845250834eda10c5a20827c045a4d76f/scripts/satellite-57866f8b64746d53a8000104-staging.js", - * "repeatCount":1, - * "stackTrace":[{ - * "functionName":"global code", - * "url":"https://assets.adobedtm.com/b46e318d845250834eda10c5a20827c045a4d76f/scripts/satellite-57866f8b64746d53a8000104-staging.js", - * "scriptId":"6", - * "lineNumber":2, - * "columnNumber":21 - * }] - * } - * - * we need, at least, `level` (in accordance with Java levels - * (https://docs.oracle.com/javase/7/docs/api/java/util/logging/Level.html)), - * `timestamp`, and `message` to satisfy the java client. In order to - * provide all the information to the client, `message` is the full - * object, stringified. - */ - const entry = { - level: - { - error: 'SEVERE', - warning: 'WARNING', - log: 'FINE', - }[out.level] || 'INFO', - timestamp: Date.now(), - message: JSON.stringify(out), - }; - this.logs.push(entry); - } - - if (_.has(out, 'count')) { - // this is a notification of the previous message being repeated - // this should _never_ be the first message, so the previous one ought to - // be populated. If it is not, nothing will break, it will just look odd - // in the output below (no url or line numbers) - const count = out.count; - out = this._previousOutput || {}; - out.text = `Previous message repeated ${util.pluralize('time', count, true)}`; - } else { - // save the most recent output - this._previousOutput = out; - } - - // format output like - // SafariConsole [WARNING][http://appium.io 2:13] Log something to warn - if (this.showLogs) { - let level = 'debug'; - if (out.level === 'warning' || out.level === 'error') { - level = out.level; - } - for (const line of out.text.split('\n')) { - // url is optional, so get formatting here - const url = out.url ? `${out.url} ` : ''; - this.log[level](`[${level.toUpperCase()}][${url}${out.line}:${out.column}] ${line}`); - } - } - } -} - -export {SafariConsoleLog}; -export default SafariConsoleLog; diff --git a/lib/device-log/safari-console-log.ts b/lib/device-log/safari-console-log.ts new file mode 100644 index 000000000..9cf6ef3c8 --- /dev/null +++ b/lib/device-log/safari-console-log.ts @@ -0,0 +1,112 @@ +import _ from 'lodash'; +import type { AppiumLogger } from '@appium/types'; +import { + toLogEntry, + DEFAULT_LOG_LEVEL, + MAX_JSON_LOG_LENGTH, + MAX_BUFFERED_EVENTS_COUNT +} from './helpers'; +import IOSLog from './ios-log'; +import type { LogEntry } from '../commands/types'; + +const LOG_LEVELS_MAP = { + error: 'SEVERE', + warning: 'WARNING', + log: 'FINE', +}; + +export interface SafariConsoleLogOptions { + showLogs: boolean; + log: AppiumLogger; +} + +export interface SafariConsoleStacktraceEntry { + functionName: string; + url: string; + scriptId: number; + lineNumber: number; + columnNumber: number; +} + +export interface SafariConsoleEntry { + source: string; + level: string; + text: string; + type: string; + line: number; + column: number; + url?: string; + repeatCount: number; + stackTrace: SafariConsoleStacktraceEntry[]; +} + +type TSerializedEntry = [SafariConsoleEntry, number]; + +export class SafariConsoleLog extends IOSLog { + private readonly _showLogs: boolean; + + constructor(opts: SafariConsoleLogOptions) { + super({ + log: opts.log, + maxBufferSize: MAX_BUFFERED_EVENTS_COUNT, + }); + this._showLogs = opts.showLogs; + } + + override async startCapture(): Promise {} + override async stopCapture(): Promise {} + override get isCapturing(): boolean { + return true; + } + + /** + * + * @param err + * @param entry The output will be like: + * { + * "source": "javascript", + * "level":"error", + * "text":"ReferenceError: Can't find variable: s_account", + * "type":"log", + * "line":2, + * "column":21, + * "url":"https://assets.adobedtm.com/b46e318d845250834eda10c5a20827c045a4d76f/scripts/satellite-57866f8b64746d53a8000104-staging.js", + * "repeatCount":1, + * "stackTrace":[{ + * "functionName":"global code", + * "url":"https://assets.adobedtm.com/b46e318d845250834eda10c5a20827c045a4d76f/scripts/satellite-57866f8b64746d53a8000104-staging.js", + * "scriptId":"6", + * "lineNumber":2, + * "columnNumber":21 + * }] + * } + * + * we need, at least, `level` (in accordance with Java levels + * (https://docs.oracle.com/javase/7/docs/api/java/util/logging/Level.html)), + * `timestamp`, and `message` to satisfy the java client. In order to + * provide all the information to the client, `message` is the full + * object, stringified. + * + */ + onConsoleLogEvent(err: object | null, entry: SafariConsoleEntry): void { + this.broadcast(entry); + if (this._showLogs) { + this.log.info(`[SafariConsole] ${_.truncate(JSON.stringify(entry), {length: MAX_JSON_LOG_LENGTH})}`); + } + } + + protected override _serializeEntry(value: SafariConsoleEntry): TSerializedEntry { + return [value, Date.now()]; + } + + protected override _deserializeEntry(value: TSerializedEntry): LogEntry { + const [entry, timestamp] = value; + return toLogEntry(JSON.stringify(entry), timestamp, mapLogLevel(entry.level)); + } +} + +function mapLogLevel(originalLevel: string): string { + return LOG_LEVELS_MAP[originalLevel] ?? DEFAULT_LOG_LEVEL; +} + +export default SafariConsoleLog; diff --git a/lib/device-log/safari-network-log.js b/lib/device-log/safari-network-log.js deleted file mode 100644 index c45506fc4..000000000 --- a/lib/device-log/safari-network-log.js +++ /dev/null @@ -1,193 +0,0 @@ -import _ from 'lodash'; -import URL from 'url'; -import {util} from 'appium/support'; -import {RotatingLog, MAX_LOG_ENTRIES_COUNT} from './rotating-log'; - -class SafariNetworkLog extends RotatingLog { - constructor(showLogs) { - super(showLogs, 'SafariNetwork'); - } - - getEntry(requestId) { - let outputEntry; - while (this.logs.length >= MAX_LOG_ENTRIES_COUNT) { - // pull the first entry, which is the oldest - const entry = this.logs.shift(); - if (entry && entry.requestId === requestId) { - // we are adding to an existing entry, and it was almost removed - // add to the end of the list and try again - outputEntry = entry; - this.logs.push(outputEntry); - continue; - } - // we've removed an element, so the count is down one - if (this.logIdxSinceLastRequest > 0) { - this.logIdxSinceLastRequest--; - } - } - - if (!outputEntry) { - // we do not yes have an entry to associate this bit of output with - // most likely the entry will be at the end of the list, so start there - for (let i = this.logs.length - 1; i >= 0; i--) { - if (this.logs[i].requestId === requestId) { - // found it! - outputEntry = this.logs[i]; - // this is now the most current entry, so remove it from the list - // to be added to the end below - this.logs.splice(i, 1); - break; - } - } - - // nothing has been found, so create a new entry - if (!outputEntry) { - outputEntry = { - requestId, - logs: [], - }; - } - - // finally, add the entry to the end of the list - this.logs.push(outputEntry); - } - - return outputEntry; - } - - addLogLine(method, out) { - if (!this.isCapturing && !this.showLogs) { - // neither capturing nor displaying, so do nothing - return; - } - - if (['Network.dataReceived'].includes(method)) { - // status update, no need to handle - return; - } - - // events we care about: - // Network.requestWillBeSent - // Network.responseReceived - // Network.loadingFinished - // Network.loadingFailed - - const outputEntry = this.getEntry(out.requestId); - if (this.isCapturing) { - // now add the output we just received to the logs for this particular entry - outputEntry.logs = outputEntry.logs || []; - - outputEntry.logs.push(out); - } - - // if we are not displaying the logs, - // or we are not finished getting events for this network call, - // we are done - if (!this.showLogs) { - return; - } - - if (method === 'Network.loadingFinished' || method === 'Network.loadingFailed') { - this.printLogLine(outputEntry); - } - } - - getLogDetails(outputEntry) { - // extract the data - const record = outputEntry.logs.reduce(function getRecord(record, entry) { - record.requestId = entry.requestId; - if (entry.response) { - const url = URL.parse(entry.response.url); - // get the last part of the url, along with the query string, if possible - record.name = - `${_.last(String(url.pathname).split('/'))}${url.search ? `?${url.search}` : ''}` || - url.host; - record.status = entry.response.status; - if (entry.response.timing) { - record.time = - entry.response.timing.receiveHeadersEnd || entry.response.timing.responseStart || 0; - } - record.source = entry.response.source; - } - if (entry.type) { - record.type = entry.type; - } - if (entry.initiator) { - record.initiator = entry.initiator; - } - if (entry.metrics) { - // Safari has a `metrics` object on it's `Network.loadingFinished` event - record.size = entry.metrics.responseBodyBytesReceived || 0; - } - if (entry.errorText) { - record.errorText = entry.errorText; - // When a network call is cancelled, Safari returns `cancelled` as error text - // but has a boolean `canceled`. Normalize the two spellings in favor of - // the text, which will also be displayed - record.cancelled = entry.canceled; - } - return record; - }, {}); - - return record; - } - - printLogLine(outputEntry) { - const { - requestId, - name, - status, - type, - initiator = {}, - size = 0, - time = 0, - source, - errorText, - cancelled = false, - } = this.getLogDetails(outputEntry); - - // print out the record, formatted appropriately - this.log.debug(`Network event:`); - this.log.debug(` Id: ${requestId}`); - this.log.debug(` Name: ${name}`); - this.log.debug(` Status: ${status}`); - this.log.debug(` Type: ${type}`); - this.log.debug(` Initiator: ${initiator.type}`); - for (const line of initiator.stackTrace || []) { - const functionName = line.functionName || '(anonymous)'; - - const url = - !line.url || line.url === '[native code]' - ? '' - : `@${_.last((URL.parse(line.url).pathname || '').split('/'))}:${line.lineNumber}`; - this.log.debug(` ${_.padEnd(_.truncate(functionName, {length: 20}), 21)} ${url}`); - } - // get `memory-cache` or `disk-cache`, etc., right - const sizeStr = source.includes('cache') ? ` (from ${source.replace('-', ' ')})` : `${size}B`; - this.log.debug(` Size: ${sizeStr}`); - this.log.debug(` Time: ${Math.round(time)}ms`); - if (errorText) { - this.log.debug(` Error: ${errorText}`); - } - if (util.hasValue(cancelled)) { - this.log.debug(` Cancelled: ${cancelled}`); - } - } - - async getLogs() { - const logs = await super.getLogs(); - // in order to satisfy certain clients, we need to have a basic structure - // to the results, with `level`, `timestamp`, and `message`, which is - // all the information stringified - return logs.map(function adjustEntry(entry) { - return Object.assign({}, entry, { - level: 'INFO', - timestamp: Date.now(), - message: JSON.stringify(entry), - }); - }); - } -} - -export {SafariNetworkLog}; -export default SafariNetworkLog; diff --git a/lib/device-log/safari-network-log.ts b/lib/device-log/safari-network-log.ts new file mode 100644 index 000000000..b3b44734d --- /dev/null +++ b/lib/device-log/safari-network-log.ts @@ -0,0 +1,80 @@ +import _ from 'lodash'; +import { LineConsumingLog } from './line-consuming-log'; +import { MAX_JSON_LOG_LENGTH, MAX_BUFFERED_EVENTS_COUNT } from './helpers'; +import type { AppiumLogger } from '@appium/types'; + +const EVENTS_TO_LOG = [ + 'Network.loadingFinished', + 'Network.loadingFailed', +]; +const MONITORED_EVENTS = [ + 'Network.requestWillBeSent', + 'Network.responseReceived', + ...EVENTS_TO_LOG, +]; + +export interface SafariConsoleLogOptions { + showLogs: boolean; + log: AppiumLogger; +} + +export interface SafariNetworkResponseTiming { + responseStart: number; + receiveHeadersEnd: number; +} + +export interface SafariNetworkResponse { + url: string; + status: number; + timing: SafariNetworkResponseTiming; + source: string; +} + +export interface SafariNetworkLogEntryMetrics { + responseBodyBytesReceived: number; +} + +export interface SafariNetworkLogEntry { + requestId: string; + response?: SafariNetworkResponse; + type?: string; + initiator?: string; + // Safari has a `metrics` object on it's `Network.loadingFinished` event + metrics?: SafariNetworkLogEntryMetrics; + errorText?: string; + // When a network call is cancelled, Safari returns `cancelled` as error text + // but has a boolean `canceled`. + canceled?: boolean; +} + +export class SafariNetworkLog extends LineConsumingLog { + private readonly _showLogs: boolean; + + constructor(opts: SafariConsoleLogOptions) { + super({ + log: opts.log, + maxBufferSize: MAX_BUFFERED_EVENTS_COUNT, + }); + this._showLogs = opts.showLogs; + } + + override async startCapture(): Promise {} + override async stopCapture(): Promise {} + override get isCapturing(): boolean { + return true; + } + + onNetworkEvent(method: string, entry: SafariNetworkLogEntry): void { + if (!MONITORED_EVENTS.includes(method)) { + return; + } + + const serializedEntry = JSON.stringify(entry); + this.broadcast(serializedEntry); + if (this._showLogs && EVENTS_TO_LOG.includes(method)) { + this.log.info(`[SafariNetwork] ${_.truncate(serializedEntry, {length: MAX_JSON_LOG_LENGTH})}`); + } + } +} + +export default SafariNetworkLog; diff --git a/package.json b/package.json index 18c3be373..cf5b40b9c 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "portscanner": "^2.2.0", "semver": "^7.5.4", "source-map-support": "^0.x", - "teen_process": "^2.1.10", + "teen_process": "^2.2.0", "ws": "^8.13.0" }, "scripts": { From 509e4f9a8263567a9584f21740e731ab8df875ae Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 2 Jul 2024 22:14:25 +0000 Subject: [PATCH 17/23] chore(release): 7.22.0 [skip ci] ## [7.22.0](https://github.com/appium/appium-xcuitest-driver/compare/v7.21.2...v7.22.0) (2024-07-02) ### Features * Update console and network log handlers ([#2421](https://github.com/appium/appium-xcuitest-driver/issues/2421)) ([3c72721](https://github.com/appium/appium-xcuitest-driver/commit/3c727219577c51d941d6fab68feda62eaf7bf774)) --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f017163e1..a8657abdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [7.22.0](https://github.com/appium/appium-xcuitest-driver/compare/v7.21.2...v7.22.0) (2024-07-02) + +### Features + +* Update console and network log handlers ([#2421](https://github.com/appium/appium-xcuitest-driver/issues/2421)) ([3c72721](https://github.com/appium/appium-xcuitest-driver/commit/3c727219577c51d941d6fab68feda62eaf7bf774)) + ## [7.21.2](https://github.com/appium/appium-xcuitest-driver/compare/v7.21.1...v7.21.2) (2024-07-01) ### Miscellaneous Chores diff --git a/package.json b/package.json index cf5b40b9c..890701f55 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "xcuitest", "xctest" ], - "version": "7.21.2", + "version": "7.22.0", "author": "Appium Contributors", "license": "Apache-2.0", "repository": { From c6b9be8d5120b8097880bef49f67dc06a8bc548e Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Wed, 3 Jul 2024 09:47:57 +0200 Subject: [PATCH 18/23] chore: Simplify subprocess output analysis (#2422) --- lib/commands/pcap.js | 6 +----- lib/commands/performance.js | 8 +++----- lib/commands/recordscreen.js | 15 +++++++-------- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/lib/commands/pcap.js b/lib/commands/pcap.js index 89818c379..044f1215c 100644 --- a/lib/commands/pcap.js +++ b/lib/commands/pcap.js @@ -21,11 +21,7 @@ export class TrafficCapture { this.mainProcess = /** @type {import('teen_process').SubProcess} */ ( await new Pyidevice(this.udid).collectPcap(this.resultPath) ); - this.mainProcess.on('output', (stdout, stderr) => { - if (stderr) { - pcapLogger.info(`${stderr}`); - } - }); + this.mainProcess.on('line-stderr', (line) => pcapLogger.info(line)); this.log.info( `Starting network traffic capture session on the device '${this.udid}'. ` + `Will timeout in ${timeoutSeconds}s`, diff --git a/lib/commands/performance.js b/lib/commands/performance.js index 6b5ee5c95..59cda3dc0 100644 --- a/lib/commands/performance.js +++ b/lib/commands/performance.js @@ -184,11 +184,9 @@ export class PerfRecorder { this._process = new SubProcess(fullCmd[0], fullCmd.slice(1)); this._archivePromise = null; this._logger.debug(`Starting performance recording: ${util.quote(fullCmd)}`); - this._process.on('output', (stdout, stderr) => { - if (_.trim(stdout || stderr)) { - this._logger.debug(`[${toolName}] ${stdout || stderr}`); - } - }); + for (const streamName of ['stdout', 'stderr']) { + this._process.on(`line-${streamName}`, (line) => this._logger.debug(`[${toolName}] ${line}`)); + } this._process.once('exit', async (code, signal) => { this._process = null; if (code === 0) { diff --git a/lib/commands/recordscreen.js b/lib/commands/recordscreen.js index ec2efc481..f70761160 100644 --- a/lib/commands/recordscreen.js +++ b/lib/commands/recordscreen.js @@ -27,6 +27,7 @@ const QUALITY_MAPPING = { high: 75, photo: 100, }; +const CAPTURE_START_MARKER = /^\s*frame=/; export class ScreenRecorder { constructor(udid, log, videoPath, opts = {}) { @@ -102,15 +103,13 @@ export class ScreenRecorder { this.mainProcess = new SubProcess(FFMPEG_BINARY, args); let isCaptureStarted = false; - this.mainProcess.on('output', (stdout, stderr) => { - if (stderr) { - if (stderr.trim().startsWith('frame=')) { - if (!isCaptureStarted) { - isCaptureStarted = true; - } - } else { - ffmpegLogger.info(`${stderr}`); + this.mainProcess.on('line-stderr', (line) => { + if (CAPTURE_START_MARKER.test(line)) { + if (!isCaptureStarted) { + isCaptureStarted = true; } + } else { + ffmpegLogger.info(line); } }); await this.mainProcess.start(0); From 953055bb5511c183212870133bae1176261f2d4a Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 3 Jul 2024 07:49:41 +0000 Subject: [PATCH 19/23] chore(release): 7.22.1 [skip ci] ## [7.22.1](https://github.com/appium/appium-xcuitest-driver/compare/v7.22.0...v7.22.1) (2024-07-03) ### Miscellaneous Chores * Simplify subprocess output analysis ([#2422](https://github.com/appium/appium-xcuitest-driver/issues/2422)) ([c6b9be8](https://github.com/appium/appium-xcuitest-driver/commit/c6b9be8d5120b8097880bef49f67dc06a8bc548e)) --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8657abdd..3041a5896 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [7.22.1](https://github.com/appium/appium-xcuitest-driver/compare/v7.22.0...v7.22.1) (2024-07-03) + +### Miscellaneous Chores + +* Simplify subprocess output analysis ([#2422](https://github.com/appium/appium-xcuitest-driver/issues/2422)) ([c6b9be8](https://github.com/appium/appium-xcuitest-driver/commit/c6b9be8d5120b8097880bef49f67dc06a8bc548e)) + ## [7.22.0](https://github.com/appium/appium-xcuitest-driver/compare/v7.21.2...v7.22.0) (2024-07-02) ### Features diff --git a/package.json b/package.json index 890701f55..d0a96d9a0 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "xcuitest", "xctest" ], - "version": "7.22.0", + "version": "7.22.1", "author": "Appium Contributors", "license": "Apache-2.0", "repository": { From 8d405e8081eb0c4a09217717eb380ab4076a9736 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Thu, 4 Jul 2024 11:31:05 +0200 Subject: [PATCH 20/23] feat: Rewrite py-ios-device client and crash reports logger into typescript (#2423) --- lib/commands/certificate.js | 2 +- lib/commands/log.js | 3 +- lib/commands/memory.js | 2 +- lib/commands/pcap.js | 7 +- lib/device-log/helpers.ts | 27 +++ lib/device-log/ios-crash-log.js | 146 --------------- lib/device-log/ios-crash-log.ts | 167 ++++++++++++++++++ lib/device-log/ios-log.ts | 3 +- lib/driver.js | 2 +- lib/py-ios-device-client.js | 167 ------------------ lib/real-device-clients/base-device-client.ts | 34 ++++ lib/{ => real-device-clients}/devicectl.js | 0 .../py-ios-device-client.ts | 149 ++++++++++++++++ lib/real-device.js | 2 +- test/unit/logs-helpers-specs.js | 46 +++++ 15 files changed, 434 insertions(+), 323 deletions(-) delete mode 100644 lib/device-log/ios-crash-log.js create mode 100644 lib/device-log/ios-crash-log.ts delete mode 100644 lib/py-ios-device-client.js create mode 100644 lib/real-device-clients/base-device-client.ts rename lib/{ => real-device-clients}/devicectl.js (100%) create mode 100644 lib/real-device-clients/py-ios-device-client.ts create mode 100644 test/unit/logs-helpers-specs.js diff --git a/lib/commands/certificate.js b/lib/commands/certificate.js index 1c18b85ab..8e0d824d9 100644 --- a/lib/commands/certificate.js +++ b/lib/commands/certificate.js @@ -7,7 +7,7 @@ import path from 'path'; import http from 'http'; import {exec} from 'teen_process'; import {findAPortNotInUse, checkPortStatus} from 'portscanner'; -import Pyidevice from '../py-ios-device-client'; +import {Pyidevice} from '../real-device-clients/py-ios-device-client'; import {errors} from 'appium/driver'; const CONFIG_EXTENSION = 'mobileconfig'; diff --git a/lib/commands/log.js b/lib/commands/log.js index 8d05fb1d0..86f921ba3 100644 --- a/lib/commands/log.js +++ b/lib/commands/log.js @@ -108,8 +108,9 @@ export default { } if (_.isUndefined(this.logs.syslog)) { this.logs.crashlog = new IOSCrashLog({ - sim: this.device, + sim: /** @type {import('appium-ios-simulator').Simulator} */ (this.device), udid: this.isRealDevice() ? this.opts.udid : undefined, + log: this.log, }); this.logs.syslog = this.isRealDevice() ? new IOSDeviceLog({ diff --git a/lib/commands/memory.js b/lib/commands/memory.js index 3faa0c242..297281bda 100644 --- a/lib/commands/memory.js +++ b/lib/commands/memory.js @@ -17,7 +17,7 @@ export default { const device = /** @type {import('../real-device').RealDevice} */ (this.device); - /** @type {import('../devicectl').AppInfo[]} */ + /** @type {import('../real-device-clients/devicectl').AppInfo[]} */ const appInfos = await device.devicectl.listApps(bundleId); if (_.isEmpty(appInfos)) { throw new errors.InvalidArgumentError( diff --git a/lib/commands/pcap.js b/lib/commands/pcap.js index 044f1215c..f92f86f36 100644 --- a/lib/commands/pcap.js +++ b/lib/commands/pcap.js @@ -1,11 +1,10 @@ -import Pyidevice from '../py-ios-device-client'; -import {fs, tempDir, logger, util} from 'appium/support'; +import { Pyidevice } from '../real-device-clients/py-ios-device-client'; +import {fs, tempDir, util} from 'appium/support'; import {encodeBase64OrUpload} from '../utils'; import {errors} from 'appium/driver'; const MAX_CAPTURE_TIME_SEC = 60 * 60 * 12; const DEFAULT_EXT = '.pcap'; -const pcapLogger = logger.getLogger('pcapd'); export class TrafficCapture { /** @type {import('teen_process').SubProcess|null} */ @@ -21,7 +20,7 @@ export class TrafficCapture { this.mainProcess = /** @type {import('teen_process').SubProcess} */ ( await new Pyidevice(this.udid).collectPcap(this.resultPath) ); - this.mainProcess.on('line-stderr', (line) => pcapLogger.info(line)); + this.mainProcess.on('line-stderr', (line) => this.log.info(`[Pcap] ${line}`)); this.log.info( `Starting network traffic capture session on the device '${this.udid}'. ` + `Will timeout in ${timeoutSeconds}s`, diff --git a/lib/device-log/helpers.ts b/lib/device-log/helpers.ts index 73903e23b..b53dc8b89 100644 --- a/lib/device-log/helpers.ts +++ b/lib/device-log/helpers.ts @@ -1,4 +1,7 @@ import type { LogEntry } from '../commands/types'; +import { fs } from 'appium/support'; +import { createInterface } from 'node:readline'; +import _ from 'lodash'; export const DEFAULT_LOG_LEVEL = 'ALL'; export const MAX_JSON_LOG_LENGTH = 200; @@ -11,3 +14,27 @@ export function toLogEntry(message: string, timestamp: number, level: string = D message, }; } + +export interface GrepOptions { + caseInsensitive?: boolean; +} + +export async function grepFile( + fullPath: string, + str: string, + opts: GrepOptions = {} +): Promise { + const input = fs.createReadStream(fullPath); + const rl = createInterface({input}); + return await new Promise((resolve, reject) => { + input.once('error', reject); + rl.on('line', (line) => { + if (opts.caseInsensitive && _.toLower(line).includes(_.toLower(str)) + || !opts.caseInsensitive && line.includes(str)) { + resolve(true); + input.close(); + } + }); + input.once('end', () => resolve(false)); + }); +} diff --git a/lib/device-log/ios-crash-log.js b/lib/device-log/ios-crash-log.js deleted file mode 100644 index b5d3898a7..000000000 --- a/lib/device-log/ios-crash-log.js +++ /dev/null @@ -1,146 +0,0 @@ -import {fs, tempDir} from 'appium/support'; -import B from 'bluebird'; -import log from '../logger'; -import {utilities} from 'appium-ios-device'; -import path from 'path'; -import _ from 'lodash'; -import Pyidevice from '../py-ios-device-client'; - -const REAL_DEVICE_MAGIC = '3620bbb0-fb9f-4b62-a668-896f2edc4d88'; -const MAGIC_SEP = '/'; -// The file format has been changed from '.crash' to '.ips' since Monterey. -const CRASH_REPORTS_GLOB_PATTERN = '**/*.@(crash|ips)'; - -class IOSCrashLog { - constructor(opts = {}) { - this.udid = opts.udid; - this.pyideviceClient = this.udid ? new Pyidevice(this.udid) : null; - const root = process.env.HOME || '/'; - const logDir = opts.udid - ? path.resolve(root, 'Library', 'Logs', 'CrashReporter', 'MobileDevice') - : path.resolve(root, 'Library', 'Logs', 'DiagnosticReports'); - this.logDir = logDir || path.resolve(root, 'Library', 'Logs', 'DiagnosticReports'); - this.prevLogs = []; - this.logsSinceLastRequest = []; - this.phoneName = null; - this.sim = opts.sim; - } - - /** - * @returns {Promise} - */ - async _gatherFromRealDevice() { - if (await this.pyideviceClient?.assertExists(false)) { - return (await /** @type {Pyidevice} */ (this.pyideviceClient).listCrashes()).map( - (x) => `${REAL_DEVICE_MAGIC}${MAGIC_SEP}${x}`, - ); - } - - let crashLogsRoot = this.logDir; - if (this.udid) { - this.phoneName = this.phoneName || (await utilities.getDeviceName(this.udid)); - crashLogsRoot = path.resolve(crashLogsRoot, this.phoneName); - } - if (!(await fs.exists(crashLogsRoot))) { - log.debug(`Crash reports root '${crashLogsRoot}' does not exist. Got nothing to gather.`); - return []; - } - return await fs.glob(CRASH_REPORTS_GLOB_PATTERN, { - cwd: crashLogsRoot, - absolute: true, - }); - } - - /** - * @returns {Promise} - */ - async _gatherFromSimulator() { - if (!(await fs.exists(this.logDir))) { - log.debug(`Crash reports root '${this.logDir}' does not exist. Got nothing to gather.`); - return []; - } - const foundFiles = await fs.glob(CRASH_REPORTS_GLOB_PATTERN, { - cwd: this.logDir, - absolute: true, - }); - // For Simulator only include files, that contain current UDID - return await B.filter(foundFiles, async (x) => { - try { - const content = await fs.readFile(x, 'utf8'); - return content.toUpperCase().includes(this.sim.udid.toUpperCase()); - } catch (err) { - return false; - } - }); - } - - /** - * @returns {Promise} - */ - async getCrashes() { - return this.udid ? await this._gatherFromRealDevice() : await this._gatherFromSimulator(); - } - - /** - * @returns {Promise} - */ - async startCapture() { - this.prevLogs = await this.getCrashes(); - } - - /** - * @returns {Promise} - */ - async stopCapture() { - // needed for consistent API with other logs - } - - /** - * @returns {Promise} - */ - async getLogs() { - let crashFiles = await this.getCrashes(); - let diff = _.difference(crashFiles, this.prevLogs, this.logsSinceLastRequest); - this.logsSinceLastRequest = _.union(this.logsSinceLastRequest, diff); - return await this.filesToJSON(diff); - } - - /** - * @param {string[]} paths - * @returns {Promise} - */ - async filesToJSON(paths) { - const tmpRoot = await tempDir.openDir(); - try { - return /** @type {import('../commands/types').LogEntry[]} */ (( - await B.map(paths, async (fullPath) => { - if (_.includes(fullPath, REAL_DEVICE_MAGIC)) { - const fileName = /** @type {string} */ (_.last(fullPath.split(MAGIC_SEP))); - try { - // @ts-expect-error If pyideviceClient is not defined, then the exception will be caught below - await this.pyideviceClient.exportCrash(fileName, tmpRoot); - } catch (e) { - log.warn( - `Cannot export the crash report '${fileName}'. Skipping it. ` + - `Original error: ${e.message}`, - ); - return; - } - fullPath = path.join(tmpRoot, fileName); - } - const stat = await fs.stat(fullPath); - return { - timestamp: stat.ctime.getTime(), - level: 'ALL', - message: await fs.readFile(fullPath, 'utf8'), - }; - }) - ).filter(Boolean)); - } finally { - await fs.rimraf(tmpRoot); - } - } -} - -export {IOSCrashLog}; -export default IOSCrashLog; diff --git a/lib/device-log/ios-crash-log.ts b/lib/device-log/ios-crash-log.ts new file mode 100644 index 000000000..cdbcede9b --- /dev/null +++ b/lib/device-log/ios-crash-log.ts @@ -0,0 +1,167 @@ +import {fs, tempDir, util} from 'appium/support'; +import B from 'bluebird'; +import path from 'path'; +import _ from 'lodash'; +import {Pyidevice} from '../real-device-clients/py-ios-device-client'; +import IOSLog from './ios-log'; +import { toLogEntry, grepFile } from './helpers'; +import type { AppiumLogger } from '@appium/types'; +import type { BaseDeviceClient } from '../real-device-clients/base-device-client'; +import type { Simulator } from 'appium-ios-simulator'; +import type { LogEntry } from '../commands/types'; + +// The file format has been changed from '.crash' to '.ips' since Monterey. +const CRASH_REPORTS_GLOB_PATTERN = '**/*.@(crash|ips)'; +// The size of a single diagnostic report might be hundreds of kilobytes. +// Thus we do not want to store too many items in the memory at once. +const MAX_RECENT_ITEMS = 20; + +type TSerializedEntry = [string, number]; + +export interface IOSCrashLogOptions { + /** UDID of a real device */ + udid?: string; + /** Simulator instance */ + sim?: Simulator; + log: AppiumLogger; +} + +export class IOSCrashLog extends IOSLog { + private readonly _udid: string | undefined; + private readonly _realDeviceClient: BaseDeviceClient | null; + private readonly _logDir: string | null; + private readonly _sim: Simulator | undefined; + private _recentCrashFiles: string[]; + private _started: boolean; + + constructor(opts: IOSCrashLogOptions) { + super({ + log: opts.log, + maxBufferSize: MAX_RECENT_ITEMS, + }); + this._udid = opts.udid; + this._sim = opts.sim; + this._realDeviceClient = this._isRealDevice() + ? new Pyidevice({ + udid: this._udid as string, + log: opts.log, + }) + : null; + this._logDir = this._isRealDevice() + ? null + : path.resolve(process.env.HOME || '/', 'Library', 'Logs', 'DiagnosticReports'); + this._recentCrashFiles = []; + this._started = false; + } + + override async startCapture(): Promise { + this._recentCrashFiles = await this._listCrashFiles(false); + this._started = true; + } + + // eslint-disable-next-line require-await + override async stopCapture(): Promise { + this._started = false; + } + + override get isCapturing(): boolean { + return this._started; + } + + override async getLogs(): Promise { + const crashFiles = (await this._listCrashFiles(true)).slice(-MAX_RECENT_ITEMS); + const diffFiles = _.difference(crashFiles, this._recentCrashFiles); + if (_.isEmpty(diffFiles)) { + return []; + } + + this.log.debug(`Found ${util.pluralize('fresh crash report', diffFiles.length, true)}`); + await this._serializeCrashes(diffFiles); + this._recentCrashFiles = crashFiles; + return super.getLogs(); + } + + protected override _serializeEntry(value: TSerializedEntry): TSerializedEntry { + return value; + } + + protected override _deserializeEntry(value: TSerializedEntry): LogEntry { + const [message, timestamp] = value; + return toLogEntry(message, timestamp); + } + + private async _serializeCrashes(paths: string[]): Promise { + const tmpRoot = await tempDir.openDir(); + try { + for (const filePath of paths) { + let fullPath = filePath; + if (this._isRealDevice()) { + const fileName = filePath; + try { + await (this._realDeviceClient as BaseDeviceClient).exportCrash(fileName, tmpRoot); + } catch (e) { + this.log.warn( + `Cannot export the crash report '${fileName}'. Skipping it. ` + + `Original error: ${e.message}`, + ); + return; + } + fullPath = path.join(tmpRoot, fileName); + } + const {ctime} = await fs.stat(fullPath); + this.broadcast([await fs.readFile(fullPath, 'utf8'), ctime.getTime()]); + } + } finally { + await fs.rimraf(tmpRoot); + } + } + + private async _gatherFromRealDevice(strict: boolean): Promise { + if (!this._realDeviceClient) { + return []; + } + if (!await this._realDeviceClient.assertExists(strict)) { + this.log.info( + `The ${_.toLower(this._realDeviceClient.constructor.name)} tool is not present in PATH. ` + + `Skipping crash logs collection for real devices.` + ); + return []; + } + + return await this._realDeviceClient.listCrashes(); + } + + private async _gatherFromSimulator(): Promise { + if (!this._logDir || !this._sim || !(await fs.exists(this._logDir))) { + this.log.debug(`Crash reports root '${this._logDir}' does not exist. Got nothing to gather.`); + return []; + } + + const foundFiles = await fs.glob(CRASH_REPORTS_GLOB_PATTERN, { + cwd: this._logDir, + absolute: true, + }); + const simUdid = (this._sim as Simulator).udid; + // For Simulator only include files, that contain current UDID + return await B.filter(foundFiles, async (filePath) => { + try { + return await grepFile(filePath, simUdid, {caseInsensitive: true}); + } catch (err) { + this.log.warn(err); + return false; + } + }); + } + + private async _listCrashFiles(strict: boolean): Promise { + return this._isRealDevice() + ? await this._gatherFromRealDevice(strict) + : await this._gatherFromSimulator(); + } + + private _isRealDevice(): boolean { + return Boolean(this._udid); + } +} + +export default IOSCrashLog; diff --git a/lib/device-log/ios-log.ts b/lib/device-log/ios-log.ts index 41910c118..27a012571 100644 --- a/lib/device-log/ios-log.ts +++ b/lib/device-log/ios-log.ts @@ -37,7 +37,8 @@ export abstract class IOSLog< return this._log; } - getLogs(): LogEntry[] { + // eslint-disable-next-line require-await + async getLogs(): Promise { const result: LogEntry[] = []; for (const value of this.logs.rvalues()) { result.push(this._deserializeEntry(value as TSerializedEntry)); diff --git a/lib/driver.js b/lib/driver.js index ff1523f6c..43a22ad94 100644 --- a/lib/driver.js +++ b/lib/driver.js @@ -23,7 +23,7 @@ import {desiredCapConstraints} from './desired-caps'; import DEVICE_CONNECTIONS_FACTORY from './device-connections-factory'; import {executeMethodMap} from './execute-method-map'; import {newMethodMap} from './method-map'; -import Pyidevice from './py-ios-device-client'; +import { Pyidevice } from './real-device-clients/py-ios-device-client'; import { installToRealDevice, runRealDeviceReset, diff --git a/lib/py-ios-device-client.js b/lib/py-ios-device-client.js deleted file mode 100644 index 1bb63c0e4..000000000 --- a/lib/py-ios-device-client.js +++ /dev/null @@ -1,167 +0,0 @@ -import {exec, SubProcess} from 'teen_process'; -import {fs, util, tempDir} from 'appium/support'; -import log from './logger'; -import path from 'path'; - -// https://github.com/YueChen-C/py-ios-device - -const BINARY_NAME = 'pyidevice'; - -class Pyidevice { - /** - * @param {string} udid - */ - constructor(udid) { - this.udid = udid; - this.binaryPath = null; - } - - /** - * @param {boolean} isStrict - * @return {Promise} - */ - async assertExists(isStrict = true) { - if (this.binaryPath) { - return true; - } - - try { - this.binaryPath = await fs.which(BINARY_NAME); - return true; - } catch (e) { - if (isStrict) { - throw new Error( - `${BINARY_NAME} binary cannot be found in PATH. ` + - `Please make sure it is installed. Visit https://github.com/YueChen-C/py-ios-device for ` + - `more details.`, - ); - } - return false; - } - } - - /** - * @typedef {Object} ExecuteOptions - * @property {string} cwd - * @property {string?} format [json] - * @property {boolean} logStdout [false] - * @property {boolean} asynchronous [false] - */ - - /** - * @param {string[]} args - * @param {Partial} opts - * @return {Promise} - */ - async execute(args, opts = {}) { - await this.assertExists(); - const {cwd, format = 'json', logStdout = false, asynchronous = false} = opts; - - const finalArgs = [...args, '--udid', this.udid, '--network']; - if (format) { - finalArgs.push('--format', format); - } - const binaryPath = /** @type {string} */ (this.binaryPath); - const cmdStr = util.quote([binaryPath, ...finalArgs]); - log.debug(`Executing ${cmdStr}`); - try { - if (asynchronous) { - const result = new SubProcess(binaryPath, finalArgs, {cwd}); - await result.start(0); - return result; - } - const result = await exec(binaryPath, finalArgs, {cwd}); - if (logStdout) { - log.debug(`Command output: ${result.stdout}`); - } - return result; - } catch (e) { - throw new Error(`'${cmdStr}' failed. Original error: ${e.stderr || e.stdout || e.message}`); - } - } - - /** - * @return {Promise} - */ - async listProfiles() { - const {stdout} = /** @type {import('teen_process').TeenProcessExecResult} */ ( - await this.execute(['profiles', 'list']) - ); - return JSON.parse(stdout); - } - - /** - * - * @param { {profilePath?: string, payload: string|Buffer} } opts - * @privateRemarks The error below seems to suggest that `payload` can be undefined, but the code suggests otherwise - */ - async installProfile(opts) { - const {profilePath, payload} = opts ?? {}; - if (!profilePath && !payload) { - throw new Error('Either the full path to the profile or its payload must be provided'); - } - - let tmpRoot; - let srcPath = profilePath; - try { - if (!srcPath) { - tmpRoot = await tempDir.openDir(); - srcPath = path.join(tmpRoot, 'cert.pem'); - await fs.writeFile(srcPath, payload, 'utf8'); - } - await this.execute(['profiles', 'install', '--path', srcPath], { - logStdout: true, - }); - } finally { - if (tmpRoot) { - await fs.rimraf(tmpRoot); - } - } - } - - /** - * - * @param {string} name - * @returns {Promise} - */ - async removeProfile(name) { - return /** @type {import('teen_process').TeenProcessExecResult} */ ( - await this.execute(['profiles', 'remove', '--name', name], {logStdout: true}) - ).stdout; - } - - /** - * @returns {Promise} - */ - async listCrashes() { - const {stdout} = /** @type {import('teen_process').TeenProcessExecResult} */ ( - await this.execute(['crash', 'list']) - ); - return JSON.parse(stdout.replace(/'/g, '"')).filter((x) => !['.', '..'].includes(x)); - } - - /** - * @param {string} name - * @param {string} dstFolder - * @returns {Promise} - */ - async exportCrash(name, dstFolder) { - await this.execute(['crash', 'export', '--name', name], { - logStdout: true, - // The tool exports crash reports to the current working dir - cwd: dstFolder, - }); - } - /** - * @param {string} dstFile - */ - async collectPcap(dstFile) { - return await this.execute(['pcapd', dstFile], { - format: null, - asynchronous: true, - }); - } -} - -export {Pyidevice}; -export default Pyidevice; diff --git a/lib/real-device-clients/base-device-client.ts b/lib/real-device-clients/base-device-client.ts new file mode 100644 index 000000000..84031cf90 --- /dev/null +++ b/lib/real-device-clients/base-device-client.ts @@ -0,0 +1,34 @@ +import type { AppiumLogger } from '@appium/types'; +import type { SubProcess } from 'teen_process'; + +export interface BaseDeviceClientOptions { + log: AppiumLogger; +} + +export interface InstallProfileArgs { + profilePath?: string; + payload?: string|Buffer; +} + +export abstract class BaseDeviceClient { + private readonly _log: AppiumLogger; + + constructor (opts: BaseDeviceClientOptions) { + this._log = opts.log; + } + + get log(): AppiumLogger { + return this._log; + } + + abstract assertExists(isStrict: boolean): Promise; + + abstract listProfiles(): Promise; + abstract installProfile(args: InstallProfileArgs): Promise; + abstract removeProfile(name: string): Promise; + + abstract listCrashes(): Promise; + abstract exportCrash(name: string, dstFolder: string): Promise; + + abstract collectPcap(dstFile: string): Promise; +} diff --git a/lib/devicectl.js b/lib/real-device-clients/devicectl.js similarity index 100% rename from lib/devicectl.js rename to lib/real-device-clients/devicectl.js diff --git a/lib/real-device-clients/py-ios-device-client.ts b/lib/real-device-clients/py-ios-device-client.ts new file mode 100644 index 000000000..8c1f03ca9 --- /dev/null +++ b/lib/real-device-clients/py-ios-device-client.ts @@ -0,0 +1,149 @@ +import {exec, SubProcess} from 'teen_process'; +import {fs, util, tempDir} from 'appium/support'; +import path from 'path'; +import { BaseDeviceClient } from './base-device-client'; +import type { BaseDeviceClientOptions, InstallProfileArgs } from './base-device-client'; +import type { TeenProcessExecResult } from 'teen_process'; +import type { CertificateList } from '../commands/types'; + +// https://github.com/YueChen-C/py-ios-device + +const BINARY_NAME = 'pyidevice'; +const CRASH_REPORT_EXT = '.ips'; + +export interface PyideviceOptions extends BaseDeviceClientOptions { + udid: string; +} + +interface ExecuteOptions { + cwd?: string; + format?: string | null; + logStdout?: boolean; + asynchronous?: boolean; +} + +export class Pyidevice extends BaseDeviceClient { + private readonly _udid: string; + private _binaryPath: string | null; + + constructor(opts: PyideviceOptions) { + super({log: opts.log}); + this._udid = opts.udid; + this._binaryPath = null; + } + + override async assertExists(isStrict = true): Promise { + if (this._binaryPath) { + return true; + } + + try { + this._binaryPath = await fs.which(BINARY_NAME); + return true; + } catch (e) { + if (isStrict) { + throw new Error( + `${BINARY_NAME} binary cannot be found in PATH. ` + + `Please make sure it is installed. Visit https://github.com/YueChen-C/py-ios-device for ` + + `more details.`, + ); + } + return false; + } + } + + override async listProfiles(): Promise { + const {stdout} = await this.execute(['profiles', 'list']) as TeenProcessExecResult; + return JSON.parse(stdout); + } + + override async installProfile(args: InstallProfileArgs): Promise { + const {profilePath, payload} = args; + if (!profilePath && !payload) { + throw new Error('Either the full path to the profile or its payload must be provided'); + } + + let tmpRoot: string | undefined; + let srcPath = profilePath; + try { + if (!srcPath) { + tmpRoot = await tempDir.openDir(); + srcPath = path.join(tmpRoot, 'cert.pem'); + if (Buffer.isBuffer(payload)) { + await fs.writeFile(srcPath, payload); + } else { + await fs.writeFile(srcPath, payload as string, 'utf8'); + } + } + await this.execute(['profiles', 'install', '--path', srcPath], { + logStdout: true, + }); + } finally { + if (tmpRoot) { + await fs.rimraf(tmpRoot); + } + } + } + + override async removeProfile(name: string): Promise { + return ( + await this.execute(['profiles', 'remove', '--name', name], {logStdout: true}) as TeenProcessExecResult + ).stdout; + } + + override async listCrashes(): Promise { + const {stdout} = await this.execute(['crash', 'list']) as TeenProcessExecResult; + // Example output: + // ['.', '..', 'SiriSearchFeedback-2023-12-06-144043.ips', ' + // SiriSearchFeedback-2024-05-22-194219.ips', 'JetsamEvent-2024-05-23-225056.ips', + // 'JetsamEvent-2023-09-18-090920.ips', 'JetsamEvent-2024-05-16-054529.ips', + // 'Assistant'] + return JSON.parse(stdout.replace(/'/g, '"')) + .filter((x: string) => x.endsWith(CRASH_REPORT_EXT)); + } + + override async exportCrash(name: string, dstFolder: string): Promise { + await this.execute(['crash', 'export', '--name', name], { + logStdout: true, + // The tool exports crash reports to the current working dir + cwd: dstFolder, + }); + } + + override async collectPcap(dstFile: string): Promise { + return await this.execute(['pcapd', dstFile], { + format: null, + asynchronous: true, + }) as SubProcess; + } + + private async execute( + args: string[], + opts: ExecuteOptions = {} + ): Promise | SubProcess> { + await this.assertExists(); + const {cwd, format = 'json', logStdout = false, asynchronous = false} = opts; + + const finalArgs = [...args, '--udid', this._udid, '--network']; + if (format) { + finalArgs.push('--format', format); + } + const binaryPath = this._binaryPath as string; + const cmdStr = util.quote([binaryPath, ...finalArgs]); + this.log.debug(`Executing ${cmdStr}`); + try { + if (asynchronous) { + const result = new SubProcess(binaryPath, finalArgs, {cwd}); + await result.start(0); + return result; + } + const result = await exec(binaryPath, finalArgs, {cwd}); + if (logStdout) { + this.log.debug(`Command output: ${result.stdout}`); + } + return result; + } catch (e) { + throw new Error(`'${cmdStr}' failed. Original error: ${e.stderr || e.stdout || e.message}`); + } + } +} diff --git a/lib/real-device.js b/lib/real-device.js index c87da56ce..9d84b696b 100644 --- a/lib/real-device.js +++ b/lib/real-device.js @@ -6,7 +6,7 @@ import defaultLogger from './logger'; import _ from 'lodash'; import {SAFARI_BUNDLE_ID} from './app-utils'; import {pushFile, pushFolder, IO_TIMEOUT_MS} from './ios-fs-helpers'; -import { Devicectl } from './devicectl'; +import { Devicectl } from './real-device-clients/devicectl'; const APPLICATION_INSTALLED_NOTIFICATION = 'com.apple.mobile.application_installed'; const APPLICATION_NOTIFICATION_TIMEOUT_MS = 30 * 1000; diff --git a/test/unit/logs-helpers-specs.js b/test/unit/logs-helpers-specs.js new file mode 100644 index 000000000..5482cc530 --- /dev/null +++ b/test/unit/logs-helpers-specs.js @@ -0,0 +1,46 @@ +import { grepFile } from '../../lib/device-log/helpers'; +import {fs, tempDir} from 'appium/support'; +import path from 'node:path'; + + +describe('log-helpers', function () { + let chai; + + before(async function () { + chai = await import('chai'); + const chaiAsPromised = await import('chai-as-promised'); + + chai.should(); + chai.use(chaiAsPromised.default); + }); + + describe('grepFile', function () { + let tmpRoot; + + beforeEach(async function () { + tmpRoot = await tempDir.openDir(); + }); + + afterEach(async function () { + await fs.rimraf(tmpRoot); + }); + + it('should grep file content case sensitive', async function () { + const filePath = path.join(tmpRoot, 'grep.test'); + await fs.writeFile(filePath, `123\n45\nab`, 'utf8'); + await grepFile(filePath, 'ab').should.eventually.be.true; + }); + + it('should grep file content case insensitive', async function () { + const filePath = path.join(tmpRoot, 'grep.test'); + await fs.writeFile(filePath, `123\n45\nAB\ncd`, 'utf8'); + await grepFile(filePath, 'ab', {caseInsensitive: true}).should.eventually.be.true; + }); + + it('should return false if no match', async function () { + const filePath = path.join(tmpRoot, 'grep.test'); + await fs.writeFile(filePath, `123\n45\nAB`, 'utf8'); + await grepFile(filePath, 'cd', {caseInsensitive: true}).should.eventually.be.false; + }); + }); +}); From bdb0b2b910e7cd43f0da7c6065062bdae190c545 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 4 Jul 2024 09:32:50 +0000 Subject: [PATCH 21/23] chore(release): 7.23.0 [skip ci] ## [7.23.0](https://github.com/appium/appium-xcuitest-driver/compare/v7.22.1...v7.23.0) (2024-07-04) ### Features * Rewrite py-ios-device client and crash reports logger into typescript ([#2423](https://github.com/appium/appium-xcuitest-driver/issues/2423)) ([8d405e8](https://github.com/appium/appium-xcuitest-driver/commit/8d405e8081eb0c4a09217717eb380ab4076a9736)) --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3041a5896..b357e18ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [7.23.0](https://github.com/appium/appium-xcuitest-driver/compare/v7.22.1...v7.23.0) (2024-07-04) + +### Features + +* Rewrite py-ios-device client and crash reports logger into typescript ([#2423](https://github.com/appium/appium-xcuitest-driver/issues/2423)) ([8d405e8](https://github.com/appium/appium-xcuitest-driver/commit/8d405e8081eb0c4a09217717eb380ab4076a9736)) + ## [7.22.1](https://github.com/appium/appium-xcuitest-driver/compare/v7.22.0...v7.22.1) (2024-07-03) ### Miscellaneous Chores diff --git a/package.json b/package.json index d0a96d9a0..ad95f50a9 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "xcuitest", "xctest" ], - "version": "7.22.1", + "version": "7.23.0", "author": "Appium Contributors", "license": "Apache-2.0", "repository": { From ab070823f7287111a085cacf63ab6d77c2d2f031 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Tue, 9 Jul 2024 13:18:42 +0200 Subject: [PATCH 22/23] chore: Remove extra import --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index ad95f50a9..a17fc1111 100644 --- a/package.json +++ b/package.json @@ -135,7 +135,6 @@ }, "devDependencies": { "@appium/docutils": "^1.0.2", - "@appium/eslint-config-appium": "^8.0.4", "@appium/eslint-config-appium-ts": "^0.x", "@appium/test-support": "^3.0.20", "@appium/tsconfig": "^0.x", From 1fd38d779005f950b1b2a56182200cbb6405a81a Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 9 Jul 2024 11:20:38 +0000 Subject: [PATCH 23/23] chore(release): 7.23.1 [skip ci] ## [7.23.1](https://github.com/appium/appium-xcuitest-driver/compare/v7.23.0...v7.23.1) (2024-07-09) ### Miscellaneous Chores * Remove extra import ([ab07082](https://github.com/appium/appium-xcuitest-driver/commit/ab070823f7287111a085cacf63ab6d77c2d2f031)) --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b357e18ba..782e9afbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [7.23.1](https://github.com/appium/appium-xcuitest-driver/compare/v7.23.0...v7.23.1) (2024-07-09) + +### Miscellaneous Chores + +* Remove extra import ([ab07082](https://github.com/appium/appium-xcuitest-driver/commit/ab070823f7287111a085cacf63ab6d77c2d2f031)) + ## [7.23.0](https://github.com/appium/appium-xcuitest-driver/compare/v7.22.1...v7.23.0) (2024-07-04) ### Features diff --git a/package.json b/package.json index a17fc1111..4d1922646 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "xcuitest", "xctest" ], - "version": "7.23.0", + "version": "7.23.1", "author": "Appium Contributors", "license": "Apache-2.0", "repository": {