@wix-pilot/webdriverio-appium 1.0.1 → 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 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 = __importDefault(require("@wix-pilot/core"));
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.default.init({
12
+ pilot = new core_1.Pilot({
15
13
  frameworkDriver,
16
14
  promptHandler,
17
15
  });
18
16
  });
19
17
  beforeEach(async () => {
20
- core_1.default.start();
18
+ pilot.start();
21
19
  });
22
20
  afterEach(async () => {
23
- core_1.default.end();
21
+ pilot.end();
24
22
  });
25
23
  it("perform test with pilot", async () => {
26
- await core_1.default.autopilot("earn 2 points in the game");
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
- // In WebdriverIO + Appium, you can retrieve the current page source (UI hierarchy) via:
48
- // https://webdriver.io/docs/api/browser/getPageSource (driver is an alias for browser)
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";
@@ -99,31 +106,49 @@ class WebdriverIOAppiumFrameworkDriver {
99
106
  signature: `$('~accessibilityId')`,
100
107
  description: "Locate an element by its accessibility ID (commonly used in Appium).",
101
108
  example: `const loginButton = await $('~loginButton'); // Accessibility ID`,
109
+ guidelines: [
110
+ "Always prefer using test IDs when available; if no ID is present, use another stable identifier.",
111
+ ],
102
112
  },
103
113
  {
104
114
  signature: `$('android=uiSelector')`,
105
115
  description: "Locate an element using an Android UIAutomator selector.",
106
116
  example: `const el = await $('android=new UiSelector().text("Login")');`,
117
+ guidelines: [
118
+ "For Android, use UIAutomator selectors to target elements by text, resource-id, or other properties. Ensure selectors are precise to avoid ambiguity.",
119
+ ],
107
120
  },
108
121
  {
109
122
  signature: `$('ios=predicateString')`,
110
123
  description: "Locate an element using an iOS NSPredicate string.",
111
124
  example: `const el = await $('ios=predicate string:type == "XCUIElementTypeButton" AND name == "Login"');`,
125
+ guidelines: [
126
+ "For iOS, use NSPredicate strings to define clear and concise conditions for element identification. Validate the predicate to ensure it uniquely identifies the element.",
127
+ ],
112
128
  },
113
129
  {
114
130
  signature: `$$('#elementSelector')`,
115
131
  description: "Locate all elements with a given selector",
116
132
  example: `const firstSite = await $$('#Site')[index];`,
133
+ guidelines: [
134
+ "Use this to select multiple elements. Make sure your selector targets a specific subset of elements to avoid excessive matches.",
135
+ ],
117
136
  },
118
137
  {
119
138
  signature: `$('//*[@text="Login"]')`,
120
139
  description: "Locate an element using an XPath expression.",
121
140
  example: `const el = await $('//*[@text="Login"]');`,
141
+ guidelines: [
142
+ "Use XPath as a last resort when other selectors are not available, as XPath can be slower and more brittle. Ensure your XPath expression is optimized for performance.",
143
+ ],
122
144
  },
123
145
  {
124
146
  signature: `$('#elementId'), $('elementTag'), $('.className')`,
125
147
  description: "Web-like selectors (useful if your app is a hybrid or has a web context).",
126
148
  example: `const el = await $('.someNativeClass');`,
149
+ guidelines: [
150
+ "Use web-like selectors in hybrid apps or web contexts. Ensure selectors are unique and adhere to best practices for CSS selectors.",
151
+ ],
127
152
  },
128
153
  ],
129
154
  },
@@ -136,6 +161,9 @@ class WebdriverIOAppiumFrameworkDriver {
136
161
  example: `
137
162
  await (await $('~loginButton')).waitForEnabled();
138
163
  await (await $('~loginButton')).click();`,
164
+ guidelines: [
165
+ "Allways use await (await $('~element')).waitForEnabled() before clicking on the element",
166
+ ],
139
167
  },
