Compare commits

...
Author SHA1 Message Date
Christopher Hiller a9a6b834bc chore: publish
- appium@2.0.0-beta.63
 - @appium/docutils@0.3.6
 - @appium/execute-driver-plugin@3.0.12
 - @appium/typedoc-plugin-appium@0.6.2
2023-04-12 10:58:48 -07:00
Mykola Mokhnach 297f4995f8 chore(appium): Perform better validation of appiumHome values (#18462) 2023-04-12 11:57:05 +02:00
renovate[bot] f42374eb6d chore(deps): update eslint-related packages to v5.58.0 2023-04-12 07:27:32 +00:00
renovate[bot] d6c70f2f52 fix(execute-driver-plugin): update dependency vm2 to v3.9.16 2023-04-12 03:38:17 +00:00
Christopher Hiller e35bc50454 chore: use conventionalcommit changelog
This changes the changelog preset from "angular" to "conventional-changelog-conventionalcommits" which should be more appropriate for our workflow.
2023-04-11 16:55:04 -07:00
Christopher Hiller 63e28a014c fix(typedoc-plugin-appium): restrict peer deps
This restricts peer deps to non-breaking versions. This is not a breaking change, because if newer versions are installed, they break anyway.
2023-04-11 16:35:12 -07:00
Christopher Hiller e9b50225dc chore: publish
- appium@2.0.0-beta.62
 - @appium/base-driver@9.3.6
 - @appium/base-plugin@2.2.6
 - @appium/doctor@2.0.12
 - @appium/docutils@0.3.5
 - @appium/driver-test-support@0.4.4
 - @appium/execute-driver-plugin@3.0.11
 - @appium/fake-plugin@3.1.10
 - @appium/plugin-test-support@0.3.12
 - @appium/strongbox@0.2.0
 - @appium/support@3.1.10
 - @appium/test-support@3.0.12
 - @appium/tsconfig@0.3.0
 - @appium/typedoc-plugin-appium@0.6.1
 - @appium/types@0.10.4
2023-04-10 16:43:21 -07:00
Christopher Hiller e9426fcfec fix(docutils): properly respects log levels
The resulting solution keeps a bunch of weak refs to `Consola` objects and sets the log level on all of them if it ever changes.

This is--at minimum--easier to understand than the broken `Proxy` implementation.
2023-04-10 15:58:58 -07:00
Christopher Hiller 5ab2fea576 feat(appium): make "ls" alias of "list"
This makes `ls` an alias of `list`; e.g., `appium driver ls`

While doing this I noticed a limitation of `argparser`, which is that an option (not a command) can only have a single (1) alias.  I have noted this behavior in a comment.

I also notice that we need to rewrite `ls` to be `list` ourselves, or we will have a bunch of conditionals checking for both values scattered around.

We need to use yargs...
2023-04-10 15:36:34 -07:00
Christopher Hiller 377e9825de feat(strongbox): export BaseItem
This exports `BaseItem`, the builtin implementation of `Item`.
2023-04-10 13:28:51 -07:00
renovate[bot] 116c9a66e2 chore(workflows): update egmacke/action-check-label action to v2 2023-04-10 13:01:51 -07:00
renovate[bot] 04b542cfcd chore(deps): update dependency eslint to v8.38.0 2023-04-09 08:21:43 +00:00
renovate[bot] c39be81d04 fix(support): update dependency shell-quote to v1.8.1 2023-04-08 16:49:21 +00:00
renovate[bot] efc847380a chore(deps): update dependency lint-staged to v13.2.1 2023-04-08 12:27:01 +00:00
renovate[bot] cf7b4d84af chore(docutils): update dependency mkdocs-material to v9.1.6 2023-04-08 09:33:44 +00:00
renovate[bot] 5a3666f653 chore(workflows): update egmacke/action-check-label digest to b6f12c1 2023-04-08 09:26:17 +00:00
renovate[bot] 5790924bf8 fix(execute-driver-plugin): update dependency vm2 to v3.9.15 2023-04-08 00:25:27 +00:00
Christopher Hiller 39b7cd5084 chore: update wallaby config 2023-04-07 12:38:29 -07:00
Christopher Hiller fd912346fa feat(strongbox): create @appium/strongbox
This PR creates a new package `@appium/strongbox`, which provides a generic persistence store for Appium extensions.
2023-04-07 12:38:29 -07:00
Christopher Hiller 1d5070e891 chore(ci): replace label checker action
This replaces [danielchabr/pr-labels-checker](/danielchabr/pr-labels-checker) with [egmacke/action-check-label](/egmacke/action-check-label).  There are many such actions.  Maybe this one won't give us problems.
2023-04-07 11:38:31 -07:00
Christopher Hiller 42953b2add chore(ci): revert to source repo of rossjrw/pr-preview-action
A bugfix PR of mine was merged into this repo, so we can use it again.
2023-04-07 11:31:28 -07:00
renovate[bot] d0f479f7a8 chore(workflows): update boneskull/pr-preview-action digest to 9dac5c4 2023-04-07 11:30:11 +00:00
Christopher Hiller e67890f463 chore(ci): use boneskull fork of pr-preview-action
My fork should fix the bug in the action.  See https://github.com/rossjrw/pr-preview-action/pull/36
2023-04-06 16:06:32 -07:00
renovate[bot] ff46dfa814 chore(workflows): pin rossjrw/pr-preview-action action to 9dac5c4 2023-04-06 16:06:32 -07:00
Christopher Hiller afc7e34402 chore(ci): try to fix on-hold workflow
permissions issues, but unclear which
2023-04-06 12:24:07 -07:00
Christopher Hiller eef4e3447a chore(ci): make docs preview job run in master only
since it writes to gh-pages branch
2023-04-06 12:23:58 -07:00
renovate[bot] d6c42e99c0 fix(types): update dependency type-fest to v3.8.0 2023-04-06 17:04:42 +00:00
renovate[bot] 6cf14802b7 fix(support): update dependency axios to v1.3.5 2023-04-06 13:47:58 +00:00
renovate[bot] d5090155ad chore(deps): update dependency eslint-import-resolver-typescript to v3.5.5 2023-04-06 09:56:04 +00:00
Christopher Hiller 591d15bc5e chore: publish
- appium@2.0.0-beta.61
 - @appium/docutils@0.3.4
 - @appium/images-plugin@2.0.10
 - @appium/opencv@2.0.10
 - @appium/typedoc-plugin-appium@0.6.0
2023-04-05 14:56:49 -07:00
Christopher Hiller 00dbd28190 chore: add a "release" script
This script executes `lerna publish`.  The Lerna config has been changed so that the `publish` command uses a message of `chore: publish`.
2023-04-05 14:55:16 -07:00
Christopher Hiller b808dc866d feat(typedoc-plugin-appium): enable comment derivation via reflection types
In the case of `appium-xcuitest-driver`, each command (or execute) method is declared like so:

```ts
import {someMixin} from './some-mixins';

class XCUITestDriver {
  foo = someMixin.foo;
}
```

The docstring exists on `someMixin.foo`, but not on the `foo` prop of an `XCUITestDriver` instance.

This change makes `@appium/typedoc-plugin-appium` dig into such references and derive the comments.
2023-04-05 14:53:56 -07:00
Christopher Hiller d866bddc15 chore(ci): filter more workflows
This causes the "Build" workflow to only trigger if the PR modifies files in paths that might affect the build.

Likewise, the "Build Docs" workflow is expanded to rebuild docs if API changes occur (e.g. in sources).

Also restricts perms a bit more on docs build.
2023-04-05 14:37:05 -07:00
Christopher Hiller 825833d1ad chore(renovate): ignore major releases of pkg 'conf'
more esm-only nonsense
2023-04-05 14:36:44 -07:00
Christopher Hiller bb11d1cfe9 fix(opencv): fix a type error causing typedoc problems 2023-04-04 14:02:50 -07:00
Christopher Hiller 11c6aaae41 chore: publish
- appium@2.0.0-beta.60
 - @appium/docutils@0.3.3
 - @appium/typedoc-plugin-appium@0.5.4
2023-04-04 11:52:51 -07:00
Christopher Hiller 822206be20 fix(typedoc-plugin-appium): allow ts v5 as peer dep 2023-04-04 11:51:16 -07:00
renovate[bot] 374c85e22a chore(deps): update eslint-related packages to v5.57.1 2023-04-04 13:50:34 +00:00
Christopher Hiller a11a6ede33 chore: publish
- appium@2.0.0-beta.59
 - @appium/base-driver@9.3.5
 - @appium/base-plugin@2.2.5
 - @appium/doctor@2.0.11
 - @appium/docutils@0.3.2
 - @appium/driver-test-support@0.4.3
 - @appium/eslint-config-appium-ts@0.3.1
 - @appium/eslint-config-appium@8.0.3
 - @appium/execute-driver-plugin@3.0.10
 - @appium/fake-driver@5.2.3
 - @appium/fake-plugin@3.1.9
 - @appium/images-plugin@2.0.9
 - @appium/opencv@2.0.9
 - @appium/plugin-test-support@0.3.11
 - @appium/support@3.1.9
 - @appium/test-support@3.0.11
 - @appium/typedoc-plugin-appium@0.5.3
 - @appium/types@0.10.3
 - @appium/universal-xml-plugin@1.0.8
2023-04-03 12:55:58 -07:00
Christopher Hiller 41eab2c938 chore: try to fix lifecycle scripts
`lerna publish` has been acting up, and I think it's because the lifecycle scripts don't do enough cleanup, or we're running them in the wrong order, or something.  so here's this.

it causes an "extra" rebuild between version and publish, but I think it's better to be safe than sorry here.
2023-04-03 12:54:29 -07:00
Christopher Hiller 296c29ad84 fix(types): make getContext & getContexts generic
These can now return `T` and `T[]`, respectively.  `T` defaults to `string`, but in `appium-xcuitest-driver`, `T` is either `string` or an object, depending on the settings.
2023-04-03 12:44:29 -07:00
Mykola Mokhnach 07f9d41bd2 chore(appium): Add base path to the server startup log line (#18437) 2023-04-03 21:06:57 +02:00
renovate[bot] d8b633406a fix(doctor): update dependency appium-adb to v9.11.0 2023-04-03 09:28:25 +00:00
renovate[bot] b9d339eaf3 fix(universal-xml-plugin): update dependency @xmldom/xmldom to v0.8.7 2023-03-31 13:23:31 +00:00
renovate[bot] 1ea309814a chore(docutils): update dependency mkdocs-material to v9.1.5 2023-03-31 11:52:55 +00:00
Christopher Hiller 19ff931bc0 chore(typedoc-plugin-appium): normalize some conversion log messages
These now contain more info about what exactly is being converted.
2023-03-30 16:58:36 -07:00
Christopher Hiller 9d2469fdf3 docs: enable typedoc generation for more workspaces
- Doc gen for:
    - `@appium/driver-test-support`
    - `@appium/opencv`
    - `@appium/plugin-test-support`
    - `@appium/test-support`

- Added a `dev:docs` script in the root monorepo. The file watcher does _not_ presently rebuild reference docs upon changes to sources (which is a limitation of `@appium/docutils`).
- Changed the `dev:docs:*` scripts in `appium` to use `appium-docs` (so it rebuilds reference docs at least once [because it runs typedoc!!])
2023-03-30 16:58:36 -07:00
Christopher Hiller 47a4ddd2a3 chore(types): deprecate Element.ELEMENT
- move `Element` into `util` since it isn't specific to action sequences
- add more docs for it
2023-03-30 16:57:41 -07:00
Christopher Hiller a1723231be chore: update labeler config
- automatically dump tooling config into "chore"
- add missing `@appium/eslint-config-appium-ts` label
2023-03-30 11:17:06 -07:00
Christopher Hiller f4a254f204 chore: rename workflow labeler.yml to auto-labeler.yml
This avoids using the same filename as the [multi-labeler action](/fuxingloh/multi-labeler)'s config file, just for clarity sake.
2023-03-30 11:17:06 -07:00
Christopher Hiller 22b943dff1 chore: attempt to get labeler workflows running against forks 2023-03-30 11:17:06 -07:00
renovate[bot] 19eaaf5764 chore(deps): update dependency eslint to v8.37.0 2023-03-30 12:29:01 +00:00
renovate[bot] 55805390b5 fix(types): update dependency type-fest to v3.7.2 2023-03-30 10:07:01 +00:00
renovate[bot] e94275c229 chore(deps): update dependency eslint-import-resolver-typescript to v3.5.4 2023-03-30 09:19:51 +00:00
renovate[bot] f795fe6056 chore(deps): update dependency @types/node to v18.15.11 2023-03-30 06:09:41 +00:00
renovate[bot] bbcaec9c05 fix(appium): update dependency package-changed to v3 2023-03-29 16:04:55 -07:00
Christopher Hiller 43a0d1778f chore: fix fossa.yml
the schema is invalid!
2023-03-29 15:48:56 -07:00
Christopher Hiller d514ebdd7e chore: update gitHead for lerna screuwp 2023-03-28 13:12:00 -07:00
Christopher Hiller 8480a85ce2 chore: publish
- appium@2.0.0-beta.58
 - @appium/base-driver@9.3.4
 - @appium/base-plugin@2.2.4
 - @appium/doctor@2.0.10
 - @appium/docutils@0.3.1
 - @appium/driver-test-support@0.4.2
 - @appium/eslint-config-appium-ts@0.3.0
 - @appium/eslint-config-appium@8.0.2
 - @appium/execute-driver-plugin@3.0.9
 - @appium/fake-driver@5.2.2
 - @appium/fake-plugin@3.1.8
 - @appium/plugin-test-support@0.3.10
 - @appium/support@3.1.8
 - @appium/test-support@3.0.10
 - @appium/typedoc-plugin-appium@0.5.2
 - @appium/types@0.10.2
 - @appium/universal-xml-plugin@1.0.7
2023-03-28 13:10:45 -07:00
renovate[bot] ad98d38efb chore(deps): update eslint-related packages to v5.57.0 2023-03-28 19:38:54 +00:00
renovate[bot] 1358425e1a chore(deps): update dependency @commitlint/cli to v17.5.1 2023-03-28 12:50:54 +00:00
renovate[bot] 43fa0d7425 chore(deps): update dependency tsd to v0.28.1 2023-03-28 10:22:58 +00:00
renovate[bot] 86eb495641 fix(driver-test-support): update definitelytyped 2023-03-28 08:00:49 +00:00
Christopher Hiller 5485f0b9cd fix(types): fix method map types
These types do not care about constraints, only the shape of the driver itself.
2023-03-27 17:07:51 -07:00
Christopher Hiller 4974403661 fix: backwards-compatible fixes for TS v5.x
These will be issues when we upgrade
2023-03-27 17:07:51 -07:00
Christopher Hiller deb631b456 fix(basedriver): convert main driver implementation to typescript
This seems to fix the bug in #18379.  There must be some sort of bug in TypeScript w/r/t `@implements` or maybe even it was not treating `@typedef` as a proper alias.  Perhaps it's user error--omitting optional type args in alias-style typedefs may end up making a new type entirely.  idk, but this was easier to deal with.

Closes #18379.
2023-03-27 17:07:51 -07:00
Christopher Hiller 238c489f3a chore(base-driver): add a failing type test 2023-03-27 17:07:51 -07:00
Christopher Hiller 3c9fa7ba73 fix(appium,types,base-driver,fake-driver,driver-test-support): normalize constraint defaults
Another handful of fixes.

- remove second "extra" parameter from `Capabilities`, `NSCapabilities` `W3CCapabilities`. we were not using it anywhere to my knowledge. it was an escape hatch anyway.
- never assign `BaseDriverCapConstraints` as the default constraints, because this is confusing as hell. instead, drivers should use the `*DriverCaps` types which automatically pulls the base constraints in. applied across several workspaces
- renamed `DeviceSettings` => `IDeviceSettings` because `DeviceSettings` is a class.
- the session ID param of `Core`'s `sessionExists()` is actually optional (per the implementation)
- allow some modicum of type narrowing at the class level around settings and values returned by `ISessionHandler`'s methods. also, not all values in the raw settings object are of the same type `T`, so fix that
- `server`, `serverHost`, `serverPort` and `serverPath` are not members of `Core` (using `AppiumDriver` as a litmus test--it interacts with `Driver` types but does not have these properties itself)
- changed a few types to interfaces where possible
- moved a lost comment
- further dumbed-down the logdef stuff
- `Driver.driverData` is _always_ defined, since `Core` makes it an empty array.
- `DriverOpts` no longer contains "partial" caps; this it not quite right as the caps are not assigned until a session is created, but there's a conflict between some props which are _both_ server args and defined in constraints.  anyway, this is what `initialOpts` is for
- fix some type disagreements in basedriver's "capabilities" module
2023-03-27 17:07:51 -07:00
Christopher Hiller e8e8252c52 chore(types): add type tests for Plugins 2023-03-27 14:23:25 -07:00
Christopher Hiller 50f1e69268 fixup 2023-03-27 14:23:25 -07:00
Christopher Hiller 0dcd5fa371 fix(base-driver,base-plugin,types): update PluginCommand and DriverCommand types
The first arg of `PluginCommand` is a `NextPluginCallback` which now returns `Promise<unknown>` instead of `Promise<void>`, which was incorrect.

We use `unknown` rather than `any` because it forces us to be explicit about the return value of `await next()` if we are to call it.

Added a type `DriverCommandToPluginCommand` which essentially converts a `DriverCommand` to a `PluginCommand`.  It's probably best used as a type guard or decorator, but it can "fill in the blanks" when overloading commands in a plugin.

Also normalized some of the `TArgs`/`TReturn` stuff.
2023-03-27 14:23:25 -07:00
Christopher Hiller 75cbb11d00 chore: rename ExtensionCommand classes due to naming conflict
mainly because a "command" is a thing in drivers/plugins, and these classes refer to something else completely unrelated.  renamed to `*CliCommand` to avoid confusion.
2023-03-27 14:23:25 -07:00
Christopher Hiller 020473afc4 fix(eslint-config-appium): disable import/no-unresolved for tsd tests
`tsd` has a apparently non-standard resolution algorithm
2023-03-27 12:44:47 -07:00
Christopher Hiller 497641a00d chore: add tsd for testing types
This adds [tsd](https://npm.im/tsd) which provides a small framework around running expectations against declaration files.  This is intended to be run against hand-written declarations, but because we (apparently) cannot rely on `tsc` to output valid declaration files, we should also use it to test the generated declarations of our packages.
2023-03-27 12:44:47 -07:00
renovate[bot] bc860c733a fix(types): update dependency type-fest to v3.7.1 2023-03-27 08:31:03 +00:00
renovate[bot] d774a8428a fix(test-support): update dependency sinon to v15.0.3 2023-03-27 07:22:50 +00:00
renovate[bot] a593269f61 chore(deps): update dependency @types/node to v18.15.10 2023-03-26 08:57:05 +00:00
renovate[bot] 89fd52a602 chore(deps): update dependency @types/node to v18.15.9 2023-03-25 15:57:36 +00:00
renovate[bot] 7f061d2362 chore(docutils): update dependency mkdocs-material to v9.1.4 2023-03-25 11:08:46 +00:00
renovate[bot] 2c1e1532ad chore(deps): update dependency lerna to v6.6.1 2023-03-25 08:02:49 +00:00
Christopher Hiller 0c52b50e65 fix(appium): remove longjohn
This removes `longjohn` and replaces it with a statement which increases the stack trace limit from the default (10) to 100.

I believe setting this overrides whatever was passed to the runtime `--stack-trace-limit` option.  If someone complains, we can try a different strategy.

Closes #18357
2023-03-24 12:22:33 -07:00
renovate[bot] 6db70f4143 chore(deps): update dependency lerna to v6.6.0 2023-03-24 14:27:16 +00:00
renovate[bot] 1800530bf2 chore(deps): update definitelytyped 2023-03-24 12:45:03 +00:00
renovate[bot] 32a5ee91df chore(deps): update dependency prettier to v2.8.7 2023-03-24 10:53:51 +00:00
renovate[bot] 5c0315d3bb chore(workflows): update actions/checkout digest to 8f4b7f8 2023-03-24 08:40:56 +00:00
Erhan AlankuşandKazuaki Matsuo 12f4ec404c docs(appium): grammar fix (#18359)
Co-authored-by: Kazuaki Matsuo <fly.49.89.over@gmail.com>
2023-03-23 23:21:19 -07:00
Erhan AlankuşandKazuaki Matsuo 8aa39c117f docs(appium): update index.md (#18363)
Co-authored-by: Kazuaki Matsuo <fly.49.89.over@gmail.com>
2023-03-23 23:20:46 -07:00
Christopher Hiller 017216533b chore(ci): suppress warnings when linting
This suppresses all warnings when running ESLint in CI (since they are noisy)
2023-03-23 13:05:31 -07:00
Christopher Hiller 499bda4533 chore(types): make the lone JS file a TS file
also: remove unused imports
2023-03-23 12:15:51 -07:00
Christopher Hiller 9ef564f0be chore(types): move more utility types into util module 2023-03-23 12:15:51 -07:00
Christopher Hiller d303527c3d fix(base-driver): misc type fixes
- `PostProcessOptions` moved into `@appium/types`
- `shutdownUnexpectedly` cannot be marked `@protected` as anything used this way is inaccessible in mixins
- suppress a `@typescript-eslint` warning. this probably needs to happen in a few other files
2023-03-23 12:15:51 -07:00
Christopher Hiller 51a8f2fc34 fix(base-driver): fix timeout mixin to use proper context types 2023-03-23 12:15:51 -07:00
Christopher Hiller c56dc98d13 chore(base-driver): fix description of mixin function to refer to BaseDriver 2023-03-23 12:15:51 -07:00
Christopher Hiller 454f6a5d63 fix(support): fix a few type problems; use new HTTPHeaders type 2023-03-23 12:15:51 -07:00
Christopher Hiller 87408e529c chore(types): organize more types; remove some deprecated types
This change removes types for methods in `ExternalDriver` (or rather the interfaces that build it) that were marked `@deprecated` and  consumed by `appium-xcuitest-driver`.  I salvaged what I could from the descriptions in these docstrings, and have moved them into `appium-xcuitest-driver` (obviously not in this repo).

Also:

- created some new files to group types logically
- created a `HTTPHeaders` type
- created a type `ConfigureAppOptions` for `DriverHelpers.configureApp`, and a `PostProcessOptions` type within it.
- added a couple other types
- fixed some signatures to look more like reality
- modified the return value of `queryAppState()` to return `0|1|2|3|4` instead of `0|1|3|4` since `appium-xcuitest-driver` returns the former
- made the `LogDef` and related types even _more_ loose.  tbh these are very awkward to type properly; I cannot figure out a good way to narrowly type the `self` param to a log `getter`.
- noted which of the remaining `@deprecated` methods in `ExternalDriver` were _not_ in use by `appium-xcuitest-driver`, so I can later remove them once I've salvaged the docstrings to put into `appium-uiautomator2-driver`.
2023-03-23 12:15:51 -07:00
Christopher Hiller 24bec8ce81 chore: downgrade log message about manifest upgrade
Most users are not going to care or know what this is.
2023-03-23 11:56:05 -07:00
Christopher Hiller d9b47bc864 feat(appium): improvements to ext commands when running extension in development
This PR does a handful of things:

1. It adds a `--verbose` flag to `appium <driver|plugin> list` which just dumps a big object (via Node's `util.inspect()`), which contains the underlying manifest file data
2. It creates a new manifest schema revision where the "install type" can be `dev`. Added a migration.
3. Displays a warning if trying to uninstall an extension in dev mode
4. Displays `[installed (dev mode)]` instead of `[installed (npm)]` when running `appium <driver|plugin> list` w/o `--verbose` flag
5. Attempted to simplify the return type of `ExtensionCommand.list()`

Closes #18277
2023-03-23 11:56:05 -07:00
renovate[bot] 6912fa14f2 fix(types): update dependency type-fest to v3.7.0 2023-03-23 11:22:16 +00:00
renovate[bot] 45291f71f4 chore(deps): update dependency @commitlint/cli to v17.5.0 2023-03-23 06:48:55 +00:00
Christopher Hiller 409192d063 fix(eslint-config-appium-ts): disable brace-style rule
In certain situations in `.ts` sources, class definition lines can get very long.  Prettier will split these into multiple lines, and add a final line with `{` at position 0.  This conflicts with the `brace-style` rule in ESLint.  I am not sure why the ESLint prettier config doesn't already handle this, but I am going to guess it's because that config doesn't work on `.ts` sources.
2023-03-22 16:25:22 -07:00
Christopher Hiller 309fe4c964 chore: lint everything 2023-03-22 16:25:22 -07:00
Christopher Hiller 34891f5657 fix(base-driver,types): fix websocket-related types on AppiumServer 2023-03-22 16:25:22 -07:00
Christopher Hiller 43df77f6b6 feat(eslint-config-appium-ts)!: enable for JS sources
BREAKING CHANGE: This makes `@appium/eslint-config-appium-ts` also apply to JS sources _and_ requires a new peer dependency (`eslint-import-resolver-typescript`). The `main` file is now a `.js` file.  The module is expected to _extend_ `@appium/eslint-config-appium`, not replace it; consumers needn't extend both in their ESLint config.

In the not-too-distant past, `@typescript-eslint/parser` was unable to understand docstrings in `.js` files. Evidently this is no longer true.

I've rewritten and heavily commented the configuration explaining the intent behind everything.
2023-03-22 16:25:22 -07:00
Christopher Hiller 78c6a0b991 chore: remove jsdoc vestiges 2023-03-22 11:59:20 -07:00
Mykola Mokhnach d3e91222c8 chore(base-driver): Check if a valid Error instance is thrown (#18369) 2023-03-22 07:20:32 +01:00
renovate[bot] f2dadd8f8f chore(deps): update eslint-related packages 2023-03-21 16:58:18 +00:00
renovate[bot] ffedb6e40e chore(deps): update dependency prettier to v2.8.6 2023-03-21 13:03:08 +00:00
renovate[bot] fdcd01ba35 chore(deps): update definitelytyped 2023-03-21 09:41:25 +00:00
renovate[bot] bd3ecc3fb1 fix(support): update definitelytyped 2023-03-20 19:24:56 +00:00
renovate[bot] dbc0bf69e8 fix(docutils): update dependency typedoc to v0.23.28 2023-03-20 15:46:26 +00:00
renovate[bot] 23b1096caa chore(deps): update dependency prettier to v2.8.5 2023-03-20 10:19:01 +00:00
Daniel Mertins 8805792089 docs(appium): fix links to drivers lists (#18355) 2023-03-16 14:52:24 -04:00
renovate[bot] ef9cbe6bcc fix(docutils): update dependency typedoc to v0.23.27 2023-03-16 14:34:02 +00:00
renovate[bot] fc19705ecd chore(workflows): update actions/checkout digest to 24cb908 2023-03-16 13:12:23 +00:00
Mykola Mokhnach d4654138ee chore(base-driver): Update HTTP caching logic and align it with the standard (#18348) 2023-03-16 07:30:11 +01:00
renovate[bot] 930a45dafe chore(docutils): update dependency mkdocs-material to v9.1.3 2023-03-15 08:39:34 +00:00
renovate[bot] f1a8ce0bba fix(test-support): update dependency sinon to v15.0.2 2023-03-14 10:19:30 -07:00
renovate[bot] 287974a93a chore(deps): update eslint-related packages to v5.55.0 2023-03-14 13:29:43 +00:00
renovate[bot] 4c081c6723 chore(deps): update dependency ws to v8.13.0 2023-03-11 16:32:10 +00:00
renovate[bot] 58ae10f887 chore(deps): update dependency eslint to v8.36.0 2023-03-11 13:01:30 +00:00
renovate[bot] f673efcba5 chore(deps): update dependency lint-staged to v13.2.0 2023-03-10 20:55:04 +00:00
renovate[bot] 503da432dc chore(deps): update dependency lint-staged to v13.1.4 2023-03-10 17:03:49 +00:00
renovate[bot] 4fb7eb8643 chore(deps): update dependency @types/node to v18.15.0 2023-03-10 12:38:39 +00:00
renovate[bot] ae303c73de chore(docutils): update dependency mkdocs-material to v9.1.2 2023-03-10 09:29:53 +00:00
Christopher Hiller ad11fbf97b fix(docutils): inherit stdio in long-running processes
This change ensures when running `mkdocs` or `mike` with the `--serve` flag, the output is unbuffered and stdio is inherited from the parent process.

This will show helpful things like _the URL at which the development server is running_.
2023-03-09 10:12:53 -08:00
Christopher Hiller 9ed6711a7e chore(ci): attempt to build and preview docs
This also isolates the docs build into a workflow that only runs when changes are likely to materially affect doc gen.
2023-03-09 10:11:53 -08:00
Christopher Hiller fa0e853bd0 chore(ci): attempt to avoid running trusted code on PRs from forks 2023-03-08 15:06:19 -08:00
174 changed files with 8216 additions and 2682 deletions
+8 -1
View File
@@ -1,5 +1,5 @@
{
"extends": ["@appium/eslint-config-appium", "@appium/eslint-config-appium-ts"],
"extends": ["@appium/eslint-config-appium-ts"],
"overrides": [
{
"files": "packages/fake-driver/**/*",
@@ -26,9 +26,16 @@
"sourceType": "script"
}
},
{
"files": ["./test/setup.js", "./**/scripts/**/*.js", "./packages/*/index.js"],
"rules": {
"@typescript-eslint/no-var-requires": "off"
}
},
{
"files": "./packages/*/test/**/*.js",
"rules": {
"@typescript-eslint/no-var-requires": "off",
"no-restricted-properties": [
"error",
{
+1 -1
View File
@@ -53,4 +53,4 @@ targets:
path: ./packages/docutils
revision:
- branch: master
branch: master
+12 -1
View File
@@ -25,6 +25,7 @@ labels:
matcher:
commits: '^chore.*?:'
title: '^chore.*?:'
files: ['.github/**', '.husky/**', '.vscode/**', './.*', './*.js', '**/test/**']
- label: 'SemVer:Breaking'
sync: true
@@ -34,7 +35,7 @@ labels:
- label: 'Dependencies'
sync: true
matcher:
files: ['**/package.json', '**/package-lock.json', '**/requirements.txt']
files: ['**/package.json', 'package-lock.json', '**/requirements.txt']
author: 'renovate'
- label: 'appium core'
@@ -52,6 +53,11 @@ labels:
matcher:
files: ['packages/eslint-config-appium/**']
- label: '@appium/eslint-config-appium-ts'
sync: true
matcher:
files: ['packages/eslint-config-appium-ts/**']
- label: '@appium/base-plugin'
sync: true
matcher:
@@ -141,3 +147,8 @@ labels:
sync: true
matcher:
files: ['packages/universal-xml-plugin/**']
- label: '@appium/strongbox'
sync: true
matcher:
files: ['packages/strongbox/**']
+16
View File
@@ -0,0 +1,16 @@
name: Labeler
on:
pull_request_target:
types: [opened, synchronize]
branches: [master]
permissions: read-all
jobs:
labeler:
permissions:
pull-requests: write
name: Auto-Label PRs
runs-on: ubuntu-latest
steps:
- uses: fuxingloh/multi-labeler@6704db0bcba106d07482efabbc79d3092af74fa2 # v2
+41 -22
View File
@@ -1,5 +1,42 @@
name: Appium Build
on: [push, pull_request]
on:
push:
# this list of paths could potentially impact the build
paths:
- '**/tsconfig*.json'
- 'packages/base-driver/static/**/*'
- 'packages/*/test/**'
- 'packages/*/bin'
- 'packages/**/*.ts'
- 'packages/**/*.json'
- 'packages/**/*.js'
- 'package*.json'
- 'lerna.json'
- '.npmrc'
- '.mocharc.js'
- '.github/workflows/build.yml' # this file
- '.eslintrc'
- '.eslintignore'
- '!**/sample-code/**'
- '!packages/*/docs/**'
pull_request:
paths:
- '**/tsconfig*.json'
- 'packages/base-driver/static/**/*'
- 'packages/*/test/**'
- 'packages/*/bin'
- 'packages/**/*.ts'
- 'packages/**/*.json'
- 'packages/**/*.js'
- 'package*.json'
- 'lerna.json'
- '.npmrc'
- '.mocharc.js'
- '.github/workflows/build.yml' # this file
- '.eslintrc'
- '.eslintignore'
- '!**/sample-code/**'
- '!packages/*/docs/**'
permissions:
contents: read
@@ -13,7 +50,7 @@ jobs:
node-version: [14.x, 16.x, 18.x]
steps:
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
with:
@@ -33,33 +70,15 @@ jobs:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3
- name: Install dependencies
uses: bahmutov/npm-install@1a235c31658a322a3b024444759650ee6345c26d # tag=v1
with:
useRollingCache: true
install-command: npm ci
- name: ESLint
run: npm run lint
run: npm run lint:ci
- name: Validate Renovate Config
uses: rinchsan/renovate-config-validator@1ea1e8514f6a33fdd71c40b0a5fa3512b9e7b936 # tag=v0
with:
pattern: '{.renovaterc.json,renovate/default.json}'
docs:
name: Docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- name: Install dependencies (node)
uses: bahmutov/npm-install@1a235c31658a322a3b024444759650ee6345c26d # tag=v1
with:
useRollingCache: true
install-command: npm ci
- name: Install dependencies (python)
run: pip install -r packages/docutils/requirements.txt
- name: Build Docs
run: |
git config user.name 'Appium Bot'
git config user.email 'appium@appium.io'
npm run --workspace packages/appium build:docs
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3
- name: Cleanup
run: |
+46
View File
@@ -0,0 +1,46 @@
name: Build Docs
on:
pull_request_target:
paths:
- '**/typedoc.json'
- '**/tsconfig*.json'
- '**/package*.json'
- '**/*mkdocs*.ya?ml'
- 'packages/docutils/requirements.txt'
- 'packages/*/README.md'
- 'packages/*/docs/**'
- 'packages/*/**/*.ts'
- 'packages/*/**/*.js'
- '.github/workflows/docs.yml' # this file
- '!**/test/**'
- '!**/sample-code/**'
branches: [master]
concurrency: preview-${{ github.ref }}
jobs:
docs:
name: Docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3
- name: Install dependencies (Node.js)
uses: bahmutov/npm-install@1a235c31658a322a3b024444759650ee6345c26d # tag=v1
with:
useRollingCache: true
install-command: npm ci
- name: Install dependencies (Python)
run: pip install -r packages/docutils/requirements.txt
- name: Configure Git User
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
- name: Build Docs
run: npm run docs:preview
- name: Deploy Preview
uses: rossjrw/pr-preview-action@2a652922e9b9c53e7e5ea62fa38da744de09043c # v1
with:
source-dir: ./packages/appium/docs/site
custom-url: 'appium.github.io/appium'
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
name: Run FOSSA Analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3
with:
ref: master
- name: Install dependencies
-22
View File
@@ -1,22 +0,0 @@
name: 'Labeler'
on:
pull_request:
types: [opened, synchronize]
permissions:
# Setting up permissions in the workflow to limit the scope of what it can do. Optional!
contents: read
issues: read # change this if/when we want to label issues
pull-requests: write
statuses: read
checks: read
jobs:
labeler:
name: Labeler
runs-on: ubuntu-latest
steps:
# follows semantic versioning. Lock to different version: v1, v1.5, v1.5.0 or use a commit hash.
- uses: fuxingloh/multi-labeler@6704db0bcba106d07482efabbc79d3092af74fa2 # v2
with:
github-token: ${{secrets.GITHUB_TOKEN}} # optional, default to '${{ github.token }}'
+6 -6
View File
@@ -1,15 +1,15 @@
name: Disallow "On Hold" PRs
on:
pull_request:
pull_request_target:
types: [opened, labeled, unlabeled, synchronize]
branches: [master]
jobs:
label-check:
if: github.base_ref == 'master'
runs-on: ubuntu-latest
steps:
- uses: danielchabr/pr-labels-checker@e1d7e79aa0933af1728011705a30528a279874fd
- name: Check for "On Hold" label
uses: egmacke/action-check-label@0eb29f801fb482a95c9bd2dd22834ecc8f4e8af9 # v2
with:
id: check-labels
hasNone: On Hold
githubToken: ${{ secrets.GITHUB_TOKEN }}
label: On Hold
state: absent
-1
View File
@@ -59,7 +59,6 @@ appium.zip
*.tsbuildinfo
packages/*/LICENSE
local_appium_home
jsdoc-out
*.tgz
typedoc-docs
packages/appium/docs/*/reference
+5 -5
View File
@@ -17,9 +17,9 @@ module.exports = (wallaby) => {
},
files: [
'./packages/*/build/**/*',
'./packages/*/lib/**/*.(j|t)s',
'./packages/*/test/**/*helper*.(j|t)s',
'./packages/*/test/**/*mock*.(j|t)s',
'./packages/*/lib/**/*.{js,ts}',
'./packages/*/test/**/*helper*.{js,ts}',
'./packages/*/test/**/*mock*.{js,ts}',
'./packages/*/package.json',
'./packages/*/tsconfig*.json',
'./packages/*/test/**/fixture?(s)/**/*',
@@ -44,11 +44,11 @@ module.exports = (wallaby) => {
pattern: './packages/typedoc-plugin-appium/lib/theme/resources/**',
},
'!./packages/*/test/**/*-specs.js',
'!./packages/*/test/**/*.e2e.spec.(j|t)s',
'!./packages/*/test/**/*.e2e.spec.{js,ts}',
'!**/local_appium_home/**',
],
testFramework: 'mocha',
tests: ['./packages/*/test/unit/**/*.spec.(j|t)s', '!**/local_appium_home/**'],
tests: ['./packages/*/test/unit/**/*.spec.{js,ts}', '!**/local_appium_home/**'],
workers: {
// restart: true,
},
+3 -2
View File
@@ -12,8 +12,9 @@
Appium is an open-source, cross-platform test automation tool for native,
hybrid, mobile web and desktop apps. Initially created to automate iOS and Android mobile
applications Appium has grown to a full-featured platform that provides [WebDriver](https://www.w3.org/TR/webdriver/)-based automation possibilities for the whole set of different mobile and desktop platforms.
See [Drivers Maintained By The Appium Team](#drivers-maintained-by-the-appium-team)
and [Drivers Provided By Third Parties](#drivers-provided-by-third-parties) sections below for more details.
See the full list of
[officially-supported](https://appium.github.io/appium/docs/en/latest/ecosystem/#drivers) and
[third-party](https://appium.github.io/appium/docs/en/latest/ecosystem/#other-drivers) drivers for more details.
:bangbang: Major documentation revision in progress
+10 -5
View File
@@ -1,5 +1,13 @@
{
"$schema": "https://json.schemastore.org/lerna",
"changelogPreset": "conventional-changelog-conventionalcommits",
"command": {
"add": {
"exact": true
},
"publish": {
"message": "chore: publish"
},
"run": {
"concurrency": 8
},
@@ -9,13 +17,10 @@
"version": {
"allowBranch": "master"
}
},
"add": {
"exact": true
}
},
"ignoreChanges": ["**/test/**", "**/*.md"],
"version": "independent",
"useNx": false,
"useWorkspaces": true,
"useNx": false
"version": "independent"
}
+4299 -846
View File
File diff suppressed because it is too large Load Diff
+47 -33
View File
@@ -27,43 +27,54 @@
"packages/*"
],
"scripts": {
"build": "tsc -b",
"build:all": "run-s build build:packages",
"build:packages": "lerna run build",
"clean": "run-s clean:artifacts clean:workspaces",
"clean:artifacts": "run-p \"clean:artifacts:*\"",
"clean:artifacts:distfiles": "npx rimraf \"./packages/*/build\" \"./packages/*/.tsconfig.tsbuildinfo\"",
"clean:artifacts:packages": "lerna run clean || true",
"clean:workspaces": "lerna clean -y",
"build": "run-s build:*",
"build:compile": "tsc -b",
"build:workspaces": "lerna run build",
"ci": "npm ci",
"dev": "run-s \"build -- --watch\"",
"clean": "run-s clean:distfiles clean:workspaces",
"clean:dangerously": "run-s clean:workspaces clean:force",
"clean:deps": "lerna clean --yes",
"clean:distfiles": "npx rimraf \"./packages/*/build\" \"./packages/*/**/.tsconfig.tsbuildinfo\"; npx nx reset",
"clean:interactive": "git clean -d -x -i",
"clean:force": "git clean -d -x -f",
"clean:workspaces": "lerna run clean || true",
"dev": "run-s build:compile \"build:workspaces\" \"build:compile -- --watch\"",
"dev:docs": "npm run -w packages/appium dev:docs",
"docs": "npm run -w packages/appium build:docs",
"docs:preview": "npm run -w packages/appium build:docs:preview",
"doctor": "node ./packages/doctor",
"husky-install": "husky install",
"generate-docs": "npm run --workspace=packages/appium generate-docs",
"prepare": "run-p husky-install build:all",
"husky-install": "husky install",
"lint": "eslint .",
"lint:fix": "run-s \"lint -- --fix\"",
"lint:ci": "eslint --quiet .",
"lint:commit": "commitlint",
"lint:fix": "run-s \"lint -- --fix\"",
"lint:staged": "lint-staged",
"prepare": "run-p husky-install rebuild",
"prepublishOnly": "run-s rebuild",
"reinstall": "run-s clean ci",
"rebuild": "run-s clean:artifacts build:all",
"preversion": "run-s rebuild",
"version": "run-s sync-pkgs stage-synced-pkgs",
"rebuild": "run-s clean build",
"reinstall": "run-s clean clean:deps ci",
"reinstall:angry": "npm run clean:dangerously && npm ci",
"release": "lerna publish",
"stage-synced-pkgs": "git add -A packages/appium/README.md \"packages/*/package.json\"",
"start": "appium",
"sync-pkgs": "run-p sync-pkgs:*",
"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\" --packages=\"!packages/typedoc-plugin-appium\" --packages=\"!packages/eslint-config-appium-ts\" ",
"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": "run-s test:quick",
"test:ci": "run-s test:smoke test:unit test:types test:e2e",
"test:e2e": "lerna run --concurrency=2 test:e2e",
"test:quick": "run-s lint test:unit test:types",
"test:slow": "run-s test:quick test:smoke test:e2e",
"test:smoke": "smoker --all test:smoke",
"test:types": "lerna run test:types",
"test:unit": "lerna run test",
"typedoc": "typedoc --logLevel Verbose",
"upload": "gulp github-upload",
"preversion": "run-s sync-pkgs stage-synced-pkgs",
"prepack": "npm run rebuild",
"zip": "zip -qr ./appium.zip ./packages/appium",
"zip-and-upload": "run-s zip upload"
},
@@ -74,30 +85,32 @@
},
"devDependencies": {
"@colors/colors": "1.5.0",
"@commitlint/cli": "17.4.4",
"@commitlint/cli": "17.5.1",
"@commitlint/config-conventional": "17.4.4",
"@tsconfig/node14": "1.0.3",
"@types/chai": "4.3.4",
"@types/chai-as-promised": "7.1.5",
"@types/diff": "5.0.2",
"@types/diff": "5.0.3",
"@types/mocha": "10.0.1",
"@types/node": "18.14.6",
"@types/node": "18.15.11",
"@types/semver": "7.3.13",
"@types/sinon": "10.0.13",
"@types/sinon-chai": "3.2.9",
"@types/teen_process": "2.0.0",
"@types/ws": "8.5.4",
"@types/yargs": "17.0.22",
"@typescript-eslint/eslint-plugin": "5.54.1",
"@typescript-eslint/parser": "5.54.1",
"@types/yargs": "17.0.24",
"@typescript-eslint/eslint-plugin": "5.58.0",
"@typescript-eslint/parser": "5.58.0",
"asyncbox": "2.9.4",
"chai": "4.3.7",
"chai-as-promised": "7.1.1",
"chai-webdriverio-async": "2.7.0",
"conventional-changelog-conventionalcommits": "5.0.0",
"cpy-cli": "4.2.0",
"eslint": "8.35.0",
"eslint-config-prettier": "8.7.0",
"eslint": "8.38.0",
"eslint-config-prettier": "8.8.0",
"eslint-find-rules": "4.1.0",
"eslint-import-resolver-typescript": "3.5.5",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-mocha": "10.1.0",
"eslint-plugin-promise": "6.1.1",
@@ -107,30 +120,31 @@
"handlebars": "4.7.7",
"husky": "8.0.3",
"json-schema-to-typescript": "12.0.0",
"lerna": "6.5.1",
"lint-staged": "13.1.2",
"lerna": "6.6.1",
"lint-staged": "13.2.1",
"log-symbols": "4.1.0",
"midnight-smoker": "2.0.1",
"mjpeg-consumer": "2.0.0",
"mjpeg-server": "0.3.1",
"mocha": "10.2.0",
"npm-run-all": "4.1.5",
"prettier": "2.8.4",
"prettier": "2.8.7",
"rewiremock": "3.14.5",
"rimraf": "3.0.2",
"serve-static": "1.15.0",
"sinon": "15.0.1",
"sinon": "15.0.3",
"sinon-chai": "3.7.0",
"sync-monorepo-packages": "1.0.2",
"through2": "4.0.2",
"ts-node": "10.9.1",
"typedoc": "0.23.26",
"tsd": "0.28.1",
"typedoc": "0.23.28",
"typedoc-plugin-markdown": "3.14.0",
"typedoc-plugin-resolve-crossmodule-references": "0.3.3",
"typescript": "4.9.5",
"validate.js": "0.13.1",
"webdriverio": "7.30.2",
"ws": "8.12.1",
"ws": "8.13.0",
"yaml-js": "0.3.1"
},
"engines": {
+73
View File
@@ -3,6 +3,79 @@
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.63](https://github.com/appium/appium/compare/appium@2.0.0-beta.62...appium@2.0.0-beta.63) (2023-04-12)
**Note:** Version bump only for package appium
# [2.0.0-beta.62](https://github.com/appium/appium/compare/appium@2.0.0-beta.61...appium@2.0.0-beta.62) (2023-04-10)
### Bug Fixes
* **support:** update dependency axios to v1.3.5 ([6cf1480](https://github.com/appium/appium/commit/6cf14802b70a462beffc12a1134476596060c005))
* **types:** update dependency type-fest to v3.8.0 ([d6c42e9](https://github.com/appium/appium/commit/d6c42e99c08efce0b34796d5982ce379fca044d3))
### Features
* **appium:** make "ls" alias of "list" ([5ab2fea](https://github.com/appium/appium/commit/5ab2fea5768ef8c67270d49c920b9515ca1ff1e4))
# [2.0.0-beta.61](https://github.com/appium/appium/compare/appium@2.0.0-beta.60...appium@2.0.0-beta.61) (2023-04-05)
**Note:** Version bump only for package appium
# [2.0.0-beta.60](https://github.com/appium/appium/compare/appium@2.0.0-beta.59...appium@2.0.0-beta.60) (2023-04-04)
**Note:** Version bump only for package appium
# [2.0.0-beta.59](https://github.com/appium/appium/compare/appium@2.0.0-beta.58...appium@2.0.0-beta.59) (2023-04-03)
### Bug Fixes
* **appium:** update dependency package-changed to v3 ([bbcaec9](https://github.com/appium/appium/commit/bbcaec9c05e93d522ae72e30368cecc876e16db7))
* **types:** update dependency type-fest to v3.7.2 ([5580539](https://github.com/appium/appium/commit/55805390b5a0c6aa718bb357b30f66651f3db281))
# [2.0.0-beta.58](https://github.com/appium/appium/compare/appium@2.0.0-beta.57...appium@2.0.0-beta.58) (2023-03-28)
### Bug Fixes
* **appium,types,base-driver,fake-driver,driver-test-support:** normalize constraint defaults ([3c9fa7b](https://github.com/appium/appium/commit/3c9fa7ba73b639e610e1f3d41d239a9402845b4c))
* **appium:** remove longjohn ([0c52b50](https://github.com/appium/appium/commit/0c52b50e6595a16287d0bb71960fb2e08278de6f)), closes [#18357](https://github.com/appium/appium/issues/18357)
* backwards-compatible fixes for TS v5.x ([4974403](https://github.com/appium/appium/commit/49744036619ecc239e0e6255a13d38cafd709920))
* **types:** update dependency type-fest to v3.7.0 ([6912fa1](https://github.com/appium/appium/commit/6912fa14f2a7d338f17e1bed060e959de7aba1d6))
* **types:** update dependency type-fest to v3.7.1 ([bc860c7](https://github.com/appium/appium/commit/bc860c733a73760f0c42cbfb384e04d50c376d5e))
### Features
* **appium:** improvements to ext commands when running extension in development ([d9b47bc](https://github.com/appium/appium/commit/d9b47bc86434c50382221397cf39c10e661c1ea9)), closes [#18277](https://github.com/appium/appium/issues/18277)
# [2.0.0-beta.57](https://github.com/appium/appium/compare/appium@2.0.0-beta.56...appium@2.0.0-beta.57) (2023-03-08)
+3 -2
View File
@@ -12,8 +12,9 @@
Appium is an open-source, cross-platform test automation tool for native,
hybrid, mobile web and desktop apps. Initially created to automate iOS and Android mobile
applications Appium has grown to a full-featured platform that provides [WebDriver](https://www.w3.org/TR/webdriver/)-based automation possibilities for the whole set of different mobile and desktop platforms.
See [Drivers Maintained By The Appium Team](#drivers-maintained-by-the-appium-team)
and [Drivers Provided By Third Parties](#drivers-provided-by-third-parties) sections below for more details.
See the full list of
[officially-supported](https://appium.github.io/appium/docs/en/latest/ecosystem/#drivers) and
[third-party](https://appium.github.io/appium/docs/en/latest/ecosystem/#other-drivers) drivers for more details.
:bangbang: Major documentation revision in progress
+1 -1
View File
@@ -54,7 +54,7 @@ These drivers are not maintained by the Appium team and can be used to target ad
## Clients
You need a clients to write and run Appium scripts. To learn more about clients, read our [Client
You need clients to write and run Appium scripts. To learn more about clients, read our [Client
Intro](../intro/clients.md). Here is the list of known Appium clients. You'll want to become very
familiar with your client documentation (as well as the documentation of any Selenium client that
the Appium client depends on) since that is what you will use as your primary interface to Appium.
+1 -1
View File
@@ -20,7 +20,7 @@ JavaScript, Python, and Java, though [Appium clients](../ecosystem/index.md#clie
Before getting started, make sure your system satisfies the
[requirements](../intro/requirements.md) for running the Appium server. Additional requirements
will be discussed in conjunction with installing the UiAutomator2 driver. The guide also assumes
you're basic command line proficiency on your platform, for example being able to run commands, set
you have basic command line proficiency on your platform, for example being able to run commands, set
and persist environment variables, etc...
Now you're ready to get started! So head on over to [Installing Appium](./install.md).
+20 -12
View File
@@ -10,7 +10,7 @@
/* eslint-disable promise/prefer-await-to-callbacks */
/* eslint-disable promise/prefer-await-to-then */
const {buildReferenceDocs, deploy, updateNav} = require('@appium/docutils');
const {buildReferenceDocs, deploy, updateNav, buildSite} = require('@appium/docutils');
const {
log,
LANGS,
@@ -28,6 +28,7 @@ const {version} = require('../../package.json');
const branch = process.env.APPIUM_DOCS_BRANCH ?? DOCS_BRANCH;
const basePrefix = process.env.APPIUM_DOCS_PREFIX ?? DOCS_PREFIX;
const remote = process.env.APPIUM_DOCS_REMOTE ?? DOCS_REMOTE;
const preview = Boolean(process.env.APPIUM_DOCS_PREVIEW);
const push = Boolean(process.env.APPIUM_DOCS_PUBLISH);
const rebase = push;
@@ -51,17 +52,24 @@ async function main() {
for (const lang of LANGS) {
log.info(`Building docs for language '${lang}' and version ${majMinVer}`);
const configFile = path.join(DOCS_DIR, `mkdocs-${lang}.yml`);
await deploy({
mkdocsYml: configFile,
branch,
prefix: path.join(basePrefix, lang),
remote,
deployVersion: majMinVer,
push,
alias: LATEST_ALIAS,
rebase,
message: `docs(appium): auto-build docs for appium@${majMinVer}, language ${lang}`,
});
if (preview) {
await buildSite({
mkdocsYml: configFile,
siteDir: path.join('site', lang),
});
} else {
await deploy({
mkdocsYml: configFile,
branch,
prefix: path.join(basePrefix, lang),
remote,
deployVersion: majMinVer,
push,
alias: LATEST_ALIAS,
rebase,
message: `docs(appium): auto-build docs for appium@${majMinVer}, language ${lang}`,
});
}
log.info(`Docs built`);
}
}
-10
View File
@@ -1,10 +0,0 @@
const {baseConfig} = require('@appium/docutils');
const lang = process.env.APPIUM_DOCS_LANG || 'en';
const config = {...baseConfig};
config.opts.source.include = [];
config.opts.tutorials = `./docs/${lang}`;
config.opts.readme = `${config.opts.tutorials}/README.md`;
module.exports = config;
+42 -31
View File
@@ -28,15 +28,12 @@ const desiredCapabilityConstraints = /** @type {const} */ ({
isString: true,
},
});
/**
* @typedef {typeof desiredCapabilityConstraints} AppiumDriverConstraints
*/
const sessionsListGuard = new AsyncLock();
const pendingDriversGuard = new AsyncLock();
/**
* @implements {SessionHandler}
* @extends {DriverCore<AppiumDriverConstraints>}
*/
class AppiumDriver extends DriverCore {
/**
@@ -92,11 +89,11 @@ class AppiumDriver extends DriverCore {
*/
desiredCapConstraints;
/** @type {DriverOpts} */
/** @type {import('@appium/types').DriverOpts<AppiumDriverConstraints>} */
args;
/**
* @param {DriverOpts} opts
* @param {import('@appium/types').DriverOpts<AppiumDriverConstraints>} opts
*/
constructor(opts) {
// It is necessary to set `--tmp` here since it should be set to
@@ -163,11 +160,10 @@ class AppiumDriver extends DriverCore {
};
}
// eslint-disable-next-line require-await
async getSessions() {
return _.toPairs(this.sessions).map(([id, driver]) => ({
id,
capabilities: driver.caps,
capabilities: /** @type {import('@appium/types').DriverCaps} */ (driver.caps),
}));
}
@@ -224,12 +220,12 @@ class AppiumDriver extends DriverCore {
/**
* Create a new session
* @param {W3CCapabilities<AppiumDriverConstraints>} jsonwpCaps JSONWP formatted desired capabilities
* @param {W3CCapabilities<AppiumDriverConstraints>} reqCaps Required capabilities (JSONWP standard)
* @param {W3CCapabilities<AppiumDriverConstraints>} w3cCapabilities W3C capabilities
* @param {DriverData[]} [driverData]
* @param {W3CAppiumDriverCaps} jsonwpCaps JSONWP formatted desired capabilities
* @param {W3CAppiumDriverCaps} [reqCaps] Required capabilities (JSONWP standard)
* @param {W3CAppiumDriverCaps} [w3cCapabilities] W3C capabilities
* @returns {Promise<SessionHandlerCreateResult>}
*/
async createSession(jsonwpCaps, reqCaps, w3cCapabilities, driverData) {
async createSession(jsonwpCaps, reqCaps, w3cCapabilities) {
const defaultCapabilities = _.cloneDeep(this.args.defaultCapabilities);
const defaultSettings = pullSettings(defaultCapabilities);
jsonwpCaps = _.cloneDeep(jsonwpCaps);
@@ -247,13 +243,14 @@ class AppiumDriver extends DriverCore {
Object.assign(w3cSettings, pullSettings(firstMatchEntry));
}
/** @type {string|undefined} */
let protocol;
let innerSessionId, dCaps;
try {
// Parse the caps into a format that the InnerDriver will accept
const parsedCaps = parseCapsForInnerDriver(
jsonwpCaps,
promoteAppiumOptions(w3cCapabilities),
promoteAppiumOptions(/** @type {W3CAppiumDriverCaps} */ (w3cCapabilities)),
this.desiredCapConstraints,
defaultCapabilities ? promoteAppiumOptionsForObject(defaultCapabilities) : undefined
);
@@ -399,7 +396,7 @@ class AppiumDriver extends DriverCore {
/**
*
* @param {Driver} driver
* @param {ExternalDriver} driver
* @param {string} innerSessionId
*/
attachUnexpectedShutdownHandler(driver, innerSessionId) {
@@ -442,10 +439,11 @@ class AppiumDriver extends DriverCore {
/**
*
* @param {DriverClass} InnerDriver
* @param {((...args: any[]) => any)|(new(...args: any[]) => any)} InnerDriver
* @returns {Promise<DriverData[]>}}
* @privateRemarks The _intent_ is that `InnerDriver` is the class of a driver, but it only really
* needs to be a function or constructor.
*/
// eslint-disable-next-line require-await
async curSessionDataForDriver(InnerDriver) {
const data = _.compact(
_.values(this.sessions)
@@ -588,7 +586,7 @@ class AppiumDriver extends DriverCore {
*
* @param {string} cmd
* @param {...any} args
* @returns {Promise<{value: any, error?: Error, protocol: string} | import('type-fest').AsyncReturnType<Driver['executeCommand']>>}
* @returns {Promise<{value: any, error?: Error, protocol: string} | import('type-fest').AsyncReturnType<ExternalDriver['executeCommand']>>}
*/
async executeCommand(cmd, ...args) {
// We have basically three cases for how to handle commands:
@@ -618,6 +616,7 @@ class AppiumDriver extends DriverCore {
let dstSession = null;
let protocol = null;
/** @type {this | ExternalDriver} */
// eslint-disable-next-line @typescript-eslint/no-this-alias
let driver = this;
if (isSessionCmd) {
sessionId = _.last(args);
@@ -840,21 +839,38 @@ export class NoDriverProxyCommandError extends Error {
export {AppiumDriver};
/**
* @typedef {import('@appium/types').ExternalDriver} ExternalDriver
* @typedef {import('@appium/types').Driver} Driver
* @typedef {import('@appium/types').DriverClass} DriverClass
* @typedef {import('@appium/types').DriverData} DriverData
* @typedef {import('@appium/types').ServerArgs} DriverOpts
* @typedef {import('@appium/types').Constraints} Constraints
* @typedef {import('@appium/types').AppiumServer} AppiumServer
* @typedef {import('@appium/types').ExtensionType} ExtensionType
* @typedef {import('./extension/driver-config').DriverConfig} DriverConfig
* @typedef {import('@appium/types').Plugin} Plugin
* @typedef {import('@appium/types').PluginClass} PluginClass
* @typedef {import('@appium/types').PluginType} PluginType
* @typedef {import('@appium/types').DriverType} DriverType
* @typedef {import('@appium/types').StringRecord} StringRecord
* @typedef {import('@appium/types').SessionHandler<SessionHandlerResult<any[]>,SessionHandlerResult<void>>} SessionHandler
* @typedef {import('@appium/types').ExternalDriver} ExternalDriver
* @typedef {import('@appium/types').PluginClass} PluginClass
* @typedef {import('@appium/types').Plugin} Plugin
* @typedef {import('@appium/types').DriverClass<import('@appium/types').Driver>} DriverClass
*/
/**
* @typedef {import('@appium/types').ISessionHandler<AppiumDriverConstraints,
* SessionHandlerCreateResult, SessionHandlerDeleteResult>} AppiumSessionHandler
*/
/**
* @typedef {SessionHandlerResult<[innerSessionId: string, caps:
* import('@appium/types').DriverCaps<Constraints>, protocol: string|undefined]>} SessionHandlerCreateResult
*/
/**
* @template {Constraints} C
* @typedef {import('@appium/types').Core<C>} Core
*/
/**
* @typedef {SessionHandlerResult<void>} SessionHandlerDeleteResult
*/
/**
@@ -868,11 +884,6 @@ export {AppiumDriver};
*/
/**
* @template {Constraints} C
* @typedef {import('@appium/types').W3CCapabilities<C>} W3CCapabilities
*/
/**
* @template {Constraints} C
* @typedef {import('@appium/types').Capabilities<C>} Capabilities
* @typedef {typeof desiredCapabilityConstraints} AppiumDriverConstraints
* @typedef {import('@appium/types').W3CDriverCaps<AppiumDriverConstraints>} W3CAppiumDriverCaps
*/
+11 -1
View File
@@ -84,6 +84,16 @@ function makeListArgs(type) {
dest: 'showUpdates',
},
],
[
['--verbose'],
{
required: false,
default: false,
action: 'store_true',
help: 'Show more information about each extension',
dest: 'verbose',
},
],
]);
}
@@ -284,5 +294,5 @@ export {getServerArgs, getExtensionArgs};
/**
* A tuple of argument aliases and argument options
* @typedef {Map<string[],import('argparse').ArgumentOptions>} ArgumentDefinitions
* @typedef {Map<[name: string]|[name: string, alias: string],import('argparse').ArgumentOptions>} ArgumentDefinitions
*/
+7 -7
View File
@@ -1,15 +1,15 @@
import _ from 'lodash';
import ExtensionCommand from './extension-command';
import ExtensionCliCommand from './extension-command';
import {KNOWN_DRIVERS} from '../constants';
import '@colors/colors';
const REQ_DRIVER_FIELDS = ['driverName', 'automationName', 'platformNames', 'mainClass'];
/**
* @extends {ExtensionCommand<DriverType>}
* @extends {ExtensionCliCommand<DriverType>}
*/
export default class DriverCommand extends ExtensionCommand {
export default class DriverCliCommand extends ExtensionCliCommand {
/**
* @param {import('./extension-command').ExtensionCommandOptions<DriverType>} opts
*/
@@ -120,7 +120,7 @@ export default class DriverCommand extends ExtensionCommand {
*/
/**
* Options for {@linkcode DriverCommand.install}
* Options for {@linkcode DriverCliCommand.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
@@ -132,20 +132,20 @@ export default class DriverCommand extends ExtensionCommand {
*/
/**
* Options for {@linkcode DriverCommand.uninstall}
* Options for {@linkcode DriverCliCommand.uninstall}
* @typedef DriverUninstallOpts
* @property {string} driver - the name or spec of a driver to uninstall
*/
/**
* Options for {@linkcode DriverCommand.update}
* Options for {@linkcode DriverCliCommand.update}
* @typedef DriverUpdateOpts
* @property {string} driver - the name of the driver to update
* @property {boolean} unsafe - if true, will perform unsafe updates past major revision boundaries
*/
/**
* Options for {@linkcode DriverCommand.run}.
* Options for {@linkcode DriverCliCommand.run}.
* @typedef DriverRunOptions
* @property {string} driver - name of the driver to run a script from
* @property {string} scriptName - name of the script to run
+76 -54
View File
@@ -9,10 +9,12 @@ import {
INSTALL_TYPE_GIT,
INSTALL_TYPE_GITHUB,
INSTALL_TYPE_LOCAL,
INSTALL_TYPE_DEV,
} from '../extension/extension-config';
import {SubProcess} from 'teen_process';
import {packageDidChange} from '../extension/package-changed';
import {spawn} from 'child_process';
import {inspect} from 'node:util';
const UPDATE_ALL = 'installed';
@@ -32,7 +34,7 @@ function receiptToManifest(receipt) {
/**
* @template {ExtensionType} ExtType
*/
class ExtensionCommand {
class ExtensionCliCommand {
/**
* This is the `DriverConfig` or `PluginConfig`, depending on `ExtType`.
* @type {ExtensionConfig<ExtType>}
@@ -99,41 +101,40 @@ class ExtensionCommand {
/**
* List extensions
*
* @template {ExtensionType} ExtType
* @param {ListOptions} opts
* @return {Promise<ExtensionListData>} map of extension names to extension data
* @return {Promise<ExtensionList<ExtType>>} map of extension names to extension data
*/
async list({showInstalled, showUpdates}) {
const lsMsg = `Listing ${showInstalled ? 'installed' : 'available'} ${this.type}s`;
async list({showInstalled, showUpdates, verbose = false}) {
let lsMsg = `Listing ${showInstalled ? 'installed' : 'available'} ${this.type}s`;
if (verbose) {
lsMsg += ' (verbose mode)';
}
const installedNames = Object.keys(this.config.installedExtensions);
const knownNames = Object.keys(this.knownExtensions);
const exts = [...installedNames, ...knownNames].reduce(
(acc, name) => {
if (!acc[name]) {
if (installedNames.includes(name)) {
acc[name] = {
...this.config.installedExtensions[name],
installed: true,
};
} else if (!showInstalled) {
acc[name] = {pkgName: this.knownExtensions[name], installed: false};
}
const listData = [...installedNames, ...knownNames].reduce((acc, name) => {
if (!acc[name]) {
if (installedNames.includes(name)) {
acc[name] = {
.../** @type {Partial<ExtManifest<ExtType>>} */ (this.config.installedExtensions[name]),
installed: true,
};
} else if (!showInstalled) {
acc[name] = /** @type {ExtensionListData<ExtType>} */ ({
pkgName: this.knownExtensions[name],
installed: false,
});
}
return acc;
},
/**
* This accumulator contains either {@linkcode UninstalledExtensionLIstData} _or_
* {@linkcode InstalledExtensionListData} without upgrade information (which is added by the below code block)
* @type {Record<string,Partial<InstalledExtensionListData>|UninstalledExtensionListData>}
*/ ({})
);
}
return acc;
}, /** @type {ExtensionList<ExtType>} */ ({}));
// if we want to show whether updates are available, put that behind a spinner
await spinWith(this.isJsonOutput, lsMsg, async () => {
if (!showUpdates) {
return;
}
for (const [ext, data] of _.toPairs(exts)) {
for (const [ext, data] of _.toPairs(listData)) {
if (!data.installed || data.installType !== INSTALL_TYPE_NPM) {
// don't need to check for updates on exts that aren't installed
// also don't need to check for updates on non-npm exts
@@ -150,7 +151,12 @@ class ExtensionCommand {
}
});
const listData = /** @type {ExtensionListData} */ (exts);
/**
* Type guard to narrow "installed" extensions, which have more data
* @param {any} data
* @returns {data is InstalledExtensionListData<ExtType>}
*/
const extIsInstalled = (data) => Boolean(data.installed);
// if we're just getting the data, short circuit return here since we don't need to do any
// formatting logic
@@ -158,12 +164,16 @@ class ExtensionCommand {
return listData;
}
if (verbose) {
this.log.log(inspect(listData, {colors: true, depth: null}));
return listData;
}
for (const [name, data] of _.toPairs(listData)) {
let installTxt = ' [not installed]'.grey;
let updateTxt = '';
let upToDateTxt = '';
let unsafeUpdateTxt = '';
if (data.installed) {
if (extIsInstalled(data)) {
const {
installType,
installSpec,
@@ -182,8 +192,11 @@ class ExtensionCommand {
case INSTALL_TYPE_LOCAL:
typeTxt = `(linked from ${installSpec})`.magenta;
break;
case INSTALL_TYPE_DEV:
typeTxt = '(dev mode)';
break;
default:
typeTxt = '(NPM)';
typeTxt = '(npm)';
}
installTxt = `@${version.yellow} ${('[installed ' + typeTxt + ']').green}`;
@@ -513,7 +526,12 @@ class ExtensionCommand {
`Can't uninstall ${this.type} '${installSpec}'; it is not installed`
);
}
const pkgName = this.config.installedExtensions[installSpec].pkgName;
const extRecord = this.config.installedExtensions[installSpec];
if (extRecord.installType === INSTALL_TYPE_DEV) {
this.log.warn(`Cannot uninstall ${this.type} "${installSpec}" because it is in development!`);
return this.config.installedExtensions;
}
const pkgName = extRecord.pkgName;
await npm.uninstallPackage(this.config.appiumHome, pkgName);
await this.config.removeExtension(installSpec);
this.log.ok(`Successfully uninstalled ${this.type} '${installSpec}'`.green);
@@ -771,11 +789,11 @@ class ExtensionCommand {
}
}
export default ExtensionCommand;
export {ExtensionCommand};
export default ExtensionCliCommand;
export {ExtensionCliCommand as ExtensionCommand};
/**
* Options for the {@linkcode ExtensionCommand} constructor
* Options for the {@linkcode ExtensionCliCommand} constructor
* @template {ExtensionType} ExtType
* @typedef ExtensionCommandOptions
* @property {ExtensionConfig<ExtType>} config - the `DriverConfig` or `PluginConfig` instance used for this command
@@ -783,14 +801,15 @@ export {ExtensionCommand};
*/
/**
* Extra stuff about extensions; used indirectly by {@linkcode ExtensionCommand.list}.
* Extra stuff about extensions; used indirectly by {@linkcode ExtensionCliCommand.list}.
*
* @typedef ExtensionMetadata
* @typedef ExtensionListMetadata
* @property {boolean} installed - If `true`, the extension is installed
* @property {string?} updateVersion - If the extension is installed, the version it can be updated to
* @property {string?} unsafeUpdateVersion - Same as above, but a major version bump
* @property {boolean} upToDate - If the extension is installed and the latest
* @property {string?} updateError - Update check error message (if present)
* @property {string|null} updateVersion - If the extension is installed, the version it can be updated to
* @property {string|null} unsafeUpdateVersion - Same as above, but a major version bump
* @property {string} [updateError] - Update check error message (if present)
* @property {boolean} [devMode] - If Appium is run from an extension's working copy
*/
/**
@@ -830,22 +849,24 @@ export {ExtensionCommand};
*/
/**
* Possible return value for {@linkcode ExtensionCommand.list}
* @typedef {Partial<InstalledExtensionListData> & {pkgName: string, installed: false}} UninstalledExtensionListData
* Possible return value for {@linkcode ExtensionCliCommand.list}
* @template {ExtensionType} ExtType
* @typedef {Partial<ExtManifest<ExtType>> & Partial<ExtensionListMetadata>} ExtensionListData
*/
/**
* Possible return value for {@linkcode ExtensionCommand.list}
* @typedef {import('appium/types').InternalMetadata & ExtensionMetadata} InstalledExtensionListData
* @template {ExtensionType} ExtType
* @typedef {ExtManifest<ExtType> & ExtensionListMetadata} InstalledExtensionListData
*/
/**
* Return value of {@linkcode ExtensionCommand.list}.
* @typedef {Record<string,InstalledExtensionListData|UninstalledExtensionListData>} ExtensionListData
* Return value of {@linkcode ExtensionCliCommand.list}.
* @template {ExtensionType} ExtType
* @typedef {Record<string,ExtensionListData<ExtType>>} ExtensionList
*/
/**
* Options for {@linkcode ExtensionCommand._run}.
* Options for {@linkcode ExtensionCliCommand._run}.
* @typedef RunOptions
* @property {string} installSpec - name of the extension to run a script from
* @property {string} scriptName - name of the script to run
@@ -854,7 +875,7 @@ export {ExtensionCommand};
*/
/**
* Return value of {@linkcode ExtensionCommand._run}
* Return value of {@linkcode ExtensionCliCommand._run}
*
* @typedef RunOutput
* @property {string} [error] - error message if script ran unsuccessfully, otherwise undefined
@@ -862,41 +883,41 @@ export {ExtensionCommand};
*/
/**
* Options for {@linkcode ExtensionCommand._update}.
* Options for {@linkcode ExtensionCliCommand._update}.
* @typedef ExtensionUpdateOpts
* @property {string} installSpec - the name of the extension to update
* @property {boolean} unsafe - if true, will perform unsafe updates past major revision boundaries
*/
/**
* Return value of {@linkcode ExtensionCommand._update}.
* Return value of {@linkcode ExtensionCliCommand._update}.
* @typedef ExtensionUpdateResult
* @property {Record<string,Error>} errors - map of ext names to error objects
* @property {Record<string,UpdateReport>} updates - map of ext names to {@linkcode UpdateReport}s
*/
/**
* Part of result of {@linkcode ExtensionCommand._update}.
* Part of result of {@linkcode ExtensionCliCommand._update}.
* @typedef UpdateReport
* @property {string} from - version the extension was updated from
* @property {string} to - version the extension was updated to
*/
/**
* Options for {@linkcode ExtensionCommand._uninstall}.
* Options for {@linkcode ExtensionCliCommand._uninstall}.
* @typedef UninstallOpts
* @property {string} installSpec - the name or spec of an extension to uninstall
*/
/**
* Used by {@linkcode ExtensionCommand.getPostInstallText}
* Used by {@linkcode ExtensionCliCommand.getPostInstallText}
* @typedef ExtensionArgs
* @property {string} extName - the name of an extension
* @property {object} extData - the data for an installed extension
*/
/**
* Options for {@linkcode ExtensionCommand.installViaNpm}
* Options for {@linkcode ExtensionCliCommand.installViaNpm}
* @typedef InstallViaNpmArgs
* @property {string} installSpec - the name or spec of an extension to install
* @property {string} pkgName - the NPM package name of the extension
@@ -905,7 +926,7 @@ export {ExtensionCommand};
*/
/**
* Object returned by {@linkcode ExtensionCommand.checkForExtensionUpdate}
* Object returned by {@linkcode ExtensionCliCommand.checkForExtensionUpdate}
* @typedef PossibleUpdates
* @property {string} current - current version
* @property {string?} safeUpdate - version we can safely update to if it exists, or null
@@ -913,7 +934,7 @@ export {ExtensionCommand};
*/
/**
* Options for {@linkcode ExtensionCommand._install}
* Options for {@linkcode ExtensionCliCommand._install}
* @typedef InstallOpts
* @property {string} installSpec - the name or spec of an extension to install
* @property {InstallType} installType - how to install this extension. One of the INSTALL_TYPES
@@ -929,10 +950,11 @@ export {ExtensionCommand};
* @typedef ListOptions
* @property {boolean} showInstalled - whether should show only installed extensions
* @property {boolean} showUpdates - whether should show available updates
* @property {boolean} [verbose] - whether to show additional data from the extension
*/
/**
* Opts for {@linkcode ExtensionCommand.getInstallationReceipt}
* Opts for {@linkcode ExtensionCliCommand.getInstallationReceipt}
* @template {ExtensionType} ExtType
* @typedef GetInstallationReceiptOpts
* @property {string} installPath
+5 -5
View File
@@ -1,14 +1,14 @@
/* eslint-disable no-console */
import {DRIVER_TYPE, PLUGIN_TYPE} from '../constants';
import {isExtensionCommandArgs} from '../utils';
import DriverCommand from './driver-command';
import PluginCommand from './plugin-command';
import DriverCliCommand from './driver-command';
import PluginCliCommand from './plugin-command';
import {errAndQuit, JSON_SPACES} from './utils';
export const commandClasses = Object.freeze(
/** @type {const} */ ({
[DRIVER_TYPE]: DriverCommand,
[PLUGIN_TYPE]: PluginCommand,
[DRIVER_TYPE]: DriverCliCommand,
[PLUGIN_TYPE]: PluginCliCommand,
})
);
@@ -59,7 +59,7 @@ export {runExtensionCommand};
/**
* @template {ExtensionType} ExtType
* @typedef {ExtType extends DriverType ? Class<DriverCommand> : ExtType extends PluginType ? Class<PluginCommand> : never} ExtCommand
* @typedef {ExtType extends DriverType ? Class<DriverCliCommand> : ExtType extends PluginType ? Class<PluginCliCommand> : never} ExtCommand
*/
/**
+16 -5
View File
@@ -101,6 +101,13 @@ class ArgParser {
try {
const parsed = this.parser.parse_known_args(args);
const [knownArgs, unknownArgs] = parsed;
// XXX: you'd think that argparse, when given an alias for a subcommand,
// would set this value to the original subcommand name, but it doesn't.
if (knownArgs?.driverCommand === 'ls') {
knownArgs.driverCommand = 'list';
} else if (knownArgs?.pluginCommand === 'ls') {
knownArgs.pluginCommand = 'list';
}
if (
unknownArgs?.length &&
(knownArgs.driverCommand === 'run' || knownArgs.pluginCommand === 'run')
@@ -210,13 +217,14 @@ class ArgParser {
});
const extensionArgs = getExtensionArgs();
/**
* @type { {command: import('appium/types').CliExtensionSubcommand, args: import('./args').ArgumentDefinitions, help: string}[] }
* @type { {command: import('appium/types').CliExtensionSubcommand, args: import('./args').ArgumentDefinitions, help: string, aliases?: import('argparse').SubArgumentParserOptions['aliases']}[] }
*/
const parserSpecs = [
{
command: 'list',
args: extensionArgs[type].list,
help: `List available and installed ${type}s`,
aliases: ['ls'],
},
{
command: 'install',
@@ -242,15 +250,18 @@ class ArgParser {
},
];
for (const {command, args, help} of parserSpecs) {
const parser = extSubParsers.add_parser(command, {help});
for (const {command, args, help, aliases} of parserSpecs) {
const parser = extSubParsers.add_parser(command, {help, aliases: aliases ?? []});
ArgParser._patchExit(parser);
for (const [flagsOrNames, opts] of args) {
// add_argument mutates params so make sure to send in copies instead
// @ts-ignore
parser.add_argument(...flagsOrNames, {...opts});
if (flagsOrNames.length === 2) {
parser.add_argument(flagsOrNames[0], flagsOrNames[1], {...opts});
} else {
parser.add_argument(flagsOrNames[0], {...opts});
}
}
}
}
+7 -7
View File
@@ -1,13 +1,13 @@
import _ from 'lodash';
import ExtensionCommand from './extension-command';
import ExtensionCliCommand from './extension-command';
import {KNOWN_PLUGINS} from '../constants';
const REQ_PLUGIN_FIELDS = ['pluginName', 'mainClass'];
/**
* @extends {ExtensionCommand<PluginType>}
* @extends {ExtensionCliCommand<PluginType>}
*/
export default class PluginCommand extends ExtensionCommand {
export default class PluginCliCommand extends ExtensionCliCommand {
/**
*
* @param {import('./extension-command').ExtensionCommandOptions<PluginType>} opts
@@ -110,7 +110,7 @@ export default class PluginCommand extends ExtensionCommand {
*/
/**
* Options for {@linkcode PluginCommand.install}
* Options for {@linkcode PluginCliCommand.install}
* @typedef PluginInstallOpts
* @property {string} plugin - the name or spec of a plugin to install
* @property {InstallType} installType - how to install this plugin. One of the INSTALL_TYPES
@@ -122,20 +122,20 @@ export default class PluginCommand extends ExtensionCommand {
*/
/**
* Options for {@linkcode PluginCommand.uninstall}
* Options for {@linkcode PluginCliCommand.uninstall}
* @typedef PluginUninstallOpts
* @property {string} plugin - the name or spec of a plugin to uninstall
*/
/**
* Options for {@linkcode PluginCommand.update}
* Options for {@linkcode PluginCliCommand.update}
* @typedef PluginUpdateOpts
* @property {string} plugin - the name of the plugin to update
* @property {boolean} unsafe - if true, will perform unsafe updates past major revision boundaries
*/
/**
* Options for {@linkcode PluginCommand.run}.
* Options for {@linkcode PluginCliCommand.run}.
* @typedef PluginRunOptions
* @property {string} plugin - name of the plugin to run a script from
* @property {string} scriptName - name of the script to run
+9 -1
View File
@@ -68,4 +68,12 @@ export const EXT_SUBCOMMAND_RUN = 'run';
/**
* Current revision of the manifest (`extensions.yaml`) schema
*/
export const CURRENT_SCHEMA_REV = 3;
export const CURRENT_SCHEMA_REV = 4;
/**
* The default number of stack frames to show in a "long" stack trace, when enabled via `--long-stacktrace`
* @remarks This value may be increased in the future.
* @privateRemarks A value like `Infinity` may provide to have deleterious effects on
* memory usage, perf, and/or log output, and higher limits may be difficult to scan.
*/
export const LONG_STACKTRACE_LIMIT = 100;
@@ -212,28 +212,29 @@ export class DriverConfig extends ExtensionConfig {
}
/**
* @template T
* @template {ExtensionType} T
* @typedef {import('appium/types').ExtMetadata<T>} ExtMetadata
*/
/**
* @template T
* @template {ExtensionType} T
* @typedef {import('appium/types').ExtManifest<T>} ExtManifest
*/
/**
* @typedef {import('@appium/types').ExtensionType} ExtensionType
* @typedef {import('appium/types').ManifestData} ManifestData
* @typedef {import('@appium/types').DriverType} DriverType
* @typedef {import('./manifest').Manifest} Manifest
*/
/**
* @template T
* @template {ExtensionType} T
* @typedef {import('appium/types').ExtRecord<T>} ExtRecord
*/
/**
* @template T
* @template {ExtensionType} T
* @typedef {import('appium/types').ExtName<T>} ExtName
*/
@@ -244,7 +245,3 @@ export class DriverConfig extends ExtensionConfig {
* @property {string} version
* @property {string} driverName
*/
/**
* @typedef {import('@appium/types').Capabilities} Capabilities
*/
@@ -1,9 +1,9 @@
import _ from 'lodash';
import {util} from '@appium/support';
import B from 'bluebird';
import _ from 'lodash';
import path from 'path';
import resolveFrom from 'resolve-from';
import {satisfies} from 'semver';
import {util} from '@appium/support';
import {commandClasses} from '../cli/extension';
import {APPIUM_VER} from '../config';
import log from '../logger';
@@ -13,17 +13,41 @@ import {
registerSchema,
} from '../schema/schema';
const INSTALL_TYPE_NPM = 'npm';
const INSTALL_TYPE_LOCAL = 'local';
const INSTALL_TYPE_GITHUB = 'github';
const INSTALL_TYPE_GIT = 'git';
/**
* "npm" install type
* Used when extension was installed by npm package name
* @remarks _All_ extensions are installed _by_ `npm`, but only this one means the package name was
* used to specify it
*/
export const INSTALL_TYPE_NPM = 'npm';
/**
* "local" install type
* Used when extension was installed from a local path
*/
export const INSTALL_TYPE_LOCAL = 'local';
/**
* "github" install type
* Used when extension was installed via GitHub URL
*/
export const INSTALL_TYPE_GITHUB = 'github';
/**
* "git" install type
* Used when extensions was installed via Git URL
*/
export const INSTALL_TYPE_GIT = 'git';
/**
* "dev" install type
* Used when automatically detected as a working copy
*/
export const INSTALL_TYPE_DEV = 'dev';
/** @type {Set<InstallType>} */
const INSTALL_TYPES = new Set([
export const INSTALL_TYPES = new Set([
INSTALL_TYPE_GIT,
INSTALL_TYPE_GITHUB,
INSTALL_TYPE_LOCAL,
INSTALL_TYPE_NPM,
INSTALL_TYPE_DEV,
]);
/**
@@ -54,7 +78,7 @@ export class ExtensionConfig {
manifest;
/**
* @type {ExtensionListData|undefined}
* @type {import('../cli/extension-command').ExtensionList<ExtType>|undefined}
*/
#listDataCache;
@@ -239,7 +263,7 @@ export class ExtensionConfig {
* This is an expensive operation, so the result is cached. Currently, there is no
* use case for invalidating the cache.
* @protected
* @returns {Promise<ExtensionListData>}
* @returns {Promise<import('../cli/extension-command').ExtensionList<ExtType>>}
*/
async getListData() {
if (this.#listDataCache) {
@@ -300,7 +324,10 @@ export class ExtensionConfig {
if (_.isString(appiumVersion) && !satisfies(APPIUM_VER, appiumVersion)) {
const listData = await this.getListData();
const extListData = /** @type {InstalledExtensionListData} */ (listData[extName]);
const extListData =
/** @type {import('../cli/extension-command').ExtensionListData<ExtType>} */ (
listData[extName]
);
if (extListData?.installed) {
const {updateVersion, upToDate} = extListData;
if (!upToDate) {
@@ -319,7 +346,10 @@ export class ExtensionConfig {
}
} else if (!_.isString(appiumVersion)) {
const listData = await this.getListData();
const extListData = /** @type {InstalledExtensionListData} */ (listData[extName]);
const extListData =
/** @type {import('../cli/extension-command').InstalledExtensionListData<ExtType>} */ (
listData[extName]
);
if (!extListData?.upToDate && extListData?.updateVersion) {
warnings.push(
createPeerWarning(
@@ -544,6 +574,7 @@ export class ExtensionConfig {
delete require.cache[reqResolved];
}
log.debug(`Requiring ${this.extensionType} at ${reqPath}`);
// eslint-disable-next-line @typescript-eslint/no-var-requires
const MainClass = require(reqPath)[mainClass];
if (!MainClass) {
throw new ReferenceError(
@@ -619,8 +650,6 @@ export class ExtensionConfig {
}
}
export {INSTALL_TYPE_NPM, INSTALL_TYPE_GIT, INSTALL_TYPE_LOCAL, INSTALL_TYPE_GITHUB, INSTALL_TYPES};
/**
* An issue with the {@linkcode ExtManifest} for a particular extension.
*
@@ -640,8 +669,6 @@ export {INSTALL_TYPE_NPM, INSTALL_TYPE_GIT, INSTALL_TYPE_LOCAL, INSTALL_TYPE_GIT
/**
* @typedef {import('@appium/types').ExtensionType} ExtensionType
* @typedef {import('./manifest').Manifest} Manifest
* @typedef {import('../cli/extension-command').ExtensionListData} ExtensionListData
* @typedef {import('../cli/extension-command').InstalledExtensionListData} InstalledExtensionListData
* @typedef {import('appium/types').InstallType} InstallType
*/
@@ -1,4 +1,4 @@
import {DRIVER_TYPE, PLUGIN_TYPE} from '../constants';
import {CURRENT_SCHEMA_REV, DRIVER_TYPE, PLUGIN_TYPE} from '../constants';
import log from '../logger';
/**
@@ -12,6 +12,11 @@ import log from '../logger';
*/
const SCHEMA_REV_3 = 3;
/**
* Constant for v4 of the schema rev.
*/
const SCHEMA_REV_4 = 4;
/**
* Collection of functions to migrate from one version to another.
*
@@ -20,7 +25,7 @@ const SCHEMA_REV_3 = 3;
* 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}}
* @type { {[P in keyof ManifestDataVersions]?: Migration} }
*/
const Migrations = {
/**
@@ -32,19 +37,32 @@ const Migrations = {
* @type {Migration}
*/
[SCHEMA_REV_3]: (manifest) => {
let shouldSync = false;
/** @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;
break;
}
return allExtData.some((metadata) => !('installPath' in metadata));
},
/**
* Updates installed extensions to use `InstallType` of `dev` if appropriate.
*
* Previously, these types of extensions (automatically discovered) would use the default `InstallType` of `npm`, so we need
* to refresh any with this install type.
*
* This should only happen once; we do not want to re-check everything with `npm` install type
* every time.
* @type {Migration}
*/
[SCHEMA_REV_4]: (manifest) => {
if (manifest.schemaRev < SCHEMA_REV_4) {
const allExtData = [
...Object.values(manifest.getExtensionData(DRIVER_TYPE)),
...Object.values(manifest.getExtensionData(PLUGIN_TYPE)),
];
return allExtData.some((metadata) => metadata.installType === 'npm');
}
return shouldSync;
return false;
},
};
@@ -61,7 +79,7 @@ const Migrations = {
* @returns {boolean} Whether the data was modified
*/
function setSchemaRev(manifest, version) {
if (manifest.schemaRev ?? 0 < version) {
if ((manifest.schemaRev ?? 0) < version) {
manifest.setSchemaRev(version);
return true;
}
@@ -78,15 +96,13 @@ function setSchemaRev(manifest, version) {
*/
export async function migrate(manifest) {
let didChange = false;
for await (const [v, migration] of Object.entries(Migrations)) {
const version = /** @type {keyof ManifestDataVersions} */ (Number(v));
for await (const migration of Object.values(Migrations)) {
didChange = (await migration(manifest)) || didChange;
didChange = setSchemaRev(manifest, version) || didChange;
}
didChange = setSchemaRev(manifest, CURRENT_SCHEMA_REV) || 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${manifest.schemaRev}`);
log.debug(`Upgraded extension manifest to schema v${manifest.schemaRev}`);
}
return didChange;
+26 -19
View File
@@ -10,7 +10,7 @@ import path from 'path';
import YAML from 'yaml';
import {CURRENT_SCHEMA_REV, DRIVER_TYPE, PLUGIN_TYPE} from '../constants';
import log from '../logger';
import {INSTALL_TYPE_NPM} from './extension-config';
import {INSTALL_TYPE_NPM, INSTALL_TYPE_DEV} from './extension-config';
import {packageDidChange} from './package-changed';
import {migrate} from './manifest-migrations';
@@ -148,27 +148,33 @@ export class Manifest {
* Returns a new or existing {@link Manifest} instance, based on the value of `appiumHome`.
*
* Maintains one instance per value of `appiumHome`.
* @param {string} appiumHome - Path to `APPIUM_HOME`
* @returns {Manifest}
*/
static getInstance = _.memoize(function _getInstance(appiumHome) {
return new Manifest(appiumHome);
});
static getInstance = _.memoize(
/**
* @param {string} appiumHome - Path to `APPIUM_HOME`
* @returns {Manifest}
*/
function _getInstance(appiumHome) {
return new Manifest(appiumHome);
}
);
/**
* Searches `APPIUM_HOME` for installed extensions and adds them to the manifest.
* @param {boolean} hasAppiumDependency - This affects whether or not the "dev" `InstallType` is used
* @returns {Promise<boolean>} `true` if any extensions were added, `false` otherwise.
*/
async syncWithInstalledExtensions() {
async syncWithInstalledExtensions(hasAppiumDependency = false) {
// this could be parallelized, but we can't use fs.walk as an async iterator
let didChange = false;
/**
* Listener for the `match` event of a `glob` instance
* @param {string} filepath - Path to a `package.json`
* @param {boolean} [devType] - If `true`, this is an extension in "dev mode"
* @returns {Promise<void>}
*/
const onMatch = async (filepath) => {
const onMatch = async (filepath, devType = false) => {
try {
const pkg = JSON.parse(await fs.readFile(filepath, 'utf8'));
if (isExtension(pkg)) {
@@ -188,7 +194,8 @@ export class Manifest {
) {
log.info(`Discovered installed ${extType} "${name}"`);
}
const changed = this.addExtensionFromPackage(pkg, filepath);
const installType = devType && hasAppiumDependency ? INSTALL_TYPE_DEV : INSTALL_TYPE_NPM;
const changed = this.addExtensionFromPackage(pkg, filepath, installType);
didChange = didChange || changed;
}
} catch {}
@@ -199,8 +206,9 @@ export class Manifest {
* @type {Promise<void>[]}
*/
const queue = [
// look at `package.json` in `APPIUM_HOME` only
onMatch(path.join(this.#appiumHome, 'package.json')),
// look at `package.json` in `APPIUM_HOME` only.
// this causes extensions in "dev mode" to be automatically found
onMatch(path.join(this.#appiumHome, 'package.json'), true),
];
// add dependencies to the queue
@@ -252,9 +260,10 @@ export class Manifest {
* @template {ExtensionType} ExtType
* @param {ExtPackageJson<ExtType>} pkgJson
* @param {string} pkgPath
* @param {typeof INSTALL_TYPE_NPM | typeof INSTALL_TYPE_DEV} [installType]
* @returns {boolean} - `true` if this method did anything.
*/
addExtensionFromPackage(pkgJson, pkgPath) {
addExtensionFromPackage(pkgJson, pkgPath, installType = INSTALL_TYPE_NPM) {
const extensionPath = path.dirname(pkgPath);
/**
@@ -264,7 +273,7 @@ export class Manifest {
pkgName: pkgJson.name,
version: pkgJson.version,
appiumVersion: pkgJson.peerDependencies?.appium,
installType: INSTALL_TYPE_NPM,
installType,
installSpec: `${pkgJson.name}@${pkgJson.version}`,
installPath: extensionPath,
};
@@ -434,6 +443,8 @@ export class Manifest {
shouldWrite = await migrate(this);
}
const hasAppiumDependency = await env.hasAppiumDependency(this.appiumHome);
/**
* 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:
@@ -445,13 +456,9 @@ export class Manifest {
* 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)))
) {
if (shouldWrite || (hasAppiumDependency && (await packageDidChange(this.appiumHome)))) {
log.debug('Discovering newly installed extensions...');
shouldWrite = (await this.syncWithInstalledExtensions()) || shouldWrite;
shouldWrite = (await this.syncWithInstalledExtensions(hasAppiumDependency)) || shouldWrite;
}
if (shouldWrite) {
+66 -17
View File
@@ -2,9 +2,8 @@
import {init as logsinkInit} from './logsink'; // this import needs to come first since it sets up global npmlog
import logger from './logger'; // logger needs to remain second
// @ts-ignore
import {routeConfiguringFunction as makeRouter, server as baseServer} from '@appium/base-driver';
import {logger as logFactory, util, env} from '@appium/support';
import {logger as logFactory, util, env, fs} from '@appium/support';
import {asyncify} from 'asyncbox';
import _ from 'lodash';
import {AppiumDriver} from './appium';
@@ -23,7 +22,7 @@ import {
} from './config';
import {readConfigFile} from './config-file';
import {loadExtensions, getActivePlugins, getActiveDrivers} from './extension';
import {SERVER_SUBCOMMAND} from './constants';
import {SERVER_SUBCOMMAND, LONG_STACKTRACE_LIMIT} from './constants';
import registerNode from './grid-register';
import {getDefaultsForSchema, validate} from './schema/schema';
import {
@@ -34,6 +33,7 @@ import {
isPluginCommandArgs,
isServerCommandArgs,
} from './utils';
import os from 'node:os';
const {resolveAppiumHome} = env;
@@ -47,7 +47,7 @@ async function preflightChecks(args, throwInsteadOfExit = false) {
checkNodeOk();
await checkNpmOk();
if (args.longStacktrace) {
require('longjohn').async_trace_limit = -1;
Error.stackTraceLimit = LONG_STACKTRACE_LIMIT;
}
if (args.showBuildInfo) {
await showBuildInfo();
@@ -114,17 +114,6 @@ async function logStartupInfo(args) {
// }
}
/**
* Logs the address and port the server is listening on
* @param {string} address - Address
* @param {number} port - Port
* @returns {void}
*/
function logServerPort(address, port) {
let logMessage = `Appium REST http interface listener started on ` + `${address}:${port}`;
logger.info(logMessage);
}
/**
* Gets a list of `updateServer` functions from all extensions
* @param {DriverNameMap} driverClasses
@@ -150,6 +139,52 @@ function getExtraMethodMap(driverClasses, pluginClasses) {
{}
);
}
/**
* Prepares and validates appium home path folder
*
* @param {string} name The name of the appium home source (needed for error messages)
* @param {string} appiumHome The actual value to be verified
* @returns {Promise<string>} Same appiumHome value
* @throws {Error} If the validation has failed
*/
async function prepareAppiumHome(name, appiumHome) {
let stat;
try {
stat = await fs.stat(appiumHome);
} catch (e) {
let err = e;
if (e.code === 'ENOENT') {
try {
await fs.mkdir(appiumHome, {recursive: true});
return appiumHome;
} catch (e1) {
err = e1;
}
}
throw new Error(
`The path '${appiumHome}' provided in the ${name} must point ` +
`to a valid folder writeable for the current user account '${os.userInfo().username}'. ` +
`Original error: ${err.message}`
);
}
if (!stat.isDirectory()) {
throw new Error(
`The path '${appiumHome}' provided in the ${name} must point to a valid folder`
);
}
try {
await fs.access(appiumHome, fs.constants.W_OK);
} catch (e) {
throw new Error(
`The folder path '${appiumHome}' provided in the ${name} must be ` +
`writeable for the current user account '${os.userInfo().username}. ` +
`Original error: ${e.message}`
);
}
return appiumHome;
}
/**
* Initializes Appium, but does not start the server.
*
@@ -169,6 +204,13 @@ function getExtraMethodMap(driverClasses, pluginClasses) {
*/
async function init(args) {
const appiumHome = args?.appiumHome ?? (await resolveAppiumHome());
let appiumHomeSourceName = 'autodetected appium home path';
if (!_.isNil(args?.appiumHome)) {
appiumHomeSourceName = 'appiumHome config value';
} else if (process.env.APPIUM_HOME) {
appiumHomeSourceName = 'APPIUM_HOME environment variable';
}
await prepareAppiumHome(appiumHomeSourceName, appiumHome);
adjustNodePath();
@@ -245,7 +287,11 @@ async function init(args) {
}
}
const appiumDriver = new AppiumDriver(serverArgs);
const appiumDriver = new AppiumDriver(
/** @type {import('@appium/types').DriverOpts<import('./appium').AppiumDriverConstraints>} */ (
serverArgs
)
);
// set the config on the umbrella driver so it can match drivers to caps
appiumDriver.driverConfig = driverConfig;
await preflightChecks(serverArgs, throwInsteadOfExit);
@@ -371,7 +417,10 @@ async function main(args) {
});
}
logServerPort(parsedArgs.address, parsedArgs.port);
logger.info(
`Appium REST http interface listener started on ${parsedArgs.address}:${parsedArgs.port}${parsedArgs.basePath}`
);
driverConfig.print();
pluginConfig.print([...pluginClasses.values()]);
+6 -3
View File
@@ -94,7 +94,7 @@ function makeDescription(schema) {
* as understood by `argparse`.
* @param {AppiumJSONSchema} subSchema - JSON schema for the option
* @param {ArgSpec} argSpec - Argument spec tuple
* @returns {[string[], import('argparse').ArgumentOptions]} Tuple of flag and options
* @returns {[[string]|[string, string], import('argparse').ArgumentOptions]} Tuple of flag and options
*/
function subSchemaToArgDef(subSchema, argSpec) {
let {type, appiumCliAliases, appiumCliTransformer, enum: enumValues} = subSchema;
@@ -207,7 +207,10 @@ function subSchemaToArgDef(subSchema, argSpec) {
}
}
return [aliases, argOpts];
// TODO: argparse only accepts the command name and a single alias; any extra aliases
// will be silently discarded.
const finalAliases = /** @type {[string]|[string, string]} */ (aliases);
return [finalAliases, argOpts];
}
/**
@@ -224,7 +227,7 @@ export function toParserArgs() {
}
/**
* @template T
* @template {string|number} T
* @typedef {import('ajv/dist/types').FormatValidator<T>} FormatValidator<T>
*/
+8 -10
View File
@@ -223,7 +223,8 @@ function adjustNodePath() {
// ! statements in client modules. It uses a private API though,
// ! so it could break (maybe, eventually).
// See https://gist.github.com/branneman/8048520#7-the-hack
// @ts-ignore
// @ts-ignore see above comment
// eslint-disable-next-line @typescript-eslint/no-var-requires
require('module').Module._initPaths();
return true;
} catch (e) {
@@ -368,21 +369,18 @@ export {
*/
/**
* @template {Constraints} [C=BaseDriverCapConstraints]
* @template {StringRecord|void} [Extra=void]
* @typedef {import('@appium/types').Capabilities<C, Extra>} Capabilities
* @template {Constraints} C
* @typedef {import('@appium/types').Capabilities<C>} Capabilities
*/
/**
* @template {Constraints} [C=BaseDriverCapConstraints]
* @template {StringRecord|void} [Extra=void]
* @typedef {import('@appium/types').W3CCapabilities<C, Extra>} W3CCapabilities
* @template {Constraints} C
* @typedef {import('@appium/types').W3CCapabilities<C>} W3CCapabilities
*/
/**
* @template {Constraints} [C=BaseDriverCapConstraints]
* @template {StringRecord|void} [Extra=void]
* @typedef {import('@appium/types').NSCapabilities<C, Extra>} NSCapabilities
* @template {Constraints} C
* @typedef {import('@appium/types').NSCapabilities<C>} NSCapabilities
*/
/**
+14 -14
View File
@@ -1,6 +1,6 @@
{
"name": "appium",
"version": "2.0.0-beta.57",
"version": "2.0.0-beta.63",
"description": "Automation for Apps.",
"keywords": [
"automation",
@@ -44,13 +44,14 @@
],
"scripts": {
"build:docs": "node docs/scripts/build-docs.js",
"build:docs:preview": "cross-env APPIUM_DOCS_PREVIEW=1 npm run build:docs",
"build:docs:assets": "node docs/scripts/copy-assets.js",
"build:docs:cli": "node docs/scripts/gen-cli-args-docs.js",
"build:docs:reference": "node docs/scripts/build-reference.js",
"dev:docs": "npm run build:docs:assets && npm run dev:docs:en",
"dev:docs:en": "mkdocs serve -f ./docs/mkdocs-en.yml",
"dev:docs:ja": "mkdocs serve -f ./docs/mkdocs-ja.yml",
"dev:docs:zh": "mkdocs serve -f ./docs/mkdocs-zh.yml",
"dev:docs:en": "appium-docs build --serve --mkdocs-yml ./docs/mkdocs-en.yml",
"dev:docs:ja": "appium-docs build --serve --mkdocs-yml ./docs/mkdocs-ja.yml",
"dev:docs:zh": "appium-docs build --serve --mkdocs-yml ./docs/mkdocs-zh.yml",
"postinstall": "node ./scripts/autoinstall-extensions.js",
"publish:docs": "cross-env APPIUM_DOCS_PUBLISH=1 npm run build:docs",
"test": "npm run test:unit",
@@ -59,12 +60,12 @@
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
},
"dependencies": {
"@appium/base-driver": "^9.3.3",
"@appium/base-plugin": "^2.2.3",
"@appium/docutils": "^0.3.0",
"@appium/base-driver": "^9.3.6",
"@appium/base-plugin": "^2.2.6",
"@appium/docutils": "^0.3.6",
"@appium/schema": "^0.2.6",
"@appium/support": "^3.1.7",
"@appium/types": "^0.10.1",
"@appium/support": "^3.1.10",
"@appium/types": "^0.10.4",
"@sidvind/better-ajv-errors": "2.1.0",
"@types/argparse": "2.0.10",
"@types/bluebird": "3.5.38",
@@ -77,22 +78,21 @@
"argparse": "2.0.1",
"async-lock": "1.4.0",
"asyncbox": "2.9.4",
"axios": "1.3.4",
"axios": "1.3.5",
"bluebird": "3.7.2",
"cross-env": "7.0.3",
"find-up": "5.0.0",
"glob": "8.1.0",
"lilconfig": "2.1.0",
"lodash": "4.17.21",
"longjohn": "0.2.12",
"npmlog": "7.0.1",
"ora": "5.4.1",
"package-changed": "2.0.0",
"package-changed": "3.0.0",
"resolve-from": "5.0.0",
"semver": "7.3.8",
"source-map-support": "0.5.21",
"teen_process": "2.0.2",
"type-fest": "3.6.1",
"type-fest": "3.8.0",
"winston": "3.8.2",
"wrap-ansi": "7.0.0",
"yaml": "2.2.1"
@@ -105,7 +105,7 @@
"access": "public",
"tag": "next"
},
"gitHead": "84b211330dc84849af9cc1f3a5c5ad32e15b2e72",
"gitHead": "8480a85ce2fa466360e0fb1a7f66628331907f02",
"typedoc": {
"entryPoint": "./lib/main.js"
}
@@ -63,7 +63,7 @@ describe('Plugin CLI', function () {
* @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('appium/lib/cli/extension-command').ExtensionList} ExtensionListData
* @typedef {import('./e2e-helpers').CliArgs} CliArgs
* @typedef {import('appium/types').CliExtensionSubcommand} CliExtensionSubcommand
*/
+1 -1
View File
@@ -182,7 +182,7 @@ export function formatAppiumArgErrorOutput(stderr) {
/**
* @typedef {import('@appium/types').ExtensionType} ExtensionType
* @typedef {import('appium/lib/cli/extension-command').ExtensionListData} ExtensionListData
* @typedef {import('appium/lib/cli/extension-command').ExtensionList} ExtensionListData
* @typedef {import('teen_process').ExecError} ExecError
* @typedef {import('teen_process').TeenProcessExecOptions} TeenProcessExecOptions
* @typedef {import('teen_process').TeenProcessExecResult<string>} TeenProcessExecResult
@@ -99,7 +99,7 @@ describe('manifest handling', function () {
});
it('should update the manifest file to the latest schema revision', function () {
expect(manifest.schemaRev).to.equal(3);
expect(manifest.schemaRev).to.equal(CURRENT_SCHEMA_REV);
});
});
});
@@ -0,0 +1,91 @@
import {Manifest} from '../../../lib/extension/manifest';
import {migrate} from '../../../lib/extension/manifest-migrations';
import {DRIVER_TYPE} from '../../../lib/constants';
const {expect} = chai;
describe('manifest-migrations', function () {
describe('when no installPath property present in manifest', function () {
it('should trigger refresh', async function () {
const manifest = Manifest.getInstance(process.cwd());
// do not explicitly set the schema rev lower here, since that will trigger
manifest.setExtension(
DRIVER_TYPE,
'derp',
// @ts-expect-error - old manifest version
{
version: '1.0.0',
automationName: 'Derp',
mainClass: 'SomeClass',
pkgName: 'derp',
platformNames: ['dogs', 'cats'],
installSpec: 'derp',
installType: 'local',
appiumVersion: '2.0.0',
}
);
await expect(migrate(manifest)).to.eventually.be.true;
});
});
describe('when installPath property present in manifest', function () {
it('should not trigger refresh', async function () {
const manifest = Manifest.getInstance(process.cwd());
// do not explicitly set the schema rev lower here, since that will trigger
manifest.setExtension(DRIVER_TYPE, 'derp', {
version: '1.0.0',
automationName: 'Derp',
mainClass: 'SomeClass',
pkgName: 'derp',
platformNames: ['dogs', 'cats'],
installPath: '/path/to/thing',
installType: 'local',
installSpec: 'derp',
appiumVersion: '2.0.0',
});
await expect(migrate(manifest)).to.eventually.be.false;
});
});
describe('when an installType is "npm" and the rev is old', function () {
it('should trigger refresh', async function () {
const manifest = Manifest.getInstance(process.cwd());
manifest.setSchemaRev(3); // this will trigger a refresh, but there's no way to tell _why_, which may be bad. YAGNI?
manifest.setExtension(DRIVER_TYPE, 'derp', {
version: '1.0.0',
automationName: 'Derp',
mainClass: 'SomeClass',
pkgName: 'derp',
platformNames: ['dogs', 'cats'],
installType: 'npm',
installPath: '/path/to/thing',
installSpec: 'derp',
appiumVersion: '2.0.0',
});
await expect(migrate(manifest)).to.eventually.be.true;
});
});
describe('when no installType is "npm"', function () {
it('should not trigger refresh', async function () {
const manifest = Manifest.getInstance(process.cwd());
// do not explicitly set the schema rev lower here, since that will trigger
manifest.setExtension(DRIVER_TYPE, 'derp', {
version: '1.0.0',
automationName: 'Derp',
mainClass: 'SomeClass',
pkgName: 'derp',
platformNames: ['dogs', 'cats'],
installType: 'local',
installPath: '/path/to/thing',
installSpec: 'derp',
appiumVersion: '2.0.0',
});
await expect(migrate(manifest)).to.eventually.be.false;
});
});
});
@@ -40,7 +40,10 @@ describe('Manifest', function () {
let overrides;
({MockPackageChanged, MockAppiumSupport, MockGlob, overrides, sandbox} = initMocks());
MockAppiumSupport.fs.readFile.resolves(yamlFixture);
({Manifest} = rewiremock.proxy(() => require('../../../lib/extension/manifest'), overrides));
({Manifest} = rewiremock.proxy(() => require('../../../lib/extension/manifest'), {
...overrides,
'../../../lib/extension/manifest-migrations.js': {migrate: sandbox.stub().resolves()},
}));
Manifest.getInstance.cache = new Map();
});
+8
View File
@@ -264,6 +264,14 @@ describe('parser', function () {
const args = p.parseArgs([DRIVER_TYPE, 'list', '--updates']);
args.showUpdates.should.eql(true);
});
it('should allow "ls" as an alias for "list"', function () {
const args = p.parseArgs([DRIVER_TYPE, 'ls']);
args.subcommand.should.eql(DRIVER_TYPE);
args.driverCommand.should.eql('list');
args.showInstalled.should.eql(false);
args.showUpdates.should.eql(false);
args.json.should.eql(false);
});
});
describe('install', function () {
it('should not allow an empty argument list', function () {
+4 -3
View File
@@ -5,17 +5,18 @@
import * as ManifestV2 from './base';
import * as ManifestV3 from './v3';
import * as ManifestV4 from './v4';
// add `import * as ManifestV<new-version> from './v<new-version>';` above
export * from './v3';
export * from './v4';
// replace above line with `export * from './v<new-version>';`
export {ManifestV2, ManifestV3};
export {ManifestV2, ManifestV3, ManifestV4};
export interface ManifestDataVersions {
2: ManifestV2.ManifestData;
3: ManifestV3.ManifestData;
4: ManifestV4.ManifestData;
}
// append to this interface your new version of `ManifestData`
+161
View File
@@ -0,0 +1,161 @@
import {DriverType, ExtensionType, PluginType} from '@appium/types';
import {SchemaObject} from 'ajv';
import {PackageJson, SetRequired} from 'type-fest';
/**
* One of the possible extension installation stratgies
*/
export type InstallType = 'npm' | 'git' | 'local' | 'github' | 'dev';
export interface InternalMetadata {
/**
* Package name of extension
*
* `name` from its `package.json`
*/
pkgName: string;
/**
* Version of extension
*
* `version` from its `package.json`
*/
version: string;
/**
* The method in which the user installed the extension (the `source` CLI arg)
*/
installType: InstallType;
/**
* Whatever the user typed as the extension to install. May be derived from `package.json`
*/
installSpec: string;
/**
* Maximum version of Appium that this extension is compatible with.
*
* If `undefined`, we'll try anyway.
*/
appiumVersion?: string;
/**
* Path to the extension's root directory
*/
installPath: string;
}
/**
* Shape of the `appium.schema` property in an extension's `package.json` (if it exists)
*/
export type ExtSchemaMetadata = string | (SchemaObject & {[key: number]: never});
/**
* Manifest data shared by all extensions, as contained in `package.json`
*/
export interface CommonExtMetadata {
/**
* The main class of the extension.
*
* The extension must export this class by name.
*/
mainClass: string;
/**
* Lookup table of scripts to run via `appium <driver|plugin> run <script>` keyed by name.
*/
scripts?: Record<string, string>;
/**
* Schema describing configuration options (and CLI args) for the extension.
*
* Can also just be a path (relative to the extension root) to an external JSON schema file.
*/
schema?: ExtSchemaMetadata;
}
/**
* Driver-specific manifest data as stored in a driver's `package.json`
*/
export interface DriverMetadata {
/**
* Automation name of the driver
*/
automationName: string;
/**
* Platforms the driver supports
*/
platformNames: string[];
/**
* Short name of the driver (displayed in `appium list`, etc.)
*/
driverName: string;
}
/**
* Plugin-specific manifest data as stored in a plugin's `package.json`
*/
export interface PluginMetadata {
/**
* Short name of the plugin (displayed in `appium list`, etc.)
*/
pluginName: string;
}
/**
* Generic extension metadata as stored in the `appium` prop of an extension's `package.json`.
*/
export type ExtMetadata<ExtType extends ExtensionType> = (ExtType extends DriverType
? DriverMetadata
: ExtType extends PluginType
? PluginMetadata
: never) &
CommonExtMetadata;
/**
* Combination of external + internal extension data with `driverName`/`pluginName` removed (it becomes a key in an {@linkcode ExtRecord} object).
* Part of `extensions.yaml`.
*/
export type ExtManifest<ExtType extends ExtensionType> = Omit<
ExtMetadata<ExtType>,
'driverName' | 'pluginName'
> &
InternalMetadata;
/**
* Lookup of extension name to {@linkcode ExtManifest}.
* @see {ManifestData}
*/
export type ExtRecord<ExtType extends ExtensionType> = Record<string, ExtManifest<ExtType>>;
/**
* The shape of the `extensions.yaml` file
*/
export interface ManifestData {
drivers: ExtRecord<DriverType>;
plugins: ExtRecord<PluginType>;
schemaRev: number;
}
/**
* The name of an installed extension, as it appears in `extensions.yaml`
* (as a property name under `drivers` or `plugins`)
*/
export type ExtName<ExtType extends ExtensionType> = keyof ExtRecord<ExtType>;
/**
* A `package.json` containing extension metadata.
* Must have the following properties:
* - `name`: the name of the extension
* - `version`: the version of the extension
* - `appium`: the metadata for the extension
* - `peerDependencies.appium`: the maximum compatible version of Appium
*/
export type ExtPackageJson<ExtType extends ExtensionType> = SetRequired<
PackageJson,
'name' | 'version'
> & {
appium: ExtMetadata<ExtType>;
peerDependencies: {appium: string; [key: string]: string};
};
/**
* A transient format between installation and insertion of extension metadata into the manifest.
*/
export type ExtInstallReceipt<ExtType extends ExtensionType> = ExtMetadata<ExtType> &
InternalMetadata;
+42
View File
@@ -3,6 +3,48 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [9.3.6](https://github.com/appium/appium/compare/@appium/base-driver@9.3.5...@appium/base-driver@9.3.6) (2023-04-10)
### Bug Fixes
* **support:** update dependency axios to v1.3.5 ([6cf1480](https://github.com/appium/appium/commit/6cf14802b70a462beffc12a1134476596060c005))
* **types:** update dependency type-fest to v3.8.0 ([d6c42e9](https://github.com/appium/appium/commit/d6c42e99c08efce0b34796d5982ce379fca044d3))
## [9.3.5](https://github.com/appium/appium/compare/@appium/base-driver@9.3.4...@appium/base-driver@9.3.5) (2023-04-03)
### Bug Fixes
* **types:** update dependency type-fest to v3.7.2 ([5580539](https://github.com/appium/appium/commit/55805390b5a0c6aa718bb357b30f66651f3db281))
## [9.3.4](https://github.com/appium/appium/compare/@appium/base-driver@9.3.3...@appium/base-driver@9.3.4) (2023-03-28)
### Bug Fixes
* **appium,types,base-driver,fake-driver,driver-test-support:** normalize constraint defaults ([3c9fa7b](https://github.com/appium/appium/commit/3c9fa7ba73b639e610e1f3d41d239a9402845b4c))
* backwards-compatible fixes for TS v5.x ([4974403](https://github.com/appium/appium/commit/49744036619ecc239e0e6255a13d38cafd709920))
* **base-driver,base-plugin,types:** update PluginCommand and DriverCommand types ([0dcd5fa](https://github.com/appium/appium/commit/0dcd5fa371af523c6527e55de4cff6cd472fde22))
* **base-driver,types:** fix websocket-related types on AppiumServer ([34891f5](https://github.com/appium/appium/commit/34891f56572f18dd740558b2348d8818680dc709))
* **base-driver:** fix timeout mixin to use proper context types ([51a8f2f](https://github.com/appium/appium/commit/51a8f2fc347f0b4e222ca2e8dc92fa4bdf44d1e3))
* **base-driver:** misc type fixes ([d303527](https://github.com/appium/appium/commit/d303527c3da4f657a28b97a2d82eb1a709c6b9bc))
* **basedriver:** convert main driver implementation to typescript ([deb631b](https://github.com/appium/appium/commit/deb631b4562a0f99e4061c89a7ad21b0621f8a47)), closes [#18379](https://github.com/appium/appium/issues/18379) [#18379](https://github.com/appium/appium/issues/18379)
* **types:** update dependency type-fest to v3.7.0 ([6912fa1](https://github.com/appium/appium/commit/6912fa14f2a7d338f17e1bed060e959de7aba1d6))
* **types:** update dependency type-fest to v3.7.1 ([bc860c7](https://github.com/appium/appium/commit/bc860c733a73760f0c42cbfb384e04d50c376d5e))
## [9.3.3](https://github.com/appium/appium/compare/@appium/base-driver@9.3.2...@appium/base-driver@9.3.3) (2023-03-08)
@@ -11,19 +11,24 @@ const PREFIXED_APPIUM_OPTS_CAP = `${APPIUM_VENDOR_PREFIX}options`;
/**
* Takes primary caps object and merges it into a secondary caps object.
* @template {Constraints} [T={}]
* @template {Constraints} [U={}]
* @param {Capabilities<T>} [primary]
* @param {Capabilities<U>} [secondary]
* @returns {Merge<Capabilities<T>, Capabilities<U>>}
* @template {Constraints} T
* @template {Constraints} U
* @template {Capabilities<T>} Primary
* @template {Capabilities<U>} Secondary
* @param {Primary} [primary]
* @param {Secondary} [secondary]
* @returns {MergeExclusive<Primary, Secondary>}
* @see https://www.w3.org/TR/webdriver/#dfn-merging-capabilities)
*/
function mergeCaps(primary = {}, secondary = {}) {
let result = /** @type {Merge<Capabilities<T>, Capabilities<U>>} */ ({
function mergeCaps(
primary = /** @type {Primary} */ ({}),
secondary = /** @type {Secondary} */ ({})
) {
let result = /** @type {MergeExclusive<Primary, Secondary>} */ ({
...primary,
});
for (let [name, value] of /** @type {[keyof typeof secondary, any]} */ (_.toPairs(secondary))) {
for (let [name, value] of Object.entries(secondary)) {
// Overwriting is not allowed. Primary and secondary must have different properties (w3c rule 4.4)
if (!_.isUndefined(primary[name])) {
throw new errors.InvalidArgumentError(
@@ -38,9 +43,9 @@ function mergeCaps(primary = {}, secondary = {}) {
return result;
}
// Validates caps against a set of constraints
/**
* @template {Constraints} [C={}]
* Validates caps against a set of constraints
* @template {Constraints} C
* @param {Capabilities<C>} caps
* @param {C} [constraints]
* @param {ValidateCapsOpts} [opts]
@@ -121,11 +126,11 @@ function isStandardCap(cap) {
/**
* If the 'appium:' prefix was provided and it's a valid capability, strip out the prefix
* @template {Constraints} [C={}]
* @param {import('@appium/types').NSCapabilities<C>} caps
* @template {Constraints} C
* @param {NSCapabilities<C>} caps
* @see https://www.w3.org/TR/webdriver/#dfn-extension-capabilities
* @internal
* @returns {import('@appium/types').Capabilities<C>}
* @returns {Capabilities<C>}
*/
function stripAppiumPrefixes(caps) {
// split into prefixed and non-prefixed.
@@ -135,14 +140,12 @@ function stripAppiumPrefixes(caps) {
);
// initialize this with the k/v pairs of the non-prefixed caps
let strippedCaps = /** @type {import('@appium/types').Capabilities<C>} */ (
_.pick(caps, nonPrefixedCaps)
);
let strippedCaps = /** @type {Capabilities<C>} */ (_.pick(caps, nonPrefixedCaps));
const badPrefixedCaps = [];
// Strip out the 'appium:' prefix
for (let prefixedCap of prefixedCaps) {
const strippedCapName = /** @type {StringKeyOf<import('@appium/types').Capabilities<C>>} */ (
const strippedCapName = /** @type {StringKeyOf<Capabilities<C>>} */ (
prefixedCap.substring(APPIUM_VENDOR_PREFIX.length)
);
@@ -175,8 +178,8 @@ function stripAppiumPrefixes(caps) {
/**
* Get an array of all the unprefixed caps that are being used in 'alwaysMatch' and all of the 'firstMatch' object
* @template {Constraints} [C={}]
* @param {import('@appium/types').W3CCapabilities<C>} caps A capabilities object
* @template {Constraints} C
* @param {W3CCapabilities<C>} caps A capabilities object
*/
function findNonPrefixedCaps({alwaysMatch = {}, firstMatch = []}) {
return _.chain([alwaysMatch, ...firstMatch])
@@ -191,13 +194,25 @@ function findNonPrefixedCaps({alwaysMatch = {}, firstMatch = []}) {
.value();
}
/**
* Returned by {@linkcode parseCaps}
* @template {Constraints} C
* @typedef ParsedCaps
* @property {NSCapabilities<C>[]} allFirstMatchCaps
* @property {Capabilities<C>[]} validatedFirstMatchCaps
* @property {NSCapabilities<C>} requiredCaps
* @property {Capabilities<C>|null} matchedCaps
* @property {string[]} validationErrors
*/
/**
* Parse capabilities
* @template {Constraints} [C={}]
* @param {import('@appium/types').W3CCapabilities<C>} caps
* @template {Constraints} C
* @param {W3CCapabilities<C>} caps
* @param {C} [constraints]
* @param {boolean} [shouldValidateCaps]
* @see https://www.w3.org/TR/webdriver/#processing-capabilities
* @returns {ParsedCaps<C>}
*/
function parseCaps(caps, constraints = /** @type {C} */ ({}), shouldValidateCaps = true) {
// If capabilities request is not an object, return error (#1.1)
@@ -210,8 +225,8 @@ function parseCaps(caps, constraints = /** @type {C} */ ({}), shouldValidateCaps
// Let 'requiredCaps' be property named 'alwaysMatch' from capabilities request (#2)
// and 'allFirstMatchCaps' be property named 'firstMatch' from capabilities request (#3)
let {
alwaysMatch: requiredCaps = {}, // If 'requiredCaps' is undefined, set it to an empty JSON object (#2.1)
firstMatch: allFirstMatchCaps = [{}], // If 'firstMatch' is undefined set it to a singleton list with one empty object (#3.1)
alwaysMatch: requiredCaps = /** @type {NSCapabilities<C>} */ ({}), // If 'requiredCaps' is undefined, set it to an empty JSON object (#2.1)
firstMatch: allFirstMatchCaps = /** @type {NSCapabilities<C>[]} */ ([{}]), // If 'firstMatch' is undefined set it to a singleton list with one empty object (#3.1)
} = caps;
// Reject 'firstMatch' argument if it's not an array (#3.2)
@@ -241,6 +256,7 @@ function parseCaps(caps, constraints = /** @type {C} */ ({}), shouldValidateCaps
// Strip out the 'appium:' prefix from all
let strippedRequiredCaps = stripAppiumPrefixes(requiredCaps);
/** @type {Capabilities<C>[]} */
let strippedAllFirstMatchCaps = allFirstMatchCaps.map(stripAppiumPrefixes);
// Validate the requiredCaps. But don't validate 'presence' because if that constraint fails on 'alwaysMatch' it could still pass on one of the 'firstMatch' keys
@@ -259,24 +275,23 @@ function parseCaps(caps, constraints = /** @type {C} */ ({}), shouldValidateCaps
/** @type {string[]} */
let validationErrors = [];
let validatedFirstMatchCaps = _.compact(
strippedAllFirstMatchCaps.map(
/**
* @param {import('@appium/types').Capabilities<C>} firstMatchCaps
*/
(firstMatchCaps) => {
try {
// Validate firstMatch caps
return shouldValidateCaps
? validateCaps(firstMatchCaps, filteredConstraints)
: firstMatchCaps;
} catch (e) {
validationErrors.push(e.message);
}
strippedAllFirstMatchCaps.map((firstMatchCaps) => {
try {
// Validate firstMatch caps
return shouldValidateCaps
? validateCaps(firstMatchCaps, filteredConstraints)
: firstMatchCaps;
} catch (e) {
validationErrors.push(e.message);
}
)
})
);
// Try to merge requiredCaps with first match capabilities, break once it finds its first match (see spec #6)
/**
* Try to merge requiredCaps with first match capabilities, break once it finds its first match
* (see spec #6)
* @type {ParsedCaps<C>['matchedCaps']}
*/
let matchedCaps = null;
for (let firstMatchCaps of validatedFirstMatchCaps) {
try {
@@ -300,13 +315,14 @@ function parseCaps(caps, constraints = /** @type {C} */ ({}), shouldValidateCaps
};
}
// Calls parseCaps and just returns the matchedCaps variable
/**
* Calls parseCaps and just returns the matchedCaps variable
* @template {Constraints} C
* @param {import('@appium/types').W3CCapabilities<C>} w3cCaps
* @template {W3CCapabilities<C>} W3CCaps
* @param {W3CCaps} w3cCaps
* @param {C} [constraints]
* @param {boolean} [shouldValidateCaps]
* @returns {import('@appium/types').Capabilities<C>}
* @returns {Capabilities<C>}
*/
function processCapabilities(
w3cCaps,
@@ -400,8 +416,8 @@ function promoteAppiumOptionsForObject(obj) {
* capability and promoted it to the top level.
*
* @template {Constraints} C
* @param {import('@appium/types').W3CCapabilities<C>} originalCaps
* @return {import('@appium/types').W3CCapabilities<C>} the capabilities with 'options' promoted if necessary
* @param {W3CCapabilities<C>} originalCaps
* @return {W3CCapabilities<C>} the capabilities with 'options' promoted if necessary
*/
function promoteAppiumOptions(originalCaps) {
const result = {};
@@ -451,28 +467,26 @@ export {
*/
/**
* @template {Constraints} [C=BaseDriverCapConstraints]
* @template {StringRecord|void} [Extra=void]
* @typedef {import('@appium/types').NSCapabilities<C, Extra>} NSCapabilities
* @template {Constraints} C
* @typedef {import('@appium/types').NSCapabilities<C>} NSCapabilities
*/
/**
* @template {Constraints} [C=BaseDriverCapConstraints]
* @template {StringRecord|void} [Extra=void]
* @typedef {import('@appium/types').Capabilities<C, Extra>} Capabilities
* @template {Constraints} C
* @typedef {import('@appium/types').Capabilities<C>} Capabilities
*/
/**
* @template {Constraints} [C=BaseDriverCapConstraints]
* @template {StringRecord|void} [Extra=void]
* @typedef {import('@appium/types').W3CCapabilities<C, Extra>} W3CCapabilities
* @template {Constraints} C
* @typedef {import('@appium/types').W3CCapabilities<C>} W3CCapabilities
*/
/**
* @template T,U
* @typedef {import('type-fest').Merge<T,U>} Merge
*/
/**
* @template T
* @typedef {import('type-fest').StringKeyOf<T>} StringKeyOf
*/
/**
* @template T,U
* @typedef {import('type-fest').MergeExclusive<T, U>} MergeExclusive<T,U>
*/
@@ -1,9 +1,10 @@
import _ from 'lodash';
import {Constraints, IEventCommands} from '@appium/types';
import {mixin} from './mixin';
import _ from 'lodash';
import {BaseDriver} from '../driver';
import {mixin} from './mixin';
declare module '../driver' {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface BaseDriver<C extends Constraints> extends IEventCommands {}
}
@@ -12,6 +12,7 @@ import {mixin} from './mixin';
import {BaseDriver} from '../driver';
declare module '../driver' {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface BaseDriver<C extends Constraints> extends IExecuteCommands {}
}
@@ -19,7 +20,7 @@ const ExecuteCommands: IExecuteCommands = {
async executeMethod<C extends Constraints>(
this: BaseDriver<C>,
script: string,
protoArgs: [StringRecord] | any[]
protoArgs: readonly [StringRecord<unknown>] | readonly unknown[]
) {
const Driver = this.constructor as DriverClass<Driver<C>>;
const commandMetadata = {...Driver.executeMethodMap?.[script]};
@@ -4,6 +4,7 @@ import {BaseDriver} from '../driver';
import {mixin} from './mixin';
declare module '../driver' {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface BaseDriver<C extends Constraints> extends IFindCommands {}
}
@@ -1,7 +1,7 @@
export * from './event';
export * from './find';
export * from './log';
export * from './session';
export * from './settings';
export * from './timeout';
export * from './execute';
import './event';
import './find';
import './log';
import './session';
import './settings';
import './timeout';
import './execute';
@@ -4,6 +4,7 @@ import {BaseDriver} from '../driver';
import {mixin} from './mixin';
declare module '../driver' {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface BaseDriver<C extends Constraints> extends ILogCommands {}
}
@@ -15,10 +16,7 @@ const LogCommands: ILogCommands = {
return Object.keys(this.supportedLogTypes);
},
async getLog<C extends Constraints>(
this: Driver<C>,
logType: keyof ILogCommands['supportedLogTypes']
) {
async getLog<C extends Constraints>(this: Driver<C>, logType: string) {
this.log.debug(`Retrieving '${String(logType)}' logs`);
if (!(logType in this.supportedLogTypes)) {
@@ -2,13 +2,14 @@ import {Constraints} from '@appium/types';
import {BaseDriver} from '../driver';
/**
* This function assigns a mixin `T` to the `FakeDriver` class' prototype.
* While each mixin has its own interface which is (in isolation) unrelated to `FakeDriver`, the constraint
* on this generic type `T` is that it must be a partial of `FakeDriver`'s interface. This enforces
* that it does not conflict with the existing interface of `FakeDriver`. In that way, you can
* This function assigns a mixin `T` to the `BaseDriver` class' prototype.
* While each mixin has its own interface which is (in isolation) unrelated to `BaseDriver`, the constraint
* on this generic type `T` is that it must be a partial of `BaseDriver`'s interface. This enforces
* that it does not conflict with the existing interface of `BaseDriver`. In that way, you can
* think of it as a type guard.
* @param mixin Mixin implementation
*/
export function mixin<C extends Constraints, T extends Partial<BaseDriver<C>>>(mixin: T): void {
// eslint-disable-next-line no-restricted-syntax
Object.assign(BaseDriver.prototype, mixin);
}
@@ -1,14 +1,17 @@
import {Constraints, ISessionCommands, MultiSessionData} from '@appium/types';
import {Constraints, ISessionCommands, MultiSessionData, SingularSessionData} from '@appium/types';
import {BaseDriver} from '../driver';
import {mixin} from './mixin';
declare module '../driver' {
interface BaseDriver<C extends Constraints> extends ISessionCommands {}
interface BaseDriver<
// eslint-disable-next-line @typescript-eslint/no-unused-vars
C extends Constraints
> extends ISessionCommands {}
}
const SessionCommands: ISessionCommands = {
async getSessions<C extends Constraints>(this: BaseDriver<C>) {
const ret: MultiSessionData[] = [];
const ret: MultiSessionData<C>[] = [];
if (this.sessionId) {
ret.push({
@@ -24,10 +27,9 @@ const SessionCommands: ISessionCommands = {
* Returns capabilities for the session and event history (if applicable)
*/
async getSession<C extends Constraints>(this: BaseDriver<C>) {
if (this.caps.eventTimings) {
return {...this.caps, events: this.eventHistory};
}
return this.caps;
return <SingularSessionData<C>>(
(this.caps.eventTimings ? {...this.caps, events: this.eventHistory} : this.caps)
);
},
};
@@ -3,6 +3,7 @@ import {Constraints, ISettingsCommands, StringRecord} from '@appium/types';
import {mixin} from './mixin';
declare module '../driver' {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface BaseDriver<C extends Constraints> extends ISettingsCommands {}
}
@@ -1,7 +1,3 @@
// @ts-check
/* eslint-disable no-unused-vars */
/* eslint-disable require-await */
import {waitForCondition} from 'asyncbox';
import _ from 'lodash';
import {util} from '@appium/support';
@@ -11,13 +7,14 @@ import {Constraints, ITimeoutCommands} from '@appium/types';
import {mixin} from './mixin';
declare module '../driver' {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface BaseDriver<C extends Constraints> extends ITimeoutCommands {}
}
const MIN_TIMEOUT = 0;
const TimeoutCommands: ITimeoutCommands = {
async timeouts(type, ms, script, pageLoad, implicit) {
async timeouts<C extends Constraints>(this: BaseDriver<C>, type, ms, script, pageLoad, implicit) {
if (util.hasValue(type) && util.hasValue(ms)) {
this.log.debug(`MJSONWP timeout arguments: ${JSON.stringify({type, ms})}}`);
@@ -66,42 +63,42 @@ const TimeoutCommands: ITimeoutCommands = {
},
// implicit
async implicitWaitW3C(ms) {
async implicitWaitW3C<C extends Constraints>(this: BaseDriver<C>, ms) {
await this.implicitWait(ms);
},
async implicitWaitMJSONWP(ms) {
async implicitWaitMJSONWP<C extends Constraints>(this: BaseDriver<C>, ms) {
await this.implicitWait(ms);
},
async implicitWait(ms) {
await this.setImplicitWait(this.parseTimeoutArgument(ms));
async implicitWait<C extends Constraints>(this: BaseDriver<C>, ms) {
this.setImplicitWait(this.parseTimeoutArgument(ms));
},
// pageLoad
async pageLoadTimeoutW3C(ms) {
async pageLoadTimeoutW3C<C extends Constraints>(this: BaseDriver<C>, ms) {
throw new errors.NotImplementedError('Not implemented yet for pageLoad.');
},
async pageLoadTimeoutMJSONWP(ms) {
async pageLoadTimeoutMJSONWP<C extends Constraints>(this: BaseDriver<C>, ms) {
throw new errors.NotImplementedError('Not implemented yet for pageLoad.');
},
// script
async scriptTimeoutW3C(ms) {
async scriptTimeoutW3C<C extends Constraints>(this: BaseDriver<C>, ms) {
throw new errors.NotImplementedError('Not implemented yet for script.');
},
async scriptTimeoutMJSONWP(ms) {
async scriptTimeoutMJSONWP<C extends Constraints>(this: BaseDriver<C>, ms) {
throw new errors.NotImplementedError('Not implemented yet for script.');
},
// command
async newCommandTimeout(ms) {
async newCommandTimeout<C extends Constraints>(this: BaseDriver<C>, ms) {
this.setNewCommandTimeout(this.parseTimeoutArgument(ms));
},
setImplicitWait(ms) {
setImplicitWait<C extends Constraints>(this: BaseDriver<C>, ms: number) {
// eslint-disable-line require-await
this.implicitWaitMs = ms;
this.log.debug(`Set implicit wait to ${ms}ms`);
@@ -115,7 +112,7 @@ const TimeoutCommands: ITimeoutCommands = {
}
},
setNewCommandTimeout(ms) {
setNewCommandTimeout<C extends Constraints>(this: BaseDriver<C>, ms: number) {
this.newCommandTimeoutMs = ms;
this.log.debug(`Set new command timeout to ${ms}ms`);
if (this.managedDrivers && this.managedDrivers.length) {
@@ -128,7 +125,10 @@ const TimeoutCommands: ITimeoutCommands = {
}
},
async implicitWaitForCondition(condFn) {
async implicitWaitForCondition<C extends Constraints>(
this: BaseDriver<C>,
condFn: (...args: any[]) => Promise<any>
) {
this.log.debug(`Waiting up to ${this.implicitWaitMs} ms for condition`);
const wrappedCondFn = async (...args: any[]) => {
// reset command timeout
+11 -25
View File
@@ -1,7 +1,7 @@
/* eslint-disable no-unused-vars */
/* eslint-disable require-await */
import {fs, logger} from '@appium/support';
import {logger} from '@appium/support';
import AsyncLock from 'async-lock';
import {EventEmitter} from 'events';
import _ from 'lodash';
@@ -9,18 +9,16 @@ import os from 'os';
import {DEFAULT_BASE_PATH, PROTOCOLS} from '../constants';
import {errors} from '../protocol';
import DeviceSettings from './device-settings';
import helpers from './helpers';
// for compat with running tests transpiled and in-place
const {version: BASEDRIVER_VER} = fs.readPackageJsonFrom(__dirname);
import helpers, {BASEDRIVER_VER} from './helpers';
const NEW_COMMAND_TIMEOUT_MS = 60 * 1000;
const ON_UNEXPECTED_SHUTDOWN_EVENT = 'onUnexpectedShutdown';
/**
* @template {Constraints} [C=BaseDriverCapConstraints]
* @implements {Core<C>}
* @template {Constraints} C
* @template {StringRecord} [Settings=StringRecord]
* @implements {Core<C, Settings>}
*/
class DriverCore {
/**
@@ -40,7 +38,7 @@ class DriverCore {
opts;
/**
* @type {ServerArgs}
* @type {import('@appium/types').DriverOpts<C>}
*/
initialOpts;
@@ -98,7 +96,6 @@ class DriverCore {
_log;
/**
* @protected
* @type {boolean}
*/
shutdownUnexpectedly;
@@ -119,7 +116,7 @@ class DriverCore {
* we set it to an empty DeviceSettings instance here to make sure that the
* default settings are applied even if an extending driver doesn't utilize
* the settings functionality itself
* @type {DeviceSettings}
* @type {DeviceSettings<Settings>}
*/
settings;
@@ -184,11 +181,9 @@ class DriverCore {
* specific driver sessions. This data can be later used to adjust
* properties for driver instances running in parallel.
* Override it in inherited driver classes if necessary.
*
* @return {Record<string,unknown>} Driver properties mapping
*/
get driverData() {
return {};
return /** @type {import('@appium/types').DriverData} */ ({});
}
/**
@@ -260,7 +255,7 @@ class DriverCore {
* @returns {Core<C> | null}
*/
driverForSession(sessionId) {
return this;
return /** @type {Core<C> | null} */ (this);
}
isMjsonwpProtocol() {
@@ -448,19 +443,10 @@ export {DriverCore};
* @typedef {import('@appium/types').BaseDriverCapConstraints} BaseDriverCapConstraints
*/
/**
* @template {Constraints} [C=BaseDriverCapConstraints]
* @template {StringRecord|void} [Extra=void]
* @typedef {import('@appium/types').Capabilities<C, Extra>} Capabilities
*/
/**
* @template {StringRecord} [T={}]
* @typedef {import('@appium/types').W3CCapabilities<T>} W3CCapabilities
*/
/**
* @template {Constraints} C
* @typedef {import('@appium/types').Core<C>} Core
* @template {StringRecord} [S=StringRecord]
* @typedef {import('@appium/types').Core<C, S>} Core
*/
/**
@@ -9,13 +9,13 @@ import {errors} from '../protocol/errors';
export const MAX_SETTINGS_SIZE = 20 * 1024 * 1024; // 20 MB
/**
* @template T
* @template {import('@appium/types').StringRecord} T
* @implements {IDeviceSettings<T>}
*/
class DeviceSettings {
export class DeviceSettings {
/**
* @protected
* @type {Record<string,T>}
* @type {T}
*/
_settings;
@@ -27,17 +27,17 @@ class DeviceSettings {
/**
* Creates a _shallow copy_ of the `defaultSettings` parameter!
* @param {Record<string,T>} [defaultSettings]
* @param {T} [defaultSettings]
* @param {import('@appium/types').SettingsUpdateListener<T>} [onSettingsUpdate]
*/
constructor(defaultSettings = {}, onSettingsUpdate = async () => {}) {
constructor(defaultSettings = /** @type {T} */ ({}), onSettingsUpdate = async () => {}) {
this._settings = {...defaultSettings};
this._onSettingsUpdate = onSettingsUpdate;
}
/**
* calls updateSettings from implementing driver every time a setting is changed.
* @param {Record<string,T>} newSettings
* @param {T} newSettings
*/
async update(newSettings) {
if (!_.isPlainObject(newSettings)) {
@@ -54,8 +54,7 @@ class DeviceSettings {
);
}
const props = /** @type {(keyof T & string)[]} */ (_.keys(newSettings));
for (const prop of props) {
for (const prop in newSettings) {
if (!_.isUndefined(this._settings[prop])) {
if (this._settings[prop] === newSettings[prop]) {
log.debug(`The value of '${prop}' setting did not change. Skipping the update for it`);
@@ -73,9 +72,8 @@ class DeviceSettings {
}
export default DeviceSettings;
export {DeviceSettings};
/**
* @template T
* @typedef {import('@appium/types').DeviceSettings<T>} IDeviceSettings
* @template {import('@appium/types').StringRecord} [T=import('@appium/types').StringRecord]
* @typedef {import('@appium/types').IDeviceSettings<T>} IDeviceSettings
*/
@@ -1,6 +1,3 @@
/* eslint-disable require-await */
/* eslint-disable no-unused-vars */
import {validateCaps, processCapabilities} from './capabilities';
import {DriverCore} from './core';
import {util} from '@appium/support';
@@ -9,7 +6,21 @@ import _ from 'lodash';
import {fixCaps, isW3cCaps} from '../helpers/capabilities';
import {DELETE_SESSION_COMMAND, determineProtocol, errors} from '../protocol';
import helpers from './helpers';
import {BASE_DESIRED_CAP_CONSTRAINTS} from '@appium/types';
import {
AppiumServer,
BaseDriverCapConstraints,
BASE_DESIRED_CAP_CONSTRAINTS,
Capabilities,
Constraints,
DefaultCreateSessionResult,
Driver,
DriverCaps,
DriverData,
DriverOpts,
ServerArgs,
StringRecord,
W3CDriverCaps,
} from '@appium/types';
const EVENT_SESSION_INIT = 'newSessionRequested';
const EVENT_SESSION_START = 'newSessionStarted';
@@ -17,56 +28,31 @@ const EVENT_SESSION_QUIT_START = 'quitSessionRequested';
const EVENT_SESSION_QUIT_DONE = 'quitSessionFinished';
const ON_UNEXPECTED_SHUTDOWN_EVENT = 'onUnexpectedShutdown';
/**
* @implements {SessionHandler<C>}
* @template {Constraints} C
* @template {StringRecord} [CArgs=StringRecord]
* @implements {Driver<C, CArgs>}
* @extends {DriverCore<C>}
*/
export class BaseDriver extends DriverCore {
/**
* @type {CArgs & ServerArgs}
*/
cliArgs;
export class BaseDriver<
C extends Constraints,
CArgs extends StringRecord = StringRecord,
Settings extends StringRecord = StringRecord
>
extends DriverCore<C, Settings>
implements Driver<C, CArgs>
{
cliArgs: CArgs & ServerArgs;
/**
* @type {Capabilities<C>}
*/
caps;
caps: DriverCaps<C>;
originalCaps: W3CDriverCaps<C>;
desiredCapConstraints: C;
opts: DriverOpts<C>;
server?: AppiumServer;
serverHost?: string;
serverPort?: number;
serverPath?: string;
/**
* @type {W3CCapabilities<C>}
*/
originalCaps;
/**
* @type {C}
*/
desiredCapConstraints;
/**
* @type {DriverOpts<C> & DriverOpts<BaseDriverCapConstraints>}
*/
opts;
/**
*
* @param {DriverOpts<C>} opts
* @param {boolean} shouldValidateCaps
*/
constructor(opts = /** @type {DriverOpts<C>} */ ({}), shouldValidateCaps = true) {
constructor(opts: DriverOpts<C>, shouldValidateCaps = true) {
super(opts, shouldValidateCaps);
/**
* This must be assigned here because the declaration of {@linkcode BaseDriver.opts} above
* blows away {@linkcode DriverCore.opts}.
*/
this.opts = opts;
this.caps = {} as DriverCaps<C>;
this.caps = {};
this.cliArgs = /** @type {CArgs & ServerArgs} */ ({});
this.cliArgs = {} as CArgs & ServerArgs;
}
/**
@@ -75,25 +61,20 @@ export class BaseDriver extends DriverCore {
* Subclasses _shouldn't_ need to use this. If you need to use this, please create
* an issue:
* @see https://github.com/appium/appium/issues/new
* @type {Readonly<BaseDriverCapConstraints & C>}
* @protected
*/
get _desiredCapConstraints() {
protected get _desiredCapConstraints(): Readonly<BaseDriverCapConstraints & C> {
return Object.freeze(_.merge({}, BASE_DESIRED_CAP_CONSTRAINTS, this.desiredCapConstraints));
}
// This is the main command handler for the driver. It wraps command
// execution with timeout logic, checking that we have a valid session,
// and ensuring that we execute commands one at a time. This method is called
// by MJSONWP's express router.
/**
* @param {string} cmd
* @param {...any} args
* @returns {Promise<any>}
* This is the main command handler for the driver. It wraps command
* execution with timeout logic, checking that we have a valid session,
* and ensuring that we execute commands one at a time. This method is called
* by MJSONWP's express router.
*/
async executeCommand(cmd, ...args) {
async executeCommand<T = unknown>(cmd: string, ...args: any[]): Promise<T> {
// get start time for this command, and log in special cases
let startTime = Date.now();
const startTime = Date.now();
if (cmd === 'createSession') {
// If creating a session determine if W3C or MJSONWP protocol was requested and remember the choice
@@ -161,12 +142,8 @@ export class BaseDriver extends DriverCore {
return res;
}
/**
*
* @param {Error|import('../protocol/errors').NoSuchDriverError} err
*/
async startUnexpectedShutdown(
err = new errors.NoSuchDriverError('The driver was unexpectedly shut down!')
err: Error = new errors.NoSuchDriverError('The driver was unexpectedly shut down!')
) {
this.eventEmitter.emit(ON_UNEXPECTED_SHUTDOWN_EVENT, err); // allow others to listen for this
this.shutdownUnexpectedly = true;
@@ -200,14 +177,7 @@ export class BaseDriver extends DriverCore {
}, this.newCommandTimeoutMs);
}
/**
*
* @param {import('@appium/types').AppiumServer} server
* @param {string} host
* @param {number} port
* @param {string} path
*/
assignServer(server, host, port, path) {
assignServer(server: AppiumServer, host: string, port: number, path: string) {
this.server = server;
this.serverHost = host;
this.serverPort = port;
@@ -223,8 +193,8 @@ export class BaseDriver extends DriverCore {
this.log.debug('Running generic full reset');
// preserving state
let currentConfig = {};
for (let property of [
const currentConfig = {};
for (const property of [
'implicitWaitMs',
'newCommandTimeoutMs',
'sessionId',
@@ -233,9 +203,6 @@ export class BaseDriver extends DriverCore {
currentConfig[property] = this[property];
}
// We also need to preserve the unexpected shutdown, and make sure it is not cancelled during reset.
this.resetOnUnexpectedShutdown = () => {};
try {
if (this.sessionId !== null) {
await this.deleteSession(this.sessionId);
@@ -244,7 +211,7 @@ export class BaseDriver extends DriverCore {
await this.createSession(this.originalCaps);
} finally {
// always restore state.
for (let [key, value] of _.toPairs(currentConfig)) {
for (const [key, value] of _.toPairs(currentConfig)) {
this[key] = value;
}
}
@@ -257,13 +224,14 @@ export class BaseDriver extends DriverCore {
* Appium 2 has dropped the support of these, so now we only accept capability
* objects in W3C format and thus allow any of the three arguments to represent
* the latter.
* @param {W3CCapabilities<C>} w3cCapabilities1
* @param {W3CCapabilities<C>} [w3cCapabilities2]
* @param {W3CCapabilities<C>} [w3cCapabilities]
* @param {DriverData[]} [driverData]
* @returns {Promise<[string,Capabilities<C>]>}
*/
async createSession(w3cCapabilities1, w3cCapabilities2, w3cCapabilities, driverData) {
async createSession(
w3cCapabilities1: W3CDriverCaps<C>,
w3cCapabilities2?: W3CDriverCaps<C>,
w3cCapabilities?: W3CDriverCaps<C>,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
driverData?: DriverData[]
): Promise<DefaultCreateSessionResult<C>> {
if (this.sessionId !== null) {
throw new errors.SessionNotCreatedError(
'Cannot create a new session while one is in progress'
@@ -289,15 +257,14 @@ export class BaseDriver extends DriverCore {
`Creating session with W3C capabilities: ${JSON.stringify(originalCaps, null, 2)}`
);
/** @type {Capabilities<C>} */
let caps;
let caps: DriverCaps<C>;
try {
caps = processCapabilities(
originalCaps,
this._desiredCapConstraints,
this.shouldValidateCaps
);
caps = fixCaps(caps, this._desiredCapConstraints, this.log);
) as DriverCaps<C>;
caps = fixCaps(caps, this._desiredCapConstraints, this.log) as DriverCaps<C>;
} catch (e) {
throw new errors.SessionNotCreatedError(e.message);
}
@@ -334,7 +301,7 @@ export class BaseDriver extends DriverCore {
}
if (!_.isUndefined(this.caps.newCommandTimeout)) {
this.newCommandTimeoutMs = /** @type {number} */ (this.caps.newCommandTimeout) * 1000;
this.newCommandTimeoutMs = (this.caps.newCommandTimeout as number) * 1000;
}
this._log.prefix = helpers.generateDriverLogPrefix(this, this.sessionId);
@@ -344,32 +311,23 @@ export class BaseDriver extends DriverCore {
return [this.sessionId, caps];
}
/**
*
* @param {string} [sessionId]
* @param {DriverData[]} [driverData]
* @returns {Promise<void>}
*/
async deleteSession(sessionId, driverData) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async deleteSession(sessionId?: string | null) {
await this.clearNewCommandTimeout();
if (this.isCommandsQueueEnabled && this.commandsQueueGuard.isBusy()) {
// simple hack to release pending commands if they exist
// @ts-ignore
for (const key of _.keys(this.commandsQueueGuard.queues)) {
// @ts-ignore
this.commandsQueueGuard.queues[key] = [];
// @ts-expect-error private API
const queues = this.commandsQueueGuard.queues;
for (const key of _.keys(queues)) {
queues[key] = [];
}
}
this.sessionId = null;
this._log.prefix = helpers.generateDriverLogPrefix(this);
}
/**
*
* @param {Capabilities<C>} caps
*/
logExtraCaps(caps) {
let extraCaps = _.difference(_.keys(caps), _.keys(this._desiredCapConstraints));
logExtraCaps(caps: Capabilities<C>) {
const extraCaps = _.difference(_.keys(caps), _.keys(this._desiredCapConstraints));
if (extraCaps.length) {
this.log.warn(
`The following capabilities were provided, but are not ` + `recognized by Appium:`
@@ -380,12 +338,7 @@ export class BaseDriver extends DriverCore {
}
}
/**
*
* @param {Capabilities<C>} caps
* @returns {boolean}
*/
validateDesiredCaps(caps) {
validateDesiredCaps(caps: any): caps is DriverCaps<C> {
if (!this.shouldValidateCaps) {
return true;
}
@@ -407,65 +360,6 @@ export class BaseDriver extends DriverCore {
}
}
// eslint-disable-next-line import/no-unresolved
export * from './commands';
export default BaseDriver;
/**
* @typedef {import('@appium/types').HTTPMethod} HTTPMethod
* @typedef {import('@appium/types').DriverData} DriverData
* @typedef {import('@appium/types').Constraints} Constraints
* @typedef {import('@appium/types').Constraint} Constraint
* @typedef {import('@appium/types').StringRecord} StringRecord
* @typedef {import('@appium/types').BaseDriverCapConstraints} BaseDriverCapConstraints
* @typedef {import('@appium/types').ServerArgs} ServerArgs
*/
/**
* @callback UpdateServerCallback
* @param {import('express').Express} app - Express app
* @param {import('@appium/types').AppiumServer} httpServer - HTTP server
* @returns {import('type-fest').Promisable<void>}
*/
/**
* This is used to extend {@linkcode BaseDriver} by the mixins and also external drivers.
* @template {Constraints} C
* @template [Proto={}]
* @template [Static={}]
* @typedef {import('@appium/types').Class<BaseDriver<C> & Proto,import('@appium/types').DriverStatic & Static>} BaseDriverBase
*/
/**
* @template {Constraints} [C=BaseDriverCapConstraints]
* @typedef {import('@appium/types').SessionHandler<[string, object],void, C>} SessionHandler
*/
/**
* @template {Constraints} [C=BaseDriverCapConstraints]
* @template {StringRecord|void} [Extra=void]
* @typedef {import('@appium/types').Capabilities<C, Extra>} Capabilities
*/
/**
* @template {Constraints} [C=BaseDriverCapConstraints]
* @template {StringRecord|void} [Extra=void]
* @typedef {import('@appium/types').W3CCapabilities<C, Extra>} W3CCapabilities
*/
/**
* @template {Constraints} [C=BaseDriverCapConstraints]
* @template {StringRecord} [CArgs=StringRecord]
* @typedef {import('@appium/types').Driver<C, CArgs>} Driver
*/
/**
* @template {Constraints} C
* @typedef {import('@appium/types').ExternalDriver<C>} ExternalDriver
*/
/**
* @template {Constraints} C
* @typedef {import('@appium/types').DriverOpts<C>} DriverOpts
*/
+216 -214
View File
@@ -2,16 +2,24 @@ import _ from 'lodash';
import path from 'path';
import url from 'url';
import logger from './logger';
import {tempDir, fs, util, zip, net, timing, node} from '@appium/support';
import {tempDir, fs, util, zip, timing, node} from '@appium/support';
import LRU from 'lru-cache';
import AsyncLock from 'async-lock';
import axios from 'axios';
import B from 'bluebird';
// for compat with running tests transpiled and in-place
const {version: BASEDRIVER_VER} = fs.readPackageJsonFrom(__dirname);
const IPA_EXT = '.ipa';
const ZIP_EXTS = ['.zip', IPA_EXT];
const ZIP_EXTS = new Set(['.zip', IPA_EXT]);
const ZIP_MIME_TYPES = ['application/zip', 'application/x-zip-compressed', 'multipart/x-zip'];
const CACHED_APPS_MAX_AGE = 1000 * 60 * 60 * 24; // ms
const MAX_CACHED_APPS = 1024;
const HTTP_STATUS_NOT_MODIFIED = 304;
const DEFAULT_REQ_HEADERS = Object.freeze({
'user-agent': `Appium (BaseDriver v${BASEDRIVER_VER})`,
});
const AVG_DOWNLOAD_SPEED_MEASUREMENT_THRESHOLD_SEC = 2;
const APPLICATIONS_CACHE = new LRU({
max: MAX_CACHED_APPS,
ttl: CACHED_APPS_MAX_AGE, // expire after 24 hours
@@ -52,77 +60,6 @@ process.on('exit', () => {
}
});
/**
*
* @param {string} url
* @returns {Promise<import('axios').AxiosResponse['headers']>}
*/
async function retrieveHeaders(url) {
try {
return (
await axios({
url,
method: 'HEAD',
timeout: 5000,
})
).headers;
} catch (e) {
logger.info(`Cannot send HEAD request to '${url}'. Original error: ${e.message}`);
}
return {};
}
function getCachedApplicationPath(link, currentAppProps = {}, cachedAppInfo = {}) {
const refresh = () => {
logger.debug(`A fresh copy of the application is going to be downloaded from ${link}`);
return null;
};
if (!_.isPlainObject(cachedAppInfo) || !_.isPlainObject(currentAppProps)) {
// if an invalid arg is passed then assume cache miss
return refresh();
}
const {
lastModified: currentModified,
immutable: currentImmutable,
// maxAge is in seconds
maxAge: currentMaxAge,
} = currentAppProps;
const {
// Date instance
lastModified,
// boolean
immutable,
// Unix time in milliseconds
timestamp,
fullPath,
} = cachedAppInfo;
if (lastModified && currentModified) {
if (currentModified.getTime() <= lastModified.getTime()) {
logger.debug(`The application at ${link} has not been modified since ${lastModified}`);
return fullPath;
}
logger.debug(`The application at ${link} has been modified since ${lastModified}`);
return refresh();
}
if (immutable && currentImmutable) {
logger.debug(`The application at ${link} is immutable`);
return fullPath;
}
if (currentMaxAge && timestamp) {
const msLeft = timestamp + currentMaxAge * 1000 - Date.now();
if (msLeft > 0) {
logger.debug(
`The cached application '${path.basename(fullPath)}' will expire in ${msLeft / 1000}s`
);
return fullPath;
}
logger.debug(`The cached application '${path.basename(fullPath)}' has expired`);
}
return refresh();
}
function verifyAppExtension(app, supportedAppExtensions) {
if (supportedAppExtensions.map(_.toLower).includes(_.toLower(path.extname(app)))) {
return app;
@@ -160,56 +97,14 @@ async function isAppIntegrityOk(currentPath, expectedIntegrity = {}) {
}
/**
* @typedef PostProcessOptions
* @property {?Object} cachedAppInfo The information about the previously cached app instance (if exists):
* - packageHash: SHA1 hash of the package if it is a file and not a folder
* - lastModified: Optional Date instance, the value of file's `Last-Modified` header
* - immutable: Optional boolean value. Contains true if the file has an `immutable` mark
* in `Cache-control` header
* - maxAge: Optional integer representation of `maxAge` parameter in `Cache-control` header
* - timestamp: The timestamp this item has been added to the cache (measured in Unix epoch
* milliseconds)
* - integrity: An object containing either `file` property with SHA1 hash of the file
* or `folder` property with total amount of cached files and subfolders
* - fullPath: the full path to the cached app
* @property {boolean} isUrl Whether the app has been downloaded from a remote URL
* @property {?Object} headers Optional headers object. Only present if `isUrl` is true and if the server
* responds to HEAD requests. All header names are normalized to lowercase.
* @property {string} appPath A string containing full path to the preprocessed application package (either
* downloaded or a local one)
*/
/**
* @typedef PostProcessResult
* @property {string} appPath The full past to the post-processed application package on the
* local file system (might be a file or a folder path)
*/
/**
* @typedef ConfigureAppOptions
* @property {(obj: PostProcessOptions) => (Promise<PostProcessResult|undefined>|PostProcessResult|undefined)} [onPostProcess]
* Optional function, which should be applied
* to the application after it is downloaded/preprocessed. This function may be async
* and is expected to accept single object parameter.
* The function is expected to either return a falsy value, which means the app must not be
* cached and a fresh copy of it is downloaded each time. If this function returns an object
* containing `appPath` property then the integrity of it will be verified and stored into
* the cache.
* @property {string[]} supportedExtensions List of supported application extensions (
* including starting dots). This property is mandatory and must not be empty.
*/
/**
* Prepares an app to be used in an automated test. The app gets cached automatically
* if it is an archive or if it is downloaded from an URL.
* If the downloaded app has `.zip` extension, this method will unzip it.
* The unzip does not work when `onPostProcess` is provided.
*
* @param {string} app Either a full path to the app or a remote URL
* @param {string|string[]|ConfigureAppOptions} options
* @returns The full path to the resulting application bundle
* @param {string} app
* @param {string|string[]|import('@appium/types').ConfigureAppOptions} options
*/
async function configureApp(app, options = /** @type {ConfigureAppOptions} */ ({})) {
async function configureApp(
app,
options = /** @type {import('@appium/types').ConfigureAppOptions} */ ({})
) {
if (!_.isString(app)) {
// immediately shortcircuit if not given an app
return;
@@ -233,13 +128,14 @@ async function configureApp(app, options = /** @type {ConfigureAppOptions} */ ({
let newApp = app;
let shouldUnzipApp = false;
let packageHash = null;
/** @type {import('axios').AxiosResponse['headers']?} */
let headers = null;
/** @type {import('axios').AxiosResponse['headers']|undefined} */
let headers = undefined;
/** @type {RemoteAppProps} */
const remoteAppProps = {
lastModified: null,
immutable: false,
maxAge: null,
etag: null,
};
const {protocol, pathname} = url.parse(newApp);
const isUrl = protocol === null ? false : ['http:', 'https:'].includes(protocol);
@@ -250,94 +146,121 @@ async function configureApp(app, options = /** @type {ConfigureAppOptions} */ ({
if (isUrl) {
// Use the app from remote URL
logger.info(`Using downloadable app '${newApp}'`);
headers = await retrieveHeaders(newApp);
if (!_.isEmpty(headers)) {
if (headers['last-modified']) {
remoteAppProps.lastModified = new Date(headers['last-modified']);
}
logger.debug(`Last-Modified: ${headers['last-modified']}`);
if (headers['cache-control']) {
remoteAppProps.immutable = /\bimmutable\b/i.test(headers['cache-control']);
const maxAgeMatch = /\bmax-age=(\d+)\b/i.exec(headers['cache-control']);
if (maxAgeMatch) {
remoteAppProps.maxAge = parseInt(maxAgeMatch[1], 10);
}
}
logger.debug(`Cache-Control: ${headers['cache-control']}`);
}
const cachedPath = getCachedApplicationPath(app, remoteAppProps, cachedAppInfo);
if (cachedPath) {
if (await isAppIntegrityOk(cachedPath, cachedAppInfo?.integrity)) {
logger.info(`Reusing previously downloaded application at '${cachedPath}'`);
return verifyAppExtension(cachedPath, supportedAppExtensions);
}
logger.info(
`The application at '${cachedPath}' does not exist anymore ` +
`or its integrity has been damaged. Deleting it from the internal cache`
);
APPLICATIONS_CACHE.delete(app);
const reqHeaders = {
...DEFAULT_REQ_HEADERS,
};
if (cachedAppInfo?.etag) {
reqHeaders['if-none-match'] = remoteAppProps.etag;
} else if (cachedAppInfo?.lastModified) {
reqHeaders['if-modified-since'] = remoteAppProps.lastModified?.toString();
}
let fileName = null;
const basename = fs.sanitizeName(path.basename(decodeURIComponent(pathname ?? '')), {
replacement: SANITIZE_REPLACEMENT,
});
const extname = path.extname(basename);
// to determine if we need to unzip the app, we have a number of places
// to look: content type, content disposition, or the file extension
if (ZIP_EXTS.includes(extname)) {
fileName = basename;
shouldUnzipApp = true;
}
if (headers['content-type']) {
const ct = headers['content-type'];
logger.debug(`Content-Type: ${ct}`);
// the filetype may not be obvious for certain urls, so check the mime type too
if (
ZIP_MIME_TYPES.some((mimeType) =>
new RegExp(`\\b${_.escapeRegExp(mimeType)}\\b`).test(ct)
)
) {
if (!fileName) {
fileName = `${DEFAULT_BASENAME}.zip`;
let {headers, stream, status} = await queryAppLink(newApp, reqHeaders);
try {
if (!_.isEmpty(headers)) {
logger.debug(`Etag: ${remoteAppProps?.etag} -> ${headers.etag}`);
if (headers.etag) {
remoteAppProps.etag = headers.etag;
}
logger.debug(
`Last-Modified: ${remoteAppProps?.['last-modified']} -> ${headers['last-modified']}`
);
if (headers['last-modified']) {
remoteAppProps.lastModified = new Date(headers['last-modified']);
}
logger.debug(
`Cache-Control: ${remoteAppProps?.['cache-control']} -> ${headers['cache-control']}`
);
if (headers['cache-control']) {
remoteAppProps.immutable = /\bimmutable\b/i.test(headers['cache-control']);
const maxAgeMatch = /\bmax-age=(\d+)\b/i.exec(headers['cache-control']);
if (maxAgeMatch) {
remoteAppProps.maxAge = parseInt(maxAgeMatch[1], 10);
}
}
}
if (cachedAppInfo && status === HTTP_STATUS_NOT_MODIFIED) {
if (await isAppIntegrityOk(cachedAppInfo.fullPath, cachedAppInfo.integrity)) {
logger.info(`Reusing previously downloaded application at '${cachedAppInfo.fullPath}'`);
return verifyAppExtension(cachedAppInfo.fullPath, supportedAppExtensions);
}
logger.info(
`The application at '${cachedAppInfo.fullPath}' does not exist anymore ` +
`or its integrity has been damaged. Deleting it from the internal cache`
);
APPLICATIONS_CACHE.delete(app);
if (!stream.closed) {
stream.destroy();
}
({stream, headers, status} = await queryAppLink(newApp, {...DEFAULT_REQ_HEADERS}));
}
let fileName = null;
const basename = fs.sanitizeName(path.basename(decodeURIComponent(pathname ?? '')), {
replacement: SANITIZE_REPLACEMENT,
});
const extname = path.extname(basename);
// to determine if we need to unzip the app, we have a number of places
// to look: content type, content disposition, or the file extension
if (ZIP_EXTS.has(extname)) {
fileName = basename;
shouldUnzipApp = true;
}
}
if (headers['content-disposition'] && /^attachment/i.test(headers['content-disposition'])) {
logger.debug(`Content-Disposition: ${headers['content-disposition']}`);
const match = /filename="([^"]+)/i.exec(headers['content-disposition']);
if (match) {
fileName = fs.sanitizeName(match[1], {
replacement: SANITIZE_REPLACEMENT,
});
shouldUnzipApp = shouldUnzipApp || ZIP_EXTS.includes(path.extname(fileName));
if (headers['content-type']) {
const ct = headers['content-type'];
logger.debug(`Content-Type: ${ct}`);
// the filetype may not be obvious for certain urls, so check the mime type too
if (
ZIP_MIME_TYPES.some((mimeType) =>
new RegExp(`\\b${_.escapeRegExp(mimeType)}\\b`).test(ct)
)
) {
if (!fileName) {
fileName = `${DEFAULT_BASENAME}.zip`;
}
shouldUnzipApp = true;
}
}
if (headers['content-disposition'] && /^attachment/i.test(headers['content-disposition'])) {
logger.debug(`Content-Disposition: ${headers['content-disposition']}`);
const match = /filename="([^"]+)/i.exec(headers['content-disposition']);
if (match) {
fileName = fs.sanitizeName(match[1], {
replacement: SANITIZE_REPLACEMENT,
});
shouldUnzipApp = shouldUnzipApp || ZIP_EXTS.has(path.extname(fileName));
}
}
if (!fileName) {
// assign the default file name and the extension if none has been detected
const resultingName = basename
? basename.substring(0, basename.length - extname.length)
: DEFAULT_BASENAME;
let resultingExt = extname;
if (!supportedAppExtensions.includes(resultingExt)) {
logger.info(
`The current file extension '${resultingExt}' is not supported. ` +
`Defaulting to '${_.first(supportedAppExtensions)}'`
);
resultingExt = /** @type {string} */ (_.first(supportedAppExtensions));
}
fileName = `${resultingName}${resultingExt}`;
}
const targetPath = await tempDir.path({
prefix: fileName,
suffix: '',
});
newApp = await fetchApp(stream, targetPath);
} finally {
if (!stream.closed) {
stream.destroy();
}
}
if (!fileName) {
// assign the default file name and the extension if none has been detected
const resultingName = basename
? basename.substring(0, basename.length - extname.length)
: DEFAULT_BASENAME;
let resultingExt = extname;
if (!supportedAppExtensions.includes(resultingExt)) {
logger.info(
`The current file extension '${resultingExt}' is not supported. ` +
`Defaulting to '${_.first(supportedAppExtensions)}'`
);
resultingExt = /** @type {string} */ (_.first(supportedAppExtensions));
}
fileName = `${resultingName}${resultingExt}`;
}
const targetPath = await tempDir.path({
prefix: fileName,
suffix: '',
});
newApp = await downloadApp(newApp, targetPath);
} else if (await fs.exists(newApp)) {
// Use the local app
logger.info(`Using local app '${newApp}'`);
shouldUnzipApp = ZIP_EXTS.includes(path.extname(newApp));
shouldUnzipApp = ZIP_EXTS.has(path.extname(newApp));
} else {
let errorMessage = `The application at '${newApp}' does not exist or is not accessible`;
// protocol value for 'C:\\temp' is 'c:', so we check the length as well
@@ -411,12 +334,14 @@ async function configureApp(app, options = /** @type {ConfigureAppOptions} */ ({
};
if (_.isFunction(onPostProcess)) {
const result = await onPostProcess({
cachedAppInfo: _.clone(cachedAppInfo),
isUrl,
headers: _.clone(headers),
appPath: newApp,
});
const result = await onPostProcess(
/** @type {import('@appium/types').PostProcessOptions<import('axios').AxiosResponseHeaders>} */ ({
cachedAppInfo: _.clone(cachedAppInfo),
isUrl,
headers: _.clone(headers),
appPath: newApp,
})
);
return !result?.appPath || app === result?.appPath || !(await fs.exists(result?.appPath))
? newApp
: await storeAppInCache(result.appPath);
@@ -429,16 +354,78 @@ async function configureApp(app, options = /** @type {ConfigureAppOptions} */ ({
});
}
async function downloadApp(app, targetPath) {
const {href} = url.parse(app);
/**
* Sends a HTTP GET query to fetch the app with caching enabled.
* Follows https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching
*
* @param {string} appLink The URL to download an app from
* @param {import('axios').RawAxiosRequestHeaders} reqHeaders Additional HTTP request headers
* @returns {Promise<RemoteAppData>}
*/
async function queryAppLink(appLink, reqHeaders) {
const {href} = url.parse(appLink);
/**
* @type {import('axios').RawAxiosRequestConfig}
*/
const requestOpts = {
url: href,
responseType: 'stream',
timeout: APP_DOWNLOAD_TIMEOUT_MS,
validateStatus: (status) =>
(status >= 200 && status < 300) || status === HTTP_STATUS_NOT_MODIFIED,
headers: reqHeaders,
};
try {
await net.downloadFile(href, targetPath, {
timeout: APP_DOWNLOAD_TIMEOUT_MS,
const {data: stream, headers, status} = await axios(requestOpts);
return {
stream,
headers,
status,
};
} catch (err) {
throw new Error(`Cannot download the app from ${href}: ${err.message}`);
}
}
/**
* Retrieves app payload from the given stream. Also meters the download performance.
*
* @param {import('stream').Readable} srcStream The incoming stream
* @param {string} dstPath The target file path to be written
* @returns {Promise<string>} The same dstPath
* @throws {Error} If there was a failure while downloading the file
*/
async function fetchApp(srcStream, dstPath) {
const timer = new timing.Timer().start();
try {
const writer = fs.createWriteStream(dstPath);
srcStream.pipe(writer);
await new B((resolve, reject) => {
srcStream.once('error', reject);
writer.once('finish', resolve);
writer.once('error', (e) => {
srcStream.unpipe(writer);
reject(e);
});
});
} catch (err) {
throw new Error(`Unable to download the app: ${err.message}`);
throw new Error(`Cannot fetch the application: ${err.message}`);
}
return targetPath;
const secondsElapsed = timer.getDuration().asSeconds;
const {size} = await fs.stat(dstPath);
logger.debug(
`The application (${util.toReadableSizeString(size)}) ` +
`has been downloaded to '${dstPath}' in ${secondsElapsed.toFixed(3)}s`
);
// it does not make much sense to approximate the speed for short downloads
if (secondsElapsed >= AVG_DOWNLOAD_SPEED_MEASUREMENT_THRESHOLD_SEC) {
const bytesPerSec = Math.floor(size / secondsElapsed);
logger.debug(`Approximate download speed: ${util.toReadableSizeString(bytesPerSec)}/s`);
}
return dstPath;
}
/**
@@ -606,11 +593,26 @@ export default {
parseCapsArray,
generateDriverLogPrefix,
};
export {configureApp, isPackageOrBundle, duplicateKeys, parseCapsArray, generateDriverLogPrefix};
export {
configureApp,
isPackageOrBundle,
duplicateKeys,
parseCapsArray,
generateDriverLogPrefix,
BASEDRIVER_VER,
};
/**
* @typedef RemoteAppProps
* @property {Date?} lastModified
* @property {boolean} immutable
* @property {number?} maxAge
* @property {string?} etag
*/
/**
* @typedef RemoteAppData Properties of the remote application (e.g. GET HTTP response) to be downloaded.
* @property {number} status The HTTP status of the response
* @property {import('stream').Readable} stream The HTTP response body represented as readable stream
* @property {import('axios').RawAxiosResponseHeaders | import('axios').AxiosResponseHeaders} headers HTTP response headers
*/
+4 -2
View File
@@ -151,8 +151,10 @@ function configureHttp({httpServer, reject, keepAliveTimeout}) {
notifier: new EventEmitter(),
closed: false,
};
// TS does not love monkeypatching.
const appiumServer = /** @type {AppiumServer} */ (/** @type {unknown} */ (httpServer));
/**
* @type {AppiumServer}
*/
const appiumServer = /** @type {any} */ (httpServer);
appiumServer.addWebSocketHandler = addWebSocketHandler;
appiumServer.removeWebSocketHandler = removeWebSocketHandler;
appiumServer.removeAllWebSocketHandlers = removeAllWebSocketHandlers;
+10 -39
View File
@@ -6,33 +6,21 @@ import B from 'bluebird';
const DEFAULT_WS_PATHNAME_PREFIX = '/ws';
/**
* Adds websocket handler to express server instance.
* It is expected this function is called in Express
* server instance context.
*
* @this {AppiumServer} - An instance of express HTTP server.
* @param {string} handlerPathname - Web socket endpoint path starting with
* a single slash character. It is recommended to always add
* DEFAULT_WS_PATHNAME_PREFIX to all web socket pathnames.
* @param {import('ws').Server} handlerServer - WebSocket server instance. See
* https://github.com/websockets/ws/pull/885 for more details
* on how to configure the handler properly.
* @returns {Promise<void>}
* @this {AppiumServer}
* @type {AppiumServer['addWebSocketHandler']}
*/
async function addWebSocketHandler(handlerPathname, handlerServer) {
const server = /** @type {AppiumServer} */ (this);
if (_.isUndefined(this.webSocketsMapping)) {
server.webSocketsMapping = {};
this.webSocketsMapping = {};
// https://github.com/websockets/ws/pull/885
server.on('upgrade', (request, socket, head) => {
this.on('upgrade', (request, socket, head) => {
let currentPathname;
try {
// @ts-expect-error
currentPathname = new URL(request.url).pathname;
currentPathname = new URL(/** @type {string} */ (request.url)).pathname;
} catch {
currentPathname = request.url;
}
for (const [pathname, wsServer] of _.toPairs(server.webSocketsMapping)) {
for (const [pathname, wsServer] of _.toPairs(this.webSocketsMapping)) {
if (currentPathname === pathname) {
wsServer.handleUpgrade(request, socket, head, (ws) => {
wsServer.emit('connection', ws, request);
@@ -47,17 +35,9 @@ async function addWebSocketHandler(handlerPathname, handlerServer) {
}
/**
* Returns web socket handlers registered for the given server
* instance.
* It is expected this function is called in Express
* server instance context.
*
* @this {AppiumServer}
* @param {string?} [keysFilter] - Only include pathnames with given
* `keysFilter` value if set. All pairs will be included by default.
* @returns {Promise<Record<string, import('ws').Server>>} pathnames to websocket server instances mapping matching the search criteria or an empty object otherwise.
* @type {AppiumServer['getWebSocketHandlers']}
*/
// eslint-disable-next-line require-await
async function getWebSocketHandlers(keysFilter = null) {
if (_.isEmpty(this.webSocketsMapping)) {
return {};
@@ -72,16 +52,9 @@ async function getWebSocketHandlers(keysFilter = null) {
}
/**
* Removes existing websocket handler from express server instance.
* The call is ignored if the given `handlerPathname` handler
* is not present in the handlers list.
* It is expected this function is called in Express
* server instance context.
* @this {AppiumServer}
* @param {string} handlerPathname - Websocket endpoint path.
* @returns {Promise<boolean>} true if the handlerPathname was found and deleted
* @type {AppiumServer['removeWebSocketHandler']}
*/
// eslint-disable-next-line require-await
async function removeWebSocketHandler(handlerPathname) {
const wsServer = this.webSocketsMapping?.[handlerPathname];
if (!wsServer) {
@@ -103,11 +76,9 @@ async function removeWebSocketHandler(handlerPathname) {
}
/**
* Removes all existing websocket handler from express server instance.
* It is expected this function is called in Express
* server instance context.
*
* @this {AppiumServer}
* @returns {Promise<boolean>} true if at least one handler has been deleted
* @type {AppiumServer['removeAllWebSocketHandlers']}
*/
async function removeAllWebSocketHandlers() {
if (_.isEmpty(this.webSocketsMapping)) {
@@ -83,7 +83,6 @@ export {isW3cCaps, fixCaps};
*/
/**
* @template {Constraints} [C=BaseDriverCapConstraints]
* @template {StringRecord|void} [Extra=void]
* @typedef {import('@appium/types').Capabilities<C, Extra>} Capabilities
* @template {Constraints} C
* @typedef {import('@appium/types').Capabilities<C>} Capabilities
*/
+11 -2
View File
@@ -277,7 +277,7 @@ function routeConfiguringFunction(driver) {
`${basePath}${path}`,
spec,
driver,
isSessionCommand(spec.command)
isSessionCommand(/** @type {import('@appium/types').DriverMethodDef} */ (spec).command)
);
}
}
@@ -454,7 +454,16 @@ function buildHandler(app, method, path, spec, driver, isSessCmd) {
} catch (err) {
// if anything goes wrong, figure out what our response should be
// based on the type of error that we encountered
let actualErr = err;
let actualErr;
if (err instanceof Error || (_.has(err, 'stack') && _.has(err, 'message'))) {
actualErr = err;
} else {
getLogger(driver, req.params.sessionId || newSessionId).warn(
'The thrown error object does not seem to be a valid instance of the Error class. This ' +
'might be a genuine bug of a driver or a plugin.'
);
actualErr = new Error(`${err ?? 'unknown'}`);
}
currentProtocol =
currentProtocol || extractProtocol(driver, req.params.sessionId || newSessionId);
+12 -13
View File
@@ -18,6 +18,7 @@ const SET_ALERT_TEXT_PAYLOAD_PARAMS = {
* define the routes, mapping of HTTP methods to particular driver commands, and
* any parameters that are expected in a request parameters can be `required` or
* `optional`
* @satisfies {import('@appium/types').MethodMap<import('../basedriver/driver').BaseDriver>}
*/
const METHOD_MAP = /** @type {const} */ ({
'/status': {
@@ -129,7 +130,11 @@ const METHOD_MAP = /** @type {const} */ ({
POST: {command: 'deactivateIMEEngine', deprecated: true},
},
'/session/:sessionId/ime/activate': {
POST: {command: 'activateIMEEngine', payloadParams: {required: ['engine']}, deprecated: true},
POST: {
command: 'activateIMEEngine',
payloadParams: {required: ['engine']},
deprecated: true,
},
},
'/session/:sessionId/frame': {
POST: {command: 'setFrame', payloadParams: {required: ['id']}},
@@ -356,7 +361,11 @@ const METHOD_MAP = /** @type {const} */ ({
DELETE: {command: 'releaseActions'},
},
'/session/:sessionId/touch/longclick': {
POST: {command: 'touchLongClick', payloadParams: {required: ['elements']}, deprecated: true},
POST: {
command: 'touchLongClick',
payloadParams: {required: ['elements']},
deprecated: true,
},
},
'/session/:sessionId/touch/flick': {
POST: {
@@ -922,7 +931,7 @@ const METHOD_MAP = /** @type {const} */ ({
let ALL_COMMANDS = [];
for (let v of _.values(METHOD_MAP)) {
for (let m of _.values(v)) {
if (m.command) {
if ('command' in m && m.command) {
ALL_COMMANDS.push(m.command);
}
}
@@ -1006,13 +1015,3 @@ function routeToCommandName(endpoint, method, basePath = DEFAULT_BASE_PATH) {
const NO_SESSION_ID_COMMANDS = ['createSession', 'getStatus', 'getSessions'];
export {METHOD_MAP, ALL_COMMANDS, NO_SESSION_ID_COMMANDS, routeToCommandName};
/**
* This checks compat with the `MethodMap` interface.
* @ignore
*/
// eslint-disable-next-line no-unused-vars
const _validMethodMap =
/** @type {import('@appium/types').MethodMap<import('@appium/types').ExternalDriver<import('@appium/types').BaseDriverCapConstraints>>} */ (
METHOD_MAP
);
+11 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/base-driver",
"version": "9.3.3",
"version": "9.3.6",
"description": "Base driver class for Appium drivers",
"keywords": [
"automation",
@@ -40,11 +40,12 @@
"test": "npm run test:unit",
"test:e2e": "mocha -p --timeout 20s --slow 10s \"./test/e2e/**/*.spec.js\"",
"test:smoke": "node ./index.js",
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
"test:unit": "mocha \"./test/unit/**/*.spec.js\"",
"test:types": "tsd"
},
"dependencies": {
"@appium/support": "^3.1.7",
"@appium/types": "^0.10.1",
"@appium/support": "^3.1.10",
"@appium/types": "^0.10.4",
"@colors/colors": "1.5.0",
"@types/async-lock": "1.4.0",
"@types/bluebird": "3.5.38",
@@ -53,7 +54,7 @@
"@types/serve-favicon": "2.5.3",
"async-lock": "1.4.0",
"asyncbox": "2.9.4",
"axios": "1.3.4",
"axios": "1.3.5",
"bluebird": "3.7.2",
"body-parser": "1.20.2",
"es6-error": "4.1.1",
@@ -65,7 +66,7 @@
"morgan": "1.10.0",
"serve-favicon": "2.5.0",
"source-map-support": "0.5.21",
"type-fest": "3.6.1",
"type-fest": "3.8.0",
"validate.js": "0.13.1"
},
"engines": {
@@ -75,8 +76,11 @@
"publishConfig": {
"access": "public"
},
"gitHead": "84b211330dc84849af9cc1f3a5c5ad32e15b2e72",
"gitHead": "8480a85ce2fa466360e0fb1a7f66628331907f02",
"typedoc": {
"entryPoint": "./lib/index.js"
},
"tsd": {
"directory": "test/types"
}
}
@@ -0,0 +1,11 @@
import {expectAssignable} from 'tsd';
// NOTE: this pulls in the distfiles
import {BaseDriver} from '.../../..';
import {BaseDriverCapConstraints, ExternalDriver, Driver, DriverOpts} from '@appium/types';
expectAssignable<Driver<BaseDriverCapConstraints>>(
new BaseDriver({} as DriverOpts<BaseDriverCapConstraints>)
);
expectAssignable<ExternalDriver<BaseDriverCapConstraints>>(
new BaseDriver({} as DriverOpts<BaseDriverCapConstraints>)
);
+27
View File
@@ -3,6 +3,33 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.2.6](https://github.com/appium/appium/compare/@appium/base-plugin@2.2.5...@appium/base-plugin@2.2.6) (2023-04-10)
**Note:** Version bump only for package @appium/base-plugin
## [2.2.5](https://github.com/appium/appium/compare/@appium/base-plugin@2.2.4...@appium/base-plugin@2.2.5) (2023-04-03)
**Note:** Version bump only for package @appium/base-plugin
## [2.2.4](https://github.com/appium/appium/compare/@appium/base-plugin@2.2.3...@appium/base-plugin@2.2.4) (2023-03-28)
### Bug Fixes
* **base-driver,base-plugin,types:** update PluginCommand and DriverCommand types ([0dcd5fa](https://github.com/appium/appium/commit/0dcd5fa371af523c6527e55de4cff6cd472fde22))
## [2.2.3](https://github.com/appium/appium/compare/@appium/base-plugin@2.2.2...@appium/base-plugin@2.2.3) (2023-03-08)
**Note:** Version bump only for package @appium/base-plugin
+1 -1
View File
@@ -60,7 +60,7 @@ class BasePlugin {
* @param {NextPluginCallback} next
* @param {Driver<C>} driver
* @param {string} script
* @param {[Record<string, any>]|[]} protoArgs
* @param {readonly [import('@appium/types').StringRecord<unknown>] | readonly any[]} protoArgs
*/
async executeMethod(next, driver, script, protoArgs) {
const Plugin = /** @type {import('@appium/types').PluginClass<Plugin>} */ (this.constructor);
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/base-plugin",
"version": "2.2.3",
"version": "2.2.6",
"description": "The base plugin used to create Appium 2.0 plugins",
"keywords": [
"automation",
@@ -40,8 +40,8 @@
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
},
"dependencies": {
"@appium/base-driver": "^9.3.3",
"@appium/support": "^3.1.7"
"@appium/base-driver": "^9.3.6",
"@appium/support": "^3.1.10"
},
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
@@ -50,7 +50,7 @@
"publishConfig": {
"access": "public"
},
"gitHead": "84b211330dc84849af9cc1f3a5c5ad32e15b2e72",
"gitHead": "8480a85ce2fa466360e0fb1a7f66628331907f02",
"tags": [
"appium"
],
+27
View File
@@ -3,6 +3,33 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.0.12](https://github.com/appium/appium/compare/@appium/doctor@2.0.11...@appium/doctor@2.0.12) (2023-04-10)
**Note:** Version bump only for package @appium/doctor
## [2.0.11](https://github.com/appium/appium/compare/@appium/doctor@2.0.10...@appium/doctor@2.0.11) (2023-04-03)
### Bug Fixes
* **doctor:** update dependency appium-adb to v9.11.0 ([d8b6334](https://github.com/appium/appium/commit/d8b633406a68e88cdd945906dde3f23a2a566a71))
## [2.0.10](https://github.com/appium/appium/compare/@appium/doctor@2.0.9...@appium/doctor@2.0.10) (2023-03-28)
**Note:** Version bump only for package @appium/doctor
## [2.0.9](https://github.com/appium/appium/compare/@appium/doctor@2.0.8...@appium/doctor@2.0.9) (2023-03-08)
**Note:** Version bump only for package @appium/doctor
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/doctor",
"version": "2.0.9",
"version": "2.0.12",
"description": "Test environment for fitness to run Appium",
"keywords": [
"automation",
@@ -44,11 +44,11 @@
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
},
"dependencies": {
"@appium/support": "^3.1.7",
"@appium/support": "^3.1.10",
"@colors/colors": "1.5.0",
"@types/bluebird": "3.5.38",
"@types/teen_process": "2.0.0",
"appium-adb": "9.10.24",
"appium-adb": "9.11.0",
"bluebird": "3.7.2",
"inquirer": "8.2.5",
"lodash": "4.17.21",
@@ -63,5 +63,5 @@
"publishConfig": {
"access": "public"
},
"gitHead": "84b211330dc84849af9cc1f3a5c5ad32e15b2e72"
"gitHead": "8480a85ce2fa466360e0fb1a7f66628331907f02"
}
+63
View File
@@ -3,6 +3,69 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.3.6](https://github.com/appium/appium/compare/@appium/docutils@0.3.5...@appium/docutils@0.3.6) (2023-04-12)
**Note:** Version bump only for package @appium/docutils
## [0.3.5](https://github.com/appium/appium/compare/@appium/docutils@0.3.4...@appium/docutils@0.3.5) (2023-04-10)
### Bug Fixes
* **docutils:** properly respects log levels ([e9426fc](https://github.com/appium/appium/commit/e9426fcfecc1d47ab503fc72af4236c4f1ac4b1e))
* **types:** update dependency type-fest to v3.8.0 ([d6c42e9](https://github.com/appium/appium/commit/d6c42e99c08efce0b34796d5982ce379fca044d3))
## [0.3.4](https://github.com/appium/appium/compare/@appium/docutils@0.3.3...@appium/docutils@0.3.4) (2023-04-05)
**Note:** Version bump only for package @appium/docutils
## [0.3.3](https://github.com/appium/appium/compare/@appium/docutils@0.3.2...@appium/docutils@0.3.3) (2023-04-04)
**Note:** Version bump only for package @appium/docutils
## [0.3.2](https://github.com/appium/appium/compare/@appium/docutils@0.3.1...@appium/docutils@0.3.2) (2023-04-03)
### Bug Fixes
* **types:** update dependency type-fest to v3.7.2 ([5580539](https://github.com/appium/appium/commit/55805390b5a0c6aa718bb357b30f66651f3db281))
## [0.3.1](https://github.com/appium/appium/compare/@appium/docutils@0.3.0...@appium/docutils@0.3.1) (2023-03-28)
### Bug Fixes
* backwards-compatible fixes for TS v5.x ([4974403](https://github.com/appium/appium/commit/49744036619ecc239e0e6255a13d38cafd709920))
* **docutils:** inherit stdio in long-running processes ([ad11fbf](https://github.com/appium/appium/commit/ad11fbf97b57a0c4302d37146660e87c564a5404))
* **docutils:** update dependency typedoc to v0.23.27 ([ef9cbe6](https://github.com/appium/appium/commit/ef9cbe6bcc27a3633eda01cd7c08a4466e304630))
* **docutils:** update dependency typedoc to v0.23.28 ([dbc0bf6](https://github.com/appium/appium/commit/dbc0bf69e855744ed69ff3b75a9981b313579fa2))
* **types:** update dependency type-fest to v3.7.0 ([6912fa1](https://github.com/appium/appium/commit/6912fa14f2a7d338f17e1bed060e959de7aba1d6))
* **types:** update dependency type-fest to v3.7.1 ([bc860c7](https://github.com/appium/appium/commit/bc860c733a73760f0c42cbfb384e04d50c376d5e))
# [0.3.0](https://github.com/appium/appium/compare/@appium/docutils@0.2.2...@appium/docutils@0.3.0) (2023-03-08)
+10 -12
View File
@@ -5,8 +5,8 @@
*/
import _ from 'lodash';
import {exec, SubProcess, SubProcessOptions, TeenProcessExecOptions} from 'teen_process';
import path from 'node:path';
import {exec, TeenProcessExecOptions} from 'teen_process';
import {
DEFAULT_DEPLOY_BRANCH,
DEFAULT_DEPLOY_REMOTE,
@@ -17,10 +17,10 @@ import {
} from '../constants';
import {DocutilsError} from '../error';
import {findMkDocsYml, readPackageJson, whichMike} from '../fs';
import logger from '../logger';
import {argify, stopwatch, TeenProcessSubprocessStartOpts} from '../util';
import {getLogger} from '../logger';
import {argify, spawnBackgroundProcess, SpawnBackgroundProcessOpts, stopwatch} from '../util';
const log = logger.withTag('builder:deploy');
const log = getLogger('builder:deploy');
/**
* Runs `mike serve`
@@ -30,15 +30,12 @@ const log = logger.withTag('builder:deploy');
*/
async function doServe(
args: string[] = [],
{startDetector, detach, timeoutMs}: TeenProcessSubprocessStartOpts = {},
opts: SpawnBackgroundProcessOpts = {},
mikePath?: string
) {
mikePath = mikePath ?? (await whichMike());
const finalArgs = ['serve', ...args];
const opts: SubProcessOptions = {stdio: 'inherit'};
log.debug('Launching %s with args: %s', mikePath, finalArgs);
const proc = new SubProcess(mikePath, finalArgs, opts);
return await proc.start(startDetector, detach, timeoutMs);
return spawnBackgroundProcess(mikePath, finalArgs, opts);
}
/**
@@ -127,6 +124,7 @@ export async function deploy({
// unsure about how SIGHUP is handled here
await doServe(mikeArgs, serveOpts);
} else {
log.info('Deploying into branch %s', branch);
const mikeArgs = [
...argify(
_.omitBy(
@@ -141,7 +139,7 @@ export async function deploy({
}
await doDeploy(mikeArgs, execOpts);
log.success('Mike finished deployment into branch %s (%dms)', branch, stop());
log.success('Finished deployment into branch %s (%dms)', branch, stop());
}
}
@@ -223,7 +221,7 @@ export interface DeployOpts {
execOpts?: TeenProcessExecOptions;
/**
* Extra options for {@linkcode teen_process.Subprocess.start}
* Extra options for {@linkcode spawnBackgroundProcess}
*/
serveOpts?: TeenProcessSubprocessStartOpts;
serveOpts?: SpawnBackgroundProcessOpts;
}
+2 -2
View File
@@ -25,11 +25,11 @@ import {
safeWriteFile,
stringifyYaml,
} from '../fs';
import logger from '../logger';
import {getLogger} from '../logger';
import {MkDocsYml, MkDocsYmlNav} from '../model';
import {relative} from '../util';
const log = logger.withTag('builder:nav');
const log = getLogger('builder:nav');
/**
* Gets a list of `.md` files relative to `docs_dir`
+2 -2
View File
@@ -16,10 +16,10 @@ import {
} from '../constants';
import {DocutilsError} from '../error';
import {findTypeDocJsonPath, readTypedocJson} from '../fs';
import logger from '../logger';
import {getLogger} from '../logger';
import {argify, relative, stopwatch} from '../util';
const log = logger.withTag('builder:reference');
const log = getLogger('builder:reference');
/**
* Executes TypeDoc _in the current process_
+12 -12
View File
@@ -6,14 +6,14 @@
*/
import path from 'node:path';
import {exec, SubProcess, SubProcessOptions, TeenProcessExecOptions} from 'teen_process';
import {NAME_BIN, NAME_THEME, NAME_MKDOCS_YML} from '../constants';
import {exec, TeenProcessExecOptions} from 'teen_process';
import {NAME_BIN, NAME_MKDOCS_YML, NAME_THEME} from '../constants';
import {DocutilsError} from '../error';
import {findMkDocsYml, readMkDocsYml, whichMkDocs} from '../fs';
import logger from '../logger';
import {relative, stopwatch, TeenProcessSubprocessStartOpts} from '../util';
import {getLogger} from '../logger';
import {relative, spawnBackgroundProcess, SpawnBackgroundProcessOpts, stopwatch} from '../util';
const log = logger.withTag('mkdocs');
const log = getLogger('mkdocs');
/**
* Runs `mkdocs serve`
@@ -23,15 +23,14 @@ const log = logger.withTag('mkdocs');
*/
async function doServe(
args: string[] = [],
{startDetector, detach, timeoutMs}: TeenProcessSubprocessStartOpts = {},
opts: SpawnBackgroundProcessOpts = {},
mkDocsPath?: string
) {
mkDocsPath = mkDocsPath ?? (await whichMkDocs());
const finalArgs = ['serve', ...args];
const opts: SubProcessOptions = {stdio: 'inherit'};
log.debug('Launching %s with args: %s', mkDocsPath, finalArgs);
const proc = new SubProcess(mkDocsPath, finalArgs, opts);
return await proc.start(startDetector, detach, timeoutMs);
return spawnBackgroundProcess(mkDocsPath, finalArgs, opts);
}
/**
@@ -81,6 +80,7 @@ export async function buildSite({
// unsure about how SIGHUP is handled here
await doServe(mkdocsArgs, serveOpts);
} else {
log.info('Building site into %s (%dms)');
await doBuild(mkdocsArgs, execOpts);
let relSiteDir;
if (siteDir) {
@@ -90,7 +90,7 @@ export async function buildSite({
log.debug('Found site_dir %s', siteDir);
relSiteDir = relative(path.dirname(mkDocsYmlPath), siteDir!);
}
log.success('MkDocs finished building into %s (%dms)', relSiteDir, stop());
log.success('Finnished building site into %s (%dms)', relSiteDir, stop());
}
}
@@ -138,7 +138,7 @@ export interface BuildMkDocsOpts {
execOpts?: TeenProcessExecOptions;
/**
* Extra options for {@linkcode teen_process.Subprocess.start}
* Extra options for {@linkcode spawnBackgroundProcess}
*/
serveOpts?: TeenProcessSubprocessStartOpts;
serveOpts?: SpawnBackgroundProcessOpts;
}
+2 -2
View File
@@ -6,9 +6,9 @@
import {fs, util} from '@appium/support';
import _ from 'lodash';
import type {Options} from 'yargs';
import logger from '../logger';
import {getLogger} from '../logger';
const log = logger.withTag('check');
const log = getLogger('check');
/**
* Given a list of objects with `id` and `path` props, filters out the ones that do not exist
+3 -2
View File
@@ -7,11 +7,11 @@ import path from 'node:path';
import type {CommandModule, InferredOptionTypes, Options} from 'yargs';
import {buildReferenceDocs, buildSite, deploy, updateNav} from '../../builder';
import {NAME_BIN} from '../../constants';
import logger from '../../logger';
import {getLogger} from '../../logger';
import {stopwatch} from '../../util';
import {checkMissingPaths} from '../check';
const log = logger.withTag('build');
const log = getLogger('build');
enum BuildCommandGroup {
Build = 'Build Config:',
@@ -220,6 +220,7 @@ export default {
);
},
async handler(args) {
log.info('Building docs...');
const stop = stopwatch('build');
log.debug('Build command called with args: %O', args);
if (!args.site && !args.reference) {
+2 -2
View File
@@ -6,11 +6,11 @@
import _ from 'lodash';
import type {CommandModule, InferredOptionTypes, Options} from 'yargs';
import {init} from '../../init';
import logger from '../../logger';
import {getLogger} from '../../logger';
import {stopwatch} from '../../util';
import {checkMissingPaths} from '../check';
const log = logger.withTag('init');
const log = getLogger('init');
enum InitCommandGroup {
MkDocs = 'MkDocs Config:',
@@ -7,10 +7,10 @@ import {util} from '@appium/support';
import type {CommandModule, InferredOptionTypes, Options} from 'yargs';
import {DocutilsError} from '../../error';
import {DocutilsValidator, ValidationKind} from '../../validate';
import logger from '../../logger';
import {getLogger} from '../../logger';
import {checkMissingPaths} from '../check';
const log = logger.withTag('validate');
const log = getLogger('validate');
enum ValidateCommandGroup {
Behavior = 'Validation Behavior:',
+17 -5
View File
@@ -11,10 +11,11 @@ import path from 'node:path';
import YAML from 'yaml';
import parser from 'yargs-parser';
import {hideBin} from 'yargs/helpers';
import {NAME_BIN} from '../constants';
import logger from '../logger';
import {DEFAULT_LOG_LEVEL, LogLevelMap, NAME_BIN} from '../constants';
import {getLogger, initLogger, isLogLevelString} from '../logger';
import {relative} from '../util';
const log = logger.withTag('config');
const log = getLogger('config');
/**
* `lilconfig` loader for YAML
@@ -43,8 +44,19 @@ const loadEsm: Loader = (filepath: string) => import(filepath);
export async function findConfig(argv: string[] = hideBin(process.argv)) {
const preArgs = parser(argv);
if (preArgs.verbose || preArgs.logLevel === 'debug') {
log.level = LogLevel.Debug;
// if --verbose is used, set the log level to debug.
// otherwise use --log-level or the default.
let logLevel: LogLevel | keyof typeof LogLevelMap;
if (preArgs.verbose) {
logLevel = LogLevel.Debug;
} else {
// if the loglevel is valid, use it, otherwise use the default
logLevel = isLogLevelString(preArgs.logLevel) ? preArgs.logLevel : DEFAULT_LOG_LEVEL;
}
initLogger(logLevel);
if (preArgs.noConfig) {
log.debug('Not loading config because --no-config was provided');
}
return preArgs.noConfig || preArgs.help || preArgs.version
+8 -19
View File
@@ -6,7 +6,7 @@
* @module
*/
import logger from '../logger';
import {getLogger} from '../logger';
import _ from 'lodash';
import {hideBin} from 'yargs/helpers';
@@ -20,7 +20,7 @@ import {sync as readPkg} from 'read-pkg';
const pkg = readPkg({cwd: fs.findRoot(__dirname)});
const log = logger.withTag('cli');
const log = getLogger('cli');
export async function main(argv = hideBin(process.argv)) {
const config = await findConfig(argv);
@@ -56,24 +56,14 @@ export async function main(argv = hideBin(process.argv)) {
describe: 'Disable config file discovery',
},
})
.middleware([
.middleware(
/**
* Configures logging; `--verbose` implies `--log-level=debug`
* Writes a startup message
*/
(argv) => {
if (argv.verbose) {
argv.logLevel = 'debug';
log.debug('Debug logging enabled via --verbose');
}
log.level = LogLevelMap[argv.logLevel];
},
/**
* Writes a startup message, if logging is enabled
*/
async () => {
() => {
log.info(`${pkg.name} @ v${pkg.version} (Node.js ${process.version})`);
},
])
}
)
.epilog(`Please report bugs at ${pkg.bugs?.url}`)
.fail(
/**
@@ -85,8 +75,7 @@ export async function main(argv = hideBin(process.argv)) {
log.error(error.message);
} else {
y.showHelp();
console.log();
log.error(msg ?? error.message);
log.error(`\n\n${msg ?? error.message}`);
}
y.exit(1, error);
}
+2 -2
View File
@@ -12,7 +12,7 @@ import {fs} from '@appium/support';
import * as JSON5 from 'json5';
import _ from 'lodash';
import _pkgDir from 'pkg-dir';
import logger from './logger';
import {getLogger} from './logger';
import {Application, TypeDocReader} from 'typedoc';
import {
NAME_TYPEDOC_JSON,
@@ -26,7 +26,7 @@ import {
import {DocutilsError} from './error';
import {MkDocsYml} from './model';
const log = logger.withTag('fs');
const log = getLogger('fs');
/**
* Finds path to closest `package.json`
+1
View File
@@ -8,3 +8,4 @@ export * from './builder';
export * from './validate';
export * from './scaffold';
export * from './constants';
export * from './logger';
+3 -3
View File
@@ -17,7 +17,7 @@ import {exec} from 'teen_process';
import {Simplify} from 'type-fest';
import {DocutilsError} from './error';
import {createScaffoldTask, ScaffoldTaskOptions} from './scaffold';
import logger from './logger';
import {getLogger} from './logger';
import {MkDocsYml, TsConfigJson, TypeDocJson} from './model';
import _ from 'lodash';
import {stringifyJson5, stringifyYaml} from './fs';
@@ -54,8 +54,8 @@ const BASE_TSCONFIG_JSON: Readonly<TsConfigJson> = Object.freeze({
},
});
const log = logger.withTag('init');
const dryRunLog = log.withTag('dry-run');
const log = getLogger('init');
const dryRunLog = getLogger('dry-run', log);
const DEFAULT_INCLUDE = ['lib', 'test', 'index.js'];
/**
+47 -46
View File
@@ -134,60 +134,61 @@ class DocutilsReporter extends FancyReporter {
*
* "Global" inasmuch as any logger created from the root logger will use this level.
*/
let globalLevel = LogLevelMap[DEFAULT_LOG_LEVEL];
let globalLevel: LogLevel = LogLevelMap[DEFAULT_LOG_LEVEL];
/**
* Type guard to see if a string is a recognized log level
* @param level any value
*/
export function isLogLevelString(level: any): level is keyof typeof LogLevelMap {
return level in LogLevelMap;
}
/**
* The logger from which all loggers are created. This one uses a unique tag and our custom reporter.
*/
const rootLogger = createLogProxy(
consola.create({defaults: {tag: 'docutils'}, reporters: [new DocutilsReporter()]})
);
const rootLogger = consola.create({
defaults: {tag: 'docutils'},
reporters: [new DocutilsReporter()],
level: globalLevel,
});
// this prevents logging before `initLogger` is called
rootLogger.pause();
/**
* @summary Creates a log-level-propagating proxy for a {@linkcode Consola} logger.
* @description
* Alright. So when you create a new logger via {@linkcode Consola.create}, it's basically a clone
* of its parent with a new set of options.
*
* If we change the log level of the root logger (which we do: see `cli/index.ts`), we may (almost
* certainly) have
* child loggers which: a) have already been created and b) have inherited the old/default log level
* from the root logger. We don't _want_ that (though this is likely a reasonable use case) for our
* purposes.
*
* The implementation below solves the problem by maintaining its own singleton log level value, and
* intercepts the `level` property of any logger created from the root logger.
*
* There are other ways to go about this which may be better, but this seemed pretty straightforward.
* A map of tags to loggers
*/
function createLogProxy(logger: Consola): Consola {
return new Proxy(logger, {
get(target, prop, receiver) {
if (prop === 'level') {
return globalLevel;
}
if (prop === 'create') {
const create = Reflect.get(target, prop, receiver) as Consola['create'];
return (opts: ConsolaOptions) => createLogProxy(create.call(receiver, opts));
}
if (prop === '_defaults') {
const defaults = Reflect.get(target, prop, receiver);
return {...defaults, level: globalLevel};
}
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
if (prop === 'level') {
globalLevel = value as LogLevel;
return true;
}
return Reflect.set(target, prop, value, receiver);
},
});
const loggers: Map<string, WeakRef<Consola>> = new Map();
export function getLogger(tag: string, parent = rootLogger) {
if (loggers.has(tag)) {
const logger = loggers.get(tag)?.deref();
if (logger) {
return logger;
}
}
const logger = parent.withTag(tag);
logger.level = globalLevel;
loggers.set(tag, new WeakRef(logger));
return logger;
}
/**
* The proxied root logger
* @see {createLogProxy}
* Initialize the logging system.
*
* This should only be called once. The loglevel cannot be changed once it is set.
*
* @remarks Child loggers seem to inherit the "paused" state of the parent, so when this is called, we must resume all of them.
*/
export default rootLogger;
export const initLogger = _.once((level: keyof typeof LogLevelMap | LogLevel) => {
globalLevel = isLogLevelString(level) ? LogLevelMap[level] : level;
rootLogger.level = globalLevel;
rootLogger.resume();
for (const ref of loggers.values()) {
const logger = ref.deref();
if (logger) {
logger.level = globalLevel;
logger.resume();
}
}
});
+3 -3
View File
@@ -4,7 +4,7 @@
*/
import {fs} from '@appium/support';
import logger from './logger';
import {getLogger} from './logger';
import path from 'node:path';
import {createPatch} from 'diff';
import {NormalizedPackageJson} from 'read-pkg';
@@ -15,8 +15,8 @@ import _ from 'lodash';
import {stringifyJson, readPackageJson, safeWriteFile} from './fs';
import {NAME_ERR_ENOENT, NAME_ERR_EEXIST} from './constants';
const log = logger.withTag('init');
const dryRunLog = log.withTag('dry-run');
const log = getLogger('init');
const dryRunLog = getLogger('dry-run', log);
/**
* Creates a unified patch for display in "dry run" mode
+24 -4
View File
@@ -4,6 +4,7 @@
*/
import _ from 'lodash';
import {SpawnOptions, spawn} from 'node:child_process';
import path from 'node:path';
import type {SubProcess} from 'teen_process';
@@ -72,8 +73,27 @@ export const argify: (obj: Record<string, string | number | boolean | undefined>
);
/**
* Conversion of the parameters of {@linkcode SubProcess.start} to an object.
* Spawns a long-running "background" child process. This is expected to only return control to the
* parent process in the case of a nonzero exit code from the child process.
* @param command Command to run
* @param args Args to pass to command
* @param opts Spawn options. `{stdio: 'inherit'}` will always be true
* @privateRemarks `teen_process` is good for running a one-shot command, but not so great for
* background tasks; we use node's `child_process` directly here to pass `stdio` through, since
* `teen_process` basically does not respect `{stdio: 'inherit'}`.
*/
export type TeenProcessSubprocessStartOpts = Partial<
TupleToObject<Parameters<SubProcess['start']>, ['startDetector', 'detach', 'timeoutMs']>
>;
export async function spawnBackgroundProcess(command: string, args: string[], opts: SpawnOptions) {
return new Promise<void>((resolve, reject) => {
spawn(command, args, {...opts, stdio: 'inherit'})
.on('error', reject)
.on('close', (code) => {
// can be null or number
if (code) {
return reject(new Error(`${command} exited with code ${code}`));
}
resolve();
});
});
}
export type SpawnBackgroundProcessOpts = Omit<SpawnOptions, 'stdio'>;
+2 -2
View File
@@ -40,7 +40,7 @@ import {
whichPython,
readMkDocsYml,
} from './fs';
import logger from './logger';
import {getLogger} from './logger';
import {MkDocsYml, PipPackage, TypeDocJson} from './model';
import {relative} from './util';
@@ -64,7 +64,7 @@ const TYPEDOC_VERSION_REGEX = /TypeDoc\s(\d+\.\d+\..+)/;
*/
const MKDOCS_VERSION_REGEX = /mkdocs,\s+version\s+(\d+\.\d+\.\S+)/;
const log = logger.withTag('validate');
const log = getLogger('validate');
/**
* The "kinds" of validation which were requested to be performed
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/docutils",
"version": "0.3.0",
"version": "0.3.6",
"description": "Documentation generation utilities for Appium and related projects",
"keywords": [
"automation",
@@ -49,9 +49,9 @@
"start": "node ./build/lib/cli/index.js"
},
"dependencies": {
"@appium/support": "^3.1.7",
"@appium/tsconfig": "^0.2.4",
"@appium/typedoc-plugin-appium": "^0.5.1",
"@appium/support": "^3.1.10",
"@appium/tsconfig": "^0.3.0",
"@appium/typedoc-plugin-appium": "^0.6.2",
"@sliphua/lilconfig-ts-loader": "3.2.2",
"chalk": "4.1.2",
"consola": "2.15.3",
@@ -67,8 +67,8 @@
"semver": "7.3.8",
"source-map-support": "0.5.21",
"teen_process": "2.0.2",
"type-fest": "3.6.1",
"typedoc": "0.23.26",
"type-fest": "3.8.0",
"typedoc": "0.23.28",
"typedoc-plugin-markdown": "3.14.0",
"typedoc-plugin-resolve-crossmodule-references": "0.3.3",
"typescript": "4.9.5",
@@ -83,5 +83,5 @@
"publishConfig": {
"access": "public"
},
"gitHead": "84b211330dc84849af9cc1f3a5c5ad32e15b2e72"
"gitHead": "8480a85ce2fa466360e0fb1a7f66628331907f02"
}
+1 -1
View File
@@ -1,4 +1,4 @@
mkdocs==1.4.2
mkdocs-material==9.1.1
mkdocs-material==9.1.6
mkdocs-material-extensions==1.1.1
mike==1.1.2
+39
View File
@@ -3,6 +3,45 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.4.4](https://github.com/appium/appium/compare/@appium/driver-test-support@0.4.3...@appium/driver-test-support@0.4.4) (2023-04-10)
### Bug Fixes
* **support:** update dependency axios to v1.3.5 ([6cf1480](https://github.com/appium/appium/commit/6cf14802b70a462beffc12a1134476596060c005))
* **types:** update dependency type-fest to v3.8.0 ([d6c42e9](https://github.com/appium/appium/commit/d6c42e99c08efce0b34796d5982ce379fca044d3))
## [0.4.3](https://github.com/appium/appium/compare/@appium/driver-test-support@0.4.2...@appium/driver-test-support@0.4.3) (2023-04-03)
### Bug Fixes
* **types:** update dependency type-fest to v3.7.2 ([5580539](https://github.com/appium/appium/commit/55805390b5a0c6aa718bb357b30f66651f3db281))
## [0.4.2](https://github.com/appium/appium/compare/@appium/driver-test-support@0.4.1...@appium/driver-test-support@0.4.2) (2023-03-28)
### Bug Fixes
* **appium,types,base-driver,fake-driver,driver-test-support:** normalize constraint defaults ([3c9fa7b](https://github.com/appium/appium/commit/3c9fa7ba73b639e610e1f3d41d239a9402845b4c))
* **driver-test-support:** update definitelytyped ([86eb495](https://github.com/appium/appium/commit/86eb495641456e3e467f507e4d230ad1bc2901c1))
* **test-support:** update dependency sinon to v15.0.2 ([f1a8ce0](https://github.com/appium/appium/commit/f1a8ce0bbac5a165ba5175c50d83668ec305b592))
* **test-support:** update dependency sinon to v15.0.3 ([d774a84](https://github.com/appium/appium/commit/d774a8428a0f0c4ecbbc8124a855bda12821ae08))
* **types:** update dependency type-fest to v3.7.0 ([6912fa1](https://github.com/appium/appium/commit/6912fa14f2a7d338f17e1bed060e959de7aba1d6))
* **types:** update dependency type-fest to v3.7.1 ([bc860c7](https://github.com/appium/appium/commit/bc860c733a73760f0c42cbfb384e04d50c376d5e))
## [0.4.1](https://github.com/appium/appium/compare/@appium/driver-test-support@0.4.0...@appium/driver-test-support@0.4.1) (2023-03-08)
+12 -7
View File
@@ -425,7 +425,12 @@ export function driverE2ETestSuite(DriverClass, defaultCaps = {}) {
* @typedef {import('@appium/types').BaseDriverCapConstraints} BaseDriverCapConstraints
* @typedef {import('@appium/types').BaseNSCapabilities} BaseNSCapabilities
* @typedef {import('axios').RawAxiosRequestConfig} RawAxiosRequestConfig
* @typedef {import('@appium/types').SingularSessionData} SingularSessionData
*/
/**
* `Constraints` is purposefully loose here
* @template {Constraints} [C=Constraints]
* @typedef {import('@appium/types').SingularSessionData<C>} SingularSessionData
*/
/**
@@ -434,18 +439,18 @@ export function driverE2ETestSuite(DriverClass, defaultCaps = {}) {
*/
/**
* @template {Constraints} [C=BaseDriverCapConstraints]
* @template {StringRecord|void} [Extra=void]
* `Constraints` is purposefully loose here
* @template {Constraints} [C=Constraints]
* @typedef NewSessionData
* @property {import('type-fest').RequireAtLeastOne<import('@appium/types').W3CCapabilities<C, Extra>, 'firstMatch'|'alwaysMatch'>} capabilities
* @property {import('type-fest').RequireAtLeastOne<import('@appium/types').W3CCapabilities<C>, 'firstMatch'|'alwaysMatch'>} capabilities
*/
/**
* @template {Constraints} [C=BaseDriverCapConstraints]
* @template {StringRecord|void} [Extra=void]
* `Constraints` is purposefully loose here
* @template {Constraints} [C=Constraints]
* @typedef NewSessionResponse
* @property {string} sessionId,
* @property {import('@appium/types').Capabilities<C, Extra>} capabilities
* @property {import('@appium/types').Capabilities<C>} capabilities
*/
/**
+2 -3
View File
@@ -11,7 +11,6 @@ export * from './stoppable';
*/
/**
* @template {import('@appium/types').Constraints} [C=import('@appium/types').BaseDriverCapConstraints]
* @template {import('@appium/types').StringRecord|void} [Extra=void]
* @typedef {import('@appium/types').W3CCapabilities<C, Extra>} W3CCapabilities
* @template {import('@appium/types').Constraints} C
* @typedef {import('@appium/types').W3CCapabilities<C>} W3CCapabilities
*/
+14 -11
View File
@@ -12,18 +12,22 @@ const {expect} = chai;
/**
* Creates unit test suites for a driver.
* @param {DriverClass} DriverClass
* @param {Partial<BaseNSCapabilities>} [defaultCaps]
* @template {Constraints} C
* @param {DriverClass<C>} DriverClass
* @param {import('@appium/types').NSDriverCaps<C>} [defaultCaps]
*/
export function driverUnitTestSuite(DriverClass, defaultCaps = {}) {
export function driverUnitTestSuite(
DriverClass,
defaultCaps = /** @type {import('@appium/types').NSDriverCaps<C>} */ ({})
) {
// to display the driver under test in report
const className = DriverClass.name ?? '(unknown driver)';
describe(`BaseDriver unit suite (as ${className})`, function () {
/** @type {InstanceType<typeof DriverClass>} */
let d;
/** @type {W3CCapabilities} */
/** @type {import('@appium/types').W3CDriverCaps<C>} */
let w3cCaps;
/** @type {import('sinon').SinonSandbox} */
let sandbox;
@@ -229,7 +233,7 @@ export function driverUnitTestSuite(DriverClass, defaultCaps = {}) {
});
describe('command queue', function () {
/** @type {InstanceType<DriverClass>} */
/** @type {InstanceType<DriverClass<Constraints>>} */
let d;
let waitMs = 10;
@@ -531,11 +535,11 @@ export function driverUnitTestSuite(DriverClass, defaultCaps = {}) {
d.logEvent('commands');
}).should.throw();
(() => {
// @ts-expect-error
// @ts-expect-error - bad type
d.logEvent(1);
}).should.throw();
(() => {
// @ts-expect-error
// @ts-expect-error - bad type
d.logEvent({});
}).should.throw();
});
@@ -631,7 +635,7 @@ export function driverUnitTestSuite(DriverClass, defaultCaps = {}) {
*/
/**
* @template {Constraints} [C=import('@appium/types').BaseDriverCapConstraints]
* @template {Constraints} C
* @typedef {import('@appium/types').DriverClass<Driver<C>>} DriverClass
*/
@@ -641,7 +645,6 @@ export function driverUnitTestSuite(DriverClass, defaultCaps = {}) {
*/
/**
* @template {Constraints} [C=import('@appium/types').BaseDriverCapConstraints]
* @template {import('@appium/types').StringRecord|void} [Extra=void]
* @typedef {import('@appium/types').W3CCapabilities<C, Extra>} W3CCapabilities
* @template {Constraints} C
* @typedef {import('@appium/types').W3CCapabilities<C>} W3CCapabilities
*/
+10 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/driver-test-support",
"version": "0.4.1",
"version": "0.4.4",
"description": "Test utilities for Appium drivers",
"keywords": [
"automation",
@@ -41,18 +41,18 @@
},
"types": "./build/lib/index.d.ts",
"dependencies": {
"@appium/types": "^0.10.1",
"@types/lodash": "4.14.191",
"@appium/types": "^0.10.4",
"@types/lodash": "4.14.192",
"@types/stoppable": "1.1.1",
"axios": "1.3.4",
"axios": "1.3.5",
"bluebird": "3.7.2",
"chai": "4.3.7",
"get-port": "5.1.1",
"lodash": "4.17.21",
"sinon": "15.0.1",
"sinon": "15.0.3",
"source-map-support": "0.5.21",
"stoppable": "1.1.0",
"type-fest": "3.6.1"
"type-fest": "3.8.0"
},
"peerDependencies": {
"appium": "^2.0.0-beta.43",
@@ -65,5 +65,8 @@
"publishConfig": {
"access": "public"
},
"gitHead": "84b211330dc84849af9cc1f3a5c5ad32e15b2e72"
"typedoc": {
"entryPoint": "./lib/index.js"
},
"gitHead": "8480a85ce2fa466360e0fb1a7f66628331907f02"
}
@@ -3,6 +3,37 @@
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/eslint-config-appium-ts@0.3.0...@appium/eslint-config-appium-ts@0.3.1) (2023-04-03)
**Note:** Version bump only for package @appium/eslint-config-appium-ts
# [0.3.0](https://github.com/appium/appium/compare/@appium/eslint-config-appium-ts@0.2.0...@appium/eslint-config-appium-ts@0.3.0) (2023-03-28)
### Bug Fixes
* **eslint-config-appium-ts:** disable brace-style rule ([409192d](https://github.com/appium/appium/commit/409192d0630244e072534391eb702de063d54d17))
* feat(eslint-config-appium-ts)!: enable for JS sources ([43df77f](https://github.com/appium/appium/commit/43df77f6b623462d20d90cb23a2d7577aa0c56de))
### BREAKING CHANGES
* This makes `@appium/eslint-config-appium-ts` also apply to JS sources _and_ requires a new peer dependency (`eslint-import-resolver-typescript`). The `main` file is now a `.js` file. The module is expected to _extend_ `@appium/eslint-config-appium`, not replace it; consumers needn't extend both in their ESLint config.
In the not-too-distant past, `@typescript-eslint/parser` was unable to understand docstrings in `.js` files. Evidently this is no longer true.
I've rewritten and heavily commented the configuration explaining the intent behind everything.
# 0.2.0 (2023-03-08)
+117
View File
@@ -0,0 +1,117 @@
/**
* `@appium/eslint-config-appium-ts` is a configuration for ESLint which extends
* `@appium/eslint-config-appium` and adds TypeScript support.
*
* It is **not** a _replacement for_ `@appium/eslint-config-appium`.
*
* It can be used _without any `.ts` sources_, as long as a `tsconfig.json` exists in the project
* root. In that case, it will run on `.js` files which are enabled for checking; this includes the
* `checkJs` setting and any `// @ts-check` directive in source files.
*/
module.exports = {
$schema: 'http://json.schemastore.org/eslintrc',
parser: '@typescript-eslint/parser',
extends: ['@appium/eslint-config-appium', 'plugin:@typescript-eslint/recommended'],
rules: {
/**
* This rule is configured to warn if a `@ts-ignore` or `@ts-expect-error` directive is used
* without explanation.
* @remarks It's good practice to explain why things break!
*/
'@typescript-eslint/ban-ts-comment': [
'warn',
{
'ts-expect-error': 'allow-with-description',
'ts-ignore': 'allow-with-description',
},
],
/**
* Empty functions are allowed.
* @remarks This is disabled because I need someone to explain to me why empty functions are bad. I suppose they _could_ be bugs, but so could literally any line of code.
*/
'@typescript-eslint/no-empty-function': 'off',
/**
* Empty interfaces are allowed.
* @remarks This is because empty interfaces have a use case in declaration merging. Otherwise,
* an empty interface can be a type alias, e.g., `type Foo = Bar` where `Bar` is an interface.
*/
'@typescript-eslint/no-empty-interface': 'off',
/**
* Explicit `any` types are allowed.
* @remarks Eventually this should be a warning, and finally an error, as we fully type the codebases.
*/
'@typescript-eslint/no-explicit-any': 'off',
/**
* Warns if a non-null assertion (`!`) is used.
* @remarks Generally, a non-null assertion should be replaced by a proper type guard or
* type-safe function, if possible. For example, `Set.prototype.has(x)` is not type-safe, and
* does not imply that `Set.prototype.get(x)` is not `undefined` (I do not know why this is, but
* I'm sure there's a good reason for it). In this case, a non-null assertion is appropriate.
* Often a simple `typeof x === 'y'` conditional is sufficient to narrow the type and avoid the
* non-null assertion.
*/
'@typescript-eslint/no-non-null-assertion': 'warn',
/**
* This disallows use of `require()`.
* @remarks We _do_ use `require()` fairly often to load files on-the-fly; however, these may
* want to be replaced with `import()` (I am not sure if there's a rule about that?). **If this check fails**, disable the rule for the particular line.
*/
'@typescript-eslint/no-var-requires': 'error',
// 'no-unused-vars': 'off',
/**
* Allow native `Promise`s. **This overrides `@appium/eslint-config-appium`.**
* @remarks Originally, this was so that we could use [bluebird](https://npm.im/bluebird)
* everywhere, but this is not strictly necessary.
*/
'promise/no-native': 'off',
/**
* Allow `async` functions without `await`. **This overrides `@appium/eslint-config-appium`.**
* @remarks Originally, this was to be more clear about the return value of a function, but with
* the addition of types, this is no longer necessary. Further, both `return somePromise` and
* `return await somePromise` have their own use-cases.
*/
'require-await': 'off',
/**
* Disables the `brace-style` rule.
* @remarks Due to the way `prettier` sometimes formats extremely verbose types, sometimes it is necessary
* to indent in a way that is not allowed by the default `brace-style` rule.
*/
'brace-style': 'off',
},
/**
* This stuff enables `eslint-plugin-import` to resolve TS modules.
*/
settings: {
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
typescript: {
project: ['tsconfig.json', './packages/*/tsconfig.json'],
},
},
},
overrides: [
/**
* Overrides for tests.
*/
{
files: ['**/test/**', '*.spec.js', '-specs.js', '*.spec.ts'],
rules: {
/**
* Both `@ts-expect-error` and `@ts-ignore` are allowed to be used with impunity in tests.
* @remarks We often test things which explicitly violate types.
*/
'@typescript-eslint/ban-ts-comment': 'off',
/**
* Allow non-null assertions in tests; do not even warn.
* @remarks The idea is that the assertions themselves will be written in such a way that if
* the non-null assertion was invalid, the assertion would fail.
*/
'@typescript-eslint/no-non-null-assertion': 'off',
},
},
],
};

Some files were not shown because too many files have changed in this diff Show More