Compare commits

...
Author SHA1 Message Date
Jonathan Lipps 2e76ba9607 chore: publish
- appium@2.0.0-beta.48
 - @appium/base-driver@9.1.0
 - @appium/base-plugin@2.0.1
 - @appium/doctor@2.0.1
 - @appium/docutils@0.1.1
 - @appium/driver-test-support@0.3.1
 - @appium/execute-driver-plugin@3.0.1
 - @appium/fake-plugin@3.0.1
 - @appium/images-plugin@2.0.1
 - @appium/opencv@2.0.1
 - @appium/plugin-test-support@0.3.1
 - @appium/support@3.0.1
 - @appium/test-support@3.0.1
 - @appium/typedoc-plugin-appium@0.2.1
 - @appium/types@0.7.0
2022-12-21 11:57:39 -08:00
Jonathan Lipps aab415685e fix(docutils): package should publish the base mkdocs yml (#17968) 2022-12-21 11:54:50 -08:00
Jonathan Lipps d13d292c4b docs(appium): add grid 4 appium setup examples (#17965) 2022-12-21 10:58:46 -08:00
Christopher Hiller df250cc1e0 fix(appium): fix manifest migrations
Manifest migrations were not being fully applied due to a method in `Manifest` which refused to overwrite data.

- `Manifest` now has full control of the manifest file and its in-memory representation. Nothing else should ever modify the in-memory data directly; instead, use its API. This restriction also applies to migrations.
- Corrected some incorrect explanations in `manifest-migrations`
- `Manifest#addExtension` became `Manifest#setExtension` and allows overwriting of data, which prevented migrations from being fully applied.
- More tests
2022-12-19 10:37:33 -08:00
renovate[bot] 5565febc9d chore(deps): update dependency typedoc to v0.23.23 2022-12-18 11:41:04 +00:00
renovate[bot] 49a0fe164b chore(deps): update dependency @types/node to v18.11.17 2022-12-18 09:58:38 +00:00
renovate[bot] 94911eb59f chore(deps): update dependency eslint to v8.30.0 2022-12-17 14:13:16 +00:00
renovate[bot] 1b4a41bce9 chore(deps): update dependency @types/node to v18.11.16 2022-12-17 11:20:33 +00:00
renovate[bot] 5d0d4f5a44 fix(test-support): update dependency sinon to v15.0.1 2022-12-16 11:18:48 +00:00
renovate[bot] f81e2e92ef fix(execute-driver-plugin): update dependency webdriverio to v7.28.0 2022-12-16 08:50:11 +00:00
Kazuaki MatsuoandChristopher Hiller 316ecca8b1 feat(base): add get computed role and label in W3C v2 (#17928)
* feat(base): add get computed role and label in W3C v2

* add command names

* chore: add type

* chore: use `?` instead of `| null`

Co-authored-by: Christopher Hiller <boneskull@boneskull.com>

* Revert "chore: use `?` instead of `| null`"

This reverts commit 62af5e80ac.

Co-authored-by: Christopher Hiller <boneskull@boneskull.com>
2022-12-15 20:46:44 -08:00
Christopher Hiller 20667be9a2 fix(appium): ensure migration always gets applied
The manifest file was not getting its migrations applied _to disk_ in all situations.  Essentially: the migration could repeat over and over, depending on the environment--the changes would only exist in memory.

Added a test, as well.  I didn't originally add a test because the whole migration thing came out of a rabbit hole intended to fix a separate issue.  My bad!
2022-12-15 17:11:35 -08:00
Christopher Hiller 48b7b05a1f chore(support): quiet some noisy debug logging 2022-12-15 17:11:35 -08:00
Christopher Hiller 2df077b08c chore: remove packweb.json 2022-12-15 17:11:14 -08:00
Jonathan Lipps 0e195caafe fix: add 'webSocketUrl' as standard cap for bidi support (#17936) 2022-12-15 16:36:50 -08:00
Christopher Hiller 9842e231c5 chore: remove @appium/gulp-plugins
Closes #17907
2022-12-15 16:11:58 -08:00
Christopher Hiller feeaac4713 chore: revert 840a3b4a02
This reverts a workaround which has since been fixed in `sync-monorepo-packages@1.0.2`.
2022-12-15 16:11:48 -08:00
renovate[bot] ad7e2648c1 chore(deps): update dependency sync-monorepo-packages to v1.0.2 2022-12-15 15:07:14 -08:00
renovate[bot] 2cb1af4357 fix(doctor): update dependency appium-adb to v9.10.19 2022-12-15 15:07:08 -08:00
Christopher Hiller 45b6aed22d chore(fossa): fix invalid .fossa.yml "revision" prop 2022-12-15 15:06:59 -08:00
Sarin Suriyakoon 26c84c9361 refactor: add type for driver install 2022-12-15 15:05:21 -08:00
Christopher Hiller ad06a0612f chore(appium): fix E2E test for update functionality
The problem was that after we published a new major of `@appium/fake-driver`, the `updateVersion` field of the response was `null`, but `unsafeUpdateVersion` was populated; the test only used the former.  Now, the test uses `updateVersion` but falls back to `unsafeUpdateVersion`.
2022-12-15 14:43:46 -08:00
Jonathan Lipps fe67e4cb2f feat(appium): add chromium driver to list of officially supported drivers (#17939) 2022-12-14 14:28:53 -08:00
Christopher Hiller fee1c98ee9 chore: run e2e tests in parallel
This change makes Lerna execute E2E tests across two cores.  Some of the slower E2E suites (`appium`, `support`, `opencv` and `base-driver`) also run in parallel via Mocha's `--parallel` flag.

In Appium, the main culprit was `cli.e2e.spec.js`, which I split into four files: `args` (argument handling), `cli-driver` (`driver` command), `cli-plugin` (`plugin` command) and `local` (behavior when Appium is a dependency).  This should run faster (in theory) since Mocha runs tests in parallel _per file_ and will expand to the available cores.

CI has limited cores, so this won't really help there--but it will speed up local development.  When running tests locally, you may see various delays, as Mocha buffers the output of a single test file.

This is a continuation of the work I did in #17252.
2022-12-14 13:28:31 -08:00
Christopher Hiller 9fea6980e5 chore(commitlint): disable DUMB RULES
This disables the rules which:

- require specific capitalization
- disallow the use of periods in subjects
- warn if there's no leading blank line in the commit body

because the above are dumb
2022-12-14 13:27:28 -08:00
renovate[bot] 37f71c327a fix(types): update dependency type-fest to v3.4.0 2022-12-14 14:57:42 +00:00
renovate[bot] 172fcb9aff fix(types): update definitelytyped 2022-12-14 13:01:18 +00:00
renovate[bot] 3993decde4 chore(workflows): update actions/checkout digest to 755da8c 2022-12-14 10:10:32 +00:00
124 changed files with 1715 additions and 11508 deletions
+1 -5
View File
@@ -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
+2 -2
View File
@@ -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:
+1 -1
View File
@@ -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
+4 -1
View File
@@ -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],
},
};
+279 -8063
View File
File diff suppressed because it is too large Load Diff
+8 -9
View File
@@ -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"
},
+13
View File
@@ -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|
+118 -24
View File
@@ -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
View File
@@ -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
*/
+3 -4
View File
@@ -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
*/
/**
+1
View File
@@ -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
*/
+113 -39
View File
@@ -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;
+8 -8
View File
@@ -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"
}
}
+106
View File
@@ -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
*/
-708
View File
@@ -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
*/
+1 -1
View File
@@ -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 {
+195
View File
@@ -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);
});
});
});
});
+3
View File
@@ -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 () {
+12
View File
@@ -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']}},
+5 -5
View File
@@ -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');
});
});
+4
View File
@@ -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
+2 -2
View File
@@ -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",
+6
View File
@@ -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
+3 -3
View File
@@ -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",
+6
View File
@@ -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))
+3 -2
View File
@@ -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
+4 -4
View File
@@ -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
+2 -2
View File
@@ -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"
+4
View File
@@ -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
+3 -3
View File
@@ -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",
-32
View File
@@ -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
}
}
}
-11
View File
@@ -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',
};
-151
View File
@@ -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
-199
View File
@@ -1,199 +0,0 @@
appium-gulp-plugins
===================
Custom plugins used across appium modules
## status
[![Build Status](https://travis-ci.org/appium/appium-gulp-plugins.svg?branch=master)](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
```
-25
View File
@@ -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',
});
-4
View File
@@ -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;
-132
View File
@@ -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);
};
},
};
-26
View File
@@ -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;
};
-215
View File
@@ -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,
};
-28
View File
@@ -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,
};
-65
View File
@@ -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,
};
-48
View File
@@ -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,
};
-162
View File
@@ -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,
};
-54
View File
@@ -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,
};
-30
View File
@@ -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,
};
-32
View File
@@ -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);
});
};
};
-56
View File
@@ -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,
};
-41
View File
@@ -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;
-111
View File
@@ -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!');
-15
View File
@@ -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