140
168
  {
141
169
  signature: `.setValue(value: string)`,
@@ -163,11 +191,6 @@ await (await $('~dragHandle')).touchAction([
163
191
  ]);
164
192
  `,
165
193
  },
166
- {
167
- signature: `.scrollIntoView() (web/hybrid context only)`,
168
- description: "Scrolls the element into view (if in a web context).",
169
- example: `await (await $('#someElement')).scrollIntoView();`,
170
- },
171
194
  {
172
195
  signature: `.dragAndDrop(target, duration?)`,
173
196
  description: "Drags the element to the target location (native or web context).",
@@ -187,31 +210,55 @@ await (await $('~draggable')).dragAndDrop(
187
210
  signature: `toBeDisplayed()`,
188
211
  description: "Asserts that the element is displayed (visible).",
189
212
  example: `await expect(await $('~loginButton')).toBeDisplayed();`,
213
+ guidelines: [
214
+ "Use this matcher to verify that the element is visible to the user.",
215
+ ],
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"],
190
222
  },
191
223
  {
192
224
  signature: `toExist()`,
193
225
  description: "Asserts that the element exists in the DOM/hierarchy.",
194
226
  example: `await expect(await $('~usernameInput')).toExist();`,
227
+ guidelines: [
228
+ "Use this matcher to check that the element exists in the DOM or view hierarchy.",
229
+ ],
195
230
  },
196
231
  {
197
232
  signature: `toHaveText(text: string)`,
198
233
  description: "Asserts that the element's text matches the given string.",
199
234
  example: `await expect(await $('~welcomeMessage')).toHaveText('Welcome, user!');`,
235
+ guidelines: [
236
+ "Use this matcher to verify the text content of an element exactly matches the expected value.",
237
+ ],
200
238
  },
201
239
  {
202
240
  signature: `toHaveValue(value: string)`,
203
241
  description: "Asserts that the element's value matches the given string (for inputs, etc.).",
204
242
  example: `await expect(await $('~usernameInput')).toHaveValue('myusername');`,
243
+ guidelines: [
244
+ "Use this matcher for form elements to verify that the input value is correct.",
245
+ ],
205
246
  },
206
247
  {
207
248
  signature: `toBeEnabled() / toBeDisabled()`,
208
249
  description: "Asserts that an element is enabled/disabled (if applicable).",
209
250
  example: `await expect(await $('~submitButton')).toBeEnabled();`,
251
+ guidelines: [
252
+ "Use these matchers to assert that an element is enabled or disabled as required by the test scenario.",
253
+ ],
210
254
  },
211
255
  {
212
256
  signature: `not`,
213
257
  description: "Negates the expectation.",
214
258
  example: `await expect(await $('~spinner')).not.toBeDisplayed();`,
259
+ guidelines: [
260
+ "Use this matcher to invert the condition of any expectation, ensuring the element does not meet the specified criteria.",
261
+ ],
215
262
  },
216
263
  ],
217
264
  },
@@ -270,29 +317,6 @@ await driver.setGeoLocation({
270
317
  latitude: 37.7749,
271
318
  longitude: -122.4194,
272
319
  altitude: 10
273
- });
274
- `,
275
- },
276
- {
277
- signature: `driver.getContext() / driver.switchContext(name: string)`,
278
- description: "Gets or switches the current context (NATIVE_APP or WEBVIEW_xxx).",
279
- example: `
280
- const contexts = await driver.getContexts();
281
- await driver.switchContext(contexts[1]); // Switch to webview
282
- await driver.switchContext('NATIVE_APP'); // Switch back
283
- `,
284
- },
285
- {
286
- signature: `driver.rotate(params: { x: number; y: number; duration: number; radius: number; rotation: number; touchCount: number })`,
287
- description: "Simulates a rotation gesture on the device (iOS only).",
288
- example: `
289
- await driver.rotate({
290
- x: 100,
291
- y: 100,
292
- duration: 2,
293
- radius: 50,
294
- rotation: 180,
295
- touchCount: 2
296
320
  });
297
321
  `,
298
322
  },
@@ -331,49 +355,6 @@ await driver.performTouchAction({
331
355
  },
332
356
  ],
333
357
  },
