mirror of
https://github.com/appium/appium.git
synced 2026-11-12 21:43:19 +08:00
Compare commits
28
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e76ba9607 | ||
|
|
aab415685e | ||
|
|
d13d292c4b | ||
|
|
df250cc1e0 | ||
|
|
5565febc9d | ||
|
|
49a0fe164b | ||
|
|
94911eb59f | ||
|
|
1b4a41bce9 | ||
|
|
5d0d4f5a44 | ||
|
|
f81e2e92ef | ||
|
|
316ecca8b1 | ||
|
|
20667be9a2 | ||
|
|
48b7b05a1f | ||
|
|
2df077b08c | ||
|
|
0e195caafe | ||
|
|
9842e231c5 | ||
|
|
feeaac4713 | ||
|
|
ad7e2648c1 | ||
|
|
2cb1af4357 | ||
|
|
45b6aed22d | ||
|
|
26c84c9361 | ||
|
|
ad06a0612f | ||
|
|
fe67e4cb2f | ||
|
|
fee1c98ee9 | ||
|
|
9fea6980e5 | ||
|
|
37f71c327a | ||
|
|
172fcb9aff | ||
|
|
3993decde4 |
+1
-5
@@ -29,8 +29,6 @@ targets:
|
||||
path: ./packages/eslint-config-appium
|
||||
- type: npm
|
||||
path: ./packages/fake-driver
|
||||
- type: npm
|
||||
path: ./packages/gulp-plugins
|
||||
- type: npm
|
||||
path: ./packages/support
|
||||
- type: npm
|
||||
@@ -54,7 +52,5 @@ targets:
|
||||
- type: npm
|
||||
path: ./packages/docutils
|
||||
|
||||
|
||||
|
||||
revision:
|
||||
branch: master
|
||||
- branch: master
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
node-version: [14.x, 16.x, 18.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@7dd9e2a3dc350cf687eb1b2a4fadfee8c8e49675 # v3
|
||||
- uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3
|
||||
with:
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@7dd9e2a3dc350cf687eb1b2a4fadfee8c8e49675 # v3
|
||||
- uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@1a235c31658a322a3b024444759650ee6345c26d # tag=v1
|
||||
with:
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
name: Run FOSSA Analysis
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@7dd9e2a3dc350cf687eb1b2a4fadfee8c8e49675 # v3
|
||||
- uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3
|
||||
with:
|
||||
ref: master
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
rules: {
|
||||
'header-max-length': [0],
|
||||
'body-leading-blank': [0],
|
||||
'body-max-line-length': [0],
|
||||
'footer-max-line-length': [0],
|
||||
'header-max-length': [0],
|
||||
'subject-case': [0],
|
||||
'subject-full-stop': [0],
|
||||
},
|
||||
};
|
||||
|
||||
Generated
+279
-8063
File diff suppressed because it is too large
Load Diff
+8
-9
@@ -53,11 +53,11 @@
|
||||
"sync-pkgs:appium-readme": "sync-monorepo-packages --force --no-package-json --packages=packages/appium README.md",
|
||||
"sync-pkgs:common-fields": "sync-monorepo-packages --fields=author,license,bugs,homepage,engines",
|
||||
"sync-pkgs:keywords": "sync-monorepo-packages --field=keywords --packages=\"packages/*\" --packages=\"!packages/eslint-config-appium\" --packages=\"!packages/types\"",
|
||||
"sync-pkgs:license": "sync-monorepo-packages --force --packages=\"packages/*\" --no-package-json LICENSE",
|
||||
"sync-pkgs:license": "sync-monorepo-packages --force --no-package-json LICENSE",
|
||||
"stage-synced-pkgs": "git add -A packages/appium/README.md \"packages/*/package.json\"",
|
||||
"test": "npm-run-all -p lint build -s test:unit",
|
||||
"test:ci": "run-s test:smoke test:unit test:e2e",
|
||||
"test:e2e": "lerna run --concurrency=1 --max-buffer=128 test:e2e",
|
||||
"test:e2e": "lerna run --concurrency=2 test:e2e",
|
||||
"test:smoke": "smoker --all test:smoke",
|
||||
"test:unit": "lerna run test",
|
||||
"typedoc": "typedoc --logLevel Verbose",
|
||||
@@ -79,7 +79,7 @@
|
||||
"@types/chai": "4.3.4",
|
||||
"@types/chai-as-promised": "7.1.5",
|
||||
"@types/mocha": "10.0.1",
|
||||
"@types/node": "18.11.14",
|
||||
"@types/node": "18.11.17",
|
||||
"@types/semver": "7.3.13",
|
||||
"@types/sinon": "10.0.13",
|
||||
"@types/sinon-chai": "3.2.9",
|
||||
@@ -90,7 +90,7 @@
|
||||
"chai-as-promised": "7.1.1",
|
||||
"chai-webdriverio-async": "2.7.0",
|
||||
"cpy-cli": "4.2.0",
|
||||
"eslint": "8.29.0",
|
||||
"eslint": "8.30.0",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-find-rules": "4.1.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
@@ -99,7 +99,6 @@
|
||||
"fancy-log": "2.0.0",
|
||||
"finalhandler": "1.2.0",
|
||||
"get-port": "5.1.1",
|
||||
"gulp": "4.0.2",
|
||||
"handlebars": "4.7.7",
|
||||
"husky": "8.0.2",
|
||||
"json-schema-to-typescript": "11.0.2",
|
||||
@@ -115,17 +114,17 @@
|
||||
"rewiremock": "3.14.4",
|
||||
"rimraf": "3.0.2",
|
||||
"serve-static": "1.15.0",
|
||||
"sinon": "15.0.0",
|
||||
"sinon": "15.0.1",
|
||||
"sinon-chai": "3.7.0",
|
||||
"sync-monorepo-packages": "1.0.1",
|
||||
"sync-monorepo-packages": "1.0.2",
|
||||
"through2": "4.0.2",
|
||||
"ts-node": "10.9.1",
|
||||
"typedoc": "0.23.22",
|
||||
"typedoc": "0.23.23",
|
||||
"typedoc-plugin-markdown": "3.14.0",
|
||||
"typedoc-plugin-resolve-crossmodule-references": "0.3.3",
|
||||
"typescript": "4.7.4",
|
||||
"validate.js": "0.13.1",
|
||||
"webdriverio": "7.27.0",
|
||||
"webdriverio": "7.28.0",
|
||||
"ws": "8.11.0",
|
||||
"yaml-js": "0.3.1"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,19 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.0.0-beta.48](https://github.com/appium/appium/compare/appium@2.0.0-beta.47...appium@2.0.0-beta.48) (2022-12-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **appium:** ensure migration always gets applied ([20667be](https://github.com/appium/appium/commit/20667be9a21e71963ecb7e25e1b8d51864bce011))
|
||||
- **appium:** fix manifest migrations ([df250cc](https://github.com/appium/appium/commit/df250cc1e024f4971abe7e756594f7feee32cba6))
|
||||
- **execute-driver-plugin:** update dependency webdriverio to v7.28.0 ([f81e2e9](https://github.com/appium/appium/commit/f81e2e92eff25c33d36f767209f423227d288218))
|
||||
- **types:** update dependency type-fest to v3.4.0 ([37f71c3](https://github.com/appium/appium/commit/37f71c327a7c1a6d882b5198af6fedc9e8d51496))
|
||||
|
||||
### Features
|
||||
|
||||
- **appium:** add chromium driver to list of officially supported drivers ([#17939](https://github.com/appium/appium/issues/17939)) ([fe67e4c](https://github.com/appium/appium/commit/fe67e4cb2fca977dd9994f1cfbba4a0edc61ef83))
|
||||
|
||||
# [2.0.0-beta.47](https://github.com/appium/appium/compare/appium@2.0.0-beta.46...appium@2.0.0-beta.47) (2022-12-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -22,6 +22,7 @@ Intro](../intro/drivers.md)
|
||||
|
||||
|Driver|Installation Key|Platform(s)|Mode(s)|
|
||||
|--|--|--|--|
|
||||
|[Chromium](https://github.com/appium/appium-chromium-driver)|`chromium`|macOS, Windows, Linux|Web|
|
||||
|[Espresso](https://github.com/appium/appium-espresso-driver)|`espresso`|Android|Native|
|
||||
|[Gecko](https://github.com/appium/appium-geckodriver)|`gecko`|macOS, Windows, Linux, Android|Web|
|
||||
|[Mac2](https://github.com/appium/appium-mac2-driver)|`mac2`|macOS|Native|
|
||||
|
||||
@@ -2,39 +2,131 @@
|
||||
title: Appium and Selenium Grid
|
||||
---
|
||||
|
||||
## Selenium Grid 4
|
||||
## Using Selenium Grid 4+
|
||||
|
||||
The **relay** feature in Grid 4 allows you to proxy Appium requests to an Appium server instance.
|
||||
The
|
||||
[relay](https://www.selenium.dev/documentation/grid/configuration/toml_options/#relaying-commands-to-a-service-endpoint-that-supports-webdriver)
|
||||
feature in Grid 4 allows you to proxy Appium requests to an Appium server instance. Here is
|
||||
an example walkthrough of how you would connect two different Appium instances to a Selenium Grid.
|
||||
|
||||
Please check [Relaying commands to a service endpoint that supports WebDriver](https://www.selenium.dev/documentation/grid/configuration/toml_options/#relaying-commands-to-a-service-endpoint-that-supports-webdriver) and [Selenium Grid 4 and Appium together in harmony](https://www.youtube.com/watch?v=3_aP2rsqZD0) about the configuration and for more details.
|
||||
### Define the Appium configs
|
||||
|
||||
## Selenium Grid 3
|
||||
Each Appium instance should have a config file that can be easily updated. It should contain any
|
||||
information which needs to be unique to that particular server (e.g., ports its drivers should use
|
||||
that others should not). We are going to have 2 Appium servers, so we will need 2 config files:
|
||||
|
||||
You are able to register your Appium server with a local [Selenium Grid 3](https://www.selenium.dev/documentation/legacy/selenium_3/grid_3/)
|
||||
([setup docs](https://www.selenium.dev/documentation/legacy/grid_3/setting_up_your_own_grid/)) by using the
|
||||
```yaml
|
||||
# appium1.yml
|
||||
server:
|
||||
port: 4723
|
||||
use-drivers:
|
||||
- xcuitest
|
||||
default-capabilities:
|
||||
wdaLocalPort: 8100
|
||||
mjpegServerPort: 9100
|
||||
mjpegScreenshotUrl: "http://localhost:9100"
|
||||
```
|
||||
|
||||
In the above YAML config file, we specify the Appium server port, the driver used, and parameters
|
||||
for the driver that will be sent in as default capabilities. Our goal is to ensure that any other
|
||||
drivers running on this host will not compete with system ports or other resources. The second
|
||||
config file could look like the following, where we simply adjust a few ports to prevent clashes:
|
||||
|
||||
```yaml
|
||||
# appium2.yml
|
||||
server:
|
||||
port: 4733
|
||||
use-drivers:
|
||||
- xcuitest
|
||||
default-capabilities:
|
||||
wdaLocalPort: 8110
|
||||
mjpegServerPort: 9110
|
||||
mjpegScreenshotUrl: "http://localhost:9110"
|
||||
```
|
||||
|
||||
### Define the Grid node configs
|
||||
|
||||
We will be launching one Grid "node" per Appium server, to manage relaying commands and determining
|
||||
capacity and online status, etc... So we should have one config file per Grid node as well. Each
|
||||
node config should include the address of the Appium server it will target, as well as a list of
|
||||
capability "configs" it should accept to relay a session request to Appium. Here is what the config
|
||||
could look like for the two nodes:
|
||||
|
||||
```toml
|
||||
# node1.toml
|
||||
[server]
|
||||
port = 5555
|
||||
|
||||
[node]
|
||||
detect-drivers = false
|
||||
|
||||
[relay]
|
||||
url = "http://localhost:4723"
|
||||
status-endpoint = "/status"
|
||||
configs = [
|
||||
"1", "{\"platformName\": \"iOS\", \"appium:platformVersion\": \"15.5\", \"appium:deviceName\": \"iPhone 13\", \"appium:automationName\": \"XCUITest\"}"
|
||||
]
|
||||
```
|
||||
|
||||
```toml
|
||||
# node2.toml
|
||||
[server]
|
||||
port = 5565
|
||||
|
||||
[node]
|
||||
detect-drivers = false
|
||||
|
||||
[relay]
|
||||
url = "http://localhost:4733"
|
||||
status-endpoint = "/status"
|
||||
configs = [
|
||||
"1", "{\"platformName\": \"iOS\", \"appium:platformVersion\": \"15.5\", \"appium:deviceName\": \"iPhone 12\", \"appium:automationName\": \"XCUITest\"}"
|
||||
]
|
||||
```
|
||||
|
||||
Note that each node config also specifies a different port itself for the node to run on.
|
||||
|
||||
### Putting it together
|
||||
|
||||
The Grid nodes aren't enough--you'll also want a Grid "hub" that acts as a load balancer and
|
||||
manager for all the nodes. So in the end we'll have 5 processes running at once: 2 Appium servers,
|
||||
2 Grid nodes, and 1 Grid hub. It's best to run each of these in a separate terminal window to avoid
|
||||
confusion of logs. Here is how you'd start each process:
|
||||
|
||||
0. `appium --config appium1.yml`
|
||||
0. `appium --config appium2.yml`
|
||||
0. `java -jar /path/to/selenium.jar node --config node1.toml`
|
||||
0. `java -jar /path/to/selenium.jar node --config node2.toml`
|
||||
0. `java -jar /path/to/selenium.jar hub`
|
||||
|
||||
Once you wait a few moments for the nodes to detect their Appium servers, and to register with the
|
||||
hub, you'll be able to send all your Appium traffic via the single hub endpoint (defaulting to
|
||||
`http://localhost:4444`).
|
||||
|
||||
And of course, you're able to link up Appium servers and nodes running on different machines in
|
||||
your network to form a larger grid.
|
||||
|
||||
## Using Selenium Grid 3
|
||||
|
||||
It is possible to register your Appium server with a local [Selenium Grid 3](https://www.selenium.dev/documentation/legacy/selenium_3/grid_3/)
|
||||
([setup docs](https://www.selenium.dev/documentation/legacy/grid_3/setting_up_your_own_grid/)) instance by using the
|
||||
`--nodeconfig` server argument.
|
||||
|
||||
```bash
|
||||
appium server --nodeconfig /path/to/nodeconfig.json --base-path=/wd/hub
|
||||
```
|
||||
|
||||
In the node config file you have to define the `browserName`,
|
||||
`version` and `platform` and based on these parameters the grid
|
||||
will re-direct your test to the right device. You will also need to
|
||||
configure your **host** details and the **Selenium Grid** details. For
|
||||
a full list of all parameters and descriptions look
|
||||
[here](https://www.selenium.dev/documentation/legacy/selenium_3/grid_setup/)
|
||||
In the referenced config file you have to define the `browserName`, `version` and `platform`
|
||||
capabilities and based on these parameters the grid will re-direct your test to the right device.
|
||||
You will also need to configure your host details and the Selenium Grid details. For a full list of
|
||||
all parameters and descriptions see
|
||||
[here](https://www.selenium.dev/documentation/legacy/selenium_3/grid_setup/).
|
||||
|
||||
Once you start the appium server and it registers with the grid,
|
||||
you will see your device on the grid console page:
|
||||
Once you start the Appium server it will register with the grid, and you will see your device on
|
||||
the grid console page:
|
||||
|
||||
`http://**\<grid-ip-adress\>**:**\<grid-port\>**/grid/console`
|
||||
|
||||
|
||||
New session capabilities should use an `appium:` prefix for non-W3C-standard capabilities such as
|
||||
`automationName`, since the grid appends the given capabilities in both `desiredCapabilities`
|
||||
and `firstMatch` in `capabilities`.
|
||||
|
||||
|
||||
### Example Grid Node Configuration JSON
|
||||
|
||||
```json
|
||||
@@ -60,13 +152,15 @@ and `firstMatch` in `capabilities`.
|
||||
"register": true,
|
||||
"registerCycle": 5000,
|
||||
"hubPort": <grid_port>,
|
||||
"hubHost": "<Grid_host_name_or_grid_ip-address>"
|
||||
"hubHost": "<Grid_host_name_or_grid_ip-address>",
|
||||
"hubProtocol": "<Protocol_of_Grid_defaults_to_http>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If `url`, `host`, and `port` are not given, the config will be auto updated
|
||||
to point to localhost:whatever-port-Appium-started-on.
|
||||
If `url`, `host`, and `port` are not given, the config will be auto updated to point to
|
||||
`localhost:<appium-port>`.
|
||||
|
||||
If your Appium server is running on a different machine to your Selenium Grid server, make sure you use an external name/IP address in your `host` & `url` docs; `localhost` and `127.0.0.1` will prevent Selenium Grid from connecting correctly.
|
||||
If your Appium server is running on a different machine to your Selenium Grid server, make sure you
|
||||
use an external name/IP address in your `host` and `url` configuration; `localhost` and `127.0.0.1`
|
||||
will prevent Selenium Grid from connecting correctly.
|
||||
|
||||
@@ -17,6 +17,11 @@ export default class DriverCommand extends ExtensionCommand {
|
||||
this.knownExtensions = KNOWN_DRIVERS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a driver
|
||||
*
|
||||
* @param {DriverInstallOpts} opts
|
||||
*/
|
||||
async install({driver, installType, packageName}) {
|
||||
return await super._install({
|
||||
installSpec: driver,
|
||||
@@ -78,3 +83,15 @@ export default class DriverCommand extends ExtensionCommand {
|
||||
/**
|
||||
* @typedef {import('@appium/types').DriverType} DriverType
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options for {@linkcode ExtensionCommand._install}
|
||||
* @typedef DriverInstallOpts
|
||||
* @property {string} driver - the name or spec of a driver to install
|
||||
* @property {InstallType} installType - how to install this driver. One of the INSTALL_TYPES
|
||||
* @property {string} [packageName] - for git/github installs, the driver node package name
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('appium/types').InstallType} InstallType
|
||||
*/
|
||||
|
||||
@@ -322,6 +322,7 @@ class ExtensionCommand {
|
||||
|
||||
// this _should_ be the same as `probablyExtName` as the one derived above unless
|
||||
// install type is local.
|
||||
/** @type {string} */
|
||||
const extName = receipt[/** @type {string} */ (`${this.type}Name`)];
|
||||
|
||||
// check _a second time_ with the more-accurate extName
|
||||
@@ -360,7 +361,7 @@ class ExtensionCommand {
|
||||
|
||||
await this.config.addExtension(extName, extManifest);
|
||||
|
||||
// update the if we've changed the local `package.json`
|
||||
// update the hash if we've changed the local `package.json`
|
||||
if (await env.hasAppiumDependency(this.config.appiumHome)) {
|
||||
await packageDidChange(this.config.appiumHome);
|
||||
}
|
||||
@@ -782,9 +783,7 @@ export {ExtensionCommand};
|
||||
|
||||
/**
|
||||
* Possible return value for {@linkcode ExtensionCommand.list}
|
||||
* @typedef UninstalledExtensionListData
|
||||
* @property {string} pkgName
|
||||
* @property {false} installed
|
||||
* @typedef {Partial<InstalledExtensionListData> & {pkgName: string, installed: false}} UninstalledExtensionListData
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -45,6 +45,7 @@ export const KNOWN_DRIVERS = Object.freeze(
|
||||
espresso: 'appium-espresso-driver',
|
||||
safari: 'appium-safari-driver',
|
||||
gecko: 'appium-geckodriver',
|
||||
chromium: 'appium-chromium-driver',
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -33,13 +33,18 @@ const INSTALL_TYPES = new Set([
|
||||
* @template {ExtensionType} ExtType
|
||||
*/
|
||||
export class ExtensionConfig {
|
||||
/** @type {ExtType} */
|
||||
/**
|
||||
* The type of extension this class is responsible for.
|
||||
* @type {ExtType}
|
||||
*/
|
||||
extensionType;
|
||||
|
||||
/** @type {`${ExtType}s`} */
|
||||
configKey;
|
||||
|
||||
/** @type {ExtRecord<ExtType>} */
|
||||
/**
|
||||
* Manifest data for the extensions of this type.
|
||||
*
|
||||
* This data should _not_ be written to by anything but {@linkcode Manifest}.
|
||||
* @type {Readonly<ExtRecord<ExtType>>}
|
||||
*/
|
||||
installedExtensions;
|
||||
|
||||
/** @type {import('@appium/types').AppiumLogger} */
|
||||
@@ -49,9 +54,9 @@ export class ExtensionConfig {
|
||||
manifest;
|
||||
|
||||
/**
|
||||
* @type {ExtensionListData}
|
||||
* @type {ExtensionListData|undefined}
|
||||
*/
|
||||
_listDataCache;
|
||||
#listDataCache;
|
||||
|
||||
/**
|
||||
* @protected
|
||||
@@ -60,7 +65,6 @@ export class ExtensionConfig {
|
||||
*/
|
||||
constructor(extensionType, manifest) {
|
||||
this.extensionType = extensionType;
|
||||
this.configKey = `${extensionType}s`;
|
||||
this.installedExtensions = manifest.getExtensionData(extensionType);
|
||||
this.manifest = manifest;
|
||||
}
|
||||
@@ -204,8 +208,8 @@ export class ExtensionConfig {
|
||||
if (!_.isEmpty(errorSummaries)) {
|
||||
log.error(
|
||||
`Appium encountered ${util.pluralize('error', errorMap.size, true)} while validating ${
|
||||
this.configKey
|
||||
} found in manifest ${this.manifestPath}`
|
||||
this.extensionType
|
||||
}s found in manifest ${this.manifestPath}`
|
||||
);
|
||||
for (const summary of errorSummaries) {
|
||||
log.error(summary);
|
||||
@@ -219,7 +223,7 @@ export class ExtensionConfig {
|
||||
'warning',
|
||||
warningMap.size,
|
||||
true
|
||||
)} while validating ${this.configKey} found in manifest ${this.manifestPath}`
|
||||
)} while validating ${this.extensionType}s found in manifest ${this.manifestPath}`
|
||||
);
|
||||
for (const summary of warningSummaries) {
|
||||
log.warn(summary);
|
||||
@@ -231,18 +235,20 @@ export class ExtensionConfig {
|
||||
|
||||
/**
|
||||
* Retrieves listing data for extensions via command class.
|
||||
* Caches the result in {@linkcode ExtensionConfig._listDataCache}
|
||||
*
|
||||
* This is an expensive operation, so the result is cached. Currently, there is no
|
||||
* use case for invalidating the cache.
|
||||
* @protected
|
||||
* @returns {Promise<ExtensionListData>}
|
||||
*/
|
||||
async getListData() {
|
||||
if (this._listDataCache) {
|
||||
return this._listDataCache;
|
||||
if (this.#listDataCache) {
|
||||
return this.#listDataCache;
|
||||
}
|
||||
const CommandClass = /** @type {ExtCommand<ExtType>} */ (commandClasses[this.extensionType]);
|
||||
const cmd = new CommandClass({config: this, json: true});
|
||||
const listData = await cmd.list({showInstalled: true, showUpdates: true});
|
||||
this._listDataCache = listData;
|
||||
this.#listDataCache = listData;
|
||||
return listData;
|
||||
}
|
||||
|
||||
@@ -433,7 +439,7 @@ export class ExtensionConfig {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async addExtension(extName, extManifest, {write = true} = {}) {
|
||||
this.manifest.addExtension(this.extensionType, extName, extManifest);
|
||||
this.manifest.setExtension(this.extensionType, extName, extManifest);
|
||||
if (write) {
|
||||
await this.manifest.write();
|
||||
}
|
||||
@@ -446,10 +452,10 @@ export class ExtensionConfig {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async updateExtension(extName, extManifest, {write = true} = {}) {
|
||||
this.installedExtensions[extName] = {
|
||||
this.manifest.setExtension(this.extensionType, extName, {
|
||||
...this.installedExtensions[extName],
|
||||
...extManifest,
|
||||
};
|
||||
});
|
||||
if (write) {
|
||||
await this.manifest.write();
|
||||
}
|
||||
@@ -463,7 +469,7 @@ export class ExtensionConfig {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async removeExtension(extName, {write = true} = {}) {
|
||||
delete this.installedExtensions[extName];
|
||||
this.manifest.deleteExtension(this.extensionType, extName);
|
||||
if (write) {
|
||||
await this.manifest.write();
|
||||
}
|
||||
@@ -477,13 +483,13 @@ export class ExtensionConfig {
|
||||
print(activeNames) {
|
||||
if (_.isEmpty(this.installedExtensions)) {
|
||||
log.info(
|
||||
`No ${this.configKey} have been installed in ${this.appiumHome}. Use the "appium ${this.extensionType}" ` +
|
||||
`No ${this.extensionType}s have been installed in ${this.appiumHome}. Use the "appium ${this.extensionType}" ` +
|
||||
'command to install the one(s) you want to use.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(`Available ${this.configKey}:`);
|
||||
log.info(`Available ${this.extensionType}s:`);
|
||||
for (const [extName, extManifest] of /** @type {[string, ExtManifest<ExtType>][]} */ (
|
||||
_.toPairs(this.installedExtensions)
|
||||
)) {
|
||||
@@ -552,7 +558,7 @@ export class ExtensionConfig {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isInstalled(extName) {
|
||||
return _.includes(Object.keys(this.installedExtensions), extName);
|
||||
return extName in this.installedExtensions;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {DRIVER_TYPE, PLUGIN_TYPE} from '../constants';
|
||||
import log from '../logger';
|
||||
|
||||
/**
|
||||
@@ -12,7 +13,13 @@ import log from '../logger';
|
||||
const SCHEMA_REV_3 = 3;
|
||||
|
||||
/**
|
||||
* Collection of functions to migrate from one version to another
|
||||
* Collection of functions to migrate from one version to another.
|
||||
*
|
||||
* These functions _should not actually perform the migration_; rather, they
|
||||
* should return `true` if the migration should be performed. The migration
|
||||
* itself will happen within {@linkcode Manifest.syncWithInstalledExtensions}; the extensions
|
||||
* will be checked and the manifest file updated per the state of the filesystem.
|
||||
*
|
||||
* @type {{[P in keyof ManifestDataVersions]?: Migration}}
|
||||
*/
|
||||
const Migrations = {
|
||||
@@ -24,9 +31,13 @@ const Migrations = {
|
||||
*
|
||||
* @type {Migration}
|
||||
*/
|
||||
[SCHEMA_REV_3]: (data) => {
|
||||
[SCHEMA_REV_3]: (manifest) => {
|
||||
let shouldSync = false;
|
||||
const allExtData = [...Object.values(data.drivers), ...Object.values(data.plugins)];
|
||||
/** @type {Array<ExtManifest<PluginType>|ExtManifest<DriverType>>} */
|
||||
const allExtData = [
|
||||
...Object.values(manifest.getExtensionData(DRIVER_TYPE)),
|
||||
...Object.values(manifest.getExtensionData(PLUGIN_TYPE)),
|
||||
];
|
||||
for (const metadata of allExtData) {
|
||||
if (!('installPath' in metadata)) {
|
||||
shouldSync = true;
|
||||
@@ -38,14 +49,20 @@ const Migrations = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Set `schemaRev` to a specific version
|
||||
* @param {AnyManifestDataVersion} data
|
||||
* Set `schemaRev` to a specific version.
|
||||
*
|
||||
* This _does_ mutate `data` in-place, unlike functions in
|
||||
* {@linkcode Migrations}.
|
||||
*
|
||||
* Again, returning `true` means that the manifest should be--at
|
||||
* minimum--persisted to disk, since we changed it.
|
||||
* @param {Readonly<Manifest>} manifest
|
||||
* @param {keyof ManifestDataVersions} version
|
||||
* @returns {boolean} Whether the data was modified
|
||||
*/
|
||||
function setSchemaRev(data, version) {
|
||||
if (data.schemaRev ?? 0 < version) {
|
||||
data.schemaRev = version;
|
||||
function setSchemaRev(manifest, version) {
|
||||
if (manifest.schemaRev ?? 0 < version) {
|
||||
manifest.setSchemaRev(version);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -56,44 +73,48 @@ function setSchemaRev(data, version) {
|
||||
*
|
||||
* `data` is modified in-place.
|
||||
*
|
||||
* @param {Manifest} manifest
|
||||
* @param {AnyManifestDataVersion} data
|
||||
* @param {Readonly<Manifest>} manifest
|
||||
* @returns {Promise<boolean>} If `true` existing packages should be synced from disk and the manifest should be persisted.
|
||||
*/
|
||||
export async function migrate(manifest, data) {
|
||||
export async function migrate(manifest) {
|
||||
let didChange = false;
|
||||
for await (const [v, migration] of Object.entries(Migrations)) {
|
||||
const version = /** @type {keyof ManifestDataVersions} */ (Number(v));
|
||||
didChange = (await migration(data, manifest)) || didChange;
|
||||
didChange = setSchemaRev(data, version) || didChange;
|
||||
didChange = (await migration(manifest)) || didChange;
|
||||
didChange = setSchemaRev(manifest, version) || didChange;
|
||||
}
|
||||
|
||||
if (didChange) {
|
||||
// this is not _technically_ true, since we don't actually write the file here.
|
||||
log.info(`Upgraded extension manifest to schema v${data.schemaRev}`);
|
||||
log.info(`Upgraded extension manifest to schema v${manifest.schemaRev}`);
|
||||
}
|
||||
|
||||
return didChange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration functions take a {@linkcode Manifest} and its data and **mutates `data` in-place**.
|
||||
* A migration function. It will return `true` if a change _should be made_.
|
||||
*
|
||||
* A migration function should not modify `schemaRev`, as this is done automatically.
|
||||
*
|
||||
* A migration function can also call out to, say, {@linkcode Manifest.syncWithInstalledExtensions}, which
|
||||
* may apply the mutation itself.
|
||||
*
|
||||
* Note that at the time of writing, we're not able to determine the version of the _current_ manifest file if there is no `schemaRev` prop present (and we may not want to trust it anyway).
|
||||
* @callback Migration
|
||||
* @param {AnyManifestDataVersion} data - The raw data from `Manifest`
|
||||
* @param {Manifest} manifest - The `Manifest` instance
|
||||
* @param {Readonly<Manifest>} manifest - The `Manifest` instance
|
||||
* @returns {boolean|Promise<boolean>} If `true`, then something changed
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('appium/types').ManifestData} ManifestData
|
||||
* @typedef {import('@appium/types').DriverType} DriverType
|
||||
* @typedef {import('@appium/types').PluginType} PluginType
|
||||
* @typedef {import('appium/types').AnyManifestDataVersion} AnyManifestDataVersion
|
||||
* @typedef {import('appium/types').ManifestDataVersions} ManifestDataVersions
|
||||
* @typedef {import('@appium/types').ExtensionType} ExtensionType
|
||||
* @typedef {import('appium/types').ManifestV2.ManifestData} ManifestDataV2
|
||||
* @typedef {import('./manifest').Manifest} Manifest
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {ExtensionType} ExtType
|
||||
* @typedef {import('appium/types').ExtManifest<ExtType>} ExtManifest
|
||||
*/
|
||||
|
||||
@@ -38,6 +38,8 @@ const INITIAL_MANIFEST_DATA = Object.freeze({
|
||||
/**
|
||||
* Given a `package.json` return `true` if it represents an Appium Extension (either a driver or plugin).
|
||||
*
|
||||
* _This is a type guard; not a validator._
|
||||
*
|
||||
* The `package.json` must have an `appium` property which is an object.
|
||||
* @param {any} value
|
||||
* @returns {value is ExtPackageJson<ExtensionType>}
|
||||
@@ -50,32 +52,35 @@ function isExtension(value) {
|
||||
_.isString(value.version)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a `package.json`, return `true` if it represents an Appium Driver.
|
||||
*
|
||||
* To be considered a driver, a `package.json` must have a fields
|
||||
* `appium.driverName`, `appium.automationName` and `appium.platformNames`.
|
||||
* _This is a type guard; not a validator._
|
||||
*
|
||||
* To be considered a driver, a `package.json` must have an `appium.driverName` field.
|
||||
*
|
||||
* Further validation of the `appium` property happens elsewhere.
|
||||
* @param {any} value - Value to test
|
||||
* @returns {value is ExtPackageJson<DriverType>}
|
||||
*/
|
||||
function isDriver(value) {
|
||||
return (
|
||||
isExtension(value) &&
|
||||
_.isString(_.get(value, 'appium.driverName')) &&
|
||||
_.isString(_.get(value, 'appium.automationName')) &&
|
||||
_.isArray(_.get(value, 'appium.platformNames'))
|
||||
);
|
||||
return isExtension(value) && 'driverName' in value.appium && _.isString(value.appium.driverName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a `package.json`, return `true` if it represents an Appium Plugin.
|
||||
*
|
||||
* _This is a type guard; not a validator._
|
||||
*
|
||||
* To be considered a plugin, a `package.json` must have an `appium.pluginName` field.
|
||||
*
|
||||
* Further validation of the `appium` property happens elsewhere.
|
||||
* @param {any} value - Value to test
|
||||
* @returns {value is ExtPackageJson<PluginType>}
|
||||
*/
|
||||
function isPlugin(value) {
|
||||
return isExtension(value) && _.isString(_.get(value, 'appium.pluginName'));
|
||||
return isExtension(value) && 'pluginName' in value.appium && _.isString(value.appium.pluginName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,7 +171,23 @@ export class Manifest {
|
||||
const onMatch = async (filepath) => {
|
||||
try {
|
||||
const pkg = JSON.parse(await fs.readFile(filepath, 'utf8'));
|
||||
if (isDriver(pkg) || isPlugin(pkg)) {
|
||||
if (isExtension(pkg)) {
|
||||
const extType = isDriver(pkg) ? DRIVER_TYPE : PLUGIN_TYPE;
|
||||
/**
|
||||
* this should only be 'unknown' if the extension's `package.json` is invalid
|
||||
* @type {string}
|
||||
*/
|
||||
const name = isDriver(pkg)
|
||||
? pkg.appium.driverName
|
||||
: isPlugin(pkg)
|
||||
? pkg.appium.pluginName
|
||||
: '(unknown)';
|
||||
if (
|
||||
(isDriver(pkg) && !this.hasDriver(name)) ||
|
||||
(isPlugin(pkg) && !this.hasPlugin(name))
|
||||
) {
|
||||
log.info(`Discovered installed ${extType} "${name}"`);
|
||||
}
|
||||
const changed = this.addExtensionFromPackage(pkg, filepath);
|
||||
didChange = didChange || changed;
|
||||
}
|
||||
@@ -228,11 +249,10 @@ export class Manifest {
|
||||
/**
|
||||
* Given a path to a `package.json`, add it as either a driver or plugin to the manifest.
|
||||
*
|
||||
* Will _not_ overwrite existing entries.
|
||||
* @template {ExtensionType} ExtType
|
||||
* @param {ExtPackageJson<ExtType>} pkgJson
|
||||
* @param {string} pkgPath
|
||||
* @returns {boolean} - `true` upon success, `false` if the extension is already registered.
|
||||
* @returns {boolean} - `true` if this method did anything.
|
||||
*/
|
||||
addExtensionFromPackage(pkgJson, pkgPath) {
|
||||
const extensionPath = path.dirname(pkgPath);
|
||||
@@ -250,26 +270,28 @@ export class Manifest {
|
||||
};
|
||||
|
||||
if (isDriver(pkgJson)) {
|
||||
if (!this.hasDriver(pkgJson.appium.driverName)) {
|
||||
this.addExtension(DRIVER_TYPE, pkgJson.appium.driverName, {
|
||||
..._.omit(pkgJson.appium, 'driverName'),
|
||||
...internal,
|
||||
});
|
||||
const value = {
|
||||
..._.omit(pkgJson.appium, 'driverName'),
|
||||
...internal,
|
||||
};
|
||||
if (!_.isEqual(value, this.#data.drivers[pkgJson.appium.driverName])) {
|
||||
this.setExtension(DRIVER_TYPE, pkgJson.appium.driverName, value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} else if (isPlugin(pkgJson)) {
|
||||
if (!this.hasPlugin(pkgJson.appium.pluginName)) {
|
||||
this.addExtension(PLUGIN_TYPE, pkgJson.appium.pluginName, {
|
||||
..._.omit(pkgJson.appium, 'pluginName'),
|
||||
...internal,
|
||||
});
|
||||
const value = {
|
||||
..._.omit(pkgJson.appium, 'pluginName'),
|
||||
...internal,
|
||||
};
|
||||
if (!_.isEqual(value, this.#data.plugins[pkgJson.appium.pluginName])) {
|
||||
this.setExtension(PLUGIN_TYPE, pkgJson.appium.pluginName, value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
throw new TypeError(
|
||||
`The extension in ${extensionPath} is neither a valid driver nor a valid plugin.`
|
||||
`The extension in ${extensionPath} is neither a valid ${DRIVER_TYPE} nor a valid ${PLUGIN_TYPE}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -285,12 +307,29 @@ export class Manifest {
|
||||
* @param {ExtManifest<ExtType>} extData - Extension metadata
|
||||
* @returns {ExtManifest<ExtType>} A clone of `extData`, potentially with a mutated `appiumVersion` field
|
||||
*/
|
||||
addExtension(extType, extName, extData) {
|
||||
const data = _.clone(extData);
|
||||
setExtension(extType, extName, extData) {
|
||||
const data = _.cloneDeep(extData);
|
||||
this.#data[`${extType}s`][extName] = data;
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the schema revision
|
||||
* @param {keyof import('./manifest-migrations').ManifestDataVersions} rev
|
||||
*/
|
||||
setSchemaRev(rev) {
|
||||
this.#data.schemaRev = rev;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an extension from the manifest.
|
||||
* @param {ExtensionType} extType
|
||||
* @param {string} extName
|
||||
*/
|
||||
deleteExtension(extType, extName) {
|
||||
delete this.#data[`${extType}s`][extName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the `APPIUM_HOME` path
|
||||
*/
|
||||
@@ -305,12 +344,19 @@ export class Manifest {
|
||||
return this.#manifestPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the schema rev of this manifest
|
||||
*/
|
||||
get schemaRev() {
|
||||
return this.#data.schemaRev;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns extension data for a particular type.
|
||||
*
|
||||
* @template {ExtensionType} ExtType
|
||||
* @param {ExtType} extType
|
||||
* @returns {ExtRecord<ExtType>}
|
||||
* @returns {Readonly<ExtRecord<ExtType>>}
|
||||
*/
|
||||
getExtensionData(extType) {
|
||||
return this.#data[/** @type {string} */ (`${extType}s`)];
|
||||
@@ -319,11 +365,18 @@ export class Manifest {
|
||||
/**
|
||||
* Reads manifest from disk and _overwrites_ the internal data.
|
||||
*
|
||||
* If the manifest does not exist on disk, an {@link INITIAL_MANIFEST_DATA "empty"} manifest file will be created.
|
||||
* If the manifest does not exist on disk, an
|
||||
* {@link INITIAL_MANIFEST_DATA "empty"} manifest file will be created, as
|
||||
* well as its directory if needed.
|
||||
*
|
||||
* If `APPIUM_HOME` contains a `package.json` with an `appium` dependency, then a hash of the `package.json` will be taken. If this hash differs from the last hash, the contents of `APPIUM_HOME/node_modules` will be scanned for extensions that may have been installed outside of the `appium` CLI. Any found extensions will be added to the manifest file, and if so, the manifest file will be written to disk.
|
||||
* This will also, if necessary:
|
||||
* 1. perform a migration of the manifest data
|
||||
* 2. sync the manifest with extensions on-disk (kind of like "auto
|
||||
* discovery")
|
||||
* 3. write the manifest to disk.
|
||||
*
|
||||
* Only one read operation can happen at a time.
|
||||
*
|
||||
* Only one read operation should happen at a time.
|
||||
* @returns {Promise<ManifestData>} The data
|
||||
*/
|
||||
async read() {
|
||||
@@ -335,17 +388,23 @@ export class Manifest {
|
||||
this.#reading = (async () => {
|
||||
/** @type {ManifestData} */
|
||||
let data;
|
||||
let isNewFile = false;
|
||||
/**
|
||||
* This will be `true` if, after reading, we need to update the manifest data
|
||||
* and write it again to disk.
|
||||
*/
|
||||
let shouldWrite = false;
|
||||
await this.#setManifestPath();
|
||||
try {
|
||||
log.debug(`Reading ${this.#manifestPath}...`);
|
||||
const yaml = await fs.readFile(this.#manifestPath, 'utf8');
|
||||
data = YAML.parse(yaml);
|
||||
log.debug(`Parsed manifest file: ${JSON.stringify(data, null, 2)}`);
|
||||
log.debug(
|
||||
`Parsed manifest file at ${this.#manifestPath}: ${JSON.stringify(data, null, 2)}`
|
||||
);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
log.debug(`No manifest file found at ${this.#manifestPath}; creating`);
|
||||
data = _.cloneDeep(INITIAL_MANIFEST_DATA);
|
||||
isNewFile = true;
|
||||
shouldWrite = true;
|
||||
} else {
|
||||
if (this.#manifestPath) {
|
||||
throw new Error(
|
||||
@@ -364,27 +423,42 @@ export class Manifest {
|
||||
|
||||
this.#data = data;
|
||||
|
||||
let shouldWrite = false;
|
||||
|
||||
if ((data.schemaRev ?? 0) < CURRENT_SCHEMA_REV) {
|
||||
/**
|
||||
* the only way `shouldWrite` is `true` is if we have a new file. a new
|
||||
* file will get the latest schema revision, so we can skip the migration.
|
||||
*/
|
||||
if (!shouldWrite && (data.schemaRev ?? 0) < CURRENT_SCHEMA_REV) {
|
||||
log.debug(
|
||||
`Updating manifest schema from rev ${data.schemaRev ?? '(none)'} to ${CURRENT_SCHEMA_REV}`
|
||||
);
|
||||
shouldWrite = await migrate(this, this.#data);
|
||||
shouldWrite = await migrate(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* we still may want to sync with installed extensions even if we have a
|
||||
* new file. right now this is limited to the following cases:
|
||||
* 1. we have a brand new manifest file
|
||||
* 2. we have performed a migration on a manifest file
|
||||
* 3. `appium` is a dependency within `package.json`, and `package.json`
|
||||
* has changed since last time we checked.
|
||||
*
|
||||
* It may also make sense to sync with the extensions in an arbitrary
|
||||
* `APPIUM_HOME`, but we don't do that here.
|
||||
*/
|
||||
if (
|
||||
shouldWrite ||
|
||||
((await env.hasAppiumDependency(this.appiumHome)) &&
|
||||
(await packageDidChange(this.appiumHome)))
|
||||
) {
|
||||
shouldWrite = await this.syncWithInstalledExtensions();
|
||||
log.debug('Discovering newly installed extensions...');
|
||||
shouldWrite = (await this.syncWithInstalledExtensions()) || shouldWrite;
|
||||
}
|
||||
|
||||
if (isNewFile || shouldWrite) {
|
||||
if (shouldWrite) {
|
||||
await this.write();
|
||||
}
|
||||
})();
|
||||
|
||||
try {
|
||||
await this.#reading;
|
||||
return this.#data;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "appium",
|
||||
"version": "2.0.0-beta.47",
|
||||
"version": "2.0.0-beta.48",
|
||||
"description": "Automation for Apps.",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -52,17 +52,17 @@
|
||||
"postinstall": "node ./scripts/autoinstall-extensions.js",
|
||||
"publish:docs": "cross-env APPIUM_DOCS_PUBLISH=1 npm run build:docs",
|
||||
"test": "npm run test:unit",
|
||||
"test:e2e": "mocha --timeout 1m --slow 30s \"./test/e2e/**/*.spec.js\"",
|
||||
"test:e2e": "mocha -p --timeout 1m --slow 30s \"./test/e2e/**/*.spec.js\"",
|
||||
"test:smoke": "cross-env APPIUM_HOME=./local_appium_home node ./index.js driver install uiautomator2 && cross-env APPIUM_HOME=./local_appium_home node ./index.js driver list",
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/base-driver": "^9.0.0",
|
||||
"@appium/base-plugin": "^2.0.0",
|
||||
"@appium/docutils": "^0.1.0",
|
||||
"@appium/base-driver": "^9.1.0",
|
||||
"@appium/base-plugin": "^2.0.1",
|
||||
"@appium/docutils": "^0.1.1",
|
||||
"@appium/schema": "^0.1.0",
|
||||
"@appium/support": "^3.0.0",
|
||||
"@appium/types": "^0.6.0",
|
||||
"@appium/support": "^3.0.1",
|
||||
"@appium/types": "^0.7.0",
|
||||
"@sidvind/better-ajv-errors": "2.1.0",
|
||||
"@types/argparse": "2.0.10",
|
||||
"@types/bluebird": "3.5.38",
|
||||
@@ -90,7 +90,7 @@
|
||||
"semver": "7.3.8",
|
||||
"source-map-support": "0.5.21",
|
||||
"teen_process": "2.0.2",
|
||||
"type-fest": "3.3.0",
|
||||
"type-fest": "3.4.0",
|
||||
"winston": "3.8.2",
|
||||
"wrap-ansi": "7.0.0",
|
||||
"yaml": "2.1.3"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"webdriverio": "7.27.0"
|
||||
"webdriverio": "7.28.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import getPort from 'get-port';
|
||||
import {tempDir, fs} from '@appium/support';
|
||||
import {exec} from 'teen_process';
|
||||
import B from 'bluebird';
|
||||
import {
|
||||
APPIUM_ROOT,
|
||||
readAppiumArgErrorFixture,
|
||||
formatAppiumArgErrorOutput,
|
||||
EXECUTABLE,
|
||||
runAppiumRaw,
|
||||
} from './e2e-helpers';
|
||||
|
||||
const {expect} = chai;
|
||||
|
||||
describe('argument parsing', function () {
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
let appiumHome;
|
||||
|
||||
before(async function () {
|
||||
appiumHome = await tempDir.openDir();
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await fs.rimraf(appiumHome);
|
||||
});
|
||||
|
||||
describe('when the user provides a very long string for an arg accepting a blob or filename', function () {
|
||||
describe('when the very long string is a JSON blob', function () {
|
||||
it('should not throw an ENAMETOOLONG exception', async function () {
|
||||
this.timeout(10000);
|
||||
const capsArg = JSON.stringify({
|
||||
'appium:platformName': 'ANDROID',
|
||||
'appium:platformVersion': '11',
|
||||
'appium:deviceName':
|
||||
'Spicy jalapeno bacon ipsum dolor amet deserunt tempor pork belly aliqua drumstick, occaecat dolor venison et labore. Rump meatball pork chop tail. Consequat adipisicing kielbasa occaecat laborum pig. Qui pork chop chicken nostrud boudin fugiat. Proident ut culpa, chuck nulla sunt pastrami ut tri-tip. Buffalo dolore adipisicing, labore venison elit beef fatback kevin burgdoggen tail pancetta filet mignon. Dolor turducken rump, anim kevin sunt exercitation ham filet mignon beef ribs ad officia eiusmod id cillum.',
|
||||
});
|
||||
|
||||
await expect(
|
||||
exec(
|
||||
process.execPath,
|
||||
[
|
||||
EXECUTABLE,
|
||||
'-pa=/wd/hub',
|
||||
'--session-override',
|
||||
'--local-timezone',
|
||||
'--relaxed-security',
|
||||
`--default-capabilities=${capsArg}`,
|
||||
'--port',
|
||||
String(await getPort()),
|
||||
],
|
||||
{
|
||||
env: {APPIUM_HOME: appiumHome, PATH: process.env.PATH},
|
||||
cwd: APPIUM_ROOT,
|
||||
timeout: 5000,
|
||||
}
|
||||
)
|
||||
).to.be.rejectedWith(Error, /timed out/);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('when the user provides an string where a number was expected', function () {
|
||||
describe('when color output is supported', function () {
|
||||
it('should output a fancy error message', async function () {
|
||||
const [{stderr: actual}, expected] = await B.all([
|
||||
runAppiumRaw(appiumHome, ['--port=sheep'], {
|
||||
env: {FORCE_COLOR: '1'},
|
||||
}),
|
||||
readAppiumArgErrorFixture('cli/cli-error-output-color.txt'),
|
||||
]);
|
||||
expect(formatAppiumArgErrorOutput(actual)).to.equal(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when color output is unsupported', function () {
|
||||
it('should output a colorless yet fancy error message', async function () {
|
||||
const [{stderr: actual}, expected] = await B.all([
|
||||
runAppiumRaw(appiumHome, ['--port=sheep'], {}),
|
||||
readAppiumArgErrorFixture('cli/cli-error-output.txt'),
|
||||
]);
|
||||
expect(formatAppiumArgErrorOutput(actual)).to.equal(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user provides a value for a boolean argument', function () {
|
||||
it('should output a basic error message', async function () {
|
||||
const [{stderr: actual}, expected] = await B.all([
|
||||
runAppiumRaw(appiumHome, ['--relaxed-security=sheep'], {}),
|
||||
readAppiumArgErrorFixture('cli/cli-error-output-boolean.txt'),
|
||||
]);
|
||||
expect(formatAppiumArgErrorOutput(actual)).to.equal(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user provides an unknown argument', function () {
|
||||
it('should output a basic error message', async function () {
|
||||
const [{stderr: actual}, expected] = await B.all([
|
||||
runAppiumRaw(appiumHome, ['--pigs=sheep'], {}),
|
||||
readAppiumArgErrorFixture('cli/cli-error-output-unknown.txt'),
|
||||
]);
|
||||
expect(formatAppiumArgErrorOutput(actual)).to.equal(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,421 @@
|
||||
// @ts-check
|
||||
|
||||
import {exec} from 'teen_process';
|
||||
import {fs, tempDir, util} from '@appium/support';
|
||||
import path from 'path';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import {
|
||||
DRIVER_TYPE,
|
||||
EXT_SUBCOMMAND_INSTALL as INSTALL,
|
||||
EXT_SUBCOMMAND_LIST as LIST,
|
||||
EXT_SUBCOMMAND_RUN as RUN,
|
||||
EXT_SUBCOMMAND_UNINSTALL as UNINSTALL,
|
||||
KNOWN_DRIVERS,
|
||||
} from '../../lib/constants';
|
||||
import {FAKE_DRIVER_DIR, resolveFixture} from '../helpers';
|
||||
import {installLocalExtension, runAppiumJson, runAppiumRaw} from './e2e-helpers';
|
||||
|
||||
const {expect} = chai;
|
||||
|
||||
const TEST_DRIVER_DIR = path.dirname(resolveFixture('test-driver/package.json'));
|
||||
|
||||
const TEST_DRIVER_INVALID_PEERS_DIR = path.dirname(
|
||||
resolveFixture('test-driver-invalid-peer-dep/package.json')
|
||||
);
|
||||
|
||||
describe('Driver CLI', function () {
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
let appiumHome;
|
||||
|
||||
/**
|
||||
* @type {(args?: string[]) => Promise<ExtensionListData>}
|
||||
*/
|
||||
let runList;
|
||||
|
||||
/**
|
||||
* @type {(args: string[]) => Promise<{ output: string, error?: string }>}
|
||||
*/
|
||||
let runRun;
|
||||
|
||||
/**
|
||||
* @type {(args: string[]) => Promise<ExtRecord<DriverType>>}
|
||||
*/
|
||||
let runInstall;
|
||||
|
||||
/**
|
||||
* @type {(args: string[]) => Promise<ExtRecord<DriverType>>}
|
||||
*/
|
||||
let runUninstall;
|
||||
|
||||
async function resetAppiumHome() {
|
||||
await fs.rimraf(appiumHome);
|
||||
await fs.mkdirp(appiumHome);
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
appiumHome = await tempDir.openDir();
|
||||
const run = runAppiumJson(appiumHome);
|
||||
runInstall = async (args) =>
|
||||
/** @type {ReturnType<typeof runInstall>} */ (await run([DRIVER_TYPE, INSTALL, ...args]));
|
||||
runUninstall = async (args) =>
|
||||
/** @type {ReturnType<typeof runUninstall>} */ (await run([DRIVER_TYPE, UNINSTALL, ...args]));
|
||||
runList = async (args = []) =>
|
||||
/** @type {ReturnType<typeof runList>} */ (await run([DRIVER_TYPE, LIST, ...args]));
|
||||
runRun = async (args) =>
|
||||
/** @type {ReturnType<typeof runRun>} */ (await run([DRIVER_TYPE, RUN, ...args]));
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await fs.rimraf(appiumHome);
|
||||
});
|
||||
|
||||
describe(LIST, function () {
|
||||
it('should list available drivers', async function () {
|
||||
// note this is raw, not json
|
||||
const {stderr} = await runAppiumRaw(appiumHome, [DRIVER_TYPE, LIST], {});
|
||||
for (const d of Object.keys(KNOWN_DRIVERS)) {
|
||||
stderr.should.match(new RegExp(`${d}.+[not installed]`));
|
||||
}
|
||||
});
|
||||
|
||||
it('should list available drivers in json format', async function () {
|
||||
const driverData = await runList();
|
||||
for (const d of Object.keys(KNOWN_DRIVERS)) {
|
||||
driverData[d].should.eql({
|
||||
installed: false,
|
||||
pkgName: KNOWN_DRIVERS[d],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should allow filtering by installed drivers', async function () {
|
||||
const out = await runList(['--installed']);
|
||||
out.should.eql({});
|
||||
});
|
||||
|
||||
it('should show updates for installed drivers with --updates', async function () {
|
||||
const versions = /** @type {string[]} */ (
|
||||
JSON.parse(
|
||||
(
|
||||
await exec('npm', ['view', '@appium/fake-driver', 'versions', '--json'], {
|
||||
encoding: 'utf-8',
|
||||
})
|
||||
).stdout
|
||||
)
|
||||
);
|
||||
|
||||
const penultimateFakeDriverVersionAsOfRightNow = versions[versions.length - 2];
|
||||
|
||||
await resetAppiumHome();
|
||||
await runInstall([
|
||||
`@appium/fake-driver@${penultimateFakeDriverVersionAsOfRightNow}`,
|
||||
'--source',
|
||||
'npm',
|
||||
]);
|
||||
const {fake} =
|
||||
/** @type {Record<string,import('appium/lib/cli/extension-command').InstalledExtensionListData>} */ (
|
||||
await runList(['--updates'])
|
||||
);
|
||||
const updateVersion = String(fake.updateVersion || fake.unsafeUpdateVersion);
|
||||
util.compareVersions(
|
||||
String(updateVersion),
|
||||
'>',
|
||||
penultimateFakeDriverVersionAsOfRightNow
|
||||
).should.be.true;
|
||||
// TODO: this could probably be replaced by looking at updateVersion in the JSON
|
||||
const {stderr} = await runAppiumRaw(appiumHome, [DRIVER_TYPE, LIST, '--updates'], {});
|
||||
stderr.should.match(new RegExp(`fake.+[${updateVersion} available]`));
|
||||
});
|
||||
|
||||
describe('if a driver is not published to npm', function () {
|
||||
it('should not throw an error', async function () {
|
||||
await resetAppiumHome();
|
||||
await installLocalExtension(appiumHome, DRIVER_TYPE, TEST_DRIVER_DIR);
|
||||
await expect(runList(['--updates'])).not.to.be.rejected;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(INSTALL, function () {
|
||||
beforeEach(async function () {
|
||||
await resetAppiumHome();
|
||||
});
|
||||
|
||||
it('should not install appium in APPIUM_HOME', async function () {
|
||||
await installLocalExtension(appiumHome, DRIVER_TYPE, FAKE_DRIVER_DIR);
|
||||
await fs.stat(path.join(appiumHome, 'node_modules', 'appium')).should.be.rejected;
|
||||
});
|
||||
|
||||
it('should install a driver from the list of known drivers', async function () {
|
||||
const ret = await runInstall(['uiautomator2']);
|
||||
ret.uiautomator2.pkgName.should.eql('appium-uiautomator2-driver');
|
||||
ret.uiautomator2.installType.should.eql('npm');
|
||||
ret.uiautomator2.installSpec.should.eql('uiautomator2');
|
||||
const list = await runList(['--installed']);
|
||||
// @ts-ignore
|
||||
delete list.uiautomator2.installed;
|
||||
list.should.eql(ret);
|
||||
});
|
||||
|
||||
it('should install a driver from npm', async function () {
|
||||
const ret = await runInstall(['@appium/fake-driver', '--source', 'npm']);
|
||||
ret.fake.pkgName.should.eql('@appium/fake-driver');
|
||||
ret.fake.installType.should.eql('npm');
|
||||
ret.fake.installSpec.should.eql('@appium/fake-driver');
|
||||
const list = await runList(['--installed']);
|
||||
// @ts-ignore
|
||||
delete list.fake.installed;
|
||||
list.should.eql(ret);
|
||||
});
|
||||
|
||||
it('should install a driver from npm and a local driver', async function () {
|
||||
await runInstall(['@appium/fake-driver', '--source', 'npm']);
|
||||
await installLocalExtension(appiumHome, DRIVER_TYPE, TEST_DRIVER_DIR);
|
||||
const list = await runList(['--installed']);
|
||||
expect(list.fake).to.exist;
|
||||
expect(list.test).to.exist;
|
||||
expect(() => resolveFrom(appiumHome, '@appium/fake-driver')).not.to.throw;
|
||||
expect(() => resolveFrom(appiumHome, 'test-driver')).not.to.throw;
|
||||
});
|
||||
|
||||
it('should install _two_ drivers from npm', async function () {
|
||||
await runInstall(['@appium/fake-driver', '--source', 'npm']);
|
||||
await runInstall(['appium-uiautomator2-driver', '--source', 'npm']);
|
||||
const list = await runList(['--installed']);
|
||||
expect(list.fake).to.exist;
|
||||
expect(list.uiautomator2).to.exist;
|
||||
expect(() => resolveFrom(appiumHome, '@appium/fake-driver')).not.to.throw;
|
||||
expect(() => resolveFrom(appiumHome, 'appium-uiautomator2-driver')).not.to.throw;
|
||||
});
|
||||
|
||||
it('should install a driver from npm with a specific version/tag', async function () {
|
||||
const currentFakeDriverVersionAsOfRightNow = '3.0.5';
|
||||
const installSpec = `@appium/fake-driver@${currentFakeDriverVersionAsOfRightNow}`;
|
||||
const ret = await runInstall([installSpec, '--source', 'npm']);
|
||||
ret.fake.pkgName.should.eql('@appium/fake-driver');
|
||||
ret.fake.installType.should.eql('npm');
|
||||
ret.fake.installSpec.should.eql(installSpec);
|
||||
const list = await runList(['--installed']);
|
||||
// @ts-ignore
|
||||
delete list.fake.installed;
|
||||
list.should.eql(ret);
|
||||
});
|
||||
|
||||
it('should install a driver from GitHub', async function () {
|
||||
const ret = await runInstall([
|
||||
'appium/appium-fake-driver',
|
||||
'--source',
|
||||
'github',
|
||||
'--package',
|
||||
'appium-fake-driver',
|
||||
]);
|
||||
ret.fake.pkgName.should.eql('appium-fake-driver');
|
||||
ret.fake.installType.should.eql('github');
|
||||
ret.fake.installSpec.should.eql('appium/appium-fake-driver');
|
||||
const list = await runList(['--installed']);
|
||||
// @ts-ignore
|
||||
delete list.fake.installed;
|
||||
list.should.eql(ret);
|
||||
});
|
||||
|
||||
it('should install a driver from a local git repo', async function () {
|
||||
const ret = await runInstall([
|
||||
FAKE_DRIVER_DIR,
|
||||
'--source',
|
||||
'git',
|
||||
'--package',
|
||||
'@appium/fake-driver',
|
||||
]);
|
||||
ret.fake.pkgName.should.eql('@appium/fake-driver');
|
||||
ret.fake.installType.should.eql('git');
|
||||
ret.fake.installSpec.should.eql(FAKE_DRIVER_DIR);
|
||||
const list = await runList(['--installed', '--json']);
|
||||
// @ts-ignore
|
||||
delete list.fake.installed;
|
||||
list.should.eql(ret);
|
||||
});
|
||||
|
||||
it('should install a driver from a remote git repo', async function () {
|
||||
const ret = await runInstall([
|
||||
'git+https://github.com/appium/appium-fake-driver.git',
|
||||
'--source',
|
||||
'git',
|
||||
'--package',
|
||||
'appium-fake-driver',
|
||||
]);
|
||||
ret.fake.pkgName.should.eql('appium-fake-driver');
|
||||
ret.fake.installType.should.eql('git');
|
||||
ret.fake.installSpec.should.eql('git+https://github.com/appium/appium-fake-driver');
|
||||
const list = await runList(['--installed']);
|
||||
// @ts-ignore
|
||||
delete list.fake.installed;
|
||||
list.should.eql(ret);
|
||||
});
|
||||
|
||||
describe('when peer dependencies are invalid', function () {
|
||||
it('should install the driver anyway', async function () {
|
||||
const ret = await installLocalExtension(
|
||||
appiumHome,
|
||||
DRIVER_TYPE,
|
||||
TEST_DRIVER_INVALID_PEERS_DIR
|
||||
);
|
||||
ret.test.pkgName.should.equal('test-driver-invalid-peer-dep');
|
||||
const list = await runList(['--installed']);
|
||||
list.test.pkgName.should.equal('test-driver-invalid-peer-dep');
|
||||
});
|
||||
|
||||
it('should warn the user that peer deps are invalid', async function () {
|
||||
const ret = await runAppiumRaw(
|
||||
appiumHome,
|
||||
[DRIVER_TYPE, INSTALL, '--source', 'local', TEST_DRIVER_INVALID_PEERS_DIR],
|
||||
{}
|
||||
);
|
||||
ret.stderr.should.match(/may be incompatible with the current version of Appium/i);
|
||||
ret.stderr.should.match(/successfully installed/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when peer dependencies are valid', function () {
|
||||
it('should not display a warning', async function () {
|
||||
const ret = await runAppiumRaw(
|
||||
appiumHome,
|
||||
[DRIVER_TYPE, INSTALL, '--source', 'local', TEST_DRIVER_DIR],
|
||||
{}
|
||||
);
|
||||
ret.stderr.should.not.match(/may be incompatible with the current version of Appium/i);
|
||||
ret.stderr.should.match(/successfully installed/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(`Local ${INSTALL}`, function () {
|
||||
// NOTE: This suite is separate because we don't to run `clear()` before each test.
|
||||
|
||||
/** @type {ExtRecord<DriverType>} */
|
||||
let installResult;
|
||||
|
||||
/** @type {ExtensionListData} */
|
||||
let listResult;
|
||||
|
||||
/** @type {string} */
|
||||
let installPath;
|
||||
|
||||
before(async function () {
|
||||
await resetAppiumHome();
|
||||
// take advantage of the fact that we know we have fake driver installed as a dependency in
|
||||
// this module, so we know its local path on disk
|
||||
installResult = await installLocalExtension(appiumHome, DRIVER_TYPE, FAKE_DRIVER_DIR);
|
||||
listResult = await runList(['--installed']);
|
||||
installPath = resolveFrom(appiumHome, '@appium/fake-driver');
|
||||
});
|
||||
|
||||
it('should install a driver from a local npm module', function () {
|
||||
expect(installResult.fake).to.include({
|
||||
pkgName: '@appium/fake-driver',
|
||||
installType: 'local',
|
||||
installSpec: FAKE_DRIVER_DIR,
|
||||
});
|
||||
});
|
||||
|
||||
it('should show the installed driver in the list of extensions', function () {
|
||||
expect(listResult.fake).to.deep.include(installResult.fake);
|
||||
});
|
||||
|
||||
// this fails in CI and I don't know why
|
||||
it.skip('should create a symlink', async function () {
|
||||
const srcStat = await fs.lstat(FAKE_DRIVER_DIR);
|
||||
const destStat = await fs.lstat(appiumHome);
|
||||
if (srcStat.dev !== destStat.dev) {
|
||||
return this.skip();
|
||||
}
|
||||
// it should be a link! this may be npm-version dependent, but it's worked
|
||||
// this way for quite awhile
|
||||
const stat = await fs.lstat(installPath);
|
||||
expect(stat.isSymbolicLink()).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('uninstall', function () {
|
||||
beforeEach(async function () {
|
||||
await resetAppiumHome();
|
||||
await installLocalExtension(appiumHome, DRIVER_TYPE, FAKE_DRIVER_DIR);
|
||||
});
|
||||
|
||||
it('should uninstall a driver based on its driver name', async function () {
|
||||
const uninstall = await runUninstall(['fake']);
|
||||
uninstall.should.not.have.key('fake');
|
||||
expect(fs.exists(path.join(appiumHome, 'node_modules', '@appium', 'fake-driver'))).to
|
||||
.eventually.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('run', function () {
|
||||
const driverName = 'fake';
|
||||
|
||||
before(async function () {
|
||||
await resetAppiumHome();
|
||||
await installLocalExtension(appiumHome, DRIVER_TYPE, FAKE_DRIVER_DIR);
|
||||
});
|
||||
|
||||
describe('when the driver and script is valid', function () {
|
||||
const scriptName = 'fake-success';
|
||||
|
||||
describe('when the script completes successfully', function () {
|
||||
it('should result in success', async function () {
|
||||
const out = await runRun([driverName, scriptName]);
|
||||
out.should.not.have.property('error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the script fails', function () {
|
||||
it('should return an error', async function () {
|
||||
const out = await runRun([driverName, 'fake-error']);
|
||||
out.should.have.property('error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when passed extra arguments', function () {
|
||||
it('should pass them to the script', async function () {
|
||||
const out = await runRun([driverName, scriptName, '--foo', '--bar']);
|
||||
out.should.not.have.property('error');
|
||||
out.output.should.match(/--foo --bar/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the driver is valid but the script is not', function () {
|
||||
it('should throw an error', async function () {
|
||||
await expect(runRun([driverName, 'foo'])).to.be.rejectedWith(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the driver and script are invalid', function () {
|
||||
it('should throw an error', async function () {
|
||||
const driverName = 'foo';
|
||||
await expect(runRun([driverName, 'bar'])).to.be.rejectedWith(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {import('@appium/types').ExtensionType} ExtensionType
|
||||
* @typedef {import('appium/types').ManifestData} ManifestData
|
||||
* @typedef {import('@appium/types').DriverType} DriverType
|
||||
* @typedef {import('@appium/types').PluginType} PluginType
|
||||
* @typedef {import('appium/lib/cli/extension-command').ExtensionListData} ExtensionListData
|
||||
* @typedef {import('./e2e-helpers').CliArgs} CliArgs
|
||||
* @typedef {import('appium/types').CliExtensionSubcommand} CliExtensionSubcommand
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template ExtSubCommand
|
||||
* @typedef {import('./e2e-helpers').CliExtArgs<ExtSubCommand>} CliExtArgs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {import('appium/types').ExtRecord<T>} ExtRecord
|
||||
*/
|
||||
@@ -0,0 +1,79 @@
|
||||
// @ts-check
|
||||
|
||||
import {fs, tempDir} from '@appium/support';
|
||||
import {EXT_SUBCOMMAND_RUN as RUN, PLUGIN_TYPE} from '../../lib/constants';
|
||||
import {FAKE_PLUGIN_DIR} from '../helpers';
|
||||
import {installLocalExtension, runAppiumJson} from './e2e-helpers';
|
||||
|
||||
const {expect} = chai;
|
||||
|
||||
describe('Plugin CLI', function () {
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
let appiumHome;
|
||||
|
||||
/**
|
||||
* @type {(args: string[]) => Promise<{ output: string, error?: string }>}
|
||||
*/
|
||||
let runRun;
|
||||
|
||||
before(async function () {
|
||||
appiumHome = await tempDir.openDir();
|
||||
const run = runAppiumJson(appiumHome);
|
||||
runRun = async (args) =>
|
||||
/** @type {ReturnType<typeof runRun>} */ (await run([PLUGIN_TYPE, RUN, ...args]));
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await fs.rimraf(appiumHome);
|
||||
});
|
||||
|
||||
describe('run', function () {
|
||||
before(async function () {
|
||||
await installLocalExtension(appiumHome, PLUGIN_TYPE, FAKE_PLUGIN_DIR);
|
||||
});
|
||||
|
||||
it('should run a valid plugin, valid script, and result in success', async function () {
|
||||
const pluginName = 'fake';
|
||||
const scriptName = 'fake-success';
|
||||
const out = await runRun([pluginName, scriptName, '--json']);
|
||||
out.should.not.have.property('error');
|
||||
});
|
||||
|
||||
it('should run a valid plugin, valid error prone script, and return error in json', async function () {
|
||||
const pluginName = 'fake';
|
||||
const out = await runRun([pluginName, 'fake-error', '--json']);
|
||||
out.should.have.property('error');
|
||||
});
|
||||
|
||||
it('should take a valid plugin, invalid script, and throw an error', async function () {
|
||||
const pluginName = 'fake';
|
||||
await expect(runRun([pluginName, 'foo', '--json'])).to.be.rejectedWith(Error);
|
||||
});
|
||||
|
||||
it('should take an invalid plugin, invalid script, and throw an error', async function () {
|
||||
await expect(runRun(['foo', 'bar', '--json'])).to.be.rejectedWith(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {import('@appium/types').ExtensionType} ExtensionType
|
||||
* @typedef {import('appium/types').ManifestData} ManifestData
|
||||
* @typedef {import('@appium/types').DriverType} DriverType
|
||||
* @typedef {import('@appium/types').PluginType} PluginType
|
||||
* @typedef {import('appium/lib/cli/extension-command').ExtensionListData} ExtensionListData
|
||||
* @typedef {import('./e2e-helpers').CliArgs} CliArgs
|
||||
* @typedef {import('appium/types').CliExtensionSubcommand} CliExtensionSubcommand
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template ExtSubCommand
|
||||
* @typedef {import('./e2e-helpers').CliExtArgs<ExtSubCommand>} CliExtArgs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {import('appium/types').ExtRecord<T>} ExtRecord
|
||||
*/
|
||||
@@ -1,708 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import getPort from 'get-port';
|
||||
import {exec} from 'teen_process';
|
||||
import {npm, env, fs, tempDir, util} from '@appium/support';
|
||||
import B from 'bluebird';
|
||||
import path from 'path';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import YAML from 'yaml';
|
||||
import {
|
||||
DRIVER_TYPE,
|
||||
EXT_SUBCOMMAND_INSTALL as INSTALL,
|
||||
EXT_SUBCOMMAND_LIST as LIST,
|
||||
EXT_SUBCOMMAND_RUN as RUN,
|
||||
EXT_SUBCOMMAND_UNINSTALL as UNINSTALL,
|
||||
PKG_HASHFILE_RELATIVE_PATH,
|
||||
KNOWN_DRIVERS,
|
||||
PLUGIN_TYPE,
|
||||
} from '../../lib/constants';
|
||||
import {FAKE_DRIVER_DIR, APPIUM_ROOT, resolveFixture} from '../helpers';
|
||||
import {
|
||||
installLocalExtension,
|
||||
runAppiumJson,
|
||||
runAppiumRaw,
|
||||
readAppiumArgErrorFixture,
|
||||
formatAppiumArgErrorOutput,
|
||||
EXECUTABLE,
|
||||
} from './e2e-helpers';
|
||||
|
||||
const {MANIFEST_RELATIVE_PATH} = env;
|
||||
const {expect} = chai;
|
||||
|
||||
describe('CLI behavior', function () {
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
let appiumHome;
|
||||
|
||||
const testDriverPath = path.dirname(resolveFixture('test-driver/package.json'));
|
||||
const testDriverInvalidPeerDepsPath = path.dirname(
|
||||
resolveFixture('test-driver-invalid-peer-dep/package.json')
|
||||
);
|
||||
|
||||
describe('when appium is already installed', function () {
|
||||
before(async function () {
|
||||
appiumHome = await tempDir.openDir();
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await fs.rimraf(appiumHome);
|
||||
});
|
||||
|
||||
it('should not duplicate it as peer dependencies for drivers', async function () {
|
||||
const run = runAppiumJson(appiumHome);
|
||||
await run([DRIVER_TYPE, INSTALL, 'appium-fake-driver', '--source', 'npm']);
|
||||
await fs.exists(path.join(appiumHome, 'node_modules', 'appium')).should.eventually.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('when appium is a dependency of the project in the current working directory', function () {
|
||||
/** @type {string} */
|
||||
let hashPath;
|
||||
/** @type {string} */
|
||||
let manifestPath;
|
||||
/** @type {string} */
|
||||
let appiumHomePkgPath;
|
||||
|
||||
/**
|
||||
* Helper fn
|
||||
* @returns {Promise<ManifestData>}
|
||||
*/
|
||||
async function readManifest() {
|
||||
const manifest = await fs.readFile(manifestPath, 'utf8');
|
||||
return YAML.parse(manifest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper fn
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function readHash() {
|
||||
return await fs.readFile(hashPath, 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {CliExtensionSubcommand} ExtSubcommand
|
||||
* @type {import('lodash').CurriedFunction1<CliExtArgs<ExtSubcommand> | CliArgs, Promise<unknown>>}
|
||||
*/
|
||||
let runJson;
|
||||
|
||||
after(async function () {
|
||||
await fs.rimraf(appiumHome);
|
||||
});
|
||||
|
||||
describe('when the project is an extension', function () {
|
||||
before(async function () {
|
||||
appiumHome = await tempDir.openDir();
|
||||
hashPath = path.join(appiumHome, PKG_HASHFILE_RELATIVE_PATH);
|
||||
manifestPath = path.join(appiumHome, MANIFEST_RELATIVE_PATH);
|
||||
appiumHomePkgPath = path.join(appiumHome, 'package.json');
|
||||
runJson = runAppiumJson(appiumHome);
|
||||
|
||||
await fs.copyFile(resolveFixture('test-driver/package.json'), appiumHomePkgPath);
|
||||
});
|
||||
|
||||
it('should automatically discover the extension', async function () {
|
||||
const res = await runJson([DRIVER_TYPE, LIST]);
|
||||
expect(res).to.have.property('test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the project is not an extension', function () {
|
||||
before(async function () {
|
||||
appiumHome = await tempDir.openDir();
|
||||
hashPath = path.join(appiumHome, PKG_HASHFILE_RELATIVE_PATH);
|
||||
manifestPath = path.join(appiumHome, MANIFEST_RELATIVE_PATH);
|
||||
appiumHomePkgPath = path.join(appiumHome, 'package.json');
|
||||
runJson = runAppiumJson(appiumHome);
|
||||
|
||||
// an example package.json referencing appium dependency
|
||||
await fs.copyFile(resolveFixture('cli/appium-dependency.package.json'), appiumHomePkgPath);
|
||||
});
|
||||
|
||||
describe('without drivers installed', function () {
|
||||
it('should list no drivers', async function () {
|
||||
const res = /** @type {ExtensionListData} */ (await runJson([DRIVER_TYPE, LIST]));
|
||||
res.should.satisfy(
|
||||
/** @param {typeof res} value */ (value) =>
|
||||
Object.values(value).every(({installed}) => !installed)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('after a driver is installed', function () {
|
||||
before(async function () {
|
||||
await installLocalExtension(appiumHome, DRIVER_TYPE, FAKE_DRIVER_DIR);
|
||||
});
|
||||
|
||||
it('should list the driver', async function () {
|
||||
const res = /** @type {ExtensionListData} */ (await runJson([DRIVER_TYPE, LIST]));
|
||||
res.should.have.property('fake');
|
||||
});
|
||||
|
||||
it('should be resolvable from the local directory', function () {
|
||||
(() => resolveFrom(appiumHome, '@appium/fake-driver/package.json')).should.not.throw();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a driver is installed via npm', function () {
|
||||
before(async function () {
|
||||
// remove the hash file and the manifest to start clean.
|
||||
// this leaves any previously-installed extension in place, which should make this a little faster.
|
||||
await fs.rimraf(path.dirname(hashPath));
|
||||
await npm.exec(INSTALL, [FAKE_DRIVER_DIR], {
|
||||
json: true,
|
||||
cwd: appiumHome,
|
||||
});
|
||||
});
|
||||
|
||||
describe(LIST, function () {
|
||||
/** @type {ExtensionListData} */
|
||||
let res;
|
||||
|
||||
beforeEach(async function () {
|
||||
res = /** @type {ExtensionListData} */ (await runJson([DRIVER_TYPE, LIST]));
|
||||
});
|
||||
|
||||
it('should list the driver', function () {
|
||||
res.should.have.property('fake');
|
||||
});
|
||||
|
||||
it('should update the manifest', async function () {
|
||||
const manifestParsed = await readManifest();
|
||||
manifestParsed.should.have.nested.property('drivers.fake');
|
||||
});
|
||||
|
||||
it('should update the package hash', async function () {
|
||||
(await fs.exists(hashPath)).should.be.true;
|
||||
});
|
||||
|
||||
describe('when a different driver is installed via "appium driver install"', function () {
|
||||
/** @type {string} */
|
||||
let oldHash;
|
||||
|
||||
before(async function () {
|
||||
await runJson([DRIVER_TYPE, LIST]);
|
||||
oldHash = await readHash();
|
||||
await installLocalExtension(appiumHome, DRIVER_TYPE, testDriverPath);
|
||||
});
|
||||
|
||||
it('should update package.json', async function () {
|
||||
const newPkg = JSON.parse(await fs.readFile(appiumHomePkgPath, 'utf8'));
|
||||
expect(newPkg).to.have.nested.property('devDependencies.@appium/test-driver');
|
||||
});
|
||||
|
||||
it('should update the hash', async function () {
|
||||
const newHash = await readHash();
|
||||
newHash.should.not.equal(oldHash);
|
||||
});
|
||||
|
||||
it('should update the manifest with the new driver', async function () {
|
||||
const manifest = await fs.readFile(manifestPath, 'utf8');
|
||||
/** @type {ManifestData} */
|
||||
const manifestParsed = YAML.parse(manifest);
|
||||
manifestParsed.should.have.nested.property('drivers.test');
|
||||
manifestParsed.should.have.nested.property('drivers.fake');
|
||||
});
|
||||
|
||||
it('should actually install both drivers', function () {
|
||||
expect(() => resolveFrom(appiumHome, '@appium/fake-driver')).not.to.throw;
|
||||
expect(() => resolveFrom(appiumHome, '@appium/test-driver')).not.to.throw;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Extension Commands', function () {
|
||||
/**
|
||||
* @type {(args?: string[]) => Promise<ExtensionListData>}
|
||||
*/
|
||||
let runList;
|
||||
|
||||
/**
|
||||
* @type {(args: string[]) => Promise<{ output: string, error?: string }>}
|
||||
*/
|
||||
let runRun;
|
||||
|
||||
before(async function () {
|
||||
appiumHome = await tempDir.openDir();
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await fs.rimraf(appiumHome);
|
||||
});
|
||||
|
||||
async function clear() {
|
||||
await fs.rimraf(appiumHome);
|
||||
await fs.mkdirp(appiumHome);
|
||||
}
|
||||
|
||||
describe('Driver CLI', function () {
|
||||
/**
|
||||
* @type {(args: string[]) => Promise<ExtRecord<DriverType>>}
|
||||
*/
|
||||
let runInstall;
|
||||
|
||||
/**
|
||||
* @type {(args: string[]) => Promise<ExtRecord<DriverType>>}
|
||||
*/
|
||||
let runUninstall;
|
||||
|
||||
before(function () {
|
||||
const run = runAppiumJson(appiumHome);
|
||||
runInstall = async (args) =>
|
||||
/** @type {ReturnType<typeof runInstall>} */ (await run([DRIVER_TYPE, INSTALL, ...args]));
|
||||
runUninstall = async (args) =>
|
||||
/** @type {ReturnType<typeof runUninstall>} */ (
|
||||
await run([DRIVER_TYPE, UNINSTALL, ...args])
|
||||
);
|
||||
runList = async (args = []) =>
|
||||
/** @type {ReturnType<typeof runList>} */ (await run([DRIVER_TYPE, LIST, ...args]));
|
||||
runRun = async (args) =>
|
||||
/** @type {ReturnType<typeof runRun>} */ (await run([DRIVER_TYPE, RUN, ...args]));
|
||||
});
|
||||
|
||||
describe(LIST, function () {
|
||||
it('should list available drivers', async function () {
|
||||
// note this is raw, not json
|
||||
const {stderr} = await runAppiumRaw(appiumHome, [DRIVER_TYPE, LIST], {});
|
||||
for (const d of Object.keys(KNOWN_DRIVERS)) {
|
||||
stderr.should.match(new RegExp(`${d}.+[not installed]`));
|
||||
}
|
||||
});
|
||||
it('should list available drivers in json format', async function () {
|
||||
const driverData = await runList();
|
||||
for (const d of Object.keys(KNOWN_DRIVERS)) {
|
||||
driverData[d].should.eql({
|
||||
installed: false,
|
||||
pkgName: KNOWN_DRIVERS[d],
|
||||
});
|
||||
}
|
||||
});
|
||||
it('should allow filtering by installed drivers', async function () {
|
||||
const out = await runList(['--installed']);
|
||||
out.should.eql({});
|
||||
});
|
||||
it('should show updates for installed drivers with --updates', async function () {
|
||||
const penultimateFakeDriverVersionAsOfRightNow = '3.0.4';
|
||||
await clear();
|
||||
await runInstall([
|
||||
`@appium/fake-driver@${penultimateFakeDriverVersionAsOfRightNow}`,
|
||||
'--source',
|
||||
'npm',
|
||||
]);
|
||||
const {fake} =
|
||||
/** @type {Record<string,import('appium/lib/cli/extension-command').InstalledExtensionListData>} */ (
|
||||
await runList(['--updates'])
|
||||
);
|
||||
util.compareVersions(
|
||||
fake.updateVersion,
|
||||
'>',
|
||||
penultimateFakeDriverVersionAsOfRightNow
|
||||
).should.be.true;
|
||||
// TODO: this could probably be replaced by looking at updateVersion in the JSON
|
||||
const {stderr} = await runAppiumRaw(appiumHome, [DRIVER_TYPE, LIST, '--updates'], {});
|
||||
stderr.should.match(new RegExp(`fake.+[${fake.updateVersion} available]`));
|
||||
});
|
||||
|
||||
describe('if a driver is not published to npm', function () {
|
||||
it('should not throw an error', async function () {
|
||||
await clear();
|
||||
await installLocalExtension(appiumHome, DRIVER_TYPE, testDriverPath);
|
||||
await expect(runList(['--updates'])).not.to.be.rejected;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(INSTALL, function () {
|
||||
beforeEach(async function () {
|
||||
await clear();
|
||||
});
|
||||
|
||||
it('should install a driver from the list of known drivers', async function () {
|
||||
const ret = await runInstall(['uiautomator2']);
|
||||
ret.uiautomator2.pkgName.should.eql('appium-uiautomator2-driver');
|
||||
ret.uiautomator2.installType.should.eql('npm');
|
||||
ret.uiautomator2.installSpec.should.eql('uiautomator2');
|
||||
const list = await runList(['--installed']);
|
||||
delete list.uiautomator2.installed;
|
||||
list.should.eql(ret);
|
||||
});
|
||||
it('should install a driver from npm', async function () {
|
||||
const ret = await runInstall(['@appium/fake-driver', '--source', 'npm']);
|
||||
ret.fake.pkgName.should.eql('@appium/fake-driver');
|
||||
ret.fake.installType.should.eql('npm');
|
||||
ret.fake.installSpec.should.eql('@appium/fake-driver');
|
||||
const list = await runList(['--installed']);
|
||||
delete list.fake.installed;
|
||||
list.should.eql(ret);
|
||||
});
|
||||
|
||||
it('should install a driver from npm and a local driver', async function () {
|
||||
await runInstall(['@appium/fake-driver', '--source', 'npm']);
|
||||
await installLocalExtension(appiumHome, DRIVER_TYPE, testDriverPath);
|
||||
const list = await runList(['--installed']);
|
||||
expect(list.fake).to.exist;
|
||||
expect(list.test).to.exist;
|
||||
expect(() => resolveFrom(appiumHome, '@appium/fake-driver')).not.to.throw;
|
||||
expect(() => resolveFrom(appiumHome, 'test-driver')).not.to.throw;
|
||||
});
|
||||
|
||||
it('should install _two_ drivers from npm', async function () {
|
||||
await runInstall(['@appium/fake-driver', '--source', 'npm']);
|
||||
await runInstall(['appium-uiautomator2-driver', '--source', 'npm']);
|
||||
const list = await runList(['--installed']);
|
||||
expect(list.fake).to.exist;
|
||||
expect(list.uiautomator2).to.exist;
|
||||
expect(() => resolveFrom(appiumHome, '@appium/fake-driver')).not.to.throw;
|
||||
expect(() => resolveFrom(appiumHome, 'appium-uiautomator2-driver')).not.to.throw;
|
||||
});
|
||||
|
||||
it('should install a driver from npm with a specific version/tag', async function () {
|
||||
const currentFakeDriverVersionAsOfRightNow = '3.0.5';
|
||||
const installSpec = `@appium/fake-driver@${currentFakeDriverVersionAsOfRightNow}`;
|
||||
const ret = await runInstall([installSpec, '--source', 'npm']);
|
||||
ret.fake.pkgName.should.eql('@appium/fake-driver');
|
||||
ret.fake.installType.should.eql('npm');
|
||||
ret.fake.installSpec.should.eql(installSpec);
|
||||
const list = await runList(['--installed']);
|
||||
delete list.fake.installed;
|
||||
list.should.eql(ret);
|
||||
});
|
||||
it('should install a driver from github', async function () {
|
||||
const ret = await runInstall([
|
||||
'appium/appium-fake-driver',
|
||||
'--source',
|
||||
'github',
|
||||
'--package',
|
||||
'appium-fake-driver',
|
||||
]);
|
||||
ret.fake.pkgName.should.eql('appium-fake-driver');
|
||||
ret.fake.installType.should.eql('github');
|
||||
ret.fake.installSpec.should.eql('appium/appium-fake-driver');
|
||||
const list = await runList(['--installed']);
|
||||
delete list.fake.installed;
|
||||
list.should.eql(ret);
|
||||
});
|
||||
it('should install a driver from a local git repo', async function () {
|
||||
const ret = await runInstall([
|
||||
FAKE_DRIVER_DIR,
|
||||
'--source',
|
||||
'git',
|
||||
'--package',
|
||||
'@appium/fake-driver',
|
||||
]);
|
||||
ret.fake.pkgName.should.eql('@appium/fake-driver');
|
||||
ret.fake.installType.should.eql('git');
|
||||
ret.fake.installSpec.should.eql(FAKE_DRIVER_DIR);
|
||||
const list = await runList(['--installed', '--json']);
|
||||
delete list.fake.installed;
|
||||
list.should.eql(ret);
|
||||
});
|
||||
it('should install a driver from a remote git repo', async function () {
|
||||
const ret = await runInstall([
|
||||
'git+https://github.com/appium/appium-fake-driver.git',
|
||||
'--source',
|
||||
'git',
|
||||
'--package',
|
||||
'appium-fake-driver',
|
||||
]);
|
||||
ret.fake.pkgName.should.eql('appium-fake-driver');
|
||||
ret.fake.installType.should.eql('git');
|
||||
ret.fake.installSpec.should.eql('git+https://github.com/appium/appium-fake-driver');
|
||||
const list = await runList(['--installed']);
|
||||
delete list.fake.installed;
|
||||
list.should.eql(ret);
|
||||
});
|
||||
|
||||
describe('when peer dependencies are invalid', function () {
|
||||
it('should install the driver anyway', async function () {
|
||||
const ret = await installLocalExtension(
|
||||
appiumHome,
|
||||
DRIVER_TYPE,
|
||||
testDriverInvalidPeerDepsPath
|
||||
);
|
||||
ret.test.pkgName.should.equal('test-driver-invalid-peer-dep');
|
||||
const list = await runList(['--installed']);
|
||||
list.test.pkgName.should.equal('test-driver-invalid-peer-dep');
|
||||
});
|
||||
|
||||
it('should warn the user that peer deps are invalid', async function () {
|
||||
const ret = await runAppiumRaw(
|
||||
appiumHome,
|
||||
[DRIVER_TYPE, INSTALL, '--source', 'local', testDriverInvalidPeerDepsPath],
|
||||
{}
|
||||
);
|
||||
ret.stderr.should.match(/may be incompatible with the current version of Appium/i);
|
||||
ret.stderr.should.match(/successfully installed/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when peer dependencies are valid', function () {
|
||||
it('should not display a warning', async function () {
|
||||
const ret = await runAppiumRaw(
|
||||
appiumHome,
|
||||
[DRIVER_TYPE, INSTALL, '--source', 'local', testDriverPath],
|
||||
{}
|
||||
);
|
||||
ret.stderr.should.not.match(/may be incompatible with the current version of Appium/i);
|
||||
ret.stderr.should.match(/successfully installed/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('local npm install', function () {
|
||||
describe('when installing from a local npm module', function () {
|
||||
/** @type {ExtRecord<DriverType>} */
|
||||
let installResult;
|
||||
/** @type {ExtensionListData} */
|
||||
let listResult;
|
||||
|
||||
/** @type {string} */
|
||||
let installPath;
|
||||
|
||||
before(async function () {
|
||||
await clear();
|
||||
// take advantage of the fact that we know we have fake driver installed as a dependency in
|
||||
// this module, so we know its local path on disk
|
||||
installResult = await installLocalExtension(appiumHome, DRIVER_TYPE, FAKE_DRIVER_DIR);
|
||||
listResult = await runList(['--installed']);
|
||||
installPath = path.join(appiumHome, 'node_modules', '@appium', 'fake-driver');
|
||||
});
|
||||
|
||||
it('should install a driver from a local npm module', function () {
|
||||
expect(installResult.fake).to.include({
|
||||
pkgName: '@appium/fake-driver',
|
||||
installType: 'local',
|
||||
installSpec: FAKE_DRIVER_DIR,
|
||||
});
|
||||
});
|
||||
|
||||
it('should show the installed driver in the list of extensions', function () {
|
||||
expect(listResult.fake).to.deep.include(installResult.fake);
|
||||
});
|
||||
|
||||
// this fails in CI and I don't know why
|
||||
it.skip('should create a symlink', async function () {
|
||||
const srcStat = await fs.lstat(FAKE_DRIVER_DIR);
|
||||
const destStat = await fs.lstat(appiumHome);
|
||||
if (srcStat.dev !== destStat.dev) {
|
||||
return this.skip();
|
||||
}
|
||||
// it should be a link! this may be npm-version dependent, but it's worked
|
||||
// this way for quite awhile
|
||||
const stat = await fs.lstat(installPath);
|
||||
expect(stat.isSymbolicLink()).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('uninstall', function () {
|
||||
/** @type {string} */
|
||||
let pkgName;
|
||||
|
||||
beforeEach(async function () {
|
||||
await clear();
|
||||
const result = await runInstall(['@appium/fake-driver', '--source', 'npm']);
|
||||
({pkgName} = result.fake);
|
||||
});
|
||||
it('should uninstall a driver based on its driver name', async function () {
|
||||
// XXX: incorrect if APPIUM_HOME is an extension working copy
|
||||
const installPath = resolveFrom(appiumHome, pkgName);
|
||||
let list = await runList(['--installed']);
|
||||
list.fake.installed.should.be.true;
|
||||
const uninstall = await runUninstall(['fake']);
|
||||
uninstall.should.not.have.key('fake');
|
||||
await fs.exists(installPath).should.eventually.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('run', function () {
|
||||
const driverName = 'fake';
|
||||
before(async function () {
|
||||
await clear();
|
||||
await installLocalExtension(appiumHome, DRIVER_TYPE, FAKE_DRIVER_DIR);
|
||||
});
|
||||
|
||||
describe('when the driver and script is valid', function () {
|
||||
const scriptName = 'fake-success';
|
||||
|
||||
describe('when the script completes successfully', function () {
|
||||
it('should result in success', async function () {
|
||||
const out = await runRun([driverName, scriptName]);
|
||||
out.should.not.have.property('error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the script fails', function () {
|
||||
it('should return an error', async function () {
|
||||
const out = await runRun([driverName, 'fake-error']);
|
||||
out.should.have.property('error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when passed extra arguments', function () {
|
||||
it('should pass them to the script', async function () {
|
||||
const out = await runRun([driverName, scriptName, '--foo', '--bar']);
|
||||
out.should.not.have.property('error');
|
||||
out.output.should.match(/--foo --bar/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the driver is valid but the script is not', function () {
|
||||
it('should throw an error', async function () {
|
||||
await expect(runRun([driverName, 'foo'])).to.be.rejectedWith(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the driver and script are invalid', function () {
|
||||
it('should throw an error', async function () {
|
||||
const driverName = 'foo';
|
||||
await expect(runRun([driverName, 'bar'])).to.be.rejectedWith(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Plugin CLI', function () {
|
||||
const FAKE_PLUGIN_DIR = path.dirname(require.resolve('@appium/fake-plugin/package.json'));
|
||||
|
||||
before(function () {
|
||||
const run = runAppiumJson(appiumHome);
|
||||
runList = async (args = []) =>
|
||||
/** @type {ReturnType<typeof runList>} */ (await run([PLUGIN_TYPE, LIST, ...args]));
|
||||
runRun = async (args) =>
|
||||
/** @type {ReturnType<typeof runRun>} */ (await run([PLUGIN_TYPE, RUN, ...args]));
|
||||
});
|
||||
|
||||
describe('run', function () {
|
||||
before(async function () {
|
||||
await clear();
|
||||
await installLocalExtension(appiumHome, PLUGIN_TYPE, FAKE_PLUGIN_DIR);
|
||||
});
|
||||
it('should run a valid plugin, valid script, and result in success', async function () {
|
||||
const pluginName = 'fake';
|
||||
const scriptName = 'fake-success';
|
||||
const out = await runRun([pluginName, scriptName, '--json']);
|
||||
out.should.not.have.property('error');
|
||||
});
|
||||
it('should run a valid plugin, valid error prone script, and return error in json', async function () {
|
||||
const pluginName = 'fake';
|
||||
const out = await runRun([pluginName, 'fake-error', '--json']);
|
||||
out.should.have.property('error');
|
||||
});
|
||||
it('should take a valid plugin, invalid script, and throw an error', async function () {
|
||||
const pluginName = 'fake';
|
||||
await expect(runRun([pluginName, 'foo', '--json'])).to.be.rejectedWith(Error);
|
||||
});
|
||||
it('should take an invalid plugin, invalid script, and throw an error', async function () {
|
||||
await expect(runRun(['foo', 'bar', '--json'])).to.be.rejectedWith(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('argument parsing', function () {
|
||||
describe('when the user provides a very long string for an arg accepting a blob or filename', function () {
|
||||
describe('when the very long string is a JSON blob', function () {
|
||||
it('should not throw an ENAMETOOLONG exception', async function () {
|
||||
this.timeout(10000);
|
||||
const capsArg = JSON.stringify({
|
||||
'appium:platformName': 'ANDROID',
|
||||
'appium:platformVersion': '11',
|
||||
'appium:deviceName':
|
||||
'Spicy jalapeno bacon ipsum dolor amet deserunt tempor pork belly aliqua drumstick, occaecat dolor venison et labore. Rump meatball pork chop tail. Consequat adipisicing kielbasa occaecat laborum pig. Qui pork chop chicken nostrud boudin fugiat. Proident ut culpa, chuck nulla sunt pastrami ut tri-tip. Buffalo dolore adipisicing, labore venison elit beef fatback kevin burgdoggen tail pancetta filet mignon. Dolor turducken rump, anim kevin sunt exercitation ham filet mignon beef ribs ad officia eiusmod id cillum.',
|
||||
});
|
||||
|
||||
await expect(
|
||||
exec(
|
||||
process.execPath,
|
||||
[
|
||||
EXECUTABLE,
|
||||
'-pa=/wd/hub',
|
||||
'--session-override',
|
||||
'--local-timezone',
|
||||
'--relaxed-security',
|
||||
`--default-capabilities=${capsArg}`,
|
||||
'--port',
|
||||
String(await getPort()),
|
||||
],
|
||||
{
|
||||
env: {APPIUM_HOME: appiumHome, PATH: process.env.PATH},
|
||||
cwd: APPIUM_ROOT,
|
||||
timeout: 5000,
|
||||
}
|
||||
)
|
||||
).to.be.rejectedWith(Error, /timed out/);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('when the user provides an string where a number was expected', function () {
|
||||
describe('when color output is supported', function () {
|
||||
it('should output a fancy error message', async function () {
|
||||
const [{stderr: actual}, expected] = await B.all([
|
||||
runAppiumRaw(appiumHome, ['--port=sheep'], {
|
||||
env: {FORCE_COLOR: '1'},
|
||||
}),
|
||||
readAppiumArgErrorFixture('cli/cli-error-output-color.txt'),
|
||||
]);
|
||||
expect(formatAppiumArgErrorOutput(actual)).to.equal(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when color output is unsupported', function () {
|
||||
it('should output a colorless yet fancy error message', async function () {
|
||||
const [{stderr: actual}, expected] = await B.all([
|
||||
runAppiumRaw(appiumHome, ['--port=sheep'], {}),
|
||||
readAppiumArgErrorFixture('cli/cli-error-output.txt'),
|
||||
]);
|
||||
expect(formatAppiumArgErrorOutput(actual)).to.equal(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user provides a value for a boolean argument', function () {
|
||||
it('should output a basic error message', async function () {
|
||||
const [{stderr: actual}, expected] = await B.all([
|
||||
runAppiumRaw(appiumHome, ['--relaxed-security=sheep'], {}),
|
||||
readAppiumArgErrorFixture('cli/cli-error-output-boolean.txt'),
|
||||
]);
|
||||
expect(formatAppiumArgErrorOutput(actual)).to.equal(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user provides an unknown argument', function () {
|
||||
it('should output a basic error message', async function () {
|
||||
const [{stderr: actual}, expected] = await B.all([
|
||||
runAppiumRaw(appiumHome, ['--pigs=sheep'], {}),
|
||||
readAppiumArgErrorFixture('cli/cli-error-output-unknown.txt'),
|
||||
]);
|
||||
expect(formatAppiumArgErrorOutput(actual)).to.equal(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {import('@appium/types').ExtensionType} ExtensionType
|
||||
* @typedef {import('appium/types').ManifestData} ManifestData
|
||||
* @typedef {import('@appium/types').DriverType} DriverType
|
||||
* @typedef {import('@appium/types').PluginType} PluginType
|
||||
* @typedef {import('appium/lib/cli/extension-command').ExtensionListData} ExtensionListData
|
||||
* @typedef {import('./e2e-helpers').CliArgs} CliArgs
|
||||
* @typedef {import('appium/types').CliExtensionSubcommand} CliExtensionSubcommand
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template ExtSubCommand
|
||||
* @typedef {import('./e2e-helpers').CliExtArgs<ExtSubCommand>} CliExtArgs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {import('appium/types').ExtRecord<T>} ExtRecord
|
||||
*/
|
||||
@@ -84,7 +84,7 @@ export const runAppium = _.curry(_runAppium);
|
||||
|
||||
/**
|
||||
* See {@link runAppiumRaw}.
|
||||
* @type {AppiumOptsRunner<import('teen_process').TeenProcessExecResult>}
|
||||
* @type {AppiumOptsRunner<import('teen_process').TeenProcessExecResult<string>>}
|
||||
*/
|
||||
async function _runAppiumRaw(appiumHome, args, opts) {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
// @ts-check
|
||||
|
||||
import {env, fs, npm, tempDir} from '@appium/support';
|
||||
import path from 'node:path';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import YAML from 'yaml';
|
||||
import {
|
||||
DRIVER_TYPE,
|
||||
EXT_SUBCOMMAND_INSTALL as INSTALL,
|
||||
EXT_SUBCOMMAND_LIST as LIST,
|
||||
PKG_HASHFILE_RELATIVE_PATH,
|
||||
} from '../../lib/constants';
|
||||
import {FAKE_DRIVER_DIR, resolveFixture} from '../helpers';
|
||||
import {installLocalExtension, runAppiumJson} from './e2e-helpers';
|
||||
|
||||
const {MANIFEST_RELATIVE_PATH} = env;
|
||||
const {expect} = chai;
|
||||
const testDriverPath = path.dirname(resolveFixture('test-driver/package.json'));
|
||||
|
||||
describe('when Appium is a dependency of the current project', function () {
|
||||
/** @type {string} */
|
||||
let hashPath;
|
||||
/** @type {string} */
|
||||
let manifestPath;
|
||||
/** @type {string} */
|
||||
let appiumHomePkgPath;
|
||||
/** @type {string} */
|
||||
let appiumHome;
|
||||
|
||||
before(async function () {
|
||||
appiumHome = await tempDir.openDir();
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await fs.rimraf(appiumHome);
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper fn
|
||||
* @returns {Promise<ManifestData>}
|
||||
*/
|
||||
async function readManifest() {
|
||||
const manifest = await fs.readFile(manifestPath, 'utf8');
|
||||
return YAML.parse(manifest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper fn
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function readHash() {
|
||||
return await fs.readFile(hashPath, 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {CliExtensionSubcommand} ExtSubcommand
|
||||
* @type {import('lodash').CurriedFunction1<CliExtArgs<ExtSubcommand> | CliArgs, Promise<unknown>>}
|
||||
*/
|
||||
let runJson;
|
||||
|
||||
describe('when the project is an extension', function () {
|
||||
before(async function () {
|
||||
appiumHome = await tempDir.openDir();
|
||||
hashPath = path.join(appiumHome, PKG_HASHFILE_RELATIVE_PATH);
|
||||
manifestPath = path.join(appiumHome, MANIFEST_RELATIVE_PATH);
|
||||
appiumHomePkgPath = path.join(appiumHome, 'package.json');
|
||||
runJson = runAppiumJson(appiumHome);
|
||||
|
||||
await fs.copyFile(resolveFixture('test-driver/package.json'), appiumHomePkgPath);
|
||||
});
|
||||
|
||||
it('should automatically discover the extension', async function () {
|
||||
const res = await runJson([DRIVER_TYPE, LIST]);
|
||||
expect(res).to.have.property('test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the project is not an extension', function () {
|
||||
before(async function () {
|
||||
appiumHome = await tempDir.openDir();
|
||||
hashPath = path.join(appiumHome, PKG_HASHFILE_RELATIVE_PATH);
|
||||
manifestPath = path.join(appiumHome, MANIFEST_RELATIVE_PATH);
|
||||
appiumHomePkgPath = path.join(appiumHome, 'package.json');
|
||||
runJson = runAppiumJson(appiumHome);
|
||||
|
||||
// an example package.json referencing appium dependency
|
||||
await fs.copyFile(resolveFixture('cli/appium-dependency.package.json'), appiumHomePkgPath);
|
||||
});
|
||||
|
||||
describe('without drivers installed', function () {
|
||||
it('should list no drivers', async function () {
|
||||
const res = /** @type {ExtensionListData} */ (await runJson([DRIVER_TYPE, LIST]));
|
||||
res.should.satisfy(
|
||||
/** @param {typeof res} value */ (value) =>
|
||||
Object.values(value).every(({installed}) => !installed)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('after a driver is installed', function () {
|
||||
before(async function () {
|
||||
await installLocalExtension(appiumHome, DRIVER_TYPE, FAKE_DRIVER_DIR);
|
||||
});
|
||||
|
||||
it('should list the driver', async function () {
|
||||
const res = /** @type {ExtensionListData} */ (await runJson([DRIVER_TYPE, LIST]));
|
||||
res.should.have.property('fake');
|
||||
});
|
||||
|
||||
it('should be resolvable from the local directory', function () {
|
||||
(() => resolveFrom(appiumHome, '@appium/fake-driver/package.json')).should.not.throw();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a driver is installed via npm', function () {
|
||||
before(async function () {
|
||||
// remove the hash file and the manifest to start clean.
|
||||
// this leaves any previously-installed extension in place, which should make this a little faster.
|
||||
await fs.rimraf(path.dirname(hashPath));
|
||||
await npm.exec(INSTALL, [FAKE_DRIVER_DIR], {
|
||||
json: true,
|
||||
cwd: appiumHome,
|
||||
});
|
||||
});
|
||||
|
||||
describe(LIST, function () {
|
||||
/** @type {ExtensionListData} */
|
||||
let res;
|
||||
|
||||
beforeEach(async function () {
|
||||
res = /** @type {ExtensionListData} */ (await runJson([DRIVER_TYPE, LIST]));
|
||||
});
|
||||
|
||||
it('should list the driver', function () {
|
||||
res.should.have.property('fake');
|
||||
});
|
||||
|
||||
it('should update the manifest', async function () {
|
||||
const manifestParsed = await readManifest();
|
||||
manifestParsed.should.have.nested.property('drivers.fake');
|
||||
});
|
||||
|
||||
it('should update the package hash', async function () {
|
||||
(await fs.exists(hashPath)).should.be.true;
|
||||
});
|
||||
|
||||
describe('when a different driver is installed via "appium driver install"', function () {
|
||||
/** @type {string} */
|
||||
let oldHash;
|
||||
|
||||
before(async function () {
|
||||
await runJson([DRIVER_TYPE, LIST]);
|
||||
oldHash = await readHash();
|
||||
await installLocalExtension(appiumHome, DRIVER_TYPE, testDriverPath);
|
||||
});
|
||||
|
||||
it('should update package.json', async function () {
|
||||
const newPkg = JSON.parse(await fs.readFile(appiumHomePkgPath, 'utf8'));
|
||||
expect(newPkg).to.have.nested.property('devDependencies.@appium/test-driver');
|
||||
});
|
||||
|
||||
it('should update the hash', async function () {
|
||||
const newHash = await readHash();
|
||||
newHash.should.not.equal(oldHash);
|
||||
});
|
||||
|
||||
it('should update the manifest with the new driver', async function () {
|
||||
const manifest = await fs.readFile(manifestPath, 'utf8');
|
||||
/** @type {ManifestData} */
|
||||
const manifestParsed = YAML.parse(manifest);
|
||||
manifestParsed.should.have.nested.property('drivers.test');
|
||||
manifestParsed.should.have.nested.property('drivers.fake');
|
||||
});
|
||||
|
||||
it('should actually install both drivers', function () {
|
||||
expect(() => resolveFrom(appiumHome, '@appium/fake-driver')).not.to.throw;
|
||||
expect(() => resolveFrom(appiumHome, '@appium/test-driver')).not.to.throw;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {import('appium/types').ManifestData} ManifestData
|
||||
* @typedef {import('./e2e-helpers').CliArgs} CliArgs
|
||||
* @typedef {import('appium/lib/cli/extension-command').ExtensionListData} ExtensionListData
|
||||
* @typedef {import('appium/types').CliExtensionSubcommand} CliExtensionSubcommand
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template ExtSubCommand
|
||||
* @typedef {import('./e2e-helpers').CliExtArgs<ExtSubCommand>} CliExtArgs
|
||||
*/
|
||||
@@ -0,0 +1,106 @@
|
||||
import {fs, tempDir} from '@appium/support';
|
||||
import path from 'node:path';
|
||||
import YAML from 'yaml';
|
||||
import {
|
||||
CACHE_DIR_RELATIVE_PATH,
|
||||
CURRENT_SCHEMA_REV,
|
||||
DRIVER_TYPE,
|
||||
EXT_SUBCOMMAND_LIST as LIST,
|
||||
} from '../../lib/constants';
|
||||
import {FAKE_DRIVER_DIR, resolveFixture} from '../helpers';
|
||||
import {installLocalExtension, runAppiumJson} from './e2e-helpers';
|
||||
|
||||
const {expect} = chai;
|
||||
|
||||
describe('manifest handling', function () {
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
let appiumHome;
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
let manifestPath;
|
||||
|
||||
/**
|
||||
* @type {(args?: string[]) => Promise<ExtensionListData>}
|
||||
*/
|
||||
let runList;
|
||||
|
||||
async function resetAppiumHome() {
|
||||
await fs.rimraf(appiumHome);
|
||||
await fs.mkdirp(appiumHome);
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
appiumHome = await tempDir.openDir();
|
||||
manifestPath = path.join(appiumHome, CACHE_DIR_RELATIVE_PATH, 'extensions.yaml');
|
||||
const run = runAppiumJson(appiumHome);
|
||||
runList = async (args = []) =>
|
||||
/** @type {ReturnType<typeof runList>} */ (await run([DRIVER_TYPE, LIST, ...args]));
|
||||
});
|
||||
|
||||
/**
|
||||
* @returns {Promise<import('appium/types').AnyManifestDataVersion>}
|
||||
*/
|
||||
async function readManifest() {
|
||||
return YAML.parse(await fs.readFile(manifestPath, 'utf8'));
|
||||
}
|
||||
|
||||
after(async function () {
|
||||
await fs.rimraf(appiumHome);
|
||||
});
|
||||
|
||||
describe('migration', function () {
|
||||
beforeEach(async function () {
|
||||
await resetAppiumHome();
|
||||
await fs.mkdirp(path.dirname(manifestPath));
|
||||
});
|
||||
|
||||
describe('schema rev update', function () {
|
||||
beforeEach(async function () {
|
||||
await fs.copyFile(resolveFixture('manifest/v2-empty.yaml'), manifestPath);
|
||||
});
|
||||
|
||||
it('should update the manifest file to the latest schema revision', async function () {
|
||||
// note: all we need to do to run the migration is invoke appium, so any command
|
||||
// would do.
|
||||
await runList();
|
||||
const manifest = await readManifest();
|
||||
expect(manifest.schemaRev).to.equal(CURRENT_SCHEMA_REV);
|
||||
});
|
||||
});
|
||||
|
||||
describe('v3', function () {
|
||||
/** @type {import('appium/types').AnyManifestDataVersion} */
|
||||
let manifest;
|
||||
// for these tests, we have to use a real extension, because the migration
|
||||
// only does anything if the extension is actually installed.
|
||||
before(async function () {
|
||||
await installLocalExtension(appiumHome, DRIVER_TYPE, FAKE_DRIVER_DIR);
|
||||
const list = await runList();
|
||||
expect(list.fake).to.exist;
|
||||
|
||||
// but the manifest is now at v3, since it's brand new. we will manually downgrade.
|
||||
let tmpManifest = await readManifest();
|
||||
tmpManifest.schemaRev = 2;
|
||||
delete tmpManifest.drivers.fake.installPath;
|
||||
await fs.writeFile(manifestPath, YAML.stringify(tmpManifest));
|
||||
tmpManifest = await readManifest();
|
||||
expect(tmpManifest.schemaRev).to.equal(2);
|
||||
expect(tmpManifest.drivers.fake.installPath).to.not.exist;
|
||||
await runList();
|
||||
manifest = await readManifest();
|
||||
});
|
||||
|
||||
it('should add an "installPath" field to each extension', function () {
|
||||
expect(manifest.drivers.fake?.installPath).to.be.a('string');
|
||||
});
|
||||
|
||||
it('should update the manifest file to the latest schema revision', function () {
|
||||
expect(manifest.schemaRev).to.equal(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
drivers: {}
|
||||
plugins: {}
|
||||
schemaRev: 2
|
||||
@@ -33,6 +33,8 @@ describe('ExtensionConfig', function () {
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
// avoids a warning about too many listeners, caused by an exit handler in base-driver
|
||||
process.removeAllListeners('exit');
|
||||
});
|
||||
|
||||
describe('constructor', function () {});
|
||||
@@ -134,9 +136,8 @@ describe('ExtensionConfig', function () {
|
||||
installType: 'npm',
|
||||
appiumVersion: APPIUM_VER,
|
||||
};
|
||||
manifest.addExtension(DRIVER_TYPE, extData.pkgName, extData);
|
||||
manifest.setExtension(DRIVER_TYPE, extData.pkgName, extData);
|
||||
config = new ExtensionConfig(DRIVER_TYPE, manifest);
|
||||
delete this._listDataCache;
|
||||
});
|
||||
|
||||
describe('when the extension data is missing an `installSpec` field', function () {
|
||||
|
||||
@@ -294,7 +294,7 @@ describe('Manifest', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('addExtension()', function () {
|
||||
describe('setExtension()', function () {
|
||||
/** @type {ExtManifest<DriverType>} */
|
||||
const extData = {
|
||||
automationName: 'derp',
|
||||
@@ -309,7 +309,7 @@ describe('Manifest', function () {
|
||||
};
|
||||
|
||||
it('should add a clone of the extension manifest to the internal data object', function () {
|
||||
manifest.addExtension(DRIVER_TYPE, 'foo', extData);
|
||||
manifest.setExtension(DRIVER_TYPE, 'foo', extData);
|
||||
expect(manifest.getExtensionData(DRIVER_TYPE).foo)
|
||||
.to.eql(extData)
|
||||
.and.not.to.equal(extData);
|
||||
@@ -319,7 +319,7 @@ describe('Manifest', function () {
|
||||
/** @type {ExtManifest<DriverType>} */
|
||||
|
||||
beforeEach(function () {
|
||||
manifest.addExtension(DRIVER_TYPE, 'foo', extData);
|
||||
manifest.setExtension(DRIVER_TYPE, 'foo', extData);
|
||||
});
|
||||
|
||||
it('should rewrite', function () {
|
||||
@@ -327,7 +327,7 @@ describe('Manifest', function () {
|
||||
...extData,
|
||||
automationName: 'BLAAHAH',
|
||||
};
|
||||
manifest.addExtension(DRIVER_TYPE, 'foo', expected);
|
||||
manifest.setExtension(DRIVER_TYPE, 'foo', expected);
|
||||
expect(manifest.getExtensionData(DRIVER_TYPE).foo).to.eql(expected);
|
||||
});
|
||||
});
|
||||
@@ -338,7 +338,7 @@ describe('Manifest', function () {
|
||||
});
|
||||
|
||||
it('should work anyway', function () {
|
||||
manifest.addExtension(DRIVER_TYPE, 'foo', extData);
|
||||
manifest.setExtension(DRIVER_TYPE, 'foo', extData);
|
||||
expect(manifest.getExtensionData(DRIVER_TYPE).foo).to.not.have.property('appiumVersion');
|
||||
});
|
||||
});
|
||||
@@ -359,7 +359,7 @@ describe('Manifest', function () {
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
manifest.addExtension(DRIVER_TYPE, 'foo', extData);
|
||||
manifest.setExtension(DRIVER_TYPE, 'foo', extData);
|
||||
});
|
||||
|
||||
it('should return all extension data for an extension type', function () {
|
||||
@@ -575,7 +575,7 @@ describe('Manifest', function () {
|
||||
describe('when the driver is registered', function () {
|
||||
beforeEach(function () {
|
||||
// @ts-expect-error
|
||||
manifest.addExtension(DRIVER_TYPE, 'foo', {});
|
||||
manifest.setExtension(DRIVER_TYPE, 'foo', {});
|
||||
});
|
||||
|
||||
it('should return `true`', function () {
|
||||
@@ -594,7 +594,7 @@ describe('Manifest', function () {
|
||||
describe('when the plugin is registered', function () {
|
||||
beforeEach(function () {
|
||||
// @ts-expect-error
|
||||
manifest.addExtension(PLUGIN_TYPE, 'foo', {});
|
||||
manifest.setExtension(PLUGIN_TYPE, 'foo', {});
|
||||
});
|
||||
|
||||
it('should return `true`', function () {
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [9.1.0](https://github.com/appium/appium/compare/@appium/base-driver@9.0.0...@appium/base-driver@9.1.0) (2022-12-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add 'webSocketUrl' as standard cap for bidi support ([#17936](https://github.com/appium/appium/issues/17936)) ([0e195ca](https://github.com/appium/appium/commit/0e195caafefe911586ee2f8be4ae33d402b2ba40))
|
||||
- **types:** update definitelytyped ([172fcb9](https://github.com/appium/appium/commit/172fcb9aff0afe5295650566c4fb92d0894bf879))
|
||||
- **types:** update dependency type-fest to v3.4.0 ([37f71c3](https://github.com/appium/appium/commit/37f71c327a7c1a6d882b5198af6fedc9e8d51496))
|
||||
|
||||
### Features
|
||||
|
||||
- **base:** add get computed role and label in W3C v2 ([#17928](https://github.com/appium/appium/issues/17928)) ([316ecca](https://github.com/appium/appium/commit/316ecca8b1f8e52806867a15ba8524a504751460))
|
||||
|
||||
# [9.0.0](https://github.com/appium/appium/compare/@appium/base-driver@8.7.3...@appium/base-driver@9.0.0) (2022-12-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -104,6 +104,7 @@ export const STANDARD_CAPS = Object.freeze(
|
||||
'setWindowRect',
|
||||
'timeouts',
|
||||
'unhandledPromptBehavior',
|
||||
'webSocketUrl',
|
||||
])
|
||||
)
|
||||
);
|
||||
@@ -348,7 +349,9 @@ function promoteAppiumOptions(originalCaps) {
|
||||
}
|
||||
|
||||
if (!_.isPlainObject(appiumOptions)) {
|
||||
throw new errors.SessionNotCreatedError(`The ${PREFIXED_APPIUM_OPTS_CAP} capability must be an object`);
|
||||
throw new errors.SessionNotCreatedError(
|
||||
`The ${PREFIXED_APPIUM_OPTS_CAP} capability must be an object`
|
||||
);
|
||||
}
|
||||
if (_.isEmpty(appiumOptions)) {
|
||||
return obj;
|
||||
@@ -377,7 +380,7 @@ function promoteAppiumOptions(originalCaps) {
|
||||
};
|
||||
const preprocessedOptions = _(appiumOptions)
|
||||
.mapKeys((value, key) => verifyIfAcceptable(key))
|
||||
.mapKeys((value, key) => shouldAddVendorPrefix(key) ? `${APPIUM_VENDOR_PREFIX}${key}` : key)
|
||||
.mapKeys((value, key) => (shouldAddVendorPrefix(key) ? `${APPIUM_VENDOR_PREFIX}${key}` : key))
|
||||
.value();
|
||||
// warn if we are going to overwrite any keys on the base caps object
|
||||
const overwrittenKeys = _.intersection(Object.keys(obj), Object.keys(preprocessedOptions));
|
||||
@@ -388,8 +391,8 @@ function promoteAppiumOptions(originalCaps) {
|
||||
);
|
||||
}
|
||||
return _.cloneDeep({
|
||||
...(_.omit(obj, PREFIXED_APPIUM_OPTS_CAP)),
|
||||
...preprocessedOptions
|
||||
..._.omit(obj, PREFIXED_APPIUM_OPTS_CAP),
|
||||
...preprocessedOptions,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -303,6 +303,14 @@ const METHOD_MAP = /** @type {const} */ ({
|
||||
payloadParams: {required: ['orientation']},
|
||||
},
|
||||
},
|
||||
// w3c v2 https://www.w3.org/TR/webdriver2/#get-computed-role
|
||||
'session/:sessionId/element/:elementId/computedrole': {
|
||||
GET: {command: 'getComputedRole'},
|
||||
},
|
||||
// W3C v2 https://www.w3.org/TR/webdriver2/#get-computed-label
|
||||
'session/:sessionId/element/:elementId/computedlabel': {
|
||||
GET: {command: 'getComputedLabel'},
|
||||
},
|
||||
'/session/:sessionId/rotation': {
|
||||
GET: {command: 'getRotation'},
|
||||
POST: {command: 'setRotation', payloadParams: {required: ['x', 'y', 'z']}},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/base-driver",
|
||||
"version": "9.0.0",
|
||||
"version": "9.1.0",
|
||||
"description": "Base driver class for Appium drivers",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -36,16 +36,16 @@
|
||||
],
|
||||
"scripts": {
|
||||
"test": "npm run test:unit",
|
||||
"test:e2e": "mocha --timeout 20s --slow 10s \"./test/e2e/**/*.spec.js\"",
|
||||
"test:e2e": "mocha -p --timeout 20s --slow 10s \"./test/e2e/**/*.spec.js\"",
|
||||
"test:smoke": "node ./index.js",
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/support": "^3.0.0",
|
||||
"@appium/support": "^3.0.1",
|
||||
"@colors/colors": "1.5.0",
|
||||
"@types/async-lock": "1.3.0",
|
||||
"@types/bluebird": "3.5.38",
|
||||
"@types/express": "4.17.14",
|
||||
"@types/express": "4.17.15",
|
||||
"@types/method-override": "0.0.32",
|
||||
"@types/serve-favicon": "2.5.3",
|
||||
"async-lock": "1.4.0",
|
||||
@@ -62,7 +62,7 @@
|
||||
"morgan": "1.10.0",
|
||||
"serve-favicon": "2.5.0",
|
||||
"source-map-support": "0.5.21",
|
||||
"type-fest": "3.3.0",
|
||||
"type-fest": "3.4.0",
|
||||
"validate.js": "0.13.1"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('Protocol', function () {
|
||||
}
|
||||
let hash = shasum.digest('hex').substring(0, 8);
|
||||
// Modify the hash whenever the protocol has intentionally been modified.
|
||||
hash.should.equal('736bf246');
|
||||
hash.should.equal('43ef41a2');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.0.1](https://github.com/appium/appium/compare/@appium/base-plugin@2.0.0...@appium/base-plugin@2.0.1) (2022-12-21)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-plugin
|
||||
|
||||
# [2.0.0](https://github.com/appium/appium/compare/@appium/base-plugin@1.10.5...@appium/base-plugin@2.0.0) (2022-12-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/base-plugin",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"description": "The base plugin used to create Appium 2.0 plugins",
|
||||
"homepage": "https://appium.io",
|
||||
"bugs": {
|
||||
@@ -28,7 +28,7 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/support": "^3.0.0"
|
||||
"@appium/support": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.0.1](https://github.com/appium/appium/compare/@appium/doctor@2.0.0...@appium/doctor@2.0.1) (2022-12-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **doctor:** update dependency appium-adb to v9.10.19 ([2cb1af4](https://github.com/appium/appium/commit/2cb1af4357e0e867c28c7045825140d45fa02393))
|
||||
|
||||
# [2.0.0](https://github.com/appium/appium/compare/@appium/doctor@1.16.28...@appium/doctor@2.0.0) (2022-12-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/doctor",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"description": "Test environment for fitness to run Appium",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -42,11 +42,11 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/support": "^3.0.0",
|
||||
"@appium/support": "^3.0.1",
|
||||
"@colors/colors": "1.5.0",
|
||||
"@types/bluebird": "3.5.38",
|
||||
"@types/teen_process": "2.0.0",
|
||||
"appium-adb": "9.10.18",
|
||||
"appium-adb": "9.10.19",
|
||||
"bluebird": "3.7.2",
|
||||
"inquirer": "8.2.5",
|
||||
"lodash": "4.17.21",
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.1.1](https://github.com/appium/appium/compare/@appium/docutils@0.1.0...@appium/docutils@0.1.1) (2022-12-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **docutils:** package should publish the base mkdocs yml ([#17968](https://github.com/appium/appium/issues/17968)) ([aab4156](https://github.com/appium/appium/commit/aab415685ec5d5578cc8faae563f5cc5c0e4bb75))
|
||||
|
||||
# [0.1.0](https://github.com/appium/appium/compare/@appium/docutils@0.0.13...@appium/docutils@0.1.0) (2022-12-14)
|
||||
|
||||
- chore!: set engines to minimum Node.js v14.17.0 ([a1dbe6c](https://github.com/appium/appium/commit/a1dbe6c43efe76604943a607d402f4c8b864d652))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/docutils",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"description": "Documentation generation utilities for Appium and related projects",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -30,6 +30,7 @@
|
||||
"files": [
|
||||
"index.js",
|
||||
"index.d.ts",
|
||||
"base-mkdocs.yml",
|
||||
"lib",
|
||||
"build"
|
||||
],
|
||||
@@ -40,7 +41,7 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/support": "^3.0.0",
|
||||
"@appium/support": "^3.0.1",
|
||||
"source-map-support": "0.5.21",
|
||||
"teen_process": "2.0.2"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.3.1](https://github.com/appium/appium/compare/@appium/driver-test-support@0.3.0...@appium/driver-test-support@0.3.1) (2022-12-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **test-support:** update dependency sinon to v15.0.1 ([5d0d4f5](https://github.com/appium/appium/commit/5d0d4f5a44dff362430159f97f8569a9be7e378d))
|
||||
- **types:** update dependency type-fest to v3.4.0 ([37f71c3](https://github.com/appium/appium/commit/37f71c327a7c1a6d882b5198af6fedc9e8d51496))
|
||||
|
||||
# [0.3.0](https://github.com/appium/appium/compare/@appium/driver-test-support@0.2.1...@appium/driver-test-support@0.3.0) (2022-12-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/driver-test-support",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"description": "Test utilities for Appium drivers",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -39,16 +39,16 @@
|
||||
},
|
||||
"types": "./build/lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@appium/types": "^0.6.0",
|
||||
"@appium/types": "^0.7.0",
|
||||
"@types/lodash": "4.14.191",
|
||||
"axios": "1.2.1",
|
||||
"bluebird": "3.7.2",
|
||||
"chai": "4.3.7",
|
||||
"get-port": "5.1.1",
|
||||
"lodash": "4.17.21",
|
||||
"sinon": "15.0.0",
|
||||
"sinon": "15.0.1",
|
||||
"source-map-support": "0.5.21",
|
||||
"type-fest": "3.3.0"
|
||||
"type-fest": "3.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"appium": "^2.0.0-beta.43",
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.0.1](https://github.com/appium/appium/compare/@appium/execute-driver-plugin@3.0.0...@appium/execute-driver-plugin@3.0.1) (2022-12-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **execute-driver-plugin:** update dependency webdriverio to v7.28.0 ([f81e2e9](https://github.com/appium/appium/commit/f81e2e92eff25c33d36f767209f423227d288218))
|
||||
|
||||
# [3.0.0](https://github.com/appium/appium/compare/@appium/execute-driver-plugin@2.0.6...@appium/execute-driver-plugin@3.0.0) (2022-12-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/execute-driver-plugin",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.1",
|
||||
"description": "Plugin for batching and executing driver commands with Appiums",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -40,7 +40,7 @@
|
||||
"lodash": "4.17.21",
|
||||
"source-map-support": "0.5.21",
|
||||
"vm2": "3.9.13",
|
||||
"webdriverio": "7.27.0"
|
||||
"webdriverio": "7.28.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"appium": "^2.0.0-beta.35"
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.0.1](https://github.com/appium/appium/compare/@appium/fake-plugin@3.0.0...@appium/fake-plugin@3.0.1) (2022-12-21)
|
||||
|
||||
**Note:** Version bump only for package @appium/fake-plugin
|
||||
|
||||
# [3.0.0](https://github.com/appium/appium/compare/@appium/fake-plugin@2.1.3...@appium/fake-plugin@3.0.0) (2022-12-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/fake-plugin",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.1",
|
||||
"description": "A fake Appium 2.0 plugin",
|
||||
"homepage": "https://appium.io",
|
||||
"bugs": {
|
||||
@@ -28,8 +28,8 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/base-plugin": "^2.0.0",
|
||||
"@appium/support": "^3.0.0",
|
||||
"@appium/base-plugin": "^2.0.1",
|
||||
"@appium/support": "^3.0.1",
|
||||
"@types/bluebird": "3.5.38",
|
||||
"bluebird": "3.7.2",
|
||||
"lodash": "4.17.21",
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"node": "12"
|
||||
},
|
||||
"shippedProposals": true
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": ["source-map-support"],
|
||||
"comments": false,
|
||||
"sourceMaps": "both",
|
||||
"env": {
|
||||
"coverage": {
|
||||
"plugins": [
|
||||
[
|
||||
"istanbul",
|
||||
{
|
||||
"exclude": ["test", "build"]
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"test": {
|
||||
"retainLines": true,
|
||||
"comments": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
require: [require.resolve('./test/setup.js')],
|
||||
// forbids use of .only() in CI
|
||||
forbidOnly: Boolean(process.env.CI),
|
||||
// increase default timeout for CI since it can be slow
|
||||
timeout: process.env.CI ? '5s' : '2s',
|
||||
};
|
||||
@@ -1,151 +0,0 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [8.0.0](https://github.com/appium/appium/compare/@appium/gulp-plugins@7.0.8...@appium/gulp-plugins@8.0.0) (2022-12-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **gulp-plugins:** update babel monorepo to v7.20.2 ([6e3bfb8](https://github.com/appium/appium/commit/6e3bfb8847db8fc906c09ba01a5dc11636ba3ab1))
|
||||
- **gulp-plugins:** update definitelytyped ([0ca2bde](https://github.com/appium/appium/commit/0ca2bdecf23965c5d454f47289983f4d52568eaa))
|
||||
- **gulp-plugins:** update dependency @babel/core to v7.20.5 ([02ad17b](https://github.com/appium/appium/commit/02ad17bce626c7c308b835804209818a9e9a0880))
|
||||
- **gulp-plugins:** update dependency eslint to v8.27.0 ([e70e672](https://github.com/appium/appium/commit/e70e6727964e2fb5f46f6085e59d38cfd21b9839))
|
||||
- **gulp-plugins:** update dependency eslint to v8.28.0 ([e054f58](https://github.com/appium/appium/commit/e054f58c5056369d3e311f4102444db68be1dc57))
|
||||
- **gulp-plugins:** update dependency eslint to v8.29.0 ([cf34e4c](https://github.com/appium/appium/commit/cf34e4c17ac8867a3614124016817dc0060c99b0))
|
||||
- **gulp-plugins:** update dependency mocha to v10.2.0 ([c787bd6](https://github.com/appium/appium/commit/c787bd6fb5fb18a37cc9a4a5be1b2aa65eaa114a))
|
||||
- **gulp-plugins:** update dependency plugin-error to v2.0.1 ([37ec293](https://github.com/appium/appium/commit/37ec2938d6ce42cf12a4217e5273f1e55a290553))
|
||||
- **gulp-plugins:** update dependency yargs to v17.6.1 ([ea1b6a3](https://github.com/appium/appium/commit/ea1b6a3334ee5330abd456a7d4467fc92d94270d))
|
||||
- **gulp-plugins:** update dependency yargs to v17.6.2 ([a95b2e0](https://github.com/appium/appium/commit/a95b2e0fe6fc646ad5e1fb3479b448ad3a66c880))
|
||||
- **support:** update dependency axios to v1.2.0 ([b80b88b](https://github.com/appium/appium/commit/b80b88bd9cf2d6325ea6104449170b8339bf23e0))
|
||||
- **support:** update dependency axios to v1.2.1 ([07d6ef6](https://github.com/appium/appium/commit/07d6ef6b8cc1608da8860f601a80ec0f6a7a7598))
|
||||
|
||||
- chore!: set engines to minimum Node.js v14.17.0 ([a1dbe6c](https://github.com/appium/appium/commit/a1dbe6c43efe76604943a607d402f4c8b864d652))
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
- Appium now supports version range `^14.17.0 || ^16.13.0 || >=18.0.0`
|
||||
|
||||
## [7.0.8](https://github.com/appium/appium/compare/@appium/gulp-plugins@7.0.7...@appium/gulp-plugins@7.0.8) (2022-10-14)
|
||||
|
||||
**Note:** Version bump only for package @appium/gulp-plugins
|
||||
|
||||
## [7.0.7](https://github.com/appium/appium/compare/@appium/gulp-plugins@7.0.6...@appium/gulp-plugins@7.0.7) (2022-10-13)
|
||||
|
||||
**Note:** Version bump only for package @appium/gulp-plugins
|
||||
|
||||
## [7.0.6](https://github.com/appium/appium/compare/@appium/gulp-plugins@7.0.5...@appium/gulp-plugins@7.0.6) (2022-09-07)
|
||||
|
||||
**Note:** Version bump only for package @appium/gulp-plugins
|
||||
|
||||
## [7.0.5](https://github.com/appium/appium/compare/@appium/gulp-plugins@7.0.4...@appium/gulp-plugins@7.0.5) (2022-08-10)
|
||||
|
||||
**Note:** Version bump only for package @appium/gulp-plugins
|
||||
|
||||
## [7.0.4](https://github.com/appium/appium/compare/@appium/gulp-plugins@7.0.3...@appium/gulp-plugins@7.0.4) (2022-08-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **appium,base-driver,base-plugin,doctor,docutils,eslint-config-appium,execute-driver-plugin,fake-driver,fake-plugin,gulp-plugins,images-plugin,opencv,relaxed-caps-plugin,schema,support,test-support,types,universal-xml-plugin:** update engines ([d8d2382](https://github.com/appium/appium/commit/d8d2382327ba7b7db8a4d1cad987c0e60184c92d))
|
||||
|
||||
## [7.0.3](https://github.com/appium/appium/compare/@appium/gulp-plugins@7.0.2...@appium/gulp-plugins@7.0.3) (2022-07-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- moved type packages to deps of specific packages ([f9129df](https://github.com/appium/appium/commit/f9129dfee32fcc3f89ffcfa69fb83b7c2419c24f))
|
||||
|
||||
## [7.0.2](https://github.com/appium/appium/compare/@appium/gulp-plugins@7.0.1...@appium/gulp-plugins@7.0.2) (2022-05-31)
|
||||
|
||||
**Note:** Version bump only for package @appium/gulp-plugins
|
||||
|
||||
## [7.0.1](https://github.com/appium/appium/compare/@appium/gulp-plugins@7.0.0...@appium/gulp-plugins@7.0.1) (2022-05-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **appium:** fix extension autoinstall postinstall script ([3e2c05d](https://github.com/appium/appium/commit/3e2c05d8a290072484afde34fe5fd968618f6359)), closes [#16924](https://github.com/appium/appium/issues/16924)
|
||||
|
||||
# [7.0.0](https://github.com/appium/appium/compare/@appium/gulp-plugins@6.0.10...@appium/gulp-plugins@7.0.0) (2022-05-03)
|
||||
|
||||
### Features
|
||||
|
||||
- **eslint-config-appium,gulp-plugins:** add prettier ([878bb6a](https://github.com/appium/appium/commit/878bb6a44f85fd43e0f3678b95cddb8d7cbba69a))
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
- **eslint-config-appium,gulp-plugins:** `@appium/eslint-config-appium` now requires peer dependency `eslint-config-prettier`. Because `@appium/gulp-plugins` always uses the latest development version of `@appium/eslint-config-appium`, the dependency needs to be added there, too.
|
||||
|
||||
In addition, this disables some rules, so _may_ cause code which previously passed lint checks _not_ to pass lint checks.
|
||||
|
||||
## [6.0.10](https://github.com/appium/appium/compare/@appium/gulp-plugins@6.0.9...@appium/gulp-plugins@6.0.10) (2022-05-03)
|
||||
|
||||
**Note:** Version bump only for package @appium/gulp-plugins
|
||||
|
||||
## [6.0.9](https://github.com/appium/appium/compare/@appium/gulp-plugins@6.0.8...@appium/gulp-plugins@6.0.9) (2022-05-02)
|
||||
|
||||
**Note:** Version bump only for package @appium/gulp-plugins
|
||||
|
||||
## [6.0.8](https://github.com/appium/appium/compare/@appium/gulp-plugins@6.0.7...@appium/gulp-plugins@6.0.8) (2022-04-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/gulp-plugins
|
||||
|
||||
## [6.0.7](https://github.com/appium/appium/compare/@appium/gulp-plugins@6.0.6...@appium/gulp-plugins@6.0.7) (2022-04-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/gulp-plugins
|
||||
|
||||
## [6.0.6](https://github.com/appium/appium/compare/@appium/gulp-plugins@6.0.5...@appium/gulp-plugins@6.0.6) (2022-04-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/gulp-plugins
|
||||
|
||||
## [6.0.5](https://github.com/appium/appium/compare/@appium/gulp-plugins@6.0.4...@appium/gulp-plugins@6.0.5) (2022-04-12)
|
||||
|
||||
**Note:** Version bump only for package @appium/gulp-plugins
|
||||
|
||||
## [6.0.4](https://github.com/appium/appium/compare/@appium/gulp-plugins@6.0.3...@appium/gulp-plugins@6.0.4) (2022-04-12)
|
||||
|
||||
**Note:** Version bump only for package @appium/gulp-plugins
|
||||
|
||||
## [6.0.3](https://github.com/appium/appium/compare/@appium/gulp-plugins@6.0.2...@appium/gulp-plugins@6.0.3) (2022-04-07)
|
||||
|
||||
**Note:** Version bump only for package @appium/gulp-plugins
|
||||
|
||||
## [6.0.2](https://github.com/appium/appium/compare/@appium/gulp-plugins@6.0.1...@appium/gulp-plugins@6.0.2) (2022-03-22)
|
||||
|
||||
**Note:** Version bump only for package @appium/gulp-plugins
|
||||
|
||||
## [6.0.1](https://github.com/appium/appium/compare/@appium/gulp-plugins@6.0.0...@appium/gulp-plugins@6.0.1) (2022-01-11)
|
||||
|
||||
**Note:** Version bump only for package @appium/gulp-plugins
|
||||
|
||||
# [6.0.0](https://github.com/appium/appium/compare/@appium/gulp-plugins@5.5.5...@appium/gulp-plugins@6.0.0) (2021-11-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **gulp-plugins:** do not transpile pkg root .js files by default ([b3771b0](https://github.com/appium/appium/commit/b3771b00421669a96a830400d97561a15ff74632))
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
- **gulp-plugins:** Package-root `.js` files (typically `index.js`) are now (typically) cjs wrappers. These files needn't be transpiled, and in fact, doing so will create an invalid module because it will attempt to reference `build/whatever` but the resulting artifact would live in `build/index.js`. `build/index.js` will no longer exist.
|
||||
|
||||
## [5.5.5](https://github.com/appium/appium/compare/@appium/gulp-plugins@5.5.4...@appium/gulp-plugins@5.5.5) (2021-11-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **gulp-plugins:** fix potential race condition re: gulpfile.js ([0ef53d4](https://github.com/appium/appium/commit/0ef53d4e9907cdb6d66364890073c3ba8b900bc1))
|
||||
|
||||
## [5.5.4](https://github.com/appium/appium/compare/@appium/gulp-plugins@5.5.3...@appium/gulp-plugins@5.5.4) (2021-11-09)
|
||||
|
||||
**Note:** Version bump only for package @appium/gulp-plugins
|
||||
|
||||
## [5.5.3](https://github.com/appium/appium/compare/@appium/gulp-plugins@5.5.2...@appium/gulp-plugins@5.5.3) (2021-09-14)
|
||||
|
||||
**Note:** Version bump only for package @appium/gulp-plugins
|
||||
|
||||
## [5.5.2](https://github.com/appium/appium/compare/@appium/gulp-plugins@5.5.1...@appium/gulp-plugins@5.5.2) (2021-09-14)
|
||||
|
||||
**Note:** Version bump only for package @appium/gulp-plugins
|
||||
|
||||
## [5.5.1](https://github.com/appium/appium/compare/@appium/gulp-plugins@5.5.1-rc.0...@appium/gulp-plugins@5.5.1) (2021-08-16)
|
||||
|
||||
# 2.0.0-beta (2021-08-13)
|
||||
|
||||
**Note:** Version bump only for package @appium/gulp-plugins
|
||||
@@ -1,199 +0,0 @@
|
||||
appium-gulp-plugins
|
||||
===================
|
||||
|
||||
Custom plugins used across appium modules
|
||||
|
||||
## status
|
||||
|
||||
[](https://travis-ci.org/appium/appium-gulp-plugins)
|
||||
|
||||
## boilerplate plugin
|
||||
|
||||
This plugin sets up all the other typical plugins we use with a simple
|
||||
configuration object.
|
||||
|
||||
### usage
|
||||
|
||||
Basically just set up the `boilerplate` plugin as follows:
|
||||
|
||||
```js
|
||||
let gulp = require('gulp'),
|
||||
boilerplate = require('@appium/gulp-plugins').boilerplate.use(gulp);
|
||||
|
||||
boilerplate({build: "My Project Name"});
|
||||
```
|
||||
|
||||
You can pass a lot of options to configure `boilerplate`. Here are the options
|
||||
along with their defaults (from `lib/boilerplate.js`):
|
||||
|
||||
```js
|
||||
const DEFAULT_OPTS = {
|
||||
files: ['*.js', 'lib/**/*.js', 'test/**/*.js', '!gulpfile.js'],
|
||||
transpile: true,
|
||||
transpileOut: 'build',
|
||||
babelOpts: {},
|
||||
linkBabelRuntime: true,
|
||||
watch: true,
|
||||
watchE2E: false,
|
||||
test: {
|
||||
files: ['${testDir}/**/*-specs.js', '!${testDir}/**/*-e2e-specs.js'],
|
||||
traceWarnings: false,
|
||||
},
|
||||
coverage: {
|
||||
files: ['./build/test/**/*-specs.js', '!./build/test/**/*-e2e-specs.js'],
|
||||
verbose: true,
|
||||
},
|
||||
'coverage-e2e': {
|
||||
files: ['./build/test/**/*-e2e-specs.js'],
|
||||
verbose: true,
|
||||
},
|
||||
e2eTest: {
|
||||
files: ['${testDir}/**/*-e2e-specs.js'],
|
||||
forceExit: false,
|
||||
traceWarnings: false,
|
||||
},
|
||||
testReporter: (process.env.TRAVIS || process.env.CI) ? 'spec' : 'nyan',
|
||||
testTimeout: 20000,
|
||||
build: 'Appium',
|
||||
extraPrepublishTasks: [],
|
||||
eslint: true,
|
||||
eslintOnWatch: false, // deprecated, move to lintOnWatch
|
||||
lintOnWatch: false,
|
||||
ci: {
|
||||
interval: 60000,
|
||||
owner: 'appium',
|
||||
repo: 'appium-build-store',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
As you can see, it defaults to transpiling with Babel, running `eslint`
|
||||
running tests, and with the default task being `gulp watch`.
|
||||
|
||||
## transpile plugin
|
||||
|
||||
Babel compilation, sourcemaps and file renaming functionality in
|
||||
one plugin. `.es7.js` and `.es6.js` files will be automatically renamed to `.js
|
||||
files`. The necessary sourcemaps, comments and imports are also
|
||||
automatically added.
|
||||
|
||||
### usage
|
||||
|
||||
1/ Configure gulp as below:
|
||||
|
||||
``` js
|
||||
let gulp = require('gulp'),
|
||||
Transpiler = require('appium-gulp-plugins').Transpiler;
|
||||
|
||||
gulp.task('transpile', function () {
|
||||
let transpiler = new Transpiler();
|
||||
// babel options are configurable in transpiler.babelOpts
|
||||
|
||||
return gulp.src('test/fixtures/es7/**/*.js')
|
||||
.pipe(transpiler.stream())
|
||||
.pipe(gulp.dest('build'));
|
||||
});
|
||||
```
|
||||
|
||||
2/ in your code you need to mark the main and mocha files as below:
|
||||
|
||||
- main: add `// transpile:main` at the beginning of the file ([example here](https://github.com/appium/appium-gulp-plugins/blob/master/test/fixtures/es7/lib/run.es7.js)) .
|
||||
- mocha: add `// transpile:mocha` at the beginning of the file ([example here](https://github.com/appium/appium-gulp-plugins/blob/master/test/fixtures/es7/test/a-specs.es7.js))
|
||||
|
||||
Regular lib files do not need any extra comments.
|
||||
|
||||
## watch plugin
|
||||
|
||||
There are some issues with Gulp 3.x error handling which cause the default
|
||||
gulp-watch to hang. This plugin is a small hack which solves that by respawning
|
||||
the whole process on error. This should not be needed in gulp 4.0.
|
||||
|
||||
Files in the `/test` directory that are named `.*-specs.js` are run. Tests which end in `.*-e2e-specs.js` are *not* run when watching. To run end-to-end tests, run `gulp e2e-test:run`.
|
||||
|
||||
### usage
|
||||
|
||||
```js
|
||||
const gulp = require('gulp');
|
||||
const spawnWatcher = require('./index').spawnWatcher.use(gulp, opts);
|
||||
|
||||
spawnWatcher.configure('watch', ['lib/**/*.js','test/**/*.js','!test/fixtures'], function () {
|
||||
// this is the watch action
|
||||
return runSequence('test');
|
||||
});
|
||||
```
|
||||
|
||||
The test function in `spawnWatcher.configure` should return a promise.
|
||||
|
||||
### error handling
|
||||
|
||||
The spawn needs to catch error as soon as they happen. To do so use the
|
||||
`spawnWatcher.handleError` method, for instance:
|
||||
|
||||
```js
|
||||
// add error handling where needed
|
||||
gulp.task('transpile', function () {
|
||||
return gulp.src('test/es7/**/*.js')
|
||||
.pipe(transpile())
|
||||
.on('error', spawnWatcher.handleError)
|
||||
.pipe(gulp.dest('build'));
|
||||
});
|
||||
|
||||
gulp.task('test', ['transpile'] , function () {
|
||||
return gulp.src('build/test/a-specs.js')
|
||||
.pipe(mocha())
|
||||
.on('error', spawnWatcher.handleError);
|
||||
});
|
||||
```
|
||||
|
||||
### notification
|
||||
|
||||
Native notification is enabled by default. To disable it use the
|
||||
`--no-notif` option.
|
||||
|
||||
### collate logging and tests
|
||||
Set the environment variable `_FORCE_LOGS`
|
||||
|
||||
## iOS app building
|
||||
|
||||
This package can create `gulp` tasks for building and cleaning iOS apps. By
|
||||
providing an `iosApps` property in the options sent to `boilerplate()` with the
|
||||
following:
|
||||
```js
|
||||
iosApps: {
|
||||
relativeLocations: {
|
||||
iphoneos: 'relative/path/to/device/app.app',
|
||||
iphonesimulator: 'relative/path/to/sim/app.app',
|
||||
},
|
||||
appName: 'AppName.app',
|
||||
},
|
||||
```
|
||||
This will create a number of `gulp` tasks, centrally one named
|
||||
`ios-apps:install`, which will go through the process of cleaning the build,
|
||||
building, and putting the products into deterministic places.
|
||||
|
||||
The simulator app will be build in `build/Release-iphonesimulator/<appName>`,
|
||||
and the real device app will be in `build/Release-iphoneos/<appName>`.
|
||||
|
||||
To build for real devices, set the environment variable `IOS_REAL_DEVICE` or
|
||||
`REAL_DEVICE`. To specify an `xcconfig` file, set its location in the
|
||||
`XCCONFIG_FILE` environment variable.
|
||||
|
||||
|
||||
## hacking this package
|
||||
|
||||
###
|
||||
|
||||
Since this package is used to build and test all the Appium packages, it should
|
||||
not _use_ any of those packages.
|
||||
|
||||
### watch
|
||||
|
||||
```
|
||||
npm run watch
|
||||
```
|
||||
|
||||
### test
|
||||
|
||||
```
|
||||
npm test
|
||||
```
|
||||
@@ -1,25 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const boilerplate = require('./index').boilerplate.use(require('gulp'));
|
||||
|
||||
require('./test/gulpfile-js');
|
||||
|
||||
boilerplate({
|
||||
transpile: true,
|
||||
files: ['index.js', 'lib/**/*.js', 'test/**/*.js', '!test/fixtures/**', '!test/generated/**'],
|
||||
test: {
|
||||
files: ['${testDir}/**/*-specs.js', '!${testDir}/fixtures', '!${testDir}/**/*-e2e-specs.js'],
|
||||
},
|
||||
coverage: {
|
||||
files: [
|
||||
'./build/test/**/*-specs.js',
|
||||
'!./build/test/fixtures',
|
||||
'!./build/test/**/*-e2e-specs.js',
|
||||
'!./build/test/generated',
|
||||
],
|
||||
verbose: true,
|
||||
},
|
||||
build: 'Appium Gulp Plugins',
|
||||
extraDefaultTasks: ['e2e-test', 'test-transpile-lots-of-files'],
|
||||
testReporter: 'dot',
|
||||
});
|
||||
@@ -1,4 +0,0 @@
|
||||
exports.Transpiler = require('./lib/transpiler');
|
||||
exports.spawnWatcher = require('./lib/spawn-watcher');
|
||||
exports.boilerplate = require('./lib/boilerplate');
|
||||
exports.isVerbose = require('./lib/utils').isVerbose;
|
||||
@@ -1,132 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const tasks = require('./tasks/');
|
||||
const log = require('fancy-log');
|
||||
|
||||
if (process.env.TRAVIS || process.env.CI) {
|
||||
process.env.REAL_DEVICE = 0;
|
||||
}
|
||||
|
||||
const DEFAULT_OPTS = {
|
||||
files: ['lib/**/*.js', 'test/**/*.js', '!gulpfile.js'],
|
||||
transpile: true,
|
||||
transpileOut: 'build',
|
||||
babelOpts: {},
|
||||
linkBabelRuntime: true,
|
||||
watch: true,
|
||||
watchE2E: false,
|
||||
test: {
|
||||
files: ['${testDir}/**/*-specs.js', '!${testDir}/**/*-e2e-specs.js'],
|
||||
traceWarnings: false,
|
||||
},
|
||||
coverage: {
|
||||
files: ['./build/test/**/*-specs.js', '!./build/test/**/*-e2e-specs.js'],
|
||||
verbose: true,
|
||||
},
|
||||
'coverage-e2e': {
|
||||
files: ['./build/test/**/*-e2e-specs.js'],
|
||||
verbose: true,
|
||||
},
|
||||
e2eTest: {
|
||||
files: ['${testDir}/**/*-e2e-specs.js'],
|
||||
forceExit: false,
|
||||
traceWarnings: false,
|
||||
},
|
||||
testReporter: 'spec',
|
||||
testTimeout: 20000,
|
||||
build: 'Appium',
|
||||
extraPrepublishTasks: [],
|
||||
eslint: true,
|
||||
eslintOnWatch: false, // deprecated, move to lintOnWatch
|
||||
lintOnWatch: false,
|
||||
ci: {
|
||||
interval: 60000,
|
||||
owner: 'appium',
|
||||
repo: 'appium-build-store',
|
||||
},
|
||||
yamllint: true,
|
||||
yaml: {
|
||||
files: [
|
||||
'**/.*.yml',
|
||||
'**/*.yml',
|
||||
'**/.*.yaml',
|
||||
'**/*.yaml',
|
||||
'!test/**',
|
||||
'!node_modules/**',
|
||||
'!**/node_modules/**',
|
||||
],
|
||||
safe: false,
|
||||
},
|
||||
};
|
||||
|
||||
const boilerplate = function (gulp, opts) {
|
||||
opts = _.merge({}, DEFAULT_OPTS, opts);
|
||||
|
||||
if (!_.isEmpty(opts.buildName)) {
|
||||
log.warn(`The 'buildName' option is deprecated. Use 'build' instead`);
|
||||
opts.build = opts.buildName = opts.build || opts.buildName;
|
||||
}
|
||||
|
||||
const spawnWatcher = require('./spawn-watcher').use(gulp, opts);
|
||||
const rootDir = opts.transpile ? opts.transpileOut : '.';
|
||||
const fileAliases = {
|
||||
rootDir,
|
||||
testDir: `${rootDir}/test`,
|
||||
libDir: `${rootDir}/lib`,
|
||||
};
|
||||
|
||||
// configure the individual tasks
|
||||
tasks.configure(gulp, opts, {
|
||||
fileAliases,
|
||||
spawnWatcher,
|
||||
});
|
||||
|
||||
// conpute and define the default sequence of tasks
|
||||
let defaultSequence = [];
|
||||
if (opts.transpile) {
|
||||
defaultSequence.push('clean');
|
||||
}
|
||||
if (opts.eslint || opts.lint) {
|
||||
defaultSequence.push('lint');
|
||||
}
|
||||
if (opts.transpile && !opts.test) {
|
||||
defaultSequence.push('transpile');
|
||||
}
|
||||
if (opts.test) {
|
||||
if (opts.watchE2E) {
|
||||
defaultSequence.push('test');
|
||||
} else {
|
||||
defaultSequence.push('unit-test');
|
||||
}
|
||||
}
|
||||
if (opts.extraDefaultTasks) {
|
||||
defaultSequence.push(...opts.extraDefaultTasks);
|
||||
}
|
||||
|
||||
if (opts.watch) {
|
||||
if (opts.eslintOnWatch) {
|
||||
log.warn(`The 'eslintOnWatch' option is deprecated. Use 'lintOnWatch' instead`);
|
||||
opts.lintOnWatch = true;
|
||||
}
|
||||
const watchSequence = opts.lintOnWatch
|
||||
? defaultSequence
|
||||
: defaultSequence.filter(function filterLintTasks(step) {
|
||||
return step !== 'lint';
|
||||
});
|
||||
spawnWatcher.configure('watch', opts.files, watchSequence);
|
||||
}
|
||||
|
||||
spawnWatcher.configure('dev', opts.files, ['transpile']);
|
||||
gulp.task('once', gulp.series(...defaultSequence));
|
||||
gulp.task('default', gulp.series(opts.watch ? 'watch' : 'once'));
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
DEFAULTS: _.cloneDeep(DEFAULT_OPTS),
|
||||
use(gulp) {
|
||||
return function callBoilerplate(opts) {
|
||||
boilerplate(gulp, opts);
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const sourcemaps = require('gulp-sourcemaps');
|
||||
const replace = require('gulp-replace');
|
||||
const {EOL} = require('os');
|
||||
|
||||
const SOURCEMAP_OPTS = {
|
||||
sourceRoot(file) {
|
||||
// Point to source root relative to the transpiled file
|
||||
return path.relative(path.join(file.cwd, file.path), file.base);
|
||||
},
|
||||
includeContent: true,
|
||||
};
|
||||
|
||||
const HEADER = `require('source-map-support').install();${EOL + EOL}`;
|
||||
|
||||
module.exports = function getSourceMapFns(opts = {}) {
|
||||
const sourceMapOpts = Object.assign({}, SOURCEMAP_OPTS, opts);
|
||||
|
||||
return {
|
||||
sourceMapInit: sourcemaps.init(),
|
||||
sourceMapHeader: replace(/$/, HEADER),
|
||||
sourceMapWrite: sourcemaps.write(sourceMapOpts),
|
||||
};
|
||||
};
|
||||
@@ -1,79 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const log = require('fancy-log');
|
||||
const red = require('ansi-red');
|
||||
const notifier = require('node-notifier');
|
||||
const moment = require('moment');
|
||||
|
||||
const COLOR_CODE_REGEXP = /\u001b\[(\d+(;\d+)*)?m/g; // eslint-disable-line no-control-regex
|
||||
|
||||
module.exports = {
|
||||
use(gulp, opts = {}) {
|
||||
this.gulp = gulp;
|
||||
this.title = opts.build || 'Appium';
|
||||
|
||||
this.exitOnError = true;
|
||||
this.errored = false;
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
notify(subtitle, message) {
|
||||
if (process.argv.includes('--no-notif')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
notifier.notify({
|
||||
title: this.title,
|
||||
subtitle: `${subtitle} ${moment().format('h:mm:ss')}`,
|
||||
message,
|
||||
});
|
||||
} catch (ign) {
|
||||
log(`Notifier: [${this.title}] ${message}`);
|
||||
}
|
||||
},
|
||||
|
||||
notifyOK() {
|
||||
this.notify('Build success!', 'All Good!');
|
||||
},
|
||||
|
||||
handleError(err) {
|
||||
this.errored = true;
|
||||
|
||||
// log the error
|
||||
const strErr = `${err}`;
|
||||
for (const line of strErr.split('\n')) {
|
||||
log.error(red(line));
|
||||
}
|
||||
|
||||
// use the system notifier
|
||||
const notifyErr = strErr.replace(COLOR_CODE_REGEXP, '');
|
||||
this.notify('Build failure!', notifyErr);
|
||||
if (this.exitOnError) {
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
|
||||
configure(taskName, filePattern, sequence) {
|
||||
const notifyWatch = (done) => {
|
||||
if (!this.errored) {
|
||||
this.notifyOK();
|
||||
}
|
||||
this.errored = false;
|
||||
done();
|
||||
};
|
||||
this.gulp.task(taskName, () => {
|
||||
this.exitOnError = false;
|
||||
|
||||
return this.gulp.watch(
|
||||
filePattern,
|
||||
{
|
||||
ignoreInitial: false,
|
||||
ignored: '**/gulpfile.js',
|
||||
},
|
||||
this.gulp.series(sequence, notifyWatch)
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const through = require('through2');
|
||||
const EE = require('events').EventEmitter;
|
||||
|
||||
module.exports = function combine(pipeFn) {
|
||||
const inStream = through.obj();
|
||||
const outStream = pipeFn(inStream);
|
||||
const combinedStream = new EE(); // not a real stream, just pretending
|
||||
combinedStream.on('pipe', function onPipe(source) {
|
||||
source.unpipe(this);
|
||||
source.pipe(inStream);
|
||||
});
|
||||
combinedStream.pipe = function pipeFn(dest, options) {
|
||||
return outStream.pipe(dest, options);
|
||||
};
|
||||
return combinedStream;
|
||||
};
|
||||
@@ -1,215 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const log = require('fancy-log');
|
||||
const {sync: findRoot} = require('pkg-dir');
|
||||
const axios = require('axios');
|
||||
const B = require('bluebird');
|
||||
const os = require('os');
|
||||
const {Octokit} = require('@octokit/rest');
|
||||
const _ = require('lodash');
|
||||
const FormData = require('form-data');
|
||||
|
||||
const readFile = B.promisify(fs.readFile, {context: fs});
|
||||
const writeFile = B.promisify(fs.writeFile, {context: fs});
|
||||
|
||||
const ASSET_NAME_REGEXP = /^appium-\S+.zip$/;
|
||||
|
||||
const GITHUB_OWNER = 'appium';
|
||||
const GITHUB_REPO = 'appium-build-store';
|
||||
|
||||
const OUTPUT_INTERVAL = 60000;
|
||||
|
||||
const MOCHA_PARALLEL_TEST_BROKEN_LINE = `if (value.type === 'test') {`;
|
||||
const MOCHA_PARALLEL_TEST_FIXED_LINE = `if (value.type === 'test') {\n delete value.fn;`;
|
||||
|
||||
const configure = function configure(gulp, opts) {
|
||||
const owner = opts.ci.owner || GITHUB_OWNER;
|
||||
const repo = opts.ci.repo || GITHUB_REPO;
|
||||
|
||||
const root = opts.projectRoot ? opts.projectRoot : findRoot(__dirname);
|
||||
|
||||
/**
|
||||
* `mocha-parallel-tests` is broken at the moment, so skipped describe blocks fail
|
||||
* the fix is simple, and this patches the error until they fix the package
|
||||
**/
|
||||
gulp.task('fix-mocha-parallel-tests', async function fixMochaParallelTests() {
|
||||
log(`Updating 'mocha-parallel-tests'`);
|
||||
const filePath = path.resolve(
|
||||
root,
|
||||
'node_modules',
|
||||
'mocha-parallel-tests',
|
||||
'dist',
|
||||
'main',
|
||||
'util.js'
|
||||
);
|
||||
|
||||
log(`File: '${filePath}'`);
|
||||
|
||||
try {
|
||||
let script = await readFile(filePath, {encoding: 'utf8'});
|
||||
script = await script.replace(
|
||||
MOCHA_PARALLEL_TEST_BROKEN_LINE,
|
||||
MOCHA_PARALLEL_TEST_FIXED_LINE
|
||||
);
|
||||
await writeFile(filePath, script);
|
||||
} catch (err) {
|
||||
const msg = err.message.includes('ENOENT')
|
||||
? `File '${filePath}' does not exist`
|
||||
: err.message;
|
||||
log.error(`Unable to fix: ${msg}`);
|
||||
}
|
||||
});
|
||||
|
||||
gulp.task('github:upload', async function githubUpload() {
|
||||
const githubToken = process.env.GITHUB_TOKEN;
|
||||
if (_.isEmpty(githubToken)) {
|
||||
log.warn('No GitHub token found in GITHUB_TOKEN environment variable');
|
||||
return;
|
||||
}
|
||||
const octokit = new Octokit({auth: githubToken});
|
||||
|
||||
const buildName = process.env.BUILD_NAME || `${Date.now()}`;
|
||||
const commitMessage = process.env.COMMIT_MESSAGE || 'No commit message provided';
|
||||
|
||||
const releaseTag = `appium-build-${buildName}`;
|
||||
const releaseFile = `appium-${buildName}.zip`;
|
||||
|
||||
let releaseId;
|
||||
try {
|
||||
log(`Creating release on '${owner}/${repo}'`);
|
||||
const res = await octokit.repos.createRelease({
|
||||
owner,
|
||||
repo,
|
||||
tag_name: releaseTag,
|
||||
name: `Appium build ${buildName}`,
|
||||
body: `Appium build for commit ${buildName}\n'${commitMessage}'`,
|
||||
});
|
||||
|
||||
releaseId = res.data.id;
|
||||
log(`Created release '${releaseTag}' (id: ${releaseId})`);
|
||||
|
||||
const url = res.data.upload_url;
|
||||
const file = path.resolve(root, 'appium.zip');
|
||||
log(`Uploading file '${file}'`);
|
||||
await octokit.repos.uploadReleaseAsset({
|
||||
headers: {
|
||||
'content-length': fs.statSync(file).size,
|
||||
'content-type': 'application/zip',
|
||||
},
|
||||
url,
|
||||
file: fs.createReadStream(file),
|
||||
name: releaseFile,
|
||||
});
|
||||
log(`Uploaded release file '${releaseFile}'`);
|
||||
} catch (err) {
|
||||
log.error(`Error uploading release asset: ${err.message}`);
|
||||
if (err.errors) {
|
||||
log.error(JSON.stringify(err.errors, 2));
|
||||
}
|
||||
|
||||
if (releaseId) {
|
||||
log('Deleting release with no asset');
|
||||
await octokit.repos.deleteRelease({
|
||||
owner,
|
||||
repo,
|
||||
release_id: releaseId,
|
||||
});
|
||||
log('Release deleted');
|
||||
}
|
||||
throw new Error(`Error uploading release: ${err.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
gulp.task('github-upload', gulp.series(['github:upload']));
|
||||
|
||||
gulp.task('github:download', async function githubDownload() {
|
||||
const githubToken = process.env.GITHUB_TOKEN;
|
||||
if (_.isEmpty(githubToken)) {
|
||||
log.warn('No GitHub token found in GITHUB_TOKEN environment variable');
|
||||
return;
|
||||
}
|
||||
const octokit = new Octokit({auth: githubToken});
|
||||
|
||||
log.info('Downloading GitHub asset');
|
||||
const tempDir = os.tmpdir();
|
||||
log.info(`Temporary directory for download: '${tempDir}'`);
|
||||
|
||||
log.info(`Downloading repository: '${owner}/${repo}'`);
|
||||
const res = await octokit.repos.getLatestRelease({owner, repo});
|
||||
// go through the assets and find the correct one
|
||||
for (const asset of res.data.assets) {
|
||||
if (ASSET_NAME_REGEXP.test(asset.name)) {
|
||||
log.info(`Downloading asset from '${asset.browser_download_url}'`);
|
||||
const url = asset.browser_download_url;
|
||||
const writer = fs.createWriteStream(`${tempDir}/appium.zip`);
|
||||
const responseStream = (
|
||||
await axios({
|
||||
url,
|
||||
responseType: 'stream',
|
||||
})
|
||||
).data;
|
||||
responseStream.pipe(writer);
|
||||
|
||||
return await new B((resolve, reject) => {
|
||||
responseStream.once('error', reject);
|
||||
writer.once('finish', resolve);
|
||||
writer.once('error', (e) => {
|
||||
responseStream.unpipe(writer);
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
throw new Error(`Unable to find Appium build asset`);
|
||||
});
|
||||
|
||||
gulp.task('saucelabs:upload', async function sauceLabsUpload() {
|
||||
// Find the latest bundle
|
||||
log.info('Uploading to Sauce Storage');
|
||||
const tempDir = os.tmpdir();
|
||||
log.info(`Temporary directory for upload: '${tempDir}'`);
|
||||
const form = new FormData();
|
||||
form.append('file', fs.createReadStream(`${tempDir}/appium.zip`));
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `https://saucelabs.com/rest/v1/storage/${process.env.SAUCE_USERNAME}/appium.zip?overwrite=true`,
|
||||
headers: form.getHeaders(),
|
||||
auth: {
|
||||
username: process.env.SAUCE_USERNAME,
|
||||
password: process.env.SAUCE_ACCESS_KEY,
|
||||
},
|
||||
data: form,
|
||||
};
|
||||
const body = (await axios(options)).data;
|
||||
// username should not end up in the logs
|
||||
body.username = '*'.repeat((body.username || '').length);
|
||||
log.info(`File uploaded: ${JSON.stringify(body)}`);
|
||||
});
|
||||
|
||||
gulp.task('sauce-storage-upload', gulp.series(['github:download', 'saucelabs:upload']));
|
||||
|
||||
/**
|
||||
* This task is meant to be backgrounded, and killed using OS tools, so it
|
||||
* never finishes.
|
||||
* $ $(npm bin)/gulp periodic-output &
|
||||
* [1] 38060
|
||||
* [08:56:53] Using gulpfile ~/code/appium-xcuitest-driver/gulpfile.js
|
||||
* [08:56:53] Starting 'periodic-output'...
|
||||
* $ kill 38060
|
||||
* [1] + 38060 terminated $(npm bin)/gulp periodic-output
|
||||
*/
|
||||
gulp.task('periodic-output', function periodicOutput() {
|
||||
const interval = opts.ci.interval || OUTPUT_INTERVAL;
|
||||
return new B(function writeToStdout() {
|
||||
setInterval(function print() {
|
||||
process.stdout.write('.');
|
||||
}, interval);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
configure,
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const B = require('bluebird');
|
||||
const vinylPaths = require('vinyl-paths');
|
||||
const del = require('del');
|
||||
const debug = require('gulp-debug');
|
||||
const gulpIf = require('gulp-if');
|
||||
const {isVerbose} = require('../utils');
|
||||
|
||||
const configure = function configure(gulp, opts) {
|
||||
gulp.task('clean', function clean() {
|
||||
if (opts.transpile) {
|
||||
return gulp
|
||||
.src(opts.transpileOut, {
|
||||
read: false,
|
||||
allowEmpty: true,
|
||||
})
|
||||
.pipe(gulpIf(isVerbose(), debug()))
|
||||
.pipe(vinylPaths(del));
|
||||
} else {
|
||||
return B.resolve();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
configure,
|
||||
};
|
||||
@@ -1,91 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const B = require('bluebird');
|
||||
const globby = require('globby');
|
||||
const _ = require('lodash');
|
||||
const cp = require('child_process');
|
||||
const spawn = cp.spawn;
|
||||
const exec = B.promisify(cp.exec);
|
||||
const path = require('path');
|
||||
const utils = require('../utils');
|
||||
const log = require('fancy-log');
|
||||
|
||||
const configure = function configure(gulp, opts, env) {
|
||||
let npmBin;
|
||||
gulp.task('npm-bin', async function getNpmBin() {
|
||||
if (npmBin) {
|
||||
return B.resolve();
|
||||
}
|
||||
let bin = await exec('npm bin');
|
||||
if (Array.isArray(bin)) {
|
||||
bin = bin[0];
|
||||
}
|
||||
npmBin = bin.trim(); // eslint-disable-line require-atomic-updates
|
||||
log(`Determined npm bin: ${npmBin}`);
|
||||
});
|
||||
|
||||
const doCoverage = function doCoverage(taskName, filePatterns, targetDir) {
|
||||
const subTaskName = `${taskName}:run`;
|
||||
const covTestFiles = utils.translatePaths([filePatterns], env.fileAliases);
|
||||
gulp.task(subTaskName, async function doSubTask() {
|
||||
const files = await globby(covTestFiles);
|
||||
const bins = ['nyc', '_mocha'].reduce(function getFullPaths(bins, item) {
|
||||
bins[item] = path.resolve(npmBin, item);
|
||||
return bins;
|
||||
}, {});
|
||||
const args = [
|
||||
'--reporter=lcov',
|
||||
'--reporter=text-lcov', // Coveralls consumes lcov and text-lcov reports
|
||||
'--reporter=cobertura', // MS Azure consumes Cobertura coverage reports
|
||||
`--report-dir=${targetDir}`,
|
||||
'--exclude-after-remap=false',
|
||||
bins._mocha,
|
||||
'--reporter=dot',
|
||||
...files,
|
||||
'--exit',
|
||||
];
|
||||
let env = _.clone(process.env);
|
||||
env.NO_PRECOMPILE = 1;
|
||||
env._TESTING = 1;
|
||||
env.NODE_ENV = 'coverage';
|
||||
log(`Running command: ${bins.nyc} ${args.join(' ')}`);
|
||||
return new B(function runCmd(resolve, reject) {
|
||||
const proc = spawn(bins.nyc, args, {
|
||||
env,
|
||||
stdio: opts.coverage.verbose ? 'inherit' : 'ignore',
|
||||
});
|
||||
proc.on('close', function onClose(code) {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Coverage command exit code: ${code}`));
|
||||
}
|
||||
});
|
||||
proc.on('error', function onError(err) {
|
||||
reject(new Error(`Coverage error: ${err}`));
|
||||
});
|
||||
});
|
||||
});
|
||||
gulp.task(taskName, gulp.series('clean', 'transpile', 'npm-bin', subTaskName));
|
||||
};
|
||||
|
||||
if (opts.coverage) {
|
||||
doCoverage('coverage', opts.coverage.files, 'coverage');
|
||||
['coveralls:run', 'coveralls'].map((taskName) =>
|
||||
gulp.task(taskName, function reportDeprecatedCoveralls() {
|
||||
log(
|
||||
`Coveralls integration has been removed as per ` +
|
||||
`https://github.com/appium/appium/issues/14648. ` +
|
||||
`Nothing will be done in scope of '${taskName}' task`
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
if (opts['coverage-e2e']) {
|
||||
doCoverage('coverage-e2e', opts['coverage-e2e'].files, 'coverage-e2e');
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
configure,
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const mocha = require('gulp-mocha');
|
||||
const B = require('bluebird');
|
||||
const globby = require('globby');
|
||||
const debug = require('gulp-debug');
|
||||
const gulpIf = require('gulp-if');
|
||||
const log = require('fancy-log');
|
||||
const utils = require('../utils');
|
||||
|
||||
const {isVerbose} = utils;
|
||||
|
||||
const configure = function configure(gulp, opts, env) {
|
||||
const e2eTestFiles = utils.translatePaths(
|
||||
[opts.e2eTest.files || opts.e2eTestFiles],
|
||||
env.fileAliases
|
||||
);
|
||||
gulp.task('e2e-test:run', async function e2eTestRun() {
|
||||
const mochaOpts = {
|
||||
reporter: utils.getTestReporter(opts),
|
||||
timeout: opts.testTimeout,
|
||||
require: opts.testRequire || [],
|
||||
exit: true,
|
||||
color: true,
|
||||
traceWarnings: opts.e2eTest.traceWarnings,
|
||||
traceDeprecation: opts.e2eTest.traceWarnings,
|
||||
};
|
||||
// set env so our code knows when it's being run in a test env
|
||||
process.env._TESTING = 1;
|
||||
|
||||
const mochaCmd = function () {
|
||||
return new B(function runCmd(resolve, reject) {
|
||||
gulp
|
||||
.src(e2eTestFiles, {read: true, allowEmpty: true})
|
||||
.pipe(gulpIf(isVerbose(), debug()))
|
||||
.pipe(mocha(mochaOpts))
|
||||
.on('error', function onError(err) {
|
||||
reject(err);
|
||||
})
|
||||
.once('_result', function onResult(...args) {
|
||||
resolve(...args);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const files = await globby(e2eTestFiles);
|
||||
// gulp-mocha has an issue where, if there are no files passed from gulp.src,
|
||||
// it will just run everything it finds
|
||||
if (!files.length) {
|
||||
log(`No e2e test files found using '${e2eTestFiles}'`);
|
||||
return;
|
||||
}
|
||||
await mochaCmd();
|
||||
} finally {
|
||||
if (opts.e2eTest.forceExit) {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
gulp.task('e2e-test', gulp.series(env.testDeps, 'e2e-test:run'));
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
configure,
|
||||
};
|
||||
@@ -1,65 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const argv = require('yargs').argv;
|
||||
const replace = require('replace-in-file');
|
||||
const log = require('fancy-log');
|
||||
const semver = require('semver');
|
||||
const globby = require('globby');
|
||||
|
||||
function logFileChanges(changes = []) {
|
||||
// `changes` will have entries like
|
||||
// { file: "app/build.gradle", hasChanged: true }
|
||||
changes = changes.filter((entry) => entry.hasChanged).map((entry) => entry.file);
|
||||
log(`Updated files: ${changes.join(', ')}`);
|
||||
}
|
||||
|
||||
const configure = function configure(gulp) {
|
||||
gulp.task('gradle-version-update', async function gradleVersionUpdate() {
|
||||
const files = await globby(['app/build.gradle']);
|
||||
if (!files.length) {
|
||||
throw new Error('No app/build.gradle file found');
|
||||
}
|
||||
const gradleFile = files[0];
|
||||
|
||||
const version = argv['package-version'];
|
||||
if (!version) {
|
||||
throw new Error('No package version argument (use `--package-version=xxx`)');
|
||||
}
|
||||
if (!semver.valid(version)) {
|
||||
throw new Error(
|
||||
`Invalid version specified '${version}'. Version should be in the form '1.2.3'`
|
||||
);
|
||||
}
|
||||
|
||||
let changedFiles = await replace({
|
||||
files: gradleFile,
|
||||
from: /^\s*versionName\s+['"](.+)['"]$/gm,
|
||||
to: (match) => {
|
||||
log(`Updating gradle build file to version name '${version}'`);
|
||||
// match will be like `versionName '1.2.3'`
|
||||
return match.replace(/\d+\.\d+\.\d+/, version);
|
||||
},
|
||||
});
|
||||
logFileChanges(changedFiles);
|
||||
|
||||
changedFiles = await replace({
|
||||
files: gradleFile,
|
||||
from: /^\s*versionCode\s+(.+)$/gm,
|
||||
to: (match) => {
|
||||
// match will be like `versionCode 42`
|
||||
const codeMatch = /\d+/.exec(match.trim());
|
||||
if (!codeMatch) {
|
||||
throw new Error('Unable to find existing version code');
|
||||
}
|
||||
const code = parseInt(codeMatch[0], 10) + 1;
|
||||
log(`Updating gradle build file to version code '${code}'`);
|
||||
return match.replace(/\d+/, code);
|
||||
},
|
||||
});
|
||||
logFileChanges(changedFiles);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
configure,
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const coverage = require('./coverage');
|
||||
const lint = require('./lint');
|
||||
const test = require('./test');
|
||||
const e2eTest = require('./e2e-test');
|
||||
const unitTest = require('./unit-test');
|
||||
const clean = require('./clean');
|
||||
const transpile = require('./transpile');
|
||||
const gradle = require('./gradle');
|
||||
const ci = require('./ci');
|
||||
const iosApps = require('./ios-apps');
|
||||
const prepublish = require('./prepublish');
|
||||
|
||||
const configure = function configure(gulp, opts, env) {
|
||||
clean.configure(gulp, opts);
|
||||
|
||||
transpile.configure(gulp, opts, env);
|
||||
|
||||
test.configure(gulp, opts, env);
|
||||
|
||||
coverage.configure(gulp, opts, env);
|
||||
|
||||
lint.configure(gulp, opts);
|
||||
|
||||
gradle.configure(gulp, opts);
|
||||
|
||||
ci.configure(gulp, opts);
|
||||
|
||||
iosApps.configure(gulp, opts);
|
||||
|
||||
// make sure this is last, os that all the tasks are available as extra
|
||||
// prepublish tasks with the `extraPrepublishTasks` option
|
||||
prepublish.configure(gulp, opts);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
configure,
|
||||
coverage,
|
||||
lint,
|
||||
e2eTest,
|
||||
unitTest,
|
||||
clean,
|
||||
transpile,
|
||||
gradle,
|
||||
ci,
|
||||
prepublish,
|
||||
};
|
||||
@@ -1,162 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const log = require('fancy-log');
|
||||
const _ = require('lodash');
|
||||
const {exec} = require('../utils');
|
||||
const B = require('bluebird');
|
||||
const fs = require('fs');
|
||||
const rimraf = B.promisify(require('rimraf'));
|
||||
|
||||
const renameFile = B.promisify(fs.rename, {context: fs});
|
||||
|
||||
const MAX_BUFFER_SIZE = 524288;
|
||||
|
||||
const REAL_DEVICE_FLAGS = ['IOS_REAL_DEVICE', 'REAL_DEVICE'];
|
||||
|
||||
function configure(gulp, opts) {
|
||||
if (_.isEmpty(opts.iosApps)) {
|
||||
// nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
// extract the paths from the enclosing package configuration
|
||||
const relativeLocations = opts.iosApps.relativeLocations;
|
||||
|
||||
// figure out where things will go in the end
|
||||
const SDKS = {
|
||||
iphonesimulator: {
|
||||
name: 'iphonesimulator',
|
||||
buildPath: path.resolve('build', 'Release-iphonesimulator', opts.iosApps.appName),
|
||||
finalPath: relativeLocations.iphonesimulator,
|
||||
},
|
||||
iphoneos: {
|
||||
name: 'iphoneos',
|
||||
buildPath: path.resolve('build', 'Release-iphoneos', opts.iosApps.appName),
|
||||
finalPath: relativeLocations.iphoneos,
|
||||
},
|
||||
};
|
||||
|
||||
// the sdks against which we will build
|
||||
let sdks = ['iphonesimulator'];
|
||||
|
||||
let sdkVer;
|
||||
async function getIOSSDK() {
|
||||
if (!sdkVer) {
|
||||
try {
|
||||
const {stdout} = await exec('xcrun', ['--sdk', 'iphonesimulator', '--show-sdk-version']);
|
||||
sdkVer = stdout.trim(); // eslint-disable-line require-atomic-updates
|
||||
} catch (err) {
|
||||
log(`Unable to get max iOS SDK: ${err.message}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
return sdkVer;
|
||||
}
|
||||
|
||||
function logErrorLines(str = '') {
|
||||
str = `${str}`;
|
||||
for (const line of str.split('\n')) {
|
||||
log.error(` ${line}`);
|
||||
}
|
||||
}
|
||||
|
||||
function logError(err, prefix = 'Failed:') {
|
||||
log.error(`${prefix}: ${err.message}`);
|
||||
log.error('Stdout:');
|
||||
logErrorLines(err.stdout);
|
||||
log.error('Stderr:');
|
||||
logErrorLines(err.stderr);
|
||||
}
|
||||
|
||||
async function cleanApp(appRoot, sdk) {
|
||||
log(`Cleaning app for ${sdk} at app root '${appRoot}'`);
|
||||
try {
|
||||
const cmd = 'xcodebuild';
|
||||
const args = ['-sdk', sdk, 'clean'];
|
||||
log(` Executing command '${cmd} ${args.join(' ')}'`);
|
||||
await exec(cmd, args, {cwd: appRoot, maxBuffer: MAX_BUFFER_SIZE});
|
||||
} catch (err) {
|
||||
logError(err, 'Failed cleaning app');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
gulp.task('ios-apps:sdks', function findSDKs(done) {
|
||||
// determine if the real device sdk should be used, too
|
||||
for (const flag of REAL_DEVICE_FLAGS) {
|
||||
const value = process.env[flag];
|
||||
if (!_.isEmpty(value) && !!parseInt(value, 10)) {
|
||||
log(
|
||||
`Enabling real device build because '${flag}' environment variable set (value is '${value}')`
|
||||
);
|
||||
sdks.push('iphoneos');
|
||||
}
|
||||
}
|
||||
log(`SDKs to process: ${sdks.map((sdk) => `'${sdk}'`).join(', ')}`);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task('ios-apps:clean', async function cleanAll() {
|
||||
log('Cleaning all sdks');
|
||||
const sdkVer = await getIOSSDK();
|
||||
for (const sdk of sdks) {
|
||||
await cleanApp('.', sdk + sdkVer);
|
||||
}
|
||||
|
||||
log('Deleting all apps');
|
||||
const apps = [
|
||||
SDKS.iphonesimulator.buildPath,
|
||||
SDKS.iphonesimulator.finalPath,
|
||||
SDKS.iphoneos.buildPath,
|
||||
SDKS.iphoneos.finalPath,
|
||||
];
|
||||
for (const app of apps) {
|
||||
log(` Deleting app '${app}'`);
|
||||
await rimraf(app);
|
||||
}
|
||||
});
|
||||
|
||||
async function buildApp(appRoot, sdk) {
|
||||
log(`Building app for ${sdk} at app root '${appRoot}'`);
|
||||
try {
|
||||
const cmd = 'xcodebuild';
|
||||
let args = ['-sdk', sdk];
|
||||
if (process.env.XCCONFIG_FILE) {
|
||||
args.push('-xcconfig', process.env.XCCONFIG_FILE);
|
||||
}
|
||||
|
||||
log(` Executing command '${cmd} ${args.join(' ')}'`);
|
||||
await exec(cmd, args, {cwd: appRoot, maxBuffer: MAX_BUFFER_SIZE});
|
||||
} catch (err) {
|
||||
logError(err, 'Failed building app');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
gulp.task('ios-apps:build', async function buildAll() {
|
||||
log('Building all apps');
|
||||
const sdkVer = await getIOSSDK();
|
||||
for (const sdk of sdks) {
|
||||
await buildApp('.', sdk + sdkVer);
|
||||
}
|
||||
});
|
||||
|
||||
gulp.task('ios-apps:rename', async function iosAppsRename() {
|
||||
log('Renaming apps');
|
||||
for (const sdk of sdks) {
|
||||
log(` Renaming for ${sdk}`);
|
||||
log(` '${SDKS[sdk].buildPath}' => '${SDKS[sdk].finalPath}'`);
|
||||
await renameFile(SDKS[sdk].buildPath, SDKS[sdk].finalPath);
|
||||
}
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
'ios-apps:install',
|
||||
gulp.series('ios-apps:sdks', 'ios-apps:clean', 'ios-apps:build', 'ios-apps:rename')
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
configure,
|
||||
};
|
||||
@@ -1,54 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const eslint = require('gulp-eslint');
|
||||
const debug = require('gulp-debug');
|
||||
const gulpIf = require('gulp-if');
|
||||
const {isVerbose} = require('../utils');
|
||||
const yamlLint = require('../yaml-lint');
|
||||
|
||||
const configure = function configure(gulp, opts) {
|
||||
const verbose = isVerbose();
|
||||
|
||||
gulp.task('eslint', function eslintTask() {
|
||||
let opts = {
|
||||
fix: process.argv.includes('--fix'),
|
||||
};
|
||||
if (process.argv.includes('--lax-lint') || process.argv.includes('-ll')) {
|
||||
// when running tests, we want to be able to use exclusive tests
|
||||
// and console logging
|
||||
opts.rules = {
|
||||
'no-console': 0,
|
||||
'mocha/no-exclusive-tests': 0,
|
||||
};
|
||||
}
|
||||
return gulp
|
||||
.src(['**/*.js', '!node_modules/**', '!**/node_modules/**', '!build/**', '!**/*.min.js'])
|
||||
.pipe(gulpIf(verbose, debug()))
|
||||
.pipe(eslint(opts))
|
||||
.pipe(eslint.format())
|
||||
.pipe(eslint.failAfterError())
|
||||
.pipe(gulpIf((file) => file.eslint && file.eslint.fixed, gulp.dest(process.cwd())));
|
||||
});
|
||||
|
||||
gulp.task('yamllint', function yamllintTask() {
|
||||
const yamlOpts = {
|
||||
safe: !!opts.yaml.safe,
|
||||
};
|
||||
return gulp.src(opts.yaml.files).pipe(gulpIf(verbose, debug())).pipe(yamlLint(yamlOpts));
|
||||
});
|
||||
|
||||
const lintTasks = [];
|
||||
if (opts.eslint) {
|
||||
lintTasks.push('eslint');
|
||||
}
|
||||
if (opts.yamllint) {
|
||||
lintTasks.push('yamllint');
|
||||
}
|
||||
if (lintTasks.length) {
|
||||
gulp.task('lint', gulp.series(lintTasks));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
configure,
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const configure = function configure(gulp, opts) {
|
||||
gulp.task('prepublish', gulp.series('clean', 'transpile', opts.extraPrepublishTasks || []));
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
configure,
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const e2eTest = require('./e2e-test');
|
||||
const unitTest = require('./unit-test');
|
||||
|
||||
const configure = function configure(gulp, opts, env) {
|
||||
const testEnv = {
|
||||
testDeps: opts.transpile ? ['transpile'] : [],
|
||||
...env,
|
||||
};
|
||||
|
||||
let testTasks = [];
|
||||
if (opts.test) {
|
||||
unitTest.configure(gulp, opts, testEnv);
|
||||
testTasks.push('unit-test');
|
||||
}
|
||||
|
||||
if (opts.e2eTest) {
|
||||
e2eTest.configure(gulp, opts, testEnv);
|
||||
testTasks.push('e2e-test');
|
||||
}
|
||||
|
||||
if (testTasks.length) {
|
||||
gulp.task('test', gulp.series(testTasks));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
configure,
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const Transpiler = require('../../index').Transpiler;
|
||||
const debug = require('gulp-debug');
|
||||
const gulpIf = require('gulp-if');
|
||||
const merge = require('merge-stream');
|
||||
const {isVerbose} = require('../utils');
|
||||
|
||||
const JSON_SOURCES = ['lib/**/*.json', 'test/**/*.json'];
|
||||
|
||||
/**
|
||||
* @param {import('gulp').Gulp} gulp - The gulp instance.
|
||||
*/
|
||||
const configure = function configure(gulp, opts, env) {
|
||||
const tasks = [
|
||||
function transpileSources() {
|
||||
// json files can be copied as is, they don't need to be transpiled
|
||||
const firstPath = gulp.src(JSON_SOURCES, {base: './'}).pipe(gulp.dest(opts.transpileOut));
|
||||
const secondPath = gulp
|
||||
.src(opts.files, {base: './'})
|
||||
.pipe(gulpIf(isVerbose(), debug()))
|
||||
.pipe(new Transpiler(opts).stream())
|
||||
.on('error', env.spawnWatcher.handleError.bind(env.spawnWatcher))
|
||||
.pipe(gulp.dest(opts.transpileOut));
|
||||
return merge(firstPath, secondPath);
|
||||
},
|
||||
];
|
||||
|
||||
if (opts.postTranspile && opts.postTranspile.length) {
|
||||
gulp.task('post-transpile', gulp.parallel(opts.postTranspile));
|
||||
tasks.push('post-transpile');
|
||||
}
|
||||
|
||||
gulp.task('transpile', gulp.series(tasks));
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
configure,
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const mocha = require('gulp-mocha');
|
||||
const debug = require('gulp-debug');
|
||||
const gulpIf = require('gulp-if');
|
||||
const utils = require('../utils');
|
||||
|
||||
const configure = function configure(gulp, opts, env) {
|
||||
const testFiles = utils.translatePaths([opts.test.files || opts.testFiles], env.fileAliases);
|
||||
gulp.task('unit-test:run', function unitTestRun() {
|
||||
const mochaOpts = {
|
||||
reporter: utils.getTestReporter(opts),
|
||||
timeout: opts.testTimeout,
|
||||
traceWarnings: opts.test.traceWarnings,
|
||||
traceDeprecation: opts.test.traceWarnings,
|
||||
color: true,
|
||||
exit: Boolean(opts.test.exit),
|
||||
};
|
||||
// set env so our code knows when it's being run in a test env
|
||||
process.env._TESTING = 1;
|
||||
return gulp
|
||||
.src(testFiles, {read: false})
|
||||
.pipe(gulpIf(utils.isVerbose(), debug()))
|
||||
.pipe(mocha(mochaOpts))
|
||||
.once('error', env.spawnWatcher.handleError.bind(env.spawnWatcher));
|
||||
});
|
||||
gulp.task('unit-test', gulp.series(env.testDeps, 'unit-test:run'));
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
configure,
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const babel = require('gulp-babel');
|
||||
const rename = require('gulp-rename');
|
||||
const streamCombiner = require('./stream-combiner');
|
||||
const path = require('path');
|
||||
const sourcemaps = require('./sourcemaps');
|
||||
|
||||
const BABEL_OPTS = {
|
||||
configFile: path.resolve(__dirname, '..', '.babelrc'),
|
||||
};
|
||||
|
||||
const renameEsX = function () {
|
||||
return rename(function renameEs(path) {
|
||||
path.basename = path.basename.replace(/\.es[67]$/, '');
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = function transpiler(opts = {}) {
|
||||
const {sourceMapInit, sourceMapHeader, sourceMapWrite} = sourcemaps(opts.sourceMapOpts);
|
||||
|
||||
this.stream = function stream() {
|
||||
return streamCombiner(function combine(source) {
|
||||
return source
|
||||
.pipe(sourceMapInit)
|
||||
.pipe(babel(Object.assign({}, BABEL_OPTS, opts.babelOpts)))
|
||||
.pipe(sourceMapHeader)
|
||||
.pipe(renameEsX())
|
||||
.pipe(sourceMapWrite);
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -1,56 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const {exec} = require('child_process');
|
||||
const B = require('bluebird');
|
||||
const {quote} = require('shell-quote');
|
||||
|
||||
// string interpolation
|
||||
const interpolate = function interpolate(s, opts) {
|
||||
return _.keys(opts).reduce(function replace(s, k) {
|
||||
return s.replace(new RegExp(`\\$\\{\\s*${k}\\s*\\}`, 'g'), opts[k]);
|
||||
}, s);
|
||||
};
|
||||
|
||||
const translatePaths = function translatePaths(files, fileAliases) {
|
||||
if (!_.isArray(files)) {
|
||||
files = [files];
|
||||
}
|
||||
return _.flatten(files).map(function interpolateFileAliases(f) {
|
||||
return interpolate(f, fileAliases);
|
||||
});
|
||||
};
|
||||
|
||||
const isVerbose = function isVerbose() {
|
||||
return process.env.VERBOSE === '1';
|
||||
};
|
||||
|
||||
const getTestReporter = function getTestReporter(opts) {
|
||||
const isForceLogMode = parseInt(process.env._FORCE_LOGS, 10) === 1;
|
||||
return isForceLogMode ? 'spec' : process.env.REPORTER ? process.env.REPORTER : opts.testReporter;
|
||||
};
|
||||
|
||||
const pExec = function pExec(cmd, args = [], opts = {}) {
|
||||
return new B(function executeCmd(resolve, reject) {
|
||||
exec(`${quote([cmd])} ${quote(args)}`, opts, function cb(err, stdout, stderr) {
|
||||
// eslint-disable-line promise/prefer-await-to-callbacks
|
||||
if (err) {
|
||||
err.stdout = stdout;
|
||||
err.stderr = stderr;
|
||||
return reject(err);
|
||||
}
|
||||
resolve({
|
||||
stdout,
|
||||
stderr,
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
interpolate,
|
||||
translatePaths,
|
||||
isVerbose,
|
||||
getTestReporter,
|
||||
exec: pExec,
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
/* eslint-disable promise/prefer-await-to-callbacks */
|
||||
const {Transform} = require('stream');
|
||||
const log = require('fancy-log');
|
||||
const yaml = require('js-yaml');
|
||||
const {EOL} = require('os');
|
||||
const PluginError = require('plugin-error');
|
||||
const red = require('ansi-red');
|
||||
|
||||
function gulpYamlLint() {
|
||||
let errCount = 0;
|
||||
return new Transform({
|
||||
objectMode: true,
|
||||
transform(file, enc, cb) {
|
||||
try {
|
||||
yaml.load(file.contents.toString(enc));
|
||||
} catch (err) {
|
||||
errCount++;
|
||||
log.error(`Invalid YAML file: '${file.path}'`);
|
||||
for (const line of err.message.split(EOL)) {
|
||||
log.error(line);
|
||||
}
|
||||
}
|
||||
cb();
|
||||
},
|
||||
flush(done) {
|
||||
if (errCount > 0) {
|
||||
log.error(`YAML errors found. Due to the limitations of YAML linting, the error `);
|
||||
log.error(`is most likely in the line immediately ${red('before')} the line reported.`);
|
||||
return done(
|
||||
new PluginError('gulp-yaml-lint', {
|
||||
name: 'YAMLError',
|
||||
message: `Failed with ${errCount} ${errCount === 1 ? 'error' : 'errors'}`,
|
||||
})
|
||||
);
|
||||
}
|
||||
done();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = gulpYamlLint;
|
||||
@@ -1,111 +0,0 @@
|
||||
{
|
||||
"name": "@appium/gulp-plugins",
|
||||
"version": "8.0.0",
|
||||
"description": "Custom gulp plugins to be used across all appium modules",
|
||||
"keywords": [
|
||||
"automation",
|
||||
"javascript",
|
||||
"selenium",
|
||||
"webdriver",
|
||||
"ios",
|
||||
"android",
|
||||
"firefoxos",
|
||||
"testing"
|
||||
],
|
||||
"homepage": "https://appium.io",
|
||||
"bugs": {
|
||||
"url": "https://github.com/appium/appium/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appium/appium.git",
|
||||
"directory": "packages/gulp-plugins"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"author": "https://github.com/appium",
|
||||
"directories": {
|
||||
"lib": "lib",
|
||||
"test": "test",
|
||||
"build": "build"
|
||||
},
|
||||
"files": [
|
||||
".babelrc",
|
||||
"index.js",
|
||||
"lib",
|
||||
"build"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "gulp transpile",
|
||||
"clean": "npx rimraf build",
|
||||
"dev": "gulp dev --no-notif",
|
||||
"prepare": "npm run build",
|
||||
"test": "gulp unit-test:run",
|
||||
"test:e2e": "gulp e2e-test:run",
|
||||
"test:smoke": "node ./index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/eslint-config-appium": "^8.0.0",
|
||||
"@babel/core": "7.20.5",
|
||||
"@babel/eslint-parser": "7.19.1",
|
||||
"@babel/preset-env": "7.20.2",
|
||||
"@octokit/rest": "19.0.5",
|
||||
"@types/fancy-log": "2.0.0",
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/semver": "7.3.13",
|
||||
"@types/through2": "2.0.38",
|
||||
"ansi-red": "0.1.1",
|
||||
"axios": "1.2.1",
|
||||
"babel-plugin-istanbul": "6.1.1",
|
||||
"babel-plugin-source-map-support": "2.2.0",
|
||||
"bluebird": "3.7.2",
|
||||
"del": "6.1.1",
|
||||
"eslint": "8.29.0",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-mocha": "10.1.0",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"fancy-log": "2.0.0",
|
||||
"form-data": "4.0.0",
|
||||
"glob": "8.0.3",
|
||||
"globby": "11.1.0",
|
||||
"gulp-babel": "8.0.0",
|
||||
"gulp-debug": "4.0.0",
|
||||
"gulp-eslint": "6.0.0",
|
||||
"gulp-if": "3.0.0",
|
||||
"gulp-mocha": "8.0.0",
|
||||
"gulp-rename": "2.0.0",
|
||||
"gulp-replace": "1.1.3",
|
||||
"gulp-sourcemaps": "3.0.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"lodash": "4.17.21",
|
||||
"merge-stream": "2.0.0",
|
||||
"mocha": "10.2.0",
|
||||
"moment": "2.29.4",
|
||||
"node-notifier": "10.0.1",
|
||||
"nyc": "15.1.0",
|
||||
"pkg-dir": "5.0.0",
|
||||
"plugin-error": "2.0.1",
|
||||
"replace-in-file": "6.3.5",
|
||||
"rimraf": "3.0.2",
|
||||
"semver": "7.3.8",
|
||||
"shell-quote": "1.7.4",
|
||||
"source-map-support": "0.5.21",
|
||||
"through2": "4.0.2",
|
||||
"vinyl-paths": "3.0.1",
|
||||
"yargs": "17.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/register": "7.18.9"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"gulp": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
|
||||
"npm": ">=8"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"gitHead": "5c7af8ee73078018e4ec52fccf19fe3f77249d72"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/* eslint-disable import/no-unresolved */
|
||||
/* eslint-disable no-console */
|
||||
import { A } from './a';
|
||||
|
||||
|
||||
// A expects a string, we pass an integer
|
||||
let a = new A(123);
|
||||
|
||||
console.log(a.getText());
|
||||
@@ -1,8 +0,0 @@
|
||||
/* eslint-disable import/no-unresolved */
|
||||
/* eslint-disable no-console */
|
||||
import { A } from './a';
|
||||
|
||||
|
||||
let a = new A('hello world!');
|
||||
|
||||
console.log(a.getText());
|
||||
@@ -1,7 +0,0 @@
|
||||
/* eslint-disable import/no-unresolved */
|
||||
import { A } from './a';
|
||||
|
||||
|
||||
let a = new A('hello world!');
|
||||
|
||||
a.throwError('This is really bad!');
|
||||
@@ -1,15 +0,0 @@
|
||||
class A {
|
||||
constructor (text/*:string*/) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
getText ()/*:string*/ {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
throwError (mess) {
|
||||
throw new Error(mess);
|
||||
}
|
||||
}
|
||||
|
||||
export { A };
|
||||
@@ -1,11 +0,0 @@
|
||||
/* eslint-disable import/no-unresolved */
|
||||
import { A } from '../lib/a';
|
||||
|
||||
|
||||
|
||||
describe('a e2e', function () {
|
||||
it('should be able to get text', function () {
|
||||
let a = new A('hello world!');
|
||||
a.getText().should.equal('hello world!');
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
/* eslint-disable import/no-unresolved */
|
||||
import { A } from '../lib/a';
|
||||
|
||||
|
||||
|
||||
describe('a', function () {
|
||||
it('should be able to get text', function () {
|
||||
let a = new A('hello world!');
|
||||
a.getText().should.equal('hello world!');
|
||||
});
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
/* eslint-disable import/no-unresolved */
|
||||
import { A } from '../lib/a';
|
||||
|
||||
|
||||
describe('a-throw', function () {
|
||||
it('should throw', function () {
|
||||
let a = new A('hello world!');
|
||||
a.throwError('This is really bad!');
|
||||
});
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
class A {
|
||||
constructor (text/*:string*/) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
getText ()/*:string*/ {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
throwError (mess) {
|
||||
throw new Error(mess);
|
||||
}
|
||||
}
|
||||
|
||||
export { A };
|
||||
@@ -1,15 +0,0 @@
|
||||
class A {
|
||||
constructor (text/*:string*/) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
getText ()/*:string*/ {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
throwError (mess) {
|
||||
throw new Error(mess);
|
||||
}
|
||||
}
|
||||
|
||||
export { A };
|
||||
@@ -1,15 +0,0 @@
|
||||
class A {
|
||||
constructor (text/*:string*/) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
getText ()/*:string*/ {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
throwError (mess) {
|
||||
throw new Error(mess);
|
||||
}
|
||||
}
|
||||
|
||||
export { A };
|
||||
@@ -1,15 +0,0 @@
|
||||
class A {
|
||||
constructor (text/*:string*/) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
getText ()/*:string*/ {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
throwError (mess) {
|
||||
throw new Error(mess);
|
||||
}
|
||||
}
|
||||
|
||||
export { A };
|
||||
@@ -1,15 +0,0 @@
|
||||
class A {
|
||||
constructor (text/*:string*/) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
getText ()/*:string*/ {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
throwError (mess) {
|
||||
throw new Error(mess);
|
||||
}
|
||||
}
|
||||
|
||||
export { A };
|
||||
@@ -1,15 +0,0 @@
|
||||
class A {
|
||||
constructor (text/*:string*/) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
getText ()/*:string*/ {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
throwError (mess) {
|
||||
throw new Error(mess);
|
||||
}
|
||||
}
|
||||
|
||||
export { A };
|
||||
@@ -1,15 +0,0 @@
|
||||
class A {
|
||||
constructor (text/*:string*/) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
getText ()/*:string*/ {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
throwError (mess) {
|
||||
throw new Error(mess);
|
||||
}
|
||||
}
|
||||
|
||||
export { A };
|
||||
@@ -1,15 +0,0 @@
|
||||
class A {
|
||||
constructor (text/*:string*/) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
getText ()/*:string*/ {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
throwError (mess) {
|
||||
throw new Error(mess);
|
||||
}
|
||||
}
|
||||
|
||||
export { A };
|
||||
@@ -1,15 +0,0 @@
|
||||
class A {
|
||||
constructor (text/*:string*/) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
getText ()/*:string*/ {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
throwError (mess) {
|
||||
throw new Error(mess);
|
||||
}
|
||||
}
|
||||
|
||||
export { A };
|
||||
@@ -1,15 +0,0 @@
|
||||
class A {
|
||||
constructor (text/*:string*/) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
getText ()/*:string*/ {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
throwError (mess) {
|
||||
throw new Error(mess);
|
||||
}
|
||||
}
|
||||
|
||||
export { A };
|
||||
@@ -1,15 +0,0 @@
|
||||
class A {
|
||||
constructor (text/*:string*/) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
getText ()/*:string*/ {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
throwError (mess) {
|
||||
throw new Error(mess);
|
||||
}
|
||||
}
|
||||
|
||||
export { A };
|
||||
@@ -1,15 +0,0 @@
|
||||
class A {
|
||||
constructor (text/*:string*/) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
getText ()/*:string*/ {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
throwError (mess) {
|
||||
throw new Error(mess);
|
||||
}
|
||||
}
|
||||
|
||||
export { A };
|
||||
@@ -1,15 +0,0 @@
|
||||
class A {
|
||||
constructor (text/*:string*/) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
getText ()/*:string*/ {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
throwError (mess) {
|
||||
throw new Error(mess);
|
||||
}
|
||||
}
|
||||
|
||||
export { A };
|
||||
@@ -1,15 +0,0 @@
|
||||
class A {
|
||||
constructor (text/*:string*/) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
getText ()/*:string*/ {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
throwError (mess) {
|
||||
throw new Error(mess);
|
||||
}
|
||||
}
|
||||
|
||||
export { A };
|
||||
@@ -1,15 +0,0 @@
|
||||
class A {
|
||||
constructor (text/*:string*/) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
getText ()/*:string*/ {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
throwError (mess) {
|
||||
throw new Error(mess);
|
||||
}
|
||||
}
|
||||
|
||||
export { A };
|
||||
@@ -1,15 +0,0 @@
|
||||
class A {
|
||||
constructor (text/*:string*/) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
getText ()/*:string*/ {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
throwError (mess) {
|
||||
throw new Error(mess);
|
||||
}
|
||||
}
|
||||
|
||||
export { A };
|
||||
@@ -1,15 +0,0 @@
|
||||
class A {
|
||||
constructor (text/*:string*/) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
getText ()/*:string*/ {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
throwError (mess) {
|
||||
throw new Error(mess);
|
||||
}
|
||||
}
|
||||
|
||||
export { A };
|
||||
@@ -1,15 +0,0 @@
|
||||
class A {
|
||||
constructor (text/*:string*/) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
getText ()/*:string*/ {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
throwError (mess) {
|
||||
throw new Error(mess);
|
||||
}
|
||||
}
|
||||
|
||||
export { A };
|
||||
@@ -1,15 +0,0 @@
|
||||
class A {
|
||||
constructor (text/*:string*/) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
getText ()/*:string*/ {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
throwError (mess) {
|
||||
throw new Error(mess);
|
||||
}
|
||||
}
|
||||
|
||||
export { A };
|
||||
@@ -1,15 +0,0 @@
|
||||
class A {
|
||||
constructor (text/*:string*/) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
getText ()/*:string*/ {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
throwError (mess) {
|
||||
throw new Error(mess);
|
||||
}
|
||||
}
|
||||
|
||||
export { A };
|
||||
@@ -1,15 +0,0 @@
|
||||
class A {
|
||||
constructor (text/*:string*/) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
getText ()/*:string*/ {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
throwError (mess) {
|
||||
throw new Error(mess);
|
||||
}
|
||||
}
|
||||
|
||||
export { A };
|
||||
@@ -1,15 +0,0 @@
|
||||
class A {
|
||||
constructor (text/*:string*/) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
getText ()/*:string*/ {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
throwError (mess) {
|
||||
throw new Error(mess);
|
||||
}
|
||||
}
|
||||
|
||||
export { A };
|
||||
@@ -1,15 +0,0 @@
|
||||
class A {
|
||||
constructor (text/*:string*/) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
getText ()/*:string*/ {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
throwError (mess) {
|
||||
throw new Error(mess);
|
||||
}
|
||||
}
|
||||
|
||||
export { A };
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user