mirror of
https://github.com/appium/appium.git
synced 2026-01-06 02:09:59 -06:00
chore: Merge recent changes from master (#21111)
This commit is contained in:
5
.github/labeler.yml
vendored
5
.github/labeler.yml
vendored
@@ -53,6 +53,11 @@ labels:
|
||||
matcher:
|
||||
files: ['packages/base-plugin/**']
|
||||
|
||||
- label: '@appium/storage-plugin'
|
||||
sync: true
|
||||
matcher:
|
||||
files: ['packages/storage-plugin/**']
|
||||
|
||||
- label: '@appium/docutils'
|
||||
sync: true
|
||||
matcher:
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_DOCS_PROJECT_ID }}
|
||||
CROWDIN_TOKEN: ${{ secrets.CROWDIN_DOCS_TOKEN }}
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7.0.7
|
||||
uses: peter-evans/create-pull-request@v7.0.8
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
commit-message: 'docs(appium): Update documentation translations'
|
||||
|
||||
@@ -30,6 +30,7 @@ to driver/client documentation
|
||||
* [opencv](packages/opencv/CHANGELOG.md)
|
||||
* [plugin-test-support](packages/plugin-test-support/CHANGELOG.md)
|
||||
* [schema](packages/schema/CHANGELOG.md)
|
||||
* [storage-plugin](packages/storage-plugin/CHANGELOG.md)
|
||||
* [strongbox](packages/strongbox/CHANGELOG.md)
|
||||
* [support](packages/support/CHANGELOG.md)
|
||||
* [test-support](packages/test-support/CHANGELOG.md)
|
||||
|
||||
4658
package-lock.json
generated
4658
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
@@ -85,15 +85,15 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@colors/colors": "1.6.0",
|
||||
"@eslint/js": "9.21.0",
|
||||
"@tsconfig/node14": "14.1.2",
|
||||
"@eslint/js": "9.22.0",
|
||||
"@tsconfig/node14": "14.1.3",
|
||||
"@types/argparse": "2.0.17",
|
||||
"@types/archiver": "6.0.3",
|
||||
"@types/async-lock": "1.4.2",
|
||||
"@types/base64-stream": "1.0.5",
|
||||
"@types/bluebird": "3.5.42",
|
||||
"@types/chai": "5.0.1",
|
||||
"@types/chai-as-promised": "8.0.1",
|
||||
"@types/chai": "5.2.0",
|
||||
"@types/chai-as-promised": "8.0.2",
|
||||
"@types/diff": "7.0.1",
|
||||
"@types/express": "5.0.0",
|
||||
"@types/jsftp": "2.1.5",
|
||||
@@ -105,7 +105,7 @@
|
||||
"@types/mocha": "10.0.10",
|
||||
"@types/mv": "2.1.4",
|
||||
"@types/ncp": "2.0.8",
|
||||
"@types/node": "22.13.9",
|
||||
"@types/node": "22.13.10",
|
||||
"@types/pluralize": "0.0.33",
|
||||
"@types/semver": "7.5.8",
|
||||
"@types/serve-favicon": "2.5.7",
|
||||
@@ -118,7 +118,7 @@
|
||||
"@types/uuid": "10.0.0",
|
||||
"@types/which": "3.0.4",
|
||||
"@types/wrap-ansi": "3.0.0",
|
||||
"@types/ws": "8.5.14",
|
||||
"@types/ws": "8.18.0",
|
||||
"@types/xmldom": "0.1.34",
|
||||
"@types/yargs": "17.0.33",
|
||||
"asyncbox": "3.0.0",
|
||||
@@ -127,13 +127,13 @@
|
||||
"conventional-changelog-conventionalcommits": "7.0.2",
|
||||
"cpy-cli": "5.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "9.21.0",
|
||||
"eslint-config-prettier": "10.0.2",
|
||||
"eslint-import-resolver-typescript": "3.8.3",
|
||||
"eslint": "9.22.0",
|
||||
"eslint-config-prettier": "10.1.1",
|
||||
"eslint-import-resolver-typescript": "3.8.7",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-mocha": "10.5.0",
|
||||
"eslint-plugin-promise": "7.2.1",
|
||||
"finalhandler": "1.3.1",
|
||||
"finalhandler": "2.1.0",
|
||||
"get-port": "5.1.1",
|
||||
"json-schema-to-typescript": "15.0.4",
|
||||
"lerna": "8.2.1",
|
||||
@@ -151,10 +151,10 @@
|
||||
"sync-monorepo-packages": "1.0.2",
|
||||
"ts-node": "10.9.2",
|
||||
"tsd": "0.31.2",
|
||||
"typescript": "5.7.3",
|
||||
"typescript-eslint": "8.25.0",
|
||||
"typescript": "5.8.2",
|
||||
"typescript-eslint": "8.26.1",
|
||||
"validate.js": "0.13.1",
|
||||
"webdriverio": "9.10.1",
|
||||
"webdriverio": "9.12.0",
|
||||
"ws": "8.18.1",
|
||||
"yaml-js": "0.3.1"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.17.0](https://github.com/appium/appium/compare/appium@2.16.2...appium@2.17.0) (2025-03-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **images-plugin:** supports image elements included in actions. ([#21055](https://github.com/appium/appium/issues/21055)) ([0c50504](https://github.com/appium/appium/commit/0c50504266eb5553dafd08bfb6161f643357114f))
|
||||
|
||||
|
||||
|
||||
## [2.16.2](https://github.com/appium/appium/compare/appium@2.16.1...appium@2.16.2) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package appium
|
||||
|
||||
@@ -65,3 +65,25 @@ Find a a list of all UI elements matching a given a locator strategy and a selec
|
||||
`any`
|
||||
|
||||
A possibly-empty list of element objects
|
||||
|
||||
### `performActions`
|
||||
|
||||
`POST` **`/session/:sessionId/actions`**
|
||||
|
||||
If the actions contains image elements as origin, convert them to viewport coordinates before sending it to the external driver
|
||||
|
||||
**`See`**
|
||||
|
||||
[https://w3c.github.io/webdriver/#perform-actions](https://w3c.github.io/webdriver/#perform-actions)
|
||||
|
||||
<!-- comment source: method-signature -->
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| :------ | :------ | :------ |
|
||||
| `actions` | `ActionSequence[]` | an array of action sequences |
|
||||
|
||||
#### Response
|
||||
|
||||
`null`
|
||||
|
||||
@@ -21,4 +21,5 @@ The command listings can be found here:
|
||||
* [Execute Driver Plugin](./execute-driver-plugin.md)
|
||||
* [Images Plugin](./images-plugin.md)
|
||||
* [Relaxed Caps Plugin](./relaxed-caps-plugin.md)
|
||||
* [Storage Plugin](./storage-plugin.md)
|
||||
* [Universal XML Plugin](./universal-xml-plugin.md)
|
||||
|
||||
113
packages/appium/docs/en/commands/storage-plugin.md
Normal file
113
packages/appium/docs/en/commands/storage-plugin.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# Plugin: storage
|
||||
|
||||
!!! tip
|
||||
|
||||
All these commands can be invoked without creating a session, allowing you to
|
||||
prepare your test environment in advance.
|
||||
|
||||
### `addStorageItem`
|
||||
|
||||
`POST` **`/storage/add`**
|
||||
|
||||
Add a new file to the storage
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| :------ | :------ | :------ |
|
||||
| `name` | `string` | the name used to save the file (must not include path separator characters) |
|
||||
| `sha1` | `string` | SHA1 hash of the file to be uploaded |
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -X POST --header "Content-Type: application/json" --data '{"name":"app.ipa","sha1":"ccc963411b2621335657963322890305ebe96186"}' http://127.0.0.1:4723/storage/add
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
`AddRequestResult`
|
||||
|
||||
A JSON object in the following format:
|
||||
```json
|
||||
{
|
||||
"ws": {
|
||||
"stream": "/storage/add/ccc963411b2621335657963322890305ebe96186/stream",
|
||||
"events": "/storage/add/ccc963411b2621335657963322890305ebe96186/events"
|
||||
},
|
||||
"ttlMs": 300000
|
||||
}
|
||||
```
|
||||
|
||||
| Name | Type | Description |
|
||||
| :------ | :------ | :------ |
|
||||
| `ws.stream` | `string` | the pathname of the streaming web socket used to upload the file content |
|
||||
| `ws.events` | `string` | the pathname of the events web socket used to notify about upload success or a failure |
|
||||
| `ttlMs` | `number` | the amount of milliseconds both web sockets will be kept active before they expire, or a file payload would be successfully uploaded |
|
||||
|
||||
|
||||
### `listStorageItems`
|
||||
|
||||
`GET` **`/storage/list`**
|
||||
|
||||
List all files present in the storage
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:4723/storage/list
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
`List<StorageItem>`
|
||||
|
||||
A list of items, where each item has the following properties:
|
||||
|
||||
| Name | Type | Description |
|
||||
| :------ | :------ | :------ |
|
||||
| `name` | `string` | the name of the file in the storage |
|
||||
| `path` | `string` | full path to the file on the remote file system |
|
||||
| `size` | `number` | file size in bytes |
|
||||
|
||||
### `deleteStorageItem`
|
||||
|
||||
`POST` **`/storage/delete`**
|
||||
|
||||
Deletes a file in the storage with the specified name
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| :------ | :------ | :------ |
|
||||
| `name` | `string` | the name of the file to be deleted |
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -X POST --header "Content-Type: application/json" --data '{"name":"app.ipa"}' http://127.0.0.1:4723/storage/delete
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
`boolean`
|
||||
|
||||
`false` if the file does not exist in the storage, or `true` upon successful file deletion
|
||||
|
||||
### `resetStorage`
|
||||
|
||||
`POST` **`/storage/reset`**
|
||||
|
||||
Deletes all uploaded files and stops any incomplete uploads.
|
||||
If the `APPIUM_STORAGE_KEEP_ALL` flag is enabled, all uploaded files will be preserved,
|
||||
and only the incomplete uploads will be stopped.
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:4723/storage/reset
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
`undefined`
|
||||
@@ -23,6 +23,7 @@ These plugins are are currently maintained by the Appium team:
|
||||
|[Execute Driver](https://github.com/appium/appium/tree/master/packages/execute-driver-plugin)|`execute-driver`|Run entire batches of commands in a single call to the Appium server|
|
||||
|[Images](https://github.com/appium/appium/tree/master/packages/images-plugin)|`images`|Image matching and comparison features|
|
||||
|[Relaxed Caps](https://github.com/appium/appium/tree/master/packages/relaxed-caps-plugin)|`relaxed-caps`|Relax Appium's requirement for vendor prefixes on capabilities|
|
||||
|[Storage](https://github.com/appium/appium/tree/master/packages/storage-plugin)|`storage`|Server-side storage with client-side management|
|
||||
|[Universal XML](https://github.com/appium/appium/tree/master/packages/universal-xml-plugin)|`universal-xml`|Instead of the standard XML format for iOS and Android, use an XML definition that is the same across both platforms|
|
||||
|
||||
### Other Plugins
|
||||
|
||||
@@ -73,9 +73,9 @@ Appium supports [WebDriver BiDi](https://w3c.github.io/webdriver-bidi/) protocol
|
||||
The actual behavior depends on individual drivers while the Appium and the baseーdriver support the protocol.
|
||||
Please make sure if a driver supports the protocol and what kind of commands/events it supports in the documentation.
|
||||
|
||||
| Capability Name | Type | Description |
|
||||
|---|---|−--|
|
||||
| `webSocketUrl` | `boolean` | To enable BiDi protocol in the session. |
|
||||
| Capability Name | Type | Description |
|
||||
|-----------------|-----------|-----------------------------------------|
|
||||
| `webSocketUrl` | `boolean` | To enable BiDi protocol in the session. |
|
||||
|
||||
## Using `appium:options` to Group Capabilities
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ nav:
|
||||
- commands/execute-driver-plugin.md
|
||||
- commands/images-plugin.md
|
||||
- commands/relaxed-caps-plugin.md
|
||||
- commands/storage-plugin.md
|
||||
- commands/universal-xml-plugin.md
|
||||
- Guides:
|
||||
- Migration:
|
||||
|
||||
@@ -35,6 +35,7 @@ export const KNOWN_PLUGINS = Object.freeze(
|
||||
images: '@appium/images-plugin',
|
||||
'execute-driver': '@appium/execute-driver-plugin',
|
||||
'relaxed-caps': '@appium/relaxed-caps-plugin',
|
||||
storage: '@appium/storage-plugin',
|
||||
'universal-xml': '@appium/universal-xml-plugin',
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -62,18 +62,18 @@
|
||||
"dependencies": {
|
||||
"@appium/base-driver": "^10.0.0-beta.0",
|
||||
"@appium/base-plugin": "^3.0.0-beta.0",
|
||||
"@appium/docutils": "^1.0.32",
|
||||
"@appium/docutils": "^1.0.33",
|
||||
"@appium/logger": "^1.6.1",
|
||||
"@appium/schema": "^0.8.1",
|
||||
"@appium/support": "^6.0.6",
|
||||
"@appium/types": "^0.25.1",
|
||||
"@appium/support": "^6.0.7",
|
||||
"@appium/types": "^0.25.2",
|
||||
"@sidvind/better-ajv-errors": "3.0.1",
|
||||
"ajv": "8.17.1",
|
||||
"ajv-formats": "3.0.1",
|
||||
"argparse": "2.0.1",
|
||||
"async-lock": "1.4.1",
|
||||
"asyncbox": "3.0.0",
|
||||
"axios": "1.8.1",
|
||||
"axios": "1.8.3",
|
||||
"bluebird": "3.7.2",
|
||||
"lilconfig": "3.1.3",
|
||||
"lodash": "4.17.21",
|
||||
@@ -84,7 +84,7 @@
|
||||
"semver": "7.7.1",
|
||||
"source-map-support": "0.5.21",
|
||||
"teen_process": "2.3.1",
|
||||
"type-fest": "4.36.0",
|
||||
"type-fest": "4.37.0",
|
||||
"winston": "3.17.0",
|
||||
"wrap-ansi": "7.0.0",
|
||||
"ws": "8.18.1",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"webdriverio": "9.10.1"
|
||||
"webdriverio": "9.12.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ describe('FakeDriver via HTTP', function () {
|
||||
should.exist(driver.sessionId);
|
||||
driver.sessionId.should.be.a('string');
|
||||
await driver.deleteSession();
|
||||
await driver.getTitle().should.eventually.be.rejectedWith(/terminated/);
|
||||
await driver.getTitle().should.eventually.be.rejected;
|
||||
});
|
||||
|
||||
it('should be able to run two FakeDriver sessions simultaneously', async function () {
|
||||
|
||||
@@ -3,6 +3,17 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [9.16.3](https://github.com/appium/appium/compare/@appium/base-driver@9.16.2...@appium/base-driver@9.16.3) (2025-03-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **base-driver:** Fix proxy url generation ([#21099](https://github.com/appium/appium/issues/21099)) ([e68757b](https://github.com/appium/appium/commit/e68757b3493a5b0f961f7136c0ae6e857d806f09))
|
||||
* **base-driver:** Tune capabilities array parsing ([#21044](https://github.com/appium/appium/issues/21044)) ([594bc04](https://github.com/appium/appium/commit/594bc04c03fb073cd7ad31d7e23f77fb8041b92e))
|
||||
* **base-driver:** Update parseCapsArray function types ([#21045](https://github.com/appium/appium/issues/21045)) ([5541142](https://github.com/appium/appium/commit/554114203fbe26f303337f049f942e046c815074))
|
||||
|
||||
|
||||
|
||||
## [9.16.2](https://github.com/appium/appium/compare/@appium/base-driver@9.16.1...@appium/base-driver@9.16.2) (2025-02-20)
|
||||
|
||||
|
||||
|
||||
@@ -10,14 +10,6 @@ directly as it does nothing on its own. Instead, you should extend this driver w
|
||||
*own* Appium drivers. Check out the [Building Drivers](https://appium.io/docs/en/latest/developing/build-drivers/)
|
||||
documentation for more details.
|
||||
|
||||
Each included utility is documented in its own README:
|
||||
|
||||
* [BaseDriver](lib/basedriver)
|
||||
* [The Appium Express Server](lib/express)
|
||||
* [The Mobile JSON Wire Protocol Encapsulation](lib/mjsonwp)
|
||||
* [The JSONWP Proxy Library](lib/jsonwp-proxy)
|
||||
* [The JSONWP Status Library](lib/jsonwp-status)
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
appium-base-driver
|
||||
===================
|
||||
This is the parent class that all [appium](appium.io) drivers inherit from. Appium drivers themselves can either be started from the command line as standalone appium servers, or can be included by another module (appium) which then proxies commands to the appropriate driver based on [Desired Capabilities](https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/caps.md).
|
||||
|
||||
An appium driver is a module which processes [Mobile Json Wire Protocol](https://code.google.com/p/selenium/source/browse/spec-draft.md?repo=mobile) commands and controls a device accordingly. The commands can either come in over HTTP as json api requests, or they can be passed to the driver object programmatically as already-parsed json object (without the HTTP headers and junk).
|
||||
|
||||
The appium Base driver already includes the [mjsonwp](https://github.com/appium/appium-base-driver/blob/master/lib/mjsonwp/README.md) module, which is the HTTP server that converts incoming requests into json objects that get sent to the driver programmatically.
|
||||
|
||||
The appium Base driver already has all the REST api routes, validation, and error codes supplied by [mjsonwp](https://github.com/appium/appium-base-driver/blob/master/lib/mjsonwp/README.md).
|
||||
|
||||
Appium drivers are designed to have a *single testing session* per instantiation. This means that one Driver object should be attached to a single device and handle commands from a single client. The main appium driver handles multiple sessions and instantiates a new instance of the desired driver for each new session.
|
||||
|
||||
## Writing your own appium driver
|
||||
|
||||
Writing your own appium driver starts with inheriting and extending this Base driver module.
|
||||
|
||||
Appium Base driver has some properties that all drivers share:
|
||||
|
||||
- `driver.opts` - these are the options passed into the driver constructor. Your driver's constructor should take an object of options and pass it on the the Base driver by calling `super(opts)` in your constructor.
|
||||
|
||||
- `driver.desiredCapConstraints` - Base driver sets this property with a customer `setter` function so that when you create a driver, you can add an object which defines the validation contraints of which desired capabilities your new driver can handle. Of course each driver will have it's own specific desired capabilities. Look for examples on our other drivers.
|
||||
|
||||
- `driver.createSession(caps)` - this is the function which gets desired capabilities and creates a session. Make sure to call `super.createSession(caps)` so that things like `this.sessionId` and `this.caps` are populated, and the caps are validated against your `desiredCapConstraints`.
|
||||
|
||||
- `driver.caps` - these are the desired capabilities for the current session.
|
||||
|
||||
- `driver.sessionId` - this is the ID of the current session. It gets populated automaticall by `baseDriver.createSession`.
|
||||
|
||||
- `driver.proxyReqRes()` - used by mjsonwp module for proxying http commands to another process (like chromedriver or selendroid)
|
||||
|
||||
- `driver.jwpProxyAvoid` - used by mjsonwp module. You can specify what REST api routes which you want to SKIP the automatic proxy to another server (which is optional) and instead be handled by your driver.
|
||||
|
||||
|
||||
Base driver exposes an event called `onUnexpectedShutdown` which is called when the driver is shut down unexpectedly (usually after invocation of the `startUnexpectedShutdown` method).
|
||||
|
||||
Your driver should also implement a startUnexpectedShutdown method?
|
||||
@@ -1,59 +0,0 @@
|
||||
## appium-express
|
||||
|
||||
[Express](http://expressjs.com/) server tuned for to serve [Appium](http://appium.io/).
|
||||
|
||||
|
||||
### Configuration
|
||||
|
||||
The `appium-express` server comes configured with:
|
||||
|
||||
1. appropriate logging formats
|
||||
2. service of necessary static assets
|
||||
3. allowance of cross-domain requests
|
||||
4. default error handling
|
||||
5. fix for invalid content types sent by certain clients
|
||||
|
||||
To configure routes, a function that takes an Express server is passed into the
|
||||
server. This function can add whatever routes are wanted.
|
||||
|
||||
|
||||
### Usage
|
||||
|
||||
```js
|
||||
import { server } from 'appium-base-driver';
|
||||
|
||||
|
||||
// configure the routes
|
||||
function configureRoutes (app) {
|
||||
app.get('/hello', (req, res) => {
|
||||
res.header['content-type'] = 'text/html';
|
||||
res.status(200).send('Hello');
|
||||
});
|
||||
app.get('/world', (req, res) => {
|
||||
res.header['content-type'] = 'text/html';
|
||||
res.status(200).send('World');
|
||||
});
|
||||
}
|
||||
|
||||
const port = 5000;
|
||||
const host = 'localhost';
|
||||
|
||||
const appiumServer = await server({
|
||||
routeConfiguringFunction,
|
||||
port,
|
||||
host,
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
## Watch
|
||||
|
||||
```
|
||||
npm run watch
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```
|
||||
npm test
|
||||
```
|
||||
@@ -1,52 +0,0 @@
|
||||
## appium-jsonwp-proxy
|
||||
|
||||
Proxy middleware for the Selenium [JSON Wire Protocol](https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md). Allows
|
||||
|
||||
|
||||
### Usage
|
||||
|
||||
The proxy is used by instantiating with the details of the Selenium server to which to proxy. The options for the constructor are passed as a hash with the following possible members:
|
||||
|
||||
- `scheme` - defaults to 'http'
|
||||
- `server` - defaults to 'localhost'
|
||||
- `port` - defaults to `4444`
|
||||
- `base` - defaults to ''
|
||||
- `sessionId` - the session id of the session on the remote server
|
||||
- `reqBasePath` - the base path of the server which the request was originally sent to (defaults to '')
|
||||
|
||||
Once the proxy is created, there are two `async` methods:
|
||||
|
||||
`command (url, method, body)`
|
||||
|
||||
Sends a "command" to the proxied server, using the "url", which is the endpoing, with the HTTP method and optional body.
|
||||
|
||||
```js
|
||||
import { JWProxy } from 'appium-base-driver';
|
||||
|
||||
let host = 'my.host.com';
|
||||
let port = 4445;
|
||||
|
||||
let proxy = new JWProxy({server: host, port: port});
|
||||
|
||||
// get the Selenium server status
|
||||
let seStatus = await proxy.command('/status', 'GET');
|
||||
```
|
||||
|
||||
`proxyReqRes (req, res)`
|
||||
|
||||
Proxies a request and response to the proxied server. Used to handle the entire conversation of a request/response cycle.
|
||||
|
||||
```js
|
||||
import { JWProxy } from 'appium-base-driver';
|
||||
import http from 'http';
|
||||
|
||||
let host = 'my.host.com';
|
||||
let port = 4445;
|
||||
|
||||
let proxy = new JWProxy({server: host, port: port});
|
||||
|
||||
|
||||
http.createServer(function (req, res) {
|
||||
await proxy.proxyReqRes(res, res);
|
||||
}).listen(9615);
|
||||
```
|
||||
@@ -503,12 +503,11 @@ export class JWProxy {
|
||||
pathname = pathname.replace('/wd/hub', '');
|
||||
}
|
||||
}
|
||||
return _.trimEnd(
|
||||
match && _.isArray(match.params?.command)
|
||||
? `/${match.params.command.join('/')}`
|
||||
: pathname,
|
||||
'/'
|
||||
);
|
||||
let result = pathname;
|
||||
if (match) {
|
||||
result = _.isArray(match.params?.command) ? `/${match.params.command.join('/')}` : '';
|
||||
}
|
||||
return _.trimEnd(result, '/');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
## jsonwp-status
|
||||
|
||||
Library of status codes for the Selenium [JSON Wire Protocol](https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md).
|
||||
|
||||
|
||||
### Usage
|
||||
|
||||
```
|
||||
import { statusCodes } from 'appium-base-driver';
|
||||
|
||||
statusCodes.NoSuchContext;
|
||||
// -> {code: 35, summary: 'No such context found'}
|
||||
```
|
||||
|
||||
```
|
||||
import { getSummaryByCode } from 'appium-base-driver';
|
||||
|
||||
getSummaryByCode(0);
|
||||
// -> 'The command executed successfully.'
|
||||
```
|
||||
@@ -1,100 +0,0 @@
|
||||
## webdriver and mobile-json-wire protocols
|
||||
|
||||
An abstraction of the Mobile JSON Wire Protocol ([spec](https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md)) and the W3C Wire Protocol ([spec](https://www.w3.org/TR/webdriver/) with Appium extensions (as specified [here](http://www.w3.org/TR/webdriver/#protocol-extensions)).
|
||||
|
||||
### Protocol Detection
|
||||
|
||||
In the event that a session is requested, and both MJSONWP _and_ W3C capabilities are provided, like this
|
||||
|
||||
```
|
||||
{
|
||||
"capabilities: {
|
||||
"alwaysMatch": {...},
|
||||
"firstMatch": [{...}, ...]
|
||||
},
|
||||
"desiredCapabilities": {...}
|
||||
}
|
||||
```
|
||||
|
||||
a W3C session will be served _unless_ the W3C Capabilities are incomplete. So if the `"desiredCapabilities"` object has more keys
|
||||
then whatever the capabilities were matched for the W3C capabilities, then an MJSONWP session will be served instead
|
||||
|
||||
|
||||
### Endpoints in the protocol
|
||||
|
||||
The Mobile JSON Wire Protocol package gives access to a number of endpoints documented [here](https://github.com/appium/appium-base-driver/blob/master/docs/mjsonwp/protocol-methods.md).
|
||||
|
||||
The W3C WebDriver Protocol package gives access to a number of endpoints documented in the [official documentation](https://www.w3.org/TR/webdriver/) and the
|
||||
[simplified spec](https://github.com/jlipps/simple-wd-spec)
|
||||
|
||||
### Protocol
|
||||
|
||||
The basic class, subclassed by drivers that will use the protocol.
|
||||
|
||||
|
||||
### routeConfiguringFunction (driver)
|
||||
|
||||
This function gives drivers access to the protocol routes. It returns a function that itself will take an [Express](http://expressjs.com/) application.
|
||||
|
||||
|
||||
### isSessionCommand (command)
|
||||
|
||||
Checks if the `command` needs to have a session associated with it.
|
||||
|
||||
|
||||
### ALL_COMMANDS
|
||||
|
||||
An array of all the commands that will be dispatched to by the Mobile JSON Wire Proxy endpoints.
|
||||
|
||||
|
||||
### NO_SESSION_ID_COMMANDS
|
||||
|
||||
An array of commands that do not need a session associated with them.
|
||||
|
||||
|
||||
### Errors
|
||||
|
||||
This package exports a number of classes and methods related to Selenium error handling. There are error classes for each Selenium error type (see [JSONWP Errors](https://code.google.com/p/selenium/wiki/JsonWireProtocol#Response_Status_Codes), as well as the context errors in the [mobile spec](https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md#webviews-and-other-contexts).
|
||||
|
||||
The list of errors, and their meanings, can be found [here for JSONWP](https://github.com/appium/appium-base-driver/blob/master/docs/mjsonwp/errors.md) and
|
||||
[here for W3C Errors](https://www.w3.org/TR/webdriver/#handling-errors))
|
||||
|
||||
There are, in addition, two helper methods for dealing with errors
|
||||
|
||||
`isErrorType (err, type)`
|
||||
|
||||
- checks if the `err` object is a Mobile JSON Wire Protocol error of a particular type
|
||||
- arguments
|
||||
- `err` - the error object to test
|
||||
- `type` - the error class to test against
|
||||
- usage
|
||||
```js
|
||||
import { errors, isErrorType } from 'mobile-json-wire-protocol';
|
||||
|
||||
try {
|
||||
// do some stuff...
|
||||
} catch (err) {
|
||||
if (isErrorType(err, errors.InvalidCookieDomainError)) {
|
||||
// process...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`errorFromCode (code, message)`
|
||||
|
||||
- retrieve the appropriate error for an error code, with the supplied message.
|
||||
- arguments
|
||||
- `code` - the integer error code for a Mobile JSON Wire Protocol error
|
||||
- `message` - the message to be encapsulated in the error
|
||||
- usage
|
||||
```js
|
||||
import { errors, errorFromCode } from 'mobile-json-wire-protocol';
|
||||
|
||||
let error = errorFromCode(6, 'an error has occurred');
|
||||
|
||||
console.log(error instanceof errors.NoSuchDriverError);
|
||||
// => true
|
||||
|
||||
console.log(error.message === 'an error has occurred');
|
||||
// => true
|
||||
```
|
||||
@@ -44,12 +44,12 @@
|
||||
"test:types": "tsd"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/support": "^6.0.6",
|
||||
"@appium/types": "^0.25.1",
|
||||
"@appium/support": "^6.0.7",
|
||||
"@appium/types": "^0.25.2",
|
||||
"@colors/colors": "1.6.0",
|
||||
"async-lock": "1.4.1",
|
||||
"asyncbox": "3.0.0",
|
||||
"axios": "1.8.1",
|
||||
"axios": "1.8.3",
|
||||
"bluebird": "3.7.2",
|
||||
"body-parser": "1.20.3",
|
||||
"express": "4.21.2",
|
||||
@@ -62,7 +62,7 @@
|
||||
"path-to-regexp": "8.2.0",
|
||||
"serve-favicon": "2.5.0",
|
||||
"source-map-support": "0.5.21",
|
||||
"type-fest": "4.36.0",
|
||||
"type-fest": "4.37.0",
|
||||
"validate.js": "0.13.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
||||
@@ -110,6 +110,11 @@ describe('proxy', function () {
|
||||
`http://${TEST_HOST}:${port}/session/123/element/200/value`
|
||||
);
|
||||
|
||||
mockProxy({sessionId: '123', reqBasePath: '/wd/hub'})
|
||||
.getUrlForProxy('/wd/hub/session/456', 'GET').should.eql(
|
||||
`http://${TEST_HOST}:${port}/session/123`
|
||||
);
|
||||
|
||||
mockProxy({reqBasePath: '/my/base/path'})
|
||||
.getUrlForProxy('/my/base/path/session', 'POST').should.eql(
|
||||
`http://${TEST_HOST}:${port}/session`
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.3.4](https://github.com/appium/appium/compare/@appium/base-plugin@2.3.3...@appium/base-plugin@2.3.4) (2025-03-11)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.3](https://github.com/appium/appium/compare/@appium/base-plugin@2.3.2...@appium/base-plugin@2.3.3) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-plugin
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/base-driver": "^10.0.0-beta.0",
|
||||
"@appium/support": "^6.0.6"
|
||||
"@appium/support": "^6.0.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.33](https://github.com/appium/appium/compare/@appium/docutils@1.0.32...@appium/docutils@1.0.33) (2025-03-11)
|
||||
|
||||
**Note:** Version bump only for package @appium/docutils
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.32](https://github.com/appium/appium/compare/@appium/docutils@1.0.31...@appium/docutils@1.0.32) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/docutils
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/docutils",
|
||||
"version": "1.0.32",
|
||||
"version": "1.0.33",
|
||||
"description": "Documentation generation utilities for Appium and related projects",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -48,8 +48,8 @@
|
||||
"start": "node ./build/lib/cli/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/support": "^6.0.6",
|
||||
"@appium/tsconfig": "^0.3.4",
|
||||
"@appium/support": "^6.0.7",
|
||||
"@appium/tsconfig": "^0.3.5",
|
||||
"@sliphua/lilconfig-ts-loader": "3.2.2",
|
||||
"chalk": "4.1.2",
|
||||
"consola": "3.4.0",
|
||||
@@ -62,8 +62,8 @@
|
||||
"semver": "7.7.1",
|
||||
"source-map-support": "0.5.21",
|
||||
"teen_process": "2.3.1",
|
||||
"type-fest": "4.36.0",
|
||||
"typescript": "5.7.3",
|
||||
"type-fest": "4.37.0",
|
||||
"typescript": "5.8.2",
|
||||
"yaml": "2.7.0",
|
||||
"yargs": "17.7.2",
|
||||
"yargs-parser": "21.1.1"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
mkdocs==1.6.1
|
||||
mkdocs-git-revision-date-localized-plugin==1.3.0
|
||||
mkdocs-material==9.6.7
|
||||
mkdocs-git-revision-date-localized-plugin==1.4.4
|
||||
mkdocs-material==9.6.8
|
||||
mkdocs-redirects==1.2.2
|
||||
mike==2.1.3
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.7.7](https://github.com/appium/appium/compare/@appium/driver-test-support@0.7.6...@appium/driver-test-support@0.7.7) (2025-03-11)
|
||||
|
||||
**Note:** Version bump only for package @appium/driver-test-support
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.6](https://github.com/appium/appium/compare/@appium/driver-test-support@0.7.5...@appium/driver-test-support@0.7.6) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/driver-test-support
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/driver-test-support",
|
||||
"version": "0.7.6",
|
||||
"version": "0.7.7",
|
||||
"description": "Test utilities for Appium drivers",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -41,8 +41,8 @@
|
||||
},
|
||||
"types": "./build/lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@appium/types": "^0.25.1",
|
||||
"axios": "1.8.1",
|
||||
"@appium/types": "^0.25.2",
|
||||
"axios": "1.8.3",
|
||||
"bluebird": "3.7.2",
|
||||
"chai": "5.2.0",
|
||||
"chai-as-promised": "8.0.1",
|
||||
@@ -50,7 +50,7 @@
|
||||
"lodash": "4.17.21",
|
||||
"sinon": "19.0.2",
|
||||
"source-map-support": "0.5.21",
|
||||
"type-fest": "4.36.0"
|
||||
"type-fest": "4.37.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"appium": "^2.0.0-beta.43 || ^3.0.0-beta.0",
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [4.0.3](https://github.com/appium/appium/compare/@appium/execute-driver-plugin@4.0.2...@appium/execute-driver-plugin@4.0.3) (2025-03-11)
|
||||
|
||||
**Note:** Version bump only for package @appium/execute-driver-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.0.2](https://github.com/appium/appium/compare/@appium/execute-driver-plugin@4.0.1...@appium/execute-driver-plugin@4.0.2) (2025-02-19)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/execute-driver-plugin",
|
||||
"version": "4.0.2",
|
||||
"version": "4.0.3",
|
||||
"description": "Plugin for batching and executing driver commands with Appiums",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -41,7 +41,7 @@
|
||||
"bluebird": "3.7.2",
|
||||
"lodash": "4.17.21",
|
||||
"source-map-support": "0.5.21",
|
||||
"webdriverio": "9.10.1"
|
||||
"webdriverio": "9.12.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"appium": "^2.0.0-beta.35 || ^3.0.0-beta.0"
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.7.2](https://github.com/appium/appium/compare/@appium/fake-driver@5.7.1...@appium/fake-driver@5.7.2) (2025-03-11)
|
||||
|
||||
**Note:** Version bump only for package @appium/fake-driver
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.7.1](https://github.com/appium/appium/compare/@appium/fake-driver@5.7.0...@appium/fake-driver@5.7.1) (2025-02-19)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/fake-driver",
|
||||
"version": "5.7.1",
|
||||
"version": "5.7.2",
|
||||
"description": "Mock driver used internally by Appium for testing. Ignore",
|
||||
"keywords": [
|
||||
"automation",
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.2.4](https://github.com/appium/appium/compare/@appium/fake-plugin@3.2.3...@appium/fake-plugin@3.2.4) (2025-03-11)
|
||||
|
||||
**Note:** Version bump only for package @appium/fake-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.2.3](https://github.com/appium/appium/compare/@appium/fake-plugin@3.2.2...@appium/fake-plugin@3.2.3) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/fake-plugin
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/fake-plugin",
|
||||
"version": "3.2.3",
|
||||
"version": "3.2.4",
|
||||
"description": "A fake Appium 2.0 plugin",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/base-plugin": "^3.0.0-beta.0",
|
||||
"@appium/support": "^6.0.6",
|
||||
"@appium/support": "^6.0.7",
|
||||
"bluebird": "3.7.2",
|
||||
"lodash": "4.17.21",
|
||||
"source-map-support": "0.5.21"
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.1.0](https://github.com/appium/appium/compare/@appium/images-plugin@3.0.30...@appium/images-plugin@3.1.0) (2025-03-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **images-plugin:** supports image elements included in actions. ([#21055](https://github.com/appium/appium/issues/21055)) ([0c50504](https://github.com/appium/appium/commit/0c50504266eb5553dafd08bfb6161f643357114f))
|
||||
|
||||
|
||||
|
||||
## [3.0.30](https://github.com/appium/appium/compare/@appium/images-plugin@3.0.29...@appium/images-plugin@3.0.30) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/images-plugin
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
|
||||
|
||||
import _ from 'lodash';
|
||||
import {errors} from 'appium/driver';
|
||||
import {util} from '@appium/support';
|
||||
import {BasePlugin} from 'appium/plugin';
|
||||
import {compareImages} from './compare';
|
||||
import ImageElementFinder from './finder';
|
||||
@@ -83,6 +82,48 @@ export default class ImageElementPlugin extends BasePlugin {
|
||||
// otherwise just do the normal thing
|
||||
return await next();
|
||||
}
|
||||
|
||||
async performActions(next, driver, ...args) {
|
||||
// Replace with coordinates when ActionSequence includes image elements.
|
||||
const [actionSequences] = /** @type {[import('@appium/types').ActionSequence[]]} */ (args);
|
||||
for (const actionSequence of actionSequences) {
|
||||
for (const action of actionSequence.actions) {
|
||||
// The actions that can have an Element as the origin are "pointerMove" and "scroll".
|
||||
if (
|
||||
!_.isPlainObject(
|
||||
/** @type {{origin?: "viewport" | "pointer" | import('@appium/types').Element}} */ (
|
||||
action
|
||||
).origin,
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const actionWithEl =
|
||||
/** @type {import('@appium/types').PointerMoveAction | import('@appium/types').ScrollAction} */ (
|
||||
action
|
||||
);
|
||||
|
||||
const elId = util.unwrapElement(/** @type {import('@appium/types').Element} */ (actionWithEl.origin));
|
||||
if (!_.startsWith(elId, IMAGE_ELEMENT_PREFIX)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const imgEl = this.finder.getImageElement(elId);
|
||||
if (!imgEl) {
|
||||
throw new errors.NoSuchElementError();
|
||||
}
|
||||
|
||||
// Add the element's center coordinates to the offset value.
|
||||
actionWithEl.x += imgEl.center.x;
|
||||
actionWithEl.y += imgEl.center.y;
|
||||
// Set the origin to the viewport so that the external driver can process it using coordinates.
|
||||
delete actionWithEl.origin;
|
||||
}
|
||||
}
|
||||
|
||||
return await next();
|
||||
}
|
||||
}
|
||||
|
||||
export {ImageElementPlugin, getImgElFromArgs, IMAGE_STRATEGY};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/images-plugin",
|
||||
"version": "3.0.30",
|
||||
"version": "3.1.0",
|
||||
"description": "Plugin for working with images and image elements in Appium",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -34,13 +34,13 @@
|
||||
],
|
||||
"scripts": {
|
||||
"test": "npm run test:unit",
|
||||
"test:e2e": "mocha --timeout 40s --slow 20s \"./test/e2e/**/*.spec.js\"",
|
||||
"test:e2e": "mocha --timeout 40s --slow 20s \"./test/e2e/**/*.spec.*js\"",
|
||||
"test:smoke": "node ./index.js",
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.*js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/opencv": "^3.0.8",
|
||||
"@appium/support": "^6.0.6",
|
||||
"@appium/support": "^6.0.7",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "10.4.3",
|
||||
"sharp": "0.33.5",
|
||||
|
||||
@@ -2,7 +2,7 @@ import _ from 'lodash';
|
||||
import path from 'path';
|
||||
import {remote as wdio} from 'webdriverio';
|
||||
import {MATCH_FEATURES_MODE, GET_SIMILARITY_MODE} from '../../lib/constants';
|
||||
import {TEST_IMG_1_B64, TEST_IMG_2_B64, APPSTORE_IMG_PATH} from '../fixtures';
|
||||
import {TEST_IMG_1_B64, TEST_IMG_2_B64, APPSTORE_IMG_PATH} from '../fixtures/index.cjs';
|
||||
import {pluginE2EHarness} from '@appium/plugin-test-support';
|
||||
import {tempDir, fs} from '@appium/support';
|
||||
import sharp from 'sharp';
|
||||
@@ -10,7 +10,7 @@ import sharp from 'sharp';
|
||||
const THIS_PLUGIN_DIR = path.join(__dirname, '..', '..');
|
||||
const APPIUM_HOME = path.join(THIS_PLUGIN_DIR, 'local_appium_home');
|
||||
const FAKE_DRIVER_DIR = path.join(THIS_PLUGIN_DIR, '..', 'fake-driver');
|
||||
const TEST_HOST = 'localhost';
|
||||
const TEST_HOST = '127.0.0.1';
|
||||
const TEST_PORT = 4723;
|
||||
const TEST_FAKE_APP = path.join(
|
||||
APPIUM_HOME,
|
||||
@@ -89,6 +89,19 @@ describe('ImageElementPlugin', function () {
|
||||
width.should.eql(80);
|
||||
height.should.eql(91);
|
||||
await imageEl.click();
|
||||
|
||||
const actionSequence = {
|
||||
type: 'pointer',
|
||||
id: 'mouse',
|
||||
parameters: {pointerType: 'touch'},
|
||||
actions: [
|
||||
{type: 'pointerMove', x: 0, y: 0, duration: 0, origin: imageEl},
|
||||
{type: 'pointerDown', button: 0},
|
||||
{type: 'pause', duration: 125},
|
||||
{type: 'pointerUp', button: 0},
|
||||
],
|
||||
};
|
||||
await driver.performActions([actionSequence]);
|
||||
});
|
||||
|
||||
it('should find subelements', async function () {
|
||||
@@ -6,7 +6,7 @@ import {IMAGE_STRATEGY} from '../../lib/constants';
|
||||
import ImageElementFinder from '../../lib/finder';
|
||||
import {ImageElement} from '../../lib/image-element';
|
||||
import sinon from 'sinon';
|
||||
import {TINY_PNG, TiNY_PNG_BUF, TINY_PNG_DIMS} from '../fixtures';
|
||||
import {TINY_PNG, TiNY_PNG_BUF, TINY_PNG_DIMS} from '../fixtures/index.cjs';
|
||||
import sharp from 'sharp';
|
||||
|
||||
const compareModule = require('../../lib/compare');
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import _ from 'lodash';
|
||||
import {ImageElementPlugin} from '../../lib/plugin';
|
||||
import {
|
||||
MATCH_FEATURES_MODE,
|
||||
@@ -6,7 +7,7 @@ import {
|
||||
IMAGE_STRATEGY,
|
||||
} from '../../lib/constants';
|
||||
import {BaseDriver} from 'appium/driver';
|
||||
import {TEST_IMG_1_B64, TEST_IMG_2_B64, TEST_IMG_2_PART_B64} from '../fixtures';
|
||||
import {TEST_IMG_1_B64, TEST_IMG_2_B64, TEST_IMG_2_PART_B64} from '../fixtures/index.cjs';
|
||||
import {util} from '@appium/support';
|
||||
|
||||
describe('ImageElementPlugin#handle', function () {
|
||||
@@ -177,4 +178,91 @@ describe('ImageElementPlugin#handle', function () {
|
||||
.should.eventually.be.rejectedWith(/not yet/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('performActions', function () {
|
||||
let imageEl;
|
||||
let nativeEl;
|
||||
before(async function () {
|
||||
imageEl = await p.findElement(next, driver, IMAGE_STRATEGY, TEST_IMG_2_PART_B64);
|
||||
nativeEl = util.wrapElement('dummy-native-element-id');
|
||||
});
|
||||
it('should replace with coords of the image elements in pointerMove, scroll actions', async function () {
|
||||
const actionSequences = [
|
||||
{
|
||||
type: 'pointer',
|
||||
id: 'mouse',
|
||||
parameters: {pointerType: 'touch'},
|
||||
actions: [
|
||||
{type: 'pointerMove', x: 0, y: 0, duration: 0, origin: imageEl},
|
||||
{type: 'pointerMove', x: 15, y: 25, duration: 0, origin: imageEl},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'wheel',
|
||||
id: 'wheel',
|
||||
actions: [
|
||||
{type: 'scroll', x: 1, y: 0, deltaX: 1, deltaY: 2, origin: imageEl},
|
||||
],
|
||||
},
|
||||
];
|
||||
await p.performActions(next, driver, actionSequences);
|
||||
actionSequences.should.eql([
|
||||
{
|
||||
type: 'pointer',
|
||||
id: 'mouse',
|
||||
parameters: {pointerType: 'touch'},
|
||||
actions: [
|
||||
{type: 'pointerMove', x: 24, y: 40, duration: 0},
|
||||
{type: 'pointerMove', x: 39, y: 65, duration: 0},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'wheel',
|
||||
id: 'wheel',
|
||||
actions: [
|
||||
{type: 'scroll', x: 25, y: 40, deltaX: 1, deltaY: 2},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
it('should not be modified except pointerMove and scroll actions includes image element as origin', async function () {
|
||||
const actionSequences = [
|
||||
{
|
||||
type: 'pointer',
|
||||
id: 'mouse',
|
||||
parameters: {pointerType: 'touch'},
|
||||
actions: [
|
||||
{type: 'pointerMove', x: 1, y: 1, duration: 0},
|
||||
{type: 'pointerMove', x: 2, y: 2, duration: 10, origin: nativeEl},
|
||||
{type: 'pointerMove', x: 3, y: 3, duration: 20, origin: 'viewport'},
|
||||
{type: 'pointerMove', x: 4, y: 4, duration: 30, origin: 'pointer'},
|
||||
{type: 'pointerDown', button: 0},
|
||||
{type: 'pause', duration: 125},
|
||||
{type: 'pointerUp', button: 0},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'wheel',
|
||||
id: 'wheel',
|
||||
actions: [
|
||||
{type: 'scroll', x: 1, y: 1, deltaX: 1, deltaY: 2},
|
||||
{type: 'scroll', x: 2, y: 2, deltaX: 2, deltaY: 3, origin: nativeEl},
|
||||
{type: 'scroll', x: 3, y: 3, deltaX: 3, deltaY: 4, origin: 'viewport'},
|
||||
{type: 'scroll', x: 4, y: 4, deltaX: 4, deltaY: 5, origin: 'pointer'},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'key',
|
||||
id: 'key',
|
||||
actions: [
|
||||
{type: 'keyDown', value: 'a'},
|
||||
{type: 'keyUp', value: 'a'},
|
||||
],
|
||||
},
|
||||
];
|
||||
const clone = _.cloneDeep(actionSequences);
|
||||
await p.performActions(next, driver, actionSequences);
|
||||
actionSequences.should.eql(clone);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.3.52](https://github.com/appium/appium/compare/@appium/plugin-test-support@0.3.51...@appium/plugin-test-support@0.3.52) (2025-03-11)
|
||||
|
||||
**Note:** Version bump only for package @appium/plugin-test-support
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.3.51](https://github.com/appium/appium/compare/@appium/plugin-test-support@0.3.50...@appium/plugin-test-support@0.3.51) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/plugin-test-support
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/plugin-test-support",
|
||||
"version": "0.3.51",
|
||||
"version": "0.3.52",
|
||||
"description": "Testing utilities for Appium plugins",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -41,7 +41,7 @@
|
||||
"test:unit": "exit 0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/types": "^0.25.1",
|
||||
"@appium/types": "^0.25.2",
|
||||
"get-port": "5.1.1",
|
||||
"log-symbols": "4.1.0",
|
||||
"source-map-support": "0.5.21",
|
||||
|
||||
12
packages/storage-plugin/CHANGELOG.md
Normal file
12
packages/storage-plugin/CHANGELOG.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## 0.1.0 (2025-03-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add storage plugin ([#21075](https://github.com/appium/appium/issues/21075)) ([ba4aa39](https://github.com/appium/appium/commit/ba4aa394d1b6676cc29644e7faa3b0590552f303))
|
||||
* **storage-plugin:** Tune the files keeping behaviour ([#21086](https://github.com/appium/appium/issues/21086)) ([15280b8](https://github.com/appium/appium/commit/15280b80d2af6b3bdf6bf2905472b05b7bca1c1d))
|
||||
83
packages/storage-plugin/README.md
Normal file
83
packages/storage-plugin/README.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# @appium/storage-plugin
|
||||
|
||||
> Appium plugin for server-side file storage
|
||||
|
||||
This plugin adds the ability to create a dedicated storage space on the server side,
|
||||
which can be managed from the client side. This can be useful for files like application packages.
|
||||
Only one storage may exist per server process, shared by all testing sessions.
|
||||
|
||||
> [!WARNING]
|
||||
> This plugin is designed to be used with servers deployed in private networks.
|
||||
> Consider validating the setup with your security department
|
||||
> if you want to enable this plugin at a public Appium server deployment.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
appium plugin install storage
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Add the plugin name to the list of plugins to use upon server startup:
|
||||
|
||||
```bash
|
||||
appium --use-plugins=storage
|
||||
```
|
||||
|
||||
By default, the plugin creates a new temporary folder where it manages uploaded files.
|
||||
|
||||
[Refer to the Appium documentation for a list of commands supported by this plugin.](https://appium.io/docs/en/latest/commands/storage-plugin/)
|
||||
|
||||
### Storing a File
|
||||
|
||||
The procedure for storing a local file on the Appium server is as follows:
|
||||
|
||||
- Calculate the [SHA1](https://en.wikipedia.org/wiki/SHA-1) hash of the source file
|
||||
- Decide the name of the destination file in the server storage (it can be the same as the original file name)
|
||||
- Send a `POST` request to the `/storage/add` endpoint, which will return the `events` and `stream` websocket paths
|
||||
- Connect to both web sockets
|
||||
- Start listening for messages on the `events` web socket. Each message there is a JSON object wrapped
|
||||
to a string. The message must be either `{"value": {"success": true, "name":"app.ipa","sha1":"ccc963411b2621335657963322890305ebe96186"}}` to notify about a successful
|
||||
file upload, or `{"value": {"error": "<error signature>", "message": "<error message>", "traceback": "<server traceback>"}}`
|
||||
to notify about any exceptions during the upload process.
|
||||
- Start reading the source file into small chunks. The recommended size of a single chunk is 64 KB.
|
||||
- After each chunk is retrieved, pass it to the `stream` web socket.
|
||||
- After the last chunk upload is completed, either close the `stream` web
|
||||
socket to explicitly notify the server about the upload completion, or
|
||||
wait until the success event is delivered from the `events` web socket
|
||||
as soon as file hashes successfully match.
|
||||
The server must always deliver either a success or a failure
|
||||
event via the `events` web socket as described above.
|
||||
|
||||
It is also possible to upload multiple files in parallel (up to 20 jobs are supported).
|
||||
Only flat files hierarchies are supported in the storage, no subfolders are allowed.
|
||||
If a file with the same name already exists in the storage, it will be overridden with the new one.
|
||||
If a folder with the same name already exists in the storage, an error will be thrown.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
#### APPIUM_STORAGE_ROOT
|
||||
|
||||
It is also possible to customize the repository root folder path by assigning a custom path to the
|
||||
`APPIUM_STORAGE_ROOT` environment variable upon server startup. The plugin automatically deletes the
|
||||
root folder recursively upon server process termination, unless the server is
|
||||
killed forcefully. If `APPIUM_STORAGE_ROOT` points to an existing folder,
|
||||
then all files there are going to be preserved by default unless a different behavior is
|
||||
requested by [APPIUM_STORAGE_KEEP_ALL](#appium_storage_keep_all) environment variable value.
|
||||
|
||||
#### APPIUM_STORAGE_KEEP_ALL
|
||||
|
||||
If this environment variable is set to `true`, `1` or `yes` then the plugin will always keep
|
||||
storage files after the server process is terminated. All other
|
||||
values of this variable enforce the plugin to always delete all files
|
||||
from the storage folder.
|
||||
|
||||
## Examples
|
||||
|
||||
Check [integration tests](./test/e2e/storage.e2e.spec.cjs) for a working
|
||||
[WebdriverIO](https://webdriver.io/) example.
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
2
packages/storage-plugin/lib/index.ts
Normal file
2
packages/storage-plugin/lib/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { StoragePlugin } from './plugin';
|
||||
export type * from './types';
|
||||
205
packages/storage-plugin/lib/plugin.ts
Normal file
205
packages/storage-plugin/lib/plugin.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { BasePlugin } from 'appium/plugin';
|
||||
import { requireValidItemOptions, Storage, StorageArgumentError } from './storage';
|
||||
import _ from 'lodash';
|
||||
import { tempDir, fs, logger } from '@appium/support';
|
||||
import { AddRequestResult, ItemOptions, StorageItem } from './types';
|
||||
import type { Express, Request, Response } from 'express';
|
||||
import { toW3cResponseError } from './utils';
|
||||
import { AppiumServer } from '@appium/types';
|
||||
import { LRUCache } from 'lru-cache';
|
||||
import WebSocket from 'ws';
|
||||
import { EventEmitter } from 'node:stream';
|
||||
|
||||
const log = logger.getLogger('StoragePlugin');
|
||||
|
||||
let SHARED_STORAGE: Storage | null = null;
|
||||
const STORAGE_PREFIX = '/storage';
|
||||
const WS_TTL_MS = 5 * 60 * 1000;
|
||||
const STORAGE_HANDLERS: Record<string, (req: Request, httpServer?: AppiumServer) => Promise<any>> = {};
|
||||
const STORAGE_ADDITIONS_CACHE: LRUCache<string, () => any> = new LRUCache({
|
||||
max: 20,
|
||||
ttl: WS_TTL_MS,
|
||||
dispose: (f: () => any) => f(),
|
||||
});
|
||||
|
||||
export class StoragePlugin extends BasePlugin {
|
||||
|
||||
static async updateServer(expressApp: Express, httpServer: AppiumServer): Promise<void> {
|
||||
const buildHandler = (methodName: string) => async (req: Request, res: Response) => {
|
||||
let status = 200;
|
||||
let body: any;
|
||||
try {
|
||||
const value = await STORAGE_HANDLERS[methodName](req, httpServer);
|
||||
body = {value: value ?? null};
|
||||
} catch (e) {
|
||||
[status, body] = toW3cResponseError(e);
|
||||
}
|
||||
log.debug(
|
||||
`Responding to ${methodName} with ${_.truncate(JSON.stringify(body.value), {length: 200})}`
|
||||
);
|
||||
res.set('content-type', 'application/json; charset=utf-8');
|
||||
res.status(status).send(body);
|
||||
};
|
||||
|
||||
expressApp.post(`${STORAGE_PREFIX}/add`, buildHandler(STORAGE_HANDLERS.addStorageItem.name));
|
||||
expressApp.get(`${STORAGE_PREFIX}/list`, buildHandler(STORAGE_HANDLERS.listStorageItems.name));
|
||||
expressApp.post(`${STORAGE_PREFIX}/reset`, buildHandler(STORAGE_HANDLERS.resetStorage.name));
|
||||
expressApp.post(`${STORAGE_PREFIX}/delete`, buildHandler(STORAGE_HANDLERS.deleteStorageItem.name));
|
||||
}
|
||||
}
|
||||
|
||||
STORAGE_HANDLERS.addStorageItem = async function addStorageItem(
|
||||
req: Request, httpServer: AppiumServer
|
||||
): Promise<AddRequestResult> {
|
||||
const itemOptions = requireValidItemOptions(
|
||||
parseRequestArgs(req, ['name', 'sha1']) as ItemOptions
|
||||
);
|
||||
const [stream, events] = prepareWebSockets(httpServer, itemOptions);
|
||||
return {
|
||||
ws: {
|
||||
stream,
|
||||
events,
|
||||
},
|
||||
ttlMs: WS_TTL_MS,
|
||||
};
|
||||
};
|
||||
|
||||
STORAGE_HANDLERS.listStorageItems = async function listStorageItems(): Promise<StorageItem[]> {
|
||||
return await executeStorageMethod(
|
||||
async (storage: Storage) => await storage.list()
|
||||
);
|
||||
};
|
||||
|
||||
STORAGE_HANDLERS.deleteStorageItem = async function deleteStorageItem(req: Request): Promise<boolean> {
|
||||
return await executeStorageMethod(
|
||||
async (storage: Storage) => await storage.delete(parseRequestArgs(req, ['name']).name)
|
||||
);
|
||||
};
|
||||
|
||||
STORAGE_HANDLERS.resetStorage = async function resetStorage(): Promise<void> {
|
||||
await executeStorageMethod(
|
||||
async (storage: Storage) => await storage.reset()
|
||||
);
|
||||
};
|
||||
|
||||
async function executeStorageMethod<T>(method: (storage: Storage) => Promise<T>): Promise<T> {
|
||||
const storage = await getStorageSingleton();
|
||||
return await method(storage);
|
||||
}
|
||||
|
||||
function parseRequestArgs(req: Request, requiredKeys: string[]): Record<string, any> {
|
||||
if (!_.isPlainObject(req.body)) {
|
||||
throw new StorageArgumentError(`The request body must be a valid JSON object`);
|
||||
}
|
||||
for (const key of requiredKeys) {
|
||||
if (!_.keys(req.body).includes(key)) {
|
||||
throw new StorageArgumentError(
|
||||
`The required argument '${key}' is missing (expected ${JSON.stringify(requiredKeys)})`
|
||||
);
|
||||
}
|
||||
}
|
||||
return req.body;
|
||||
}
|
||||
|
||||
function prepareWebSockets(httpServer: AppiumServer, itemOptions: ItemOptions): [string, string] {
|
||||
const commonPathname = `${STORAGE_PREFIX}/add/${itemOptions.sha1}`;
|
||||
const streamPathname = `${commonPathname}/stream`;
|
||||
const eventsPathname = `${commonPathname}/events`;
|
||||
if (!_.isEmpty(httpServer.getWebSocketHandlers(streamPathname))) {
|
||||
return [streamPathname, eventsPathname];
|
||||
}
|
||||
|
||||
const streamServer = new WebSocket.Server({
|
||||
noServer: true,
|
||||
});
|
||||
const eventsServer = new WebSocket.Server({
|
||||
noServer: true,
|
||||
});
|
||||
const signaler = new EventEmitter();
|
||||
const streamDoneCallback = () => {
|
||||
log.debug(`Unmounting stream and events web sockets at ${commonPathname}`);
|
||||
httpServer.removeWebSocketHandler(streamPathname);
|
||||
httpServer.removeWebSocketHandler(eventsPathname);
|
||||
setTimeout(() => {
|
||||
streamServer.close();
|
||||
eventsServer.close();
|
||||
signaler.removeAllListeners();
|
||||
}, 100);
|
||||
};
|
||||
STORAGE_ADDITIONS_CACHE.set(itemOptions.sha1, streamDoneCallback);
|
||||
eventsServer.on('connection', async (wsUpstream: WebSocket) => {
|
||||
signaler.on('status', (value) => wsUpstream.send(JSON.stringify(value)));
|
||||
});
|
||||
eventsServer.on('error', (e) => {
|
||||
log.info(`The ${eventsPathname} web socket server has notified about an error: ${e.message}`);
|
||||
});
|
||||
streamServer.on('connection', async (wsUpstream: WebSocket) => {
|
||||
log.info(`Starting a new server storage upload of '${itemOptions.name}' at ${streamPathname}`);
|
||||
const storage = await getStorageSingleton();
|
||||
try {
|
||||
await storage.add(itemOptions, wsUpstream);
|
||||
const successEvent = {
|
||||
value: {
|
||||
success: true,
|
||||
...itemOptions,
|
||||
}
|
||||
};
|
||||
log.debug(`Notifying about the successful addition of '${itemOptions.name}' to the server storage`);
|
||||
signaler.emit('status', successEvent);
|
||||
STORAGE_ADDITIONS_CACHE.delete(itemOptions.sha1);
|
||||
} catch (e) {
|
||||
log.debug(`Notifying about a failure while adding '${itemOptions.name}' to the server storage`);
|
||||
// in case of a failure we do not want to close the server yet
|
||||
// in anticipation of a retry
|
||||
log.error(e);
|
||||
const [, errorBody] = toW3cResponseError(e);
|
||||
signaler.emit('status', errorBody);
|
||||
}
|
||||
});
|
||||
streamServer.on('error', (e) => {
|
||||
log.info(`The ${streamPathname} web socket server has notified about an error: ${e.message}`);
|
||||
});
|
||||
httpServer.addWebSocketHandler(streamPathname, streamServer);
|
||||
httpServer.addWebSocketHandler(eventsPathname, eventsServer);
|
||||
|
||||
return [streamPathname, eventsPathname];
|
||||
}
|
||||
|
||||
const getStorageSingleton = _.memoize(async () => {
|
||||
let storageRoot: string;
|
||||
let shouldPreserveRoot = false;
|
||||
let shouldPreserveFiles = false;
|
||||
if (process.env.APPIUM_STORAGE_ROOT) {
|
||||
storageRoot = process.env.APPIUM_STORAGE_ROOT;
|
||||
shouldPreserveRoot = shouldPreserveFiles = await fs.exists(storageRoot);
|
||||
log.info(`Set '${storageRoot}' as the server storage root folder`);
|
||||
} else {
|
||||
storageRoot = await tempDir.openDir();
|
||||
log.info(`Created '${storageRoot}' as the temporary server storage root folder`);
|
||||
}
|
||||
if (process.env.APPIUM_STORAGE_KEEP_ALL) {
|
||||
shouldPreserveFiles = ['true', '1', 'yes'].includes(_.toLower(process.env.APPIUM_STORAGE_KEEP_ALL));
|
||||
}
|
||||
if (shouldPreserveFiles) {
|
||||
log.info(`All server storage items will be always preserved unless deleted explicitly`);
|
||||
} else {
|
||||
log.info(
|
||||
`All server storage items will be cleaned up automatically from '${storageRoot}' after ` +
|
||||
`Appium server termination`
|
||||
);
|
||||
}
|
||||
SHARED_STORAGE = new Storage(
|
||||
storageRoot,
|
||||
shouldPreserveRoot,
|
||||
shouldPreserveFiles,
|
||||
log,
|
||||
);
|
||||
await SHARED_STORAGE.reset();
|
||||
return SHARED_STORAGE;
|
||||
});
|
||||
|
||||
process.once('exit', () => {
|
||||
SHARED_STORAGE?.cleanupSync();
|
||||
});
|
||||
|
||||
export default StoragePlugin;
|
||||
276
packages/storage-plugin/lib/storage.ts
Normal file
276
packages/storage-plugin/lib/storage.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import { fs, timing } from '@appium/support';
|
||||
import _ from 'lodash';
|
||||
import B from 'bluebird';
|
||||
import type { Path } from 'path-scurry';
|
||||
import path from 'node:path';
|
||||
import type { Stats } from 'node:fs';
|
||||
import nativeFs from 'node:fs';
|
||||
import { rimrafSync } from 'rimraf';
|
||||
import { ItemOptions, StorageItem } from './types';
|
||||
import AsyncLock from 'async-lock';
|
||||
import type { AppiumLogger } from '@appium/types';
|
||||
import type Stream from 'node:stream';
|
||||
import type WebSocket from 'ws';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
const MAX_TASKS = 5;
|
||||
const TMP_EXT = '.filepart';
|
||||
const ADDITION_LOCK = new AsyncLock();
|
||||
const WS_SERVER_ERROR = 1011;
|
||||
const SHA1_HASH_LEN = 40;
|
||||
|
||||
|
||||
export class Storage {
|
||||
private readonly _root: string;
|
||||
private readonly _log: AppiumLogger;
|
||||
private readonly _shouldPreserveRoot: boolean;
|
||||
private readonly _shouldPreserveFiles: boolean;
|
||||
|
||||
constructor(
|
||||
root: string,
|
||||
shouldPreserveRoot: boolean,
|
||||
shouldPreserveFiles: boolean,
|
||||
log: AppiumLogger,
|
||||
) {
|
||||
this._root = root;
|
||||
this._log = log;
|
||||
this._shouldPreserveRoot = shouldPreserveRoot;
|
||||
this._shouldPreserveFiles = shouldPreserveFiles;
|
||||
}
|
||||
|
||||
async list(): Promise<StorageItem[]> {
|
||||
const items = (await this._listFiles())
|
||||
.filter((p) => !p.fullpath().endsWith(TMP_EXT));
|
||||
if (_.isEmpty(items)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const statPromises: B<Stats>[] = [];
|
||||
for (const item of items) {
|
||||
const pending = statPromises.filter((p) => p.isPending());
|
||||
if (pending.length >= MAX_TASKS) {
|
||||
await B.any(pending);
|
||||
}
|
||||
statPromises.push(B.resolve(fs.stat(item.fullpath())));
|
||||
}
|
||||
return _.zip(
|
||||
items.map((f) => f.fullpath()),
|
||||
await B.all(statPromises)
|
||||
)
|
||||
.map(([fullpath, stat]) => ({
|
||||
name: path.basename(fullpath as string),
|
||||
path: fullpath as string,
|
||||
size: (stat as Stats).size,
|
||||
}));
|
||||
}
|
||||
|
||||
async add(opts: ItemOptions, source: Stream | WebSocket): Promise<void> {
|
||||
const {name} = requireValidItemOptions(opts);
|
||||
// _.toLower is needed for case-insensitive server filesystems
|
||||
await ADDITION_LOCK.acquire(_.toLower(name), async () => {
|
||||
if (_.isFunction((source as any).pipe)) {
|
||||
await this._addFromStream(opts, source as Stream);
|
||||
} else {
|
||||
await this._addFromWebSocket(opts, source as WebSocket);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async delete(name: string): Promise<boolean> {
|
||||
if (_.toLower(name).endsWith(TMP_EXT)) {
|
||||
return false;
|
||||
}
|
||||
const destinationPath = path.join(this._root, name);
|
||||
if (!await fs.exists(destinationPath)) {
|
||||
return false;
|
||||
}
|
||||
await fs.rimraf(destinationPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
async reset(): Promise<void> {
|
||||
if (!this._shouldPreserveRoot && !this._shouldPreserveFiles) {
|
||||
await fs.rimraf(this._root);
|
||||
}
|
||||
|
||||
if (!await fs.exists(this._root)) {
|
||||
await fs.mkdirp(this._root);
|
||||
return;
|
||||
}
|
||||
|
||||
const files = (await this._listFiles())
|
||||
.map((p) => p.fullpath())
|
||||
.filter(
|
||||
(fullPath) => !this._shouldPreserveFiles || _.toLower(path.basename(fullPath)).endsWith(TMP_EXT)
|
||||
);
|
||||
if (_.isEmpty(files)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const promises: B<any>[] = [];
|
||||
for (const fullPath of files) {
|
||||
const pending = promises.filter((p) => p.isPending());
|
||||
if (pending.length >= MAX_TASKS) {
|
||||
await B.any(pending);
|
||||
}
|
||||
promises.push(B.resolve(fs.rimraf(fullPath)));
|
||||
}
|
||||
await B.all(promises);
|
||||
}
|
||||
|
||||
cleanupSync(): void {
|
||||
this._log.debug(`Cleaning up the '${this._root}' server storage folder`);
|
||||
|
||||
if (!this._shouldPreserveRoot && !this._shouldPreserveFiles) {
|
||||
rimrafSync(this._root);
|
||||
return;
|
||||
}
|
||||
|
||||
let itemNames: string[];
|
||||
try {
|
||||
itemNames = nativeFs.readdirSync(this._root)
|
||||
.filter((name) => !_.startsWith(name, '.'));
|
||||
} catch (e) {
|
||||
this._log.warn(
|
||||
`Cannot list the '${this._root}' server storage folder. Original error: ${e.message}. ` +
|
||||
`Skipping the cleanup.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (_.isEmpty(itemNames)) {
|
||||
if (!this._shouldPreserveRoot) {
|
||||
rimrafSync(this._root);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const matchedNames = itemNames.filter(
|
||||
(name) => !this._shouldPreserveFiles || _.toLower(name).endsWith(TMP_EXT)
|
||||
);
|
||||
for (const matchedName of matchedNames) {
|
||||
rimrafSync(path.join(this._root, matchedName));
|
||||
}
|
||||
if (!this._shouldPreserveRoot && _.isEmpty(_.without(itemNames, ...matchedNames))) {
|
||||
rimrafSync(this._root);
|
||||
}
|
||||
}
|
||||
|
||||
private async _listFiles(): Promise<Path[]> {
|
||||
const paths = (await fs.glob('*', {
|
||||
cwd: this._root,
|
||||
withFileTypes: true,
|
||||
})) as unknown as Path[];
|
||||
return paths.filter((item) => item.isFile());
|
||||
}
|
||||
|
||||
private async _addFromStream(opts: ItemOptions, source: Stream): Promise<void> {
|
||||
const {name} = opts;
|
||||
const fullPath = path.join(this._root, toTempName(name));
|
||||
const timer = new timing.Timer().start();
|
||||
const destination = fs.createWriteStream(fullPath);
|
||||
source.pipe(destination);
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
destination.once('finish', () => resolve(true));
|
||||
source.once('error', reject);
|
||||
destination.once('error', reject);
|
||||
});
|
||||
await this._finalizeItem(opts, timer, fullPath, await fs.hash(fullPath));
|
||||
} catch (e) {
|
||||
await fs.rimraf(fullPath);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private async _addFromWebSocket(opts: ItemOptions, source: WebSocket): Promise<void> {
|
||||
const {name, sha1} = opts;
|
||||
const fullPath = path.join(this._root, toTempName(name));
|
||||
const timer = new timing.Timer().start();
|
||||
const destination = fs.createWriteStream(fullPath);
|
||||
const sha1sum = createHash('sha1');
|
||||
let didDigestMatch = false;
|
||||
let recentDigest: string | null = null;
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
source.on('message', (data: WebSocket.RawData) => {
|
||||
if (didDigestMatch) {
|
||||
// ignore further chunks if hashes have already matched
|
||||
return;
|
||||
}
|
||||
destination.write(data, (e) => {
|
||||
if (e) {
|
||||
source.close(WS_SERVER_ERROR);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
sha1sum.update(data as any);
|
||||
recentDigest = sha1sum.copy().digest('hex');
|
||||
if (_.toLower(recentDigest) === _.toLower(sha1)) {
|
||||
didDigestMatch = true;
|
||||
destination.close(() => resolve(true));
|
||||
}
|
||||
});
|
||||
source.once('close', () => {
|
||||
destination.close(() => resolve(true));
|
||||
});
|
||||
source.once('error', reject);
|
||||
destination.once('error', (e) => {
|
||||
source.close(WS_SERVER_ERROR);
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
await this._finalizeItem(opts, timer, fullPath, recentDigest ?? sha1sum.digest('hex'));
|
||||
} catch (e) {
|
||||
await fs.rimraf(fullPath);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private async _finalizeItem(
|
||||
opts: ItemOptions,
|
||||
timer: timing.Timer,
|
||||
fullPath: string,
|
||||
actualHashDigest: string,
|
||||
): Promise<void> {
|
||||
const {name, sha1} = opts;
|
||||
this._log.info(
|
||||
`'${name}' has been added to the server storage within ` +
|
||||
`${timer.getDuration().asMilliSeconds}ms. Verifying hashes.`
|
||||
);
|
||||
if (_.toLower(actualHashDigest) !== _.toLower(sha1)) {
|
||||
throw new StorageArgumentError(
|
||||
`The actual SHA1 hash value '${actualHashDigest}' must be equal ` +
|
||||
`to the expected hash value of '${sha1}' for '${name}'`
|
||||
);
|
||||
}
|
||||
await fs.mv(fullPath, path.join(this._root, name));
|
||||
}
|
||||
}
|
||||
|
||||
function toTempName(origName: string): string {
|
||||
return `${origName}${TMP_EXT}`;
|
||||
}
|
||||
|
||||
export function requireValidItemOptions(opts: ItemOptions): ItemOptions {
|
||||
if (_.isEmpty(opts.name)) {
|
||||
throw new StorageArgumentError(`The provided file name '${opts.name}' must not be empty`);
|
||||
}
|
||||
const sanitizedName = fs.sanitizeName(opts.name, {
|
||||
replacement: '_',
|
||||
});
|
||||
if (opts.name !== sanitizedName) {
|
||||
throw new StorageArgumentError(
|
||||
`The provided name value '${opts.name}' must be a valid file name. ` +
|
||||
`Did you mean '${sanitizedName}'?`
|
||||
);
|
||||
}
|
||||
if (!_.isString(opts.sha1) || opts.sha1.length !== SHA1_HASH_LEN) {
|
||||
throw new StorageArgumentError(
|
||||
`The provided hash value '${opts.sha1}' must be a valid SHA1 string, for ` +
|
||||
`example 'ccc963411b2621335657963322890305ebe96186'`
|
||||
);
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
|
||||
export class StorageArgumentError extends Error {}
|
||||
29
packages/storage-plugin/lib/types.ts
Normal file
29
packages/storage-plugin/lib/types.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export interface ItemOptions {
|
||||
/** Destination file name */
|
||||
name: string;
|
||||
/** SHA1 hash of the file */
|
||||
sha1: string;
|
||||
}
|
||||
|
||||
export interface StorageItem {
|
||||
/** Item name */
|
||||
name: string;
|
||||
/** Full path to the item on the server FS */
|
||||
path: string;
|
||||
/** Item size in bytes */
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface WsEndpoints {
|
||||
/** Websocket pathname used to upload file chunks */
|
||||
stream: string;
|
||||
/** Websocket pathname used to deliver events to the client */
|
||||
events: string;
|
||||
}
|
||||
|
||||
export interface AddRequestResult {
|
||||
/** The websocket pathname where the further upload is expected */
|
||||
ws: WsEndpoints;
|
||||
/** For how long the websocket server is going to be alive in milliseconds */
|
||||
ttlMs: number;
|
||||
}
|
||||
14
packages/storage-plugin/lib/utils.ts
Normal file
14
packages/storage-plugin/lib/utils.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import _ from 'lodash';
|
||||
import { errors } from 'appium/driver';
|
||||
|
||||
|
||||
export function toW3cResponseError(err: any): [number, Record<string, any>] {
|
||||
const protocolError = _.has(err, 'w3cStatus') ? err : new errors.UnknownError(err);
|
||||
return [protocolError.w3cStatus, {
|
||||
value: {
|
||||
error: protocolError.error,
|
||||
message: protocolError.message,
|
||||
stacktrace: protocolError.stacktrace || protocolError.stack,
|
||||
},
|
||||
}];
|
||||
}
|
||||
71
packages/storage-plugin/package.json
Normal file
71
packages/storage-plugin/package.json
Normal file
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"name": "@appium/storage-plugin",
|
||||
"version": "0.1.0",
|
||||
"description": "Server-side storage plugin",
|
||||
"keywords": [
|
||||
"automation",
|
||||
"javascript",
|
||||
"selenium",
|
||||
"webdriver",
|
||||
"ios",
|
||||
"android",
|
||||
"firefoxos",
|
||||
"testing"
|
||||
],
|
||||
"homepage": "https://appium.io",
|
||||
"bugs": {
|
||||
"url": "https://github.com/appium/appium/issues"
|
||||
},
|
||||
"main": "./build/lib/index.js",
|
||||
"types": "./build/lib/index.d.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appium/appium.git",
|
||||
"directory": "packages/storage-plugin"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"author": "https://github.com/appium",
|
||||
"directories": {
|
||||
"lib": "./lib"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"build",
|
||||
"tsconfig.json",
|
||||
"!build/tsconfig.tsbuildinfo"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "npm run test:unit",
|
||||
"test:smoke": "node ./build/lib/index.js",
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.*js\"",
|
||||
"test:e2e": "mocha --timeout 20s --slow 10s \"./test/e2e/**/*.spec.*js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/base-plugin": "^2.3.4",
|
||||
"@appium/support": "^6.0.7",
|
||||
"async-lock": "1.4.1",
|
||||
"bluebird": "3.7.2",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "10.4.3",
|
||||
"rimraf": "5.0.10",
|
||||
"source-map-support": "0.5.21",
|
||||
"ws": "8.18.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"appium": "^2.0.0 || ^3.0.0-beta.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
|
||||
"npm": ">=8"
|
||||
},
|
||||
"appium": {
|
||||
"pluginName": "storage",
|
||||
"mainClass": "StoragePlugin"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"tags": [
|
||||
"appium"
|
||||
]
|
||||
}
|
||||
168
packages/storage-plugin/test/e2e/storage.e2e.spec.cjs
Normal file
168
packages/storage-plugin/test/e2e/storage.e2e.spec.cjs
Normal file
@@ -0,0 +1,168 @@
|
||||
import _ from 'lodash';
|
||||
import path from 'path';
|
||||
import {remote as wdio} from 'webdriverio';
|
||||
import {pluginE2EHarness} from '@appium/plugin-test-support';
|
||||
import {tempDir, fs} from '@appium/support';
|
||||
import axios from 'axios';
|
||||
import { WebSocket } from 'ws';
|
||||
|
||||
const BUFFER_SIZE = 0xFFFF;
|
||||
const THIS_PLUGIN_DIR = path.join(__dirname, '..', '..');
|
||||
const APPIUM_HOME = path.join(THIS_PLUGIN_DIR, 'local_appium_home');
|
||||
const FAKE_DRIVER_DIR = path.join(THIS_PLUGIN_DIR, '..', 'fake-driver');
|
||||
const TEST_HOST = '127.0.0.1';
|
||||
const TEST_PORT = 4725;
|
||||
const TEST_FAKE_APP = path.join(
|
||||
APPIUM_HOME,
|
||||
'node_modules',
|
||||
'@appium',
|
||||
'fake-driver',
|
||||
'test',
|
||||
'fixtures',
|
||||
'app.xml'
|
||||
);
|
||||
const TEST_CAPS = {
|
||||
platformName: 'Fake',
|
||||
'appium:automationName': 'Fake',
|
||||
'appium:deviceName': 'Fake',
|
||||
'appium:app': TEST_FAKE_APP,
|
||||
};
|
||||
const WDIO_OPTS = {
|
||||
hostname: TEST_HOST,
|
||||
port: TEST_PORT,
|
||||
connectionRetryCount: 0,
|
||||
capabilities: TEST_CAPS,
|
||||
};
|
||||
|
||||
describe('StoragePlugin', function () {
|
||||
let server;
|
||||
let driver;
|
||||
/** @type {string | undefined | undefined} */
|
||||
let storageRoot;
|
||||
|
||||
beforeEach(async function () {
|
||||
storageRoot = await tempDir.openDir();
|
||||
driver = await wdio(WDIO_OPTS);
|
||||
const baseUrl = `http://${TEST_HOST}:${TEST_PORT}/storage`;
|
||||
driver.addCommand(
|
||||
'addStorageItem',
|
||||
async (name, sha1) => (await axios.post(
|
||||
`${baseUrl}/add`, {name, sha1}
|
||||
)).data.value
|
||||
);
|
||||
driver.addCommand(
|
||||
'listStorageItems',
|
||||
async () => (await axios.get(`${baseUrl}/list`)).data.value
|
||||
);
|
||||
driver.addCommand(
|
||||
'resetStorageItems',
|
||||
async () => (await axios.post(`${baseUrl}/reset`)).data.value
|
||||
);
|
||||
driver.addCommand(
|
||||
'deleteStorageItem',
|
||||
async (name) => (await axios.post(`${baseUrl}/delete`, {name})).data.value
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
if (driver) {
|
||||
await driver.deleteSession();
|
||||
driver = null;
|
||||
}
|
||||
if (storageRoot && await fs.exists(storageRoot)) {
|
||||
await fs.rimraf(storageRoot);
|
||||
storageRoot = null;
|
||||
}
|
||||
});
|
||||
|
||||
pluginE2EHarness({
|
||||
before,
|
||||
after,
|
||||
server,
|
||||
port: TEST_PORT,
|
||||
host: TEST_HOST,
|
||||
appiumHome: APPIUM_HOME,
|
||||
driverName: 'fake',
|
||||
driverSource: 'local',
|
||||
driverSpec: FAKE_DRIVER_DIR,
|
||||
pluginName: 'storage',
|
||||
pluginSource: 'local',
|
||||
pluginSpec: THIS_PLUGIN_DIR,
|
||||
});
|
||||
|
||||
it('should manage storage files', async function () {
|
||||
let items = await driver.listStorageItems();
|
||||
_.isEmpty(items).should.be.true;
|
||||
const name1 = path.basename('foo1.bar');
|
||||
const name2 = path.basename('foo2.bar');
|
||||
const pkgPath = path.join(__dirname, '..', '..', 'package.json');
|
||||
await Promise.all([
|
||||
addFileToStorage(TEST_FAKE_APP, name1),
|
||||
addFileToStorage(pkgPath, name2),
|
||||
]);
|
||||
items = await driver.listStorageItems();
|
||||
items.length.should.eql(2);
|
||||
_.isEqual(new Set(items.map(({name}) => name)), new Set([name1, name2])).should.be.true;
|
||||
const isDeleted = await driver.deleteStorageItem(name1);
|
||||
isDeleted.should.be.true;
|
||||
items = await driver.listStorageItems();
|
||||
items.length.should.eql(1);
|
||||
items[0].name.should.eql(name2);
|
||||
await driver.resetStorageItems();
|
||||
items = await driver.listStorageItems();
|
||||
items.length.should.eql(0);
|
||||
});
|
||||
|
||||
async function addFileToStorage(sourcePath, name) {
|
||||
const hash = await fs.hash(sourcePath);
|
||||
const {size} = await fs.stat(sourcePath);
|
||||
const {ws: {events, stream}} = await driver.addStorageItem(name, hash, sourcePath);
|
||||
const streamWs = new WebSocket(`ws://${TEST_HOST}:${TEST_PORT}${stream}`);
|
||||
const eventsWs = new WebSocket(`ws://${TEST_HOST}:${TEST_PORT}${events}`);
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
streamWs.once('error', reject);
|
||||
eventsWs.once('error', reject);
|
||||
eventsWs.once('message', async (data) => {
|
||||
let strData;
|
||||
if (_.isBuffer(data)) {
|
||||
strData = data.toString();
|
||||
} else if (_.isString(data)) {
|
||||
strData = data;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const {value} = JSON.parse(strData);
|
||||
if (value?.success) {
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error(JSON.stringify(value)));
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
streamWs.once('open', async () => {
|
||||
const fhandle = await fs.openFile(sourcePath, 'r');
|
||||
try {
|
||||
let bytesRead = 0;
|
||||
while (bytesRead < size) {
|
||||
const bufferSize = Math.min(BUFFER_SIZE, size - bytesRead);
|
||||
const buffer = Buffer.alloc(bufferSize);
|
||||
await fhandle.read(buffer, 0, bufferSize, bytesRead);
|
||||
streamWs.send(buffer);
|
||||
bytesRead += bufferSize;
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
} finally {
|
||||
await fhandle.close();
|
||||
streamWs.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
streamWs.close();
|
||||
eventsWs.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
124
packages/storage-plugin/test/unit/storage.spec.cjs
Normal file
124
packages/storage-plugin/test/unit/storage.spec.cjs
Normal file
@@ -0,0 +1,124 @@
|
||||
import _ from 'lodash';
|
||||
import { Storage } from '../../lib/storage';
|
||||
import { tempDir, fs, logger } from '@appium/support';
|
||||
import path from 'node:path';
|
||||
|
||||
const log = logger.getLogger();
|
||||
|
||||
describe('storage', function () {
|
||||
/** @type {string | undefined} */
|
||||
let tmpRoot;
|
||||
/** @type {Storage | undefined | null} */
|
||||
let storage;
|
||||
/** @type {string | undefined | null} */
|
||||
let storageRoot;
|
||||
|
||||
before(async function () {
|
||||
const chai = await import('chai');
|
||||
const chaiAsPromised = await import('chai-as-promised');
|
||||
chai.use(chaiAsPromised.default);
|
||||
chai.should();
|
||||
tmpRoot = await tempDir.openDir();
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
if (tmpRoot && await fs.exists(tmpRoot)) {
|
||||
await fs.rimraf(tmpRoot);
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
storageRoot = await tempDir.openDir();
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
if (storage) {
|
||||
await storage.reset();
|
||||
storage = null;
|
||||
}
|
||||
if (storageRoot && await fs.exists(storageRoot)) {
|
||||
await fs.rimraf(storageRoot);
|
||||
storageRoot = null;
|
||||
}
|
||||
});
|
||||
|
||||
it('should be initially empty', async function () {
|
||||
storage = new Storage(storageRoot, false, false, log);
|
||||
const files = await storage.list();
|
||||
_.isEmpty(files).should.be.true;
|
||||
(await storage.delete('foo')).should.be.false;
|
||||
});
|
||||
|
||||
it('should reset all files if shouldPreserveFiles is not requested', async function () {
|
||||
const name = 'foo.bar';
|
||||
const tmpName = 'bar.baz.filepart';
|
||||
await fs.writeFile(path.join(storageRoot, name), Buffer.alloc(1));
|
||||
await fs.writeFile(path.join(storageRoot, tmpName), Buffer.alloc(1));
|
||||
storage = new Storage(storageRoot, true, false, log);
|
||||
const files = await storage.list();
|
||||
files.length.should.eql(1);
|
||||
await storage.reset();
|
||||
(await fs.exists(path.join(storageRoot, name))).should.be.false;
|
||||
(await fs.exists(path.join(storageRoot, tmpName))).should.be.false;
|
||||
});
|
||||
|
||||
it('should only reset parital files if shouldPreserveFiles requested', async function () {
|
||||
const name = 'foo.bar';
|
||||
const tmpName = 'bar.baz.filepart';
|
||||
await fs.writeFile(path.join(storageRoot, name), Buffer.alloc(1));
|
||||
await fs.writeFile(path.join(storageRoot, tmpName), Buffer.alloc(1));
|
||||
storage = new Storage(storageRoot, true, true, log);
|
||||
let files = await storage.list();
|
||||
files.length.should.eql(1);
|
||||
await storage.reset();
|
||||
files = await storage.list();
|
||||
files.length.should.eql(1);
|
||||
(await fs.exists(path.join(storageRoot, tmpName))).should.be.false;
|
||||
});
|
||||
|
||||
it('should perform basic operations', async function () {
|
||||
storage = new Storage(storageRoot, false, false, log);
|
||||
const name = 'foo.bar';
|
||||
const size = 1 * 1024 * 1024;
|
||||
await addFileToStorage(name, size);
|
||||
let files = await storage.list();
|
||||
_.isEmpty(files).should.be.false;
|
||||
files[0].name.should.eql(name);
|
||||
files[0].size.should.eql(size);
|
||||
files[0].path.should.eql(path.join(storageRoot, name));
|
||||
(await storage.delete(name)).should.be.true;
|
||||
files = await storage.list();
|
||||
_.isEmpty(files).should.be.true;
|
||||
});
|
||||
|
||||
it('should be reset and preserve the root', async function () {
|
||||
storage = new Storage(storageRoot, true, false, log);
|
||||
const name = 'foo.bar';
|
||||
const size = 1 * 1024 * 1024;
|
||||
await addFileToStorage(name, size);
|
||||
await storage.reset();
|
||||
const files = await storage.list();
|
||||
_.isEmpty(files).should.be.true;
|
||||
(await fs.exists(storageRoot)).should.be.true;
|
||||
});
|
||||
|
||||
it('should be reset and preserve items', async function () {
|
||||
storage = new Storage(storageRoot, false, true, log);
|
||||
const name = 'foo.bar';
|
||||
const size = 1 * 1024 * 1024;
|
||||
await addFileToStorage(name, size);
|
||||
await storage.reset();
|
||||
const files = await storage.list();
|
||||
_.isEmpty(files).should.be.false;
|
||||
(await fs.exists(storageRoot)).should.be.true;
|
||||
});
|
||||
|
||||
async function addFileToStorage(name, size) {
|
||||
const dummyPath = path.join(tmpRoot, name);
|
||||
await fs.writeFile(dummyPath, Buffer.alloc(size));
|
||||
const sha1 = await fs.hash(dummyPath);
|
||||
await storage.add({name, sha1}, fs.createReadStream(dummyPath));
|
||||
return dummyPath;
|
||||
}
|
||||
|
||||
});
|
||||
13
packages/storage-plugin/tsconfig.json
Normal file
13
packages/storage-plugin/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "build",
|
||||
"checkJs": true
|
||||
},
|
||||
"extends": "@appium/tsconfig/tsconfig.plugin.json",
|
||||
"include": ["lib"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../base-plugin"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [6.0.7](https://github.com/appium/appium/compare/@appium/support@6.0.6...@appium/support@6.0.7) (2025-03-11)
|
||||
|
||||
**Note:** Version bump only for package @appium/support
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [6.0.6](https://github.com/appium/appium/compare/@appium/support@6.0.5...@appium/support@6.0.6) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/support
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/support",
|
||||
"version": "6.0.6",
|
||||
"version": "6.0.7",
|
||||
"description": "Support libs used across appium packages",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -42,11 +42,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/logger": "^1.6.1",
|
||||
"@appium/tsconfig": "^0.3.4",
|
||||
"@appium/types": "^0.25.1",
|
||||
"@appium/tsconfig": "^0.3.5",
|
||||
"@appium/types": "^0.25.2",
|
||||
"@colors/colors": "1.6.0",
|
||||
"archiver": "7.0.1",
|
||||
"axios": "1.8.1",
|
||||
"axios": "1.8.3",
|
||||
"base64-stream": "1.0.0",
|
||||
"bluebird": "3.7.2",
|
||||
"bplist-creator": "0.1.1",
|
||||
@@ -73,7 +73,7 @@
|
||||
"source-map-support": "0.5.21",
|
||||
"supports-color": "8.1.1",
|
||||
"teen_process": "2.3.1",
|
||||
"type-fest": "4.36.0",
|
||||
"type-fest": "4.37.0",
|
||||
"uuid": "11.1.0",
|
||||
"which": "4.0.0",
|
||||
"yauzl": "3.2.0"
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.1.7](https://github.com/appium/appium/compare/@appium/test-support@3.1.6...@appium/test-support@3.1.7) (2025-03-11)
|
||||
|
||||
**Note:** Version bump only for package @appium/test-support
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.1.6](https://github.com/appium/appium/compare/@appium/test-support@3.1.5...@appium/test-support@3.1.6) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/test-support
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/test-support",
|
||||
"version": "3.1.6",
|
||||
"version": "3.1.7",
|
||||
"description": "A collection of test utilities used across Appium packages",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -45,7 +45,7 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/support": "^6.0.6",
|
||||
"@appium/support": "^6.0.7",
|
||||
"@colors/colors": "1.6.0",
|
||||
"bluebird": "3.7.2",
|
||||
"lodash": "4.17.21",
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.3.5](https://github.com/appium/appium/compare/@appium/tsconfig@0.3.4...@appium/tsconfig@0.3.5) (2025-03-11)
|
||||
|
||||
**Note:** Version bump only for package @appium/tsconfig
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.3.4](https://github.com/appium/appium/compare/@appium/tsconfig@0.3.3...@appium/tsconfig@0.3.4) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/tsconfig
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/tsconfig",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"description": "Shared TypeScript Config for Appium",
|
||||
"main": "tsconfig.json",
|
||||
"keywords": [
|
||||
@@ -32,7 +32,7 @@
|
||||
"npm": ">=8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tsconfig/node14": "14.1.2"
|
||||
"@tsconfig/node14": "14.1.3"
|
||||
},
|
||||
"scripts": {
|
||||
"test:smoke": "node tsconfig.json && node tsconfig.plugin.json"
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.25.2](https://github.com/appium/appium/compare/@appium/types@0.25.1...@appium/types@0.25.2) (2025-03-11)
|
||||
|
||||
**Note:** Version bump only for package @appium/types
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.25.1](https://github.com/appium/appium/compare/@appium/types@0.25.0...@appium/types@0.25.1) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/types
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/types",
|
||||
"version": "0.25.1",
|
||||
"version": "0.25.2",
|
||||
"description": "Various type declarations used across Appium",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -40,8 +40,8 @@
|
||||
"dependencies": {
|
||||
"@appium/logger": "^1.6.1",
|
||||
"@appium/schema": "^0.8.1",
|
||||
"@appium/tsconfig": "^0.3.4",
|
||||
"type-fest": "4.36.0"
|
||||
"@appium/tsconfig": "^0.3.5",
|
||||
"type-fest": "4.37.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.31](https://github.com/appium/appium/compare/@appium/universal-xml-plugin@1.0.30...@appium/universal-xml-plugin@1.0.31) (2025-03-11)
|
||||
|
||||
**Note:** Version bump only for package @appium/universal-xml-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.30](https://github.com/appium/appium/compare/@appium/universal-xml-plugin@1.0.29...@appium/universal-xml-plugin@1.0.30) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/universal-xml-plugin
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/universal-xml-plugin",
|
||||
"version": "1.0.30",
|
||||
"version": "1.0.31",
|
||||
"description": "Appium plugin for making XML source and XPath queries the same across iOS and Android",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -38,7 +38,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@xmldom/xmldom": "0.9.8",
|
||||
"fast-xml-parser": "5.0.8",
|
||||
"fast-xml-parser": "5.0.9",
|
||||
"lodash": "4.17.21",
|
||||
"source-map-support": "0.5.21",
|
||||
"xpath": "0.0.34"
|
||||
|
||||
@@ -68,5 +68,8 @@
|
||||
{
|
||||
"path": "packages/logger"
|
||||
},
|
||||
{
|
||||
"path": "packages/storage-plugin"
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user