334
- {
335
- title: "Web APIs (Hybrid / Mobile Web)",
336
- items: [
337
- {
338
- signature: `driver.switchContext('WEBVIEW')`,
339
- description: "Switches the context from native to web view (if a webview is present).",
340
- example: `
341
- const contexts = await driver.getContexts();
342
- await driver.switchContext(contexts.find(c => c.includes('WEBVIEW')));
343
- `,
344
- guidelines: [
345
- "Use this when your app has a webview or is a hybrid app.",
346
- ],
347
- },
348
- {
349
- signature: `$('css or xpath').click()`,
350
- description: "In a webview context, click (tap) a web element using CSS or XPath.",
351
- example: `await (await $('button#login')).click();`,
352
- },
353
- {
354
- signature: `$('selector').setValue('text')`,
355
- description: "In a webview context, sets text in a web input field.",
356
- example: `await (await $('input#username')).setValue('myusername');`,
357
- },
358
- {
359
- signature: `.getText()`,
360
- description: "Retrieves the visible text of a web element (hybrid/web context).",
361
- example: `
362
- const text = await (await $('h1.main-title')).getText();
363
- expect(text).toBe('Welcome to My App');
364
- `,
365
- },
366
- {
367
- signature: `driver.executeScript(script: string, args?: any[])`,
368
- description: "Executes JavaScript in the context of the webview.",
369
- example: `
370
- await driver.executeScript("document.getElementById('hidden-button').click()");
371
- const title = await driver.executeScript('return document.title');
372
- expect(title).toBe('My Page Title');
373
- `,
374
- },
375
- ],
376
- },
377
358
  ],
378
359
  };
379
360
  }
@@ -0,0 +1,3 @@
1
+ /** Waits till the view hierarchy is stable than returns it*/
2
+ export declare function waitForStableState(pollInterval?: number, timeout?: number): Promise<string | undefined>;
3
+ export declare function compareViewHierarchies(current: string, last: string): boolean;
@@ -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
+ }
@@ -0,0 +1,28 @@
1
+ export declare const config: {
2
+ runner: string;
3
+ specs: string[];
4
+ maxInstances: number;
5
+ capabilities: {
6
+ platformName: string;
7
+ deviceName: string;
8
+ automationName: string;
9
+ app: string;
10
+ }[];
11
+ logLevel: string;
12
+ bail: number;
13
+ baseUrl: string;
14
+ waitforTimeout: number;
15
+ connectionRetryTimeout: number;
16
+ connectionRetryCount: number;
17
+ services: string[];
18
+ appium: {
19
+ command: string;
20
+ };
21
+ framework: string;
22
+ reporters: string[];
23
+ mochaOpts: {
24
+ ui: string;
25
+ timeout: number;
26
+ };
27
+ before: () => void;
28
+ };
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.config = void 0;
4
+ // wdio.conf.ts
5
+ exports.config = {
6
+ runner: 'local',
7
+ specs: ['./examples/**/*.test.ts'],
8
+ maxInstances: 1,
9
+ capabilities: [{
10
+ platformName: 'iOS',
11
+ deviceName: 'iPhone 14',
12
+ automationName: 'XCUITest',
13
+ app: './Users/tzviels/Library/Developer/Xcode/DerivedData/ExampleApp-ccggovixskkojadmtpriqiridvii/Build/Products/Debug-iphonesimulator/ExampleApp.app',
14
+ }],
15
+ logLevel: 'info',
16
+ bail: 0,
17
+ baseUrl: 'http://localhost',
18
+ waitforTimeout: 10000,
19
+ connectionRetryTimeout: 90000,
20
+ connectionRetryCount: 3,
21
+ services: ['appium'],
22
+ appium: {
23
+ command: 'appium',
24
+ },
25
+ framework: 'mocha',
26
+ reporters: ['spec'],
27
+ mochaOpts: {
28
+ ui: 'bdd',
29
+ timeout: 60000,
30
+ },
31
+ before: function () {
32
+ global.driver = browser;
33
+ },
34
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix-pilot/webdriverio-appium",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "WebdriverIO and Appium driver for Wix Pilot usage",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",