diff --git a/CHANGELOG.md b/CHANGELOG.md index 60b06991e..782e9afbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,70 @@ +## [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 + +* 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 + +* 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 + +* 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 + +* 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 + +* 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 + +* 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 + +* 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 + +* 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 + +* 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 + +* 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/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/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/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/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/context.js b/lib/commands/context.js index 8d64ebf7d..3bd707eb5 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(); } @@ -566,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 6ced42692..86f921ba3 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,26 +48,23 @@ 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 - */ +const LOG_NAMES_TO_CAPABILITY_NAMES_MAP = { + safariConsole: 'showSafariConsoleLog', + safariNetwork: 'showSafariNetworkLog', + enablePerformanceLogging: 'enablePerformanceLogging', +}; export default { supportedLogTypes: SUPPORTED_LOG_TYPES, @@ -85,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(); } - throw new Error(`No logs of type '${String(logType)}' found.`); + 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 '${logType}' found. Supported log types are: ${_.keys(SUPPORTED_LOG_TYPES)}.` + ); }, /** @@ -103,25 +108,34 @@ 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, }); - - if (this.isRealDevice()) { - this.logs.syslog = new IOSDeviceLog({ + this.logs.syslog = this.isRealDevice() + ? new IOSDeviceLog({ udid: this.opts.udid, showLogs: this.opts.showIOSLog, - }); - } else { - this.logs.syslog = new IOSSimulatorLog({ - sim: this.device, + log: this.log, + }) + : new IOSSimulatorLog({ + sim: /** @type {import('appium-ios-simulator').Simulator} */ (this.device), showLogs: this.opts.showIOSLog, - xcodeVersion: this.xcodeVersion, 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; @@ -137,8 +151,6 @@ export default { } })(), this.logs.crashlog.startCapture(), - this.logs.safariConsole.startCapture(), - this.logs.safariNetwork.startCapture(), ]; await B.all(promises); 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 89818c379..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,11 +20,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) => 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/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); diff --git a/lib/commands/screenshots.js b/lib/commands/screenshots.js index 9d55009fa..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'); @@ -72,6 +93,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(); 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/commands/web.js b/lib/commands/web.js index af0ee8b9d..63233b511 100644 --- a/lib/commands/web.js +++ b/lib/commands/web.js @@ -892,9 +892,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}`; }, diff --git a/lib/device-log/helpers.ts b/lib/device-log/helpers.ts new file mode 100644 index 000000000..b53dc8b89 --- /dev/null +++ b/lib/device-log/helpers.ts @@ -0,0 +1,40 @@ +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; +export const MAX_BUFFERED_EVENTS_COUNT = 5000; + +export function toLogEntry(message: string, timestamp: number, level: string = DEFAULT_LOG_LEVEL): LogEntry { + return { + timestamp, + level, + 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 5c0a4a4c3..000000000 --- a/lib/device-log/ios-crash-log.js +++ /dev/null @@ -1,162 +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)'; - -/** - * @typedef {Object} LogRecord - * @property {number} timestamp - * @property {string} level - * @property {string} message - */ - -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); - } - - /** - * @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} - */ - async filesToJSON(paths) { - const tmpRoot = await tempDir.openDir(); - try { - return /** @type {LogRecord[]} */ (( - 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-device-log.js b/lib/device-log/ios-device-log.js deleted file mode 100644 index 10d641def..000000000 --- a/lib/device-log/ios-device-log.js +++ /dev/null @@ -1,54 +0,0 @@ -import {logger} from 'appium/support'; -import {IOSLog} from './ios-log'; -import {services} from 'appium-ios-device'; - -const log = logger.getLogger('IOSDeviceLog'); - -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)); - } - - 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 {IOSDeviceLog}; -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..d53bf3981 --- /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 readonly udid: string; + private readonly 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 03c65e813..000000000 --- a/lib/device-log/ios-log.js +++ /dev/null @@ -1,65 +0,0 @@ -import {EventEmitter} from 'events'; - -// 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() { - super(); - this.logs = []; - this.logIdxSinceLastRequest = -1; - this.maxBufferSize = MAX_LOG_ENTRIES_COUNT; - } - - /** @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`); - } - - 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; - } - } - } - - 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 []; - } - - getAllLogs() { - return this.logs; - } -} - -export {IOSLog}; -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..27a012571 --- /dev/null +++ b/lib/device-log/ios-log.ts @@ -0,0 +1,71 @@ +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 _log: AppiumLogger; + + constructor(opts: IOSLogOptions = {}) { + super(); + this.maxBufferSize = opts.maxBufferSize ?? MAX_LOG_ENTRIES_COUNT; + this.logs = new LRUCache({ + max: this.maxBufferSize, + }); + 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; + } + + // 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)); + } + this._clearEntries(); + return result; + } + + protected abstract _serializeEntry(value: TRawEntry): TSerializedEntry; + protected abstract _deserializeEntry(value: TSerializedEntry): LogEntry; + + 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.js b/lib/device-log/ios-performance-log.js deleted file mode 100644 index 3d7678fe7..000000000 --- a/lib/device-log/ios-performance-log.js +++ /dev/null @@ -1,57 +0,0 @@ -import {logger} from 'appium/support'; -import _ from 'lodash'; - -const log = logger.getLogger('IOSPerformanceLog'); -const MAX_EVENTS = 5000; - -class IOSPerformanceLog { - constructor(remoteDebugger, maxEvents = MAX_EVENTS) { - this.remoteDebugger = remoteDebugger; - this.maxEvents = parseInt(String(maxEvents), 10); - - this.timelineEvents = []; - } - - async startCapture() { - log.debug('Starting performance (Timeline) log capture'); - this.timelineEvents = []; - return await this.remoteDebugger.startTimeline(this.onTimelineEvent.bind(this)); - } - - async stopCapture() { - log.debug('Stopping performance (Timeline) log capture'); - return await this.remoteDebugger.stopTimeline(); - } - - 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))}`, - ); - } - } - - // eslint-disable-next-line require-await - async getLogs() { - let events = this.timelineEvents; - - // flush events - log.debug('Flushing Timeline events'); - this.timelineEvents = []; - - return events; - } - - // eslint-disable-next-line require-await - async getAllLogs() { - return this.getLogs(); - } -} - -export {IOSPerformanceLog}; -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..97cadfab5 --- /dev/null +++ b/lib/device-log/ios-performance-log.ts @@ -0,0 +1,50 @@ +import _ from 'lodash'; +import type { AppiumLogger } from '@appium/types'; +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; +} + +export class IOSPerformanceLog extends LineConsumingLog { + private readonly remoteDebugger: any; + private _started: boolean; + + constructor(opts: IOSPerformanceLogOptions) { + super({ + maxBufferSize: opts.maxEvents ?? MAX_BUFFERED_EVENTS_COUNT, + log: opts.log, + }); + this.remoteDebugger = opts.remoteDebugger; + this._started = false; + } + + override async startCapture(): Promise { + this.log.debug('Starting performance (Timeline) log capture'); + this._clearEntries(); + await this.remoteDebugger.startTimeline(this.onTimelineEvent.bind(this)); + this._started = true; + } + + override async stopCapture(): Promise { + this.log.debug('Stopping performance (Timeline) log capture'); + await this.remoteDebugger.stopTimeline(); + this._started = false; + } + + override get isCapturing(): boolean { + return this._started; + } + + private onTimelineEvent(event: PerformanceLogEntry): void { + const serializedEntry = JSON.stringify(event); + this.broadcast(serializedEntry); + this.log.debug(`Received Timeline event: ${_.truncate(serializedEntry, {length: MAX_JSON_LOG_LENGTH})}`); + } +} + +export default IOSPerformanceLog; diff --git a/lib/device-log/ios-simulator-log.js b/lib/device-log/ios-simulator-log.js deleted file mode 100644 index 1ac75acec..000000000 --- a/lib/device-log/ios-simulator-log.js +++ /dev/null @@ -1,124 +0,0 @@ -import _ from 'lodash'; -import {IOSLog} from './ios-log'; -import {logger} from 'appium/support'; -import {exec} from 'teen_process'; - -const log = logger.getLogger('IOSSimulatorLog'); - -const START_TIMEOUT = 10000; - -class IOSSimulatorLog extends IOSLog { - constructor({sim, showLogs, xcodeVersion, iosSimulatorLogsPredicate}) { - super(); - this.sim = sim; - this.showLogs = !!showLogs; - this.xcodeVersion = xcodeVersion; - this.predicate = iosSimulatorLogsPredicate; - this.proc = null; - } - - async startCapture() { - if (_.isUndefined(this.sim.udid)) { - throw new Error(`Log capture requires a sim udid`); - } - - if (!(await this.sim.isRunning())) { - throw new Error(`iOS Simulator with udid '${this.sim.udid}' is not running`); - } - const spawnArgs = ['log', 'stream', '--style', 'compact']; - if (this.predicate) { - spawnArgs.push('--predicate', this.predicate); - } - log.debug( - `Starting log capture for iOS Simulator with udid '${this.sim.udid}' ` + `using simctl`, - ); - try { - // cleanup existing listeners if the previous session has not been terminated properly - await exec('pkill', ['-f', [this.sim.udid, ...spawnArgs].join(' ')]); - } catch (ign) {} - try { - this.proc = await this.sim.simctl.spawnSubProcess(spawnArgs); - await this.finishStartingLogCapture(); - } catch (e) { - throw new Error(`Simulator log capture failed. Original error: ${e.message}`); - } - } - - 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); - } - - async stopCapture() { - if (!this.proc) { - return; - } - await this.killLogSubProcess(); - this.proc = null; - } - - async killLogSubProcess() { - if (!this.proc.isRunning) { - return; - } - 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'); - await this.proc.stop('SIGKILL'); - } - } - - get isCapturing() { - return this.proc && this.proc.isRunning; - } - - 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}`); - } - } - } -} - -export {IOSSimulatorLog}; -export default IOSSimulatorLog; diff --git a/lib/device-log/ios-simulator-log.ts b/lib/device-log/ios-simulator-log.ts new file mode 100644 index 000000000..be4d2ec9b --- /dev/null +++ b/lib/device-log/ios-simulator-log.ts @@ -0,0 +1,117 @@ +import _ from 'lodash'; +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 EXECVP_ERROR_PATTERN = /execvp\(\)/; + +const START_TIMEOUT = 10000; + +export interface IOSSimulatorLogOptions { + sim: Simulator; + showLogs?: boolean; + iosSimulatorLogsPredicate?: string; + log: AppiumLogger; +} + +export class IOSSimulatorLog extends LineConsumingLog { + private readonly sim: Simulator; + private readonly showLogs: boolean; + private readonly 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(): Promise { + if (_.isUndefined(this.sim.udid)) { + throw new Error(`Log capture requires a sim udid`); + } + + if (!(await this.sim.isRunning())) { + throw new Error(`iOS Simulator with udid '${this.sim.udid}' is not running`); + } + const spawnArgs = ['log', 'stream', '--style', 'compact']; + if (this.predicate) { + spawnArgs.push('--predicate', this.predicate); + } + this.log.debug( + `Starting log capture for iOS Simulator with udid '${this.sim.udid}' ` + `using simctl`, + ); + try { + // cleanup existing listeners if the previous session has not been terminated properly + await exec('pkill', ['-f', [this.sim.udid, ...spawnArgs].join(' ')]); + } catch (ign) {} + try { + this.proc = await this.sim.simctl.spawnSubProcess(spawnArgs); + await this.finishStartingLogCapture(); + } catch (e) { + throw new Error(`Simulator log capture failed. Original error: ${e.message}`); + } + } + + override async stopCapture(): Promise { + if (!this.proc) { + return; + } + await this.killLogSubProcess(); + this.proc = null; + } + + override get isCapturing(): boolean { + return Boolean(this.proc && this.proc.isRunning); + } + + private onOutput(logRow: string, prefix: string = ''): void { + this.broadcast(logRow); + if (this.showLogs) { + const space = prefix.length > 0 ? ' ' : ''; + this.log.info(`[IOS_SYSLOG_ROW${space}${prefix}] ${logRow}`); + } + } + + private async killLogSubProcess(): Promise { + if (!this.proc?.isRunning) { + return; + } + + this.log.debug('Stopping iOS log capture'); + try { + await this.proc.stop('SIGTERM', 1000); + } catch (e) { + if (!this.proc.isRunning) { + return; + } + this.log.warn('Cannot stop log capture process. Sending SIGKILL'); + await this.proc.stop('SIGKILL'); + } + } + + 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(`line-${streamName}`, (line: string) => { + this.onOutput(line, ...(streamName === 'stderr' ? ['STDERR'] : [])); + }); + } + + const startDetector = (stdout: string, stderr: string) => { + if (EXECVP_ERROR_PATTERN.test(stderr)) { + throw new Error('iOS log capture process failed to start'); + } + return Boolean(stdout || stderr); + }; + await this.proc.start(startDetector, START_TIMEOUT); + } +} + +export default IOSSimulatorLog; 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); + } +} 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/lib/driver.js b/lib/driver.js index f2ce62049..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, @@ -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/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: { 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 98% rename from lib/devicectl.js rename to lib/real-device-clients/devicectl.js index 4187d0602..cd1e3daaf 100644 --- a/lib/devicectl.js +++ b/lib/real-device-clients/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}`); } 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/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, }); diff --git a/package.json b/package.json index bd132b070..4d1922646 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "xcuitest", "xctest" ], - "version": "7.18.0", + "version": "7.23.1", "author": "Appium Contributors", "license": "Apache-2.0", "repository": { @@ -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.2.0", "ws": "^8.13.0" }, "scripts": { @@ -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", @@ -143,19 +142,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 +160,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/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; + }); + }); +}); 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" ] }