@wix-pilot/webdriverio-appium 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -0
- package/dist/examples/example.test.js +6 -8
- package/dist/index.d.ts +7 -1
- package/dist/index.js +18 -71
- package/dist/utils/getStableViewHierarchy.d.ts +3 -0
- package/dist/utils/getStableViewHierarchy.js +37 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @wix-pilot/appium 📱
|
|
2
|
+
|
|
3
|
+
> Appium driver for Wix Pilot - Cross-platform mobile testing with natural language
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The `@wix-pilot/webdriverio-appium` package provides Appium integration for Wix Pilot, enabling:
|
|
8
|
+
- Native iOS and Android app testing
|
|
9
|
+
- Hybrid app testing
|
|
10
|
+
- Mobile web testing
|
|
11
|
+
- Cross-platform test automation
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# npm
|
|
17
|
+
npm install --save-dev @wix-pilot/webdriverio-appium webdriverio @wix-pilot/core
|
|
18
|
+
|
|
19
|
+
# yarn
|
|
20
|
+
yarn add -D @wix-pilot/webdriverio-appium webdriverio @wix-pilot/core
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
### 1. Set up your LLM Handler
|
|
26
|
+
```typescript
|
|
27
|
+
import { PromptHandler } from '@wix-pilot/core';
|
|
28
|
+
|
|
29
|
+
class CustomPromptHandler implements PromptHandler {
|
|
30
|
+
async runPrompt(prompt: string, image?: string): Promise<string> {
|
|
31
|
+
// Integrate with your preferred LLM service
|
|
32
|
+
const response = await yourLLMService.complete({
|
|
33
|
+
prompt,
|
|
34
|
+
imageUrl: image, // Optional: for visual testing support
|
|
35
|
+
});
|
|
36
|
+
return response.text;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
isSnapshotImageSupported(): boolean {
|
|
40
|
+
return true; // Set to true if your LLM supports image analysis
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 2. Create and Run Tests
|
|
46
|
+
```typescript
|
|
47
|
+
import pilot from '@wix-pilot/core';
|
|
48
|
+
import { WebdriverIOAppiumFrameworkDriver } from '@wix-pilot/webdriverio-appium';
|
|
49
|
+
|
|
50
|
+
describe('Mobile App Testing', () => {
|
|
51
|
+
beforeAll(async () => {
|
|
52
|
+
pilot.init({
|
|
53
|
+
driver: new WebdriverIOAppiumFrameworkDriver(),
|
|
54
|
+
promptHandler: new CustomPromptHandler(),
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
beforeEach(() => pilot.start());
|
|
59
|
+
afterEach(() => pilot.end());
|
|
60
|
+
|
|
61
|
+
it('should handle shopping cart flow', async () => {
|
|
62
|
+
await pilot.perform([
|
|
63
|
+
'Launch the app',
|
|
64
|
+
'Allow any permission prompts',
|
|
65
|
+
'Tap the "Products" tab',
|
|
66
|
+
'Scroll to "Wireless Headphones"',
|
|
67
|
+
'Add item to cart',
|
|
68
|
+
'The cart badge should show "1"'
|
|
69
|
+
]);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Related Packages
|
|
75
|
+
|
|
76
|
+
- [@wix-pilot/core](../../core) - Core Wix Pilot engine
|
|
77
|
+
- [@wix-pilot/detox](../detox) - Alternative React Native testing driver
|
|
@@ -1,28 +1,26 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const core_1 =
|
|
3
|
+
const core_1 = require("@wix-pilot/core");
|
|
7
4
|
const promptHandler_1 = require("../utils/promptHandler");
|
|
8
5
|
const index_1 = require("../index");
|
|
9
6
|
describe("Example Test Suite", () => {
|
|
10
7
|
let frameworkDriver;
|
|
8
|
+
let pilot;
|
|
11
9
|
before(async () => {
|
|
12
10
|
const promptHandler = new promptHandler_1.PromptHandler();
|
|
13
11
|
frameworkDriver = new index_1.WebdriverIOAppiumFrameworkDriver();
|
|
14
|
-
core_1.
|
|
12
|
+
pilot = new core_1.Pilot({
|
|
15
13
|
frameworkDriver,
|
|
16
14
|
promptHandler,
|
|
17
15
|
});
|
|
18
16
|
});
|
|
19
17
|
beforeEach(async () => {
|
|
20
|
-
|
|
18
|
+
pilot.start();
|
|
21
19
|
});
|
|
22
20
|
afterEach(async () => {
|
|
23
|
-
|
|
21
|
+
pilot.end();
|
|
24
22
|
});
|
|
25
23
|
it("perform test with pilot", async () => {
|
|
26
|
-
await
|
|
24
|
+
await pilot.autopilot("earn 2 points in the game");
|
|
27
25
|
});
|
|
28
26
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import { TestingFrameworkAPICatalog, TestingFrameworkDriver } from "@wix-pilot/core";
|
|
1
|
+
import { TestingFrameworkAPICatalog, TestingFrameworkDriver, TestingFrameworkDriverConfig } from "@wix-pilot/core";
|
|
2
2
|
export declare class WebdriverIOAppiumFrameworkDriver implements TestingFrameworkDriver {
|
|
3
3
|
constructor();
|
|
4
|
+
/**
|
|
5
|
+
* Additional driver configuration.
|
|
6
|
+
*
|
|
7
|
+
* @property useSnapshotStabilitySync - Indicates whether the driver should use wait for screen stability.
|
|
8
|
+
*/
|
|
9
|
+
get driverConfig(): TestingFrameworkDriverConfig;
|
|
4
10
|
/**
|
|
5
11
|
* Attempts to capture the current view hierarchy (source) of the mobile app as XML.
|
|
6
12
|
* If there's no active session or the app isn't running, returns an error message.
|
package/dist/index.js
CHANGED
|
@@ -36,18 +36,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.WebdriverIOAppiumFrameworkDriver = void 0;
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
|
+
const getStableViewHierarchy_1 = require("./utils/getStableViewHierarchy");
|
|
39
40
|
class WebdriverIOAppiumFrameworkDriver {
|
|
40
41
|
constructor() { }
|
|
42
|
+
/**
|
|
43
|
+
* Additional driver configuration.
|
|
44
|
+
*
|
|
45
|
+
* @property useSnapshotStabilitySync - Indicates whether the driver should use wait for screen stability.
|
|
46
|
+
*/
|
|
47
|
+
get driverConfig() {
|
|
48
|
+
return { useSnapshotStabilitySync: true };
|
|
49
|
+
}
|
|
41
50
|
/**
|
|
42
51
|
* Attempts to capture the current view hierarchy (source) of the mobile app as XML.
|
|
43
52
|
* If there's no active session or the app isn't running, returns an error message.
|
|
44
53
|
*/
|
|
45
54
|
async captureViewHierarchyString() {
|
|
46
55
|
try {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const pageSource = await driver.getPageSource();
|
|
50
|
-
return pageSource;
|
|
56
|
+
const result = await (0, getStableViewHierarchy_1.waitForStableState)();
|
|
57
|
+
return result ?? "";
|
|
51
58
|
}
|
|
52
59
|
catch (_error) {
|
|
53
60
|
return "NO ACTIVE APP FOUND, LAUNCH THE APP TO SEE THE VIEW HIERARCHY";
|
|
@@ -155,7 +162,7 @@ class WebdriverIOAppiumFrameworkDriver {
|
|
|
155
162
|
await (await $('~loginButton')).waitForEnabled();
|
|
156
163
|
await (await $('~loginButton')).click();`,
|
|
157
164
|
guidelines: [
|
|
158
|
-
"
|
|
165
|
+
"Allways use await (await $('~element')).waitForEnabled() before clicking on the element",
|
|
159
166
|
],
|
|
160
167
|
},
|
|
161
168
|
{
|
|
@@ -207,6 +214,12 @@ await (await $('~draggable')).dragAndDrop(
|
|
|
207
214
|
"Use this matcher to verify that the element is visible to the user.",
|
|
208
215
|
],
|
|
209
216
|
},
|
|
217
|
+
{
|
|
218
|
+
signature: `.waitForEnabled()`,
|
|
219
|
+
description: "Waits until the element is enabled and ready to be clicked",
|
|
220
|
+
example: `await (await $('~usernameInput')).waitForEnabled();`,
|
|
221
|
+
guidelines: ["Allways use this before clicking on an element"],
|
|
222
|
+
},
|
|
210
223
|
{
|
|
211
224
|
signature: `toExist()`,
|
|
212
225
|
description: "Asserts that the element exists in the DOM/hierarchy.",
|
|
@@ -304,29 +317,6 @@ await driver.setGeoLocation({
|
|
|
304
317
|
latitude: 37.7749,
|
|
305
318
|
longitude: -122.4194,
|
|
306
319
|
altitude: 10
|
|
307
|
-
});
|
|
308
|
-
`,
|
|
309
|
-
},
|
|
310
|
-
{
|
|
311
|
-
signature: `driver.getContext() / driver.switchContext(name: string)`,
|
|
312
|
-
description: "Gets or switches the current context (NATIVE_APP or WEBVIEW_xxx).",
|
|
313
|
-
example: `
|
|
314
|
-
const contexts = await driver.getContexts();
|
|
315
|
-
await driver.switchContext(contexts[1]); // Switch to webview
|
|
316
|
-
await driver.switchContext('NATIVE_APP'); // Switch back
|
|
317
|
-
`,
|
|
318
|
-
},
|
|
319
|
-
{
|
|
320
|
-
signature: `driver.rotate(params: { x: number; y: number; duration: number; radius: number; rotation: number; touchCount: number })`,
|
|
321
|
-
description: "Simulates a rotation gesture on the device (iOS only).",
|
|
322
|
-
example: `
|
|
323
|
-
await driver.rotate({
|
|
324
|
-
x: 100,
|
|
325
|
-
y: 100,
|
|
326
|
-
duration: 2,
|
|
327
|
-
radius: 50,
|
|
328
|
-
rotation: 180,
|
|
329
|
-
touchCount: 2
|
|
330
320
|
});
|
|
331
321
|
`,
|
|
332
322
|
},
|
|
@@ -365,49 +355,6 @@ await driver.performTouchAction({
|
|
|
365
355
|
},
|
|
366
356
|
],
|
|
367
357
|
},
|
|
368
|
-
{
|
|
369
|
-
title: "Web APIs (Hybrid / Mobile Web)",
|
|
370
|
-
items: [
|
|
371
|
-
{
|
|
372
|
-
signature: `driver.switchContext('WEBVIEW')`,
|
|
373
|
-
description: "Switches the context from native to web view (if a webview is present).",
|
|
374
|
-
example: `
|
|
375
|
-
const contexts = await driver.getContexts();
|
|
376
|
-
await driver.switchContext(contexts.find(c => c.includes('WEBVIEW')));
|
|
377
|
-
`,
|
|
378
|
-
guidelines: [
|
|
379
|
-
"Use this when your app has a webview or is a hybrid app.",
|
|
380
|
-
],
|
|
381
|
-
},
|
|
382
|
-
{
|
|
383
|
-
signature: `$('css or xpath').click()`,
|
|
384
|
-
description: "In a webview context, click (tap) a web element using CSS or XPath.",
|
|
385
|
-
example: `await (await $('button#login')).click();`,
|
|
386
|
-
},
|
|
387
|
-
{
|
|
388
|
-
signature: `$('selector').setValue('text')`,
|
|
389
|
-
description: "In a webview context, sets text in a web input field.",
|
|
390
|
-
example: `await (await $('input#username')).setValue('myusername');`,
|
|
391
|
-
},
|
|
392
|
-
{
|
|
393
|
-
signature: `.getText()`,
|
|
394
|
-
description: "Retrieves the visible text of a web element (hybrid/web context).",
|
|
395
|
-
example: `
|
|
396
|
-
const text = await (await $('h1.main-title')).getText();
|
|
397
|
-
expect(text).toBe('Welcome to My App');
|
|
398
|
-
`,
|
|
399
|
-
},
|
|
400
|
-
{
|
|
401
|
-
signature: `driver.executeScript(script: string, args?: any[])`,
|
|
402
|
-
description: "Executes JavaScript in the context of the webview.",
|
|
403
|
-
example: `
|
|
404
|
-
await driver.executeScript("document.getElementById('hidden-button').click()");
|
|
405
|
-
const title = await driver.executeScript('return document.title');
|
|
406
|
-
expect(title).toBe('My Page Title');
|
|
407
|
-
`,
|
|
408
|
-
},
|
|
409
|
-
],
|
|
410
|
-
},
|
|
411
358
|
],
|
|
412
359
|
};
|
|
413
360
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.waitForStableState = waitForStableState;
|
|
7
|
+
exports.compareViewHierarchies = compareViewHierarchies;
|
|
8
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
9
|
+
const DEFAULT_POLL_INTERVAL = 500; // ms
|
|
10
|
+
const DEFAULT_TIMEOUT = 5000; // ms
|
|
11
|
+
/** Waits till the view hierarchy is stable than returns it*/
|
|
12
|
+
async function waitForStableState(pollInterval = DEFAULT_POLL_INTERVAL, timeout = DEFAULT_TIMEOUT) {
|
|
13
|
+
const startTime = Date.now();
|
|
14
|
+
let lastSnapshot;
|
|
15
|
+
while (Date.now() - startTime < timeout) {
|
|
16
|
+
const currentSnapshot = (await driver.getPageSource()) || "";
|
|
17
|
+
if (!currentSnapshot) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
if (lastSnapshot) {
|
|
21
|
+
const isStable = compareViewHierarchies(currentSnapshot, lastSnapshot);
|
|
22
|
+
if (isStable) {
|
|
23
|
+
return currentSnapshot;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
lastSnapshot = currentSnapshot;
|
|
27
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
28
|
+
}
|
|
29
|
+
// Return the last snapshot if timeout is reached
|
|
30
|
+
return lastSnapshot;
|
|
31
|
+
}
|
|
32
|
+
function compareViewHierarchies(current, last) {
|
|
33
|
+
// Use MD5 for fast comparison of view hierarchies
|
|
34
|
+
const currentHash = crypto_1.default.createHash("md5").update(current).digest("hex");
|
|
35
|
+
const lastHash = crypto_1.default.createHash("md5").update(last).digest("hex");
|
|
36
|
+
return currentHash === lastHash;
|
|
37
|
+
}
|