@samsara-dev/appwright 0.2.0

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.
Files changed (81) hide show
  1. package/.eslintrc.js +4 -0
  2. package/CHANGELOG.md +538 -0
  3. package/LICENSE +202 -0
  4. package/README.md +183 -0
  5. package/dist/bin/index.d.ts +3 -0
  6. package/dist/bin/index.d.ts.map +1 -0
  7. package/dist/bin/index.js +53 -0
  8. package/dist/config.d.ts +4 -0
  9. package/dist/config.d.ts.map +1 -0
  10. package/dist/config.js +65 -0
  11. package/dist/device/index.d.ts +171 -0
  12. package/dist/device/index.d.ts.map +1 -0
  13. package/dist/device/index.js +415 -0
  14. package/dist/fixture/index.d.ts +38 -0
  15. package/dist/fixture/index.d.ts.map +1 -0
  16. package/dist/fixture/index.js +78 -0
  17. package/dist/fixture/workerInfo.d.ts +27 -0
  18. package/dist/fixture/workerInfo.d.ts.map +1 -0
  19. package/dist/fixture/workerInfo.js +87 -0
  20. package/dist/global-setup.d.ts +5 -0
  21. package/dist/global-setup.d.ts.map +1 -0
  22. package/dist/global-setup.js +30 -0
  23. package/dist/index.d.ts +5 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +25 -0
  26. package/dist/locator/index.d.ts +25 -0
  27. package/dist/locator/index.d.ts.map +1 -0
  28. package/dist/locator/index.js +296 -0
  29. package/dist/logger.d.ts +9 -0
  30. package/dist/logger.d.ts.map +1 -0
  31. package/dist/logger.js +19 -0
  32. package/dist/providers/appium.d.ts +15 -0
  33. package/dist/providers/appium.d.ts.map +1 -0
  34. package/dist/providers/appium.js +274 -0
  35. package/dist/providers/browserstack/index.d.ts +26 -0
  36. package/dist/providers/browserstack/index.d.ts.map +1 -0
  37. package/dist/providers/browserstack/index.js +272 -0
  38. package/dist/providers/browserstack/utils.d.ts +2 -0
  39. package/dist/providers/browserstack/utils.d.ts.map +1 -0
  40. package/dist/providers/browserstack/utils.js +34 -0
  41. package/dist/providers/emulator/index.d.ts +13 -0
  42. package/dist/providers/emulator/index.d.ts.map +1 -0
  43. package/dist/providers/emulator/index.js +86 -0
  44. package/dist/providers/index.d.ts +5 -0
  45. package/dist/providers/index.d.ts.map +1 -0
  46. package/dist/providers/index.js +31 -0
  47. package/dist/providers/lambdatest/index.d.ts +27 -0
  48. package/dist/providers/lambdatest/index.d.ts.map +1 -0
  49. package/dist/providers/lambdatest/index.js +280 -0
  50. package/dist/providers/lambdatest/utils.d.ts +3 -0
  51. package/dist/providers/lambdatest/utils.d.ts.map +1 -0
  52. package/dist/providers/lambdatest/utils.js +36 -0
  53. package/dist/providers/local/index.d.ts +13 -0
  54. package/dist/providers/local/index.d.ts.map +1 -0
  55. package/dist/providers/local/index.js +86 -0
  56. package/dist/reporter.d.ts +13 -0
  57. package/dist/reporter.d.ts.map +1 -0
  58. package/dist/reporter.js +216 -0
  59. package/dist/tests/locator.spec.d.ts +2 -0
  60. package/dist/tests/locator.spec.d.ts.map +1 -0
  61. package/dist/tests/locator.spec.js +89 -0
  62. package/dist/tests/regex.spec.d.ts +2 -0
  63. package/dist/tests/regex.spec.d.ts.map +1 -0
  64. package/dist/tests/regex.spec.js +19 -0
  65. package/dist/tests/vitest.config.d.mts +3 -0
  66. package/dist/tests/vitest.config.d.mts.map +1 -0
  67. package/dist/tests/vitest.config.mjs +6 -0
  68. package/dist/types/errors.d.ts +7 -0
  69. package/dist/types/errors.d.ts.map +1 -0
  70. package/dist/types/errors.js +15 -0
  71. package/dist/types/index.d.ts +234 -0
  72. package/dist/types/index.d.ts.map +1 -0
  73. package/dist/types/index.js +22 -0
  74. package/dist/utils.d.ts +8 -0
  75. package/dist/utils.d.ts.map +1 -0
  76. package/dist/utils.js +66 -0
  77. package/dist/vision/index.d.ts +64 -0
  78. package/dist/vision/index.d.ts.map +1 -0
  79. package/dist/vision/index.js +106 -0
  80. package/package.json +63 -0
  81. package/tsconfig.json +15 -0
package/dist/utils.js ADDED
@@ -0,0 +1,66 @@
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.boxedStep = boxedStep;
7
+ exports.validateBuildPath = validateBuildPath;
8
+ exports.getLatestBuildToolsVersions = getLatestBuildToolsVersions;
9
+ exports.longestDeterministicGroup = longestDeterministicGroup;
10
+ exports.basePath = basePath;
11
+ const test_1 = __importDefault(require("@playwright/test"));
12
+ const fs_1 = __importDefault(require("fs"));
13
+ const path_1 = __importDefault(require("path"));
14
+ function boxedStep(target, context) {
15
+ return function replacementMethod(...args) {
16
+ const path = this.selector ? `("${this.selector}")` : "";
17
+ const argsString = args.length
18
+ ? "(" +
19
+ Array.from(args)
20
+ .map((a) => JSON.stringify(a))
21
+ .join(" , ") +
22
+ ")"
23
+ : "";
24
+ const name = `${context.name}${path}${argsString}`;
25
+ return test_1.default.step(name, async () => {
26
+ return await target.call(this, ...args);
27
+ }, { box: true });
28
+ };
29
+ }
30
+ function validateBuildPath(buildPath, expectedExtension) {
31
+ if (!buildPath) {
32
+ throw new Error(`Build path not found. Please set the build path in appwright.config.ts`);
33
+ }
34
+ if (!buildPath.endsWith(expectedExtension)) {
35
+ throw new Error(`File path is not supported for the given combination of platform and provider. Please provide build with ${expectedExtension} file extension in the appwright.config.ts`);
36
+ }
37
+ if (!fs_1.default.existsSync(buildPath)) {
38
+ throw new Error(`File not found at given path: ${buildPath}
39
+ Please provide the correct path of the build.`);
40
+ }
41
+ }
42
+ function getLatestBuildToolsVersions(versions) {
43
+ return versions.sort((a, b) => (a > b ? -1 : 1))[0];
44
+ }
45
+ function longestDeterministicGroup(pattern) {
46
+ const patternToString = pattern.toString();
47
+ const matches = [...patternToString.matchAll(/\(([^)]+)\)/g)].map((match) => match[1]);
48
+ if (!matches || !matches.length) {
49
+ return undefined;
50
+ }
51
+ const noSpecialChars = matches.filter((match) => {
52
+ if (!match) {
53
+ return false;
54
+ }
55
+ const regexSpecialCharsPattern = /[.*+?^${}()|[\]\\]/;
56
+ return !regexSpecialCharsPattern.test(match);
57
+ });
58
+ const longestString = noSpecialChars.reduce((max, str) => (str.length > max.length ? str : max), "");
59
+ if (longestString == "") {
60
+ return undefined;
61
+ }
62
+ return longestString;
63
+ }
64
+ function basePath() {
65
+ return path_1.default.join(process.cwd(), "playwright-report", "data", "videos-store");
66
+ }
@@ -0,0 +1,64 @@
1
+ import { Client as WebDriverClient } from "webdriver";
2
+ import { Device } from "../device";
3
+ import { z } from "zod";
4
+ import { LLMModel } from "@empiricalrun/llm";
5
+ import { ExtractType } from "../types";
6
+ export interface AppwrightVision {
7
+ /**
8
+ * Extracts text from the screenshot based on the specified prompt.
9
+ * Ensure the `OPENAI_API_KEY` environment variable is set to authenticate the API request.
10
+ *
11
+ * **Usage:**
12
+ * ```js
13
+ * await device.beta.query("Extract contact details present in the footer from the screenshot");
14
+ * ```
15
+ *
16
+ * @param prompt that defines the specific area or context from which text should be extracted.
17
+ * @returns
18
+ */
19
+ query<T extends z.ZodType>(prompt: string, options?: {
20
+ responseFormat?: T;
21
+ model?: LLMModel;
22
+ screenshot?: string;
23
+ telemetry?: {
24
+ tags?: string[];
25
+ };
26
+ }): Promise<ExtractType<T>>;
27
+ /**
28
+ * Performs a tap action on the screen based on the provided prompt.
29
+ * Ensure the `EMPIRICAL_API_KEY` environment variable is set to authenticate the API request.
30
+ *
31
+ * **Usage:**
32
+ * ```js
33
+ * await device.beta.tap("Tap on the search button");
34
+ * ```
35
+ *
36
+ * @param prompt that defines where on the screen the tap action should occur
37
+ */
38
+ tap(prompt: string, options?: {
39
+ useCache?: boolean;
40
+ telemetry?: {
41
+ tags?: string[];
42
+ };
43
+ }): Promise<{
44
+ x: number;
45
+ y: number;
46
+ }>;
47
+ }
48
+ export declare class VisionProvider {
49
+ private device;
50
+ private webDriverClient;
51
+ constructor(device: Device, webDriverClient: WebDriverClient);
52
+ query<T extends z.ZodType>(prompt: string, options?: {
53
+ responseFormat?: T;
54
+ model?: LLMModel;
55
+ screenshot?: string;
56
+ }): Promise<ExtractType<T>>;
57
+ tap(prompt: string, options?: {
58
+ useCache?: boolean;
59
+ }): Promise<{
60
+ x: number;
61
+ y: number;
62
+ }>;
63
+ }
64
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vision/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,MAAM,IAAI,eAAe,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAGnC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAGvC,MAAM,WAAW,eAAe;IAC9B;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EACvB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;QACR,cAAc,CAAC,EAAE,CAAC,CAAC;QACnB,KAAK,CAAC,EAAE,QAAQ,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,SAAS,CAAC,EAAE;YACV,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;SACjB,CAAC;KACH,GACA,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;IAE3B;;;;;;;;;;OAUG;IACH,GAAG,CACD,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;QACR,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,SAAS,CAAC,EAAE;YACV,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;SACjB,CAAC;KACH,GACA,OAAO,CAAC;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACtC;AAED,qBAAa,cAAc;IAEvB,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,eAAe;gBADf,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,eAAe;IAIpC,KAAK,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAC7B,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;QACR,cAAc,CAAC,EAAE,CAAC,CAAC;QACnB,KAAK,CAAC,EAAE,QAAQ,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GACA,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IAapB,GAAG,CACP,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAC/B,OAAO,CAAC;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAsCrC"}
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
3
+ var useValue = arguments.length > 2;
4
+ for (var i = 0; i < initializers.length; i++) {
5
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
6
+ }
7
+ return useValue ? value : void 0;
8
+ };
9
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
10
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
11
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
12
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
13
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
14
+ var _, done = false;
15
+ for (var i = decorators.length - 1; i >= 0; i--) {
16
+ var context = {};
17
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
18
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
19
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
20
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
21
+ if (kind === "accessor") {
22
+ if (result === void 0) continue;
23
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
24
+ if (_ = accept(result.get)) descriptor.get = _;
25
+ if (_ = accept(result.set)) descriptor.set = _;
26
+ if (_ = accept(result.init)) initializers.unshift(_);
27
+ }
28
+ else if (_ = accept(result)) {
29
+ if (kind === "field") initializers.unshift(_);
30
+ else descriptor[key] = _;
31
+ }
32
+ }
33
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
34
+ done = true;
35
+ };
36
+ var __importDefault = (this && this.__importDefault) || function (mod) {
37
+ return (mod && mod.__esModule) ? mod : { "default": mod };
38
+ };
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.VisionProvider = void 0;
41
+ const vision_1 = require("@empiricalrun/llm/vision");
42
+ const point_1 = require("@empiricalrun/llm/vision/point");
43
+ const fs_1 = __importDefault(require("fs"));
44
+ const test_1 = __importDefault(require("@playwright/test"));
45
+ const utils_1 = require("../utils");
46
+ const logger_1 = require("../logger");
47
+ let VisionProvider = (() => {
48
+ let _instanceExtraInitializers = [];
49
+ let _query_decorators;
50
+ let _tap_decorators;
51
+ return class VisionProvider {
52
+ static {
53
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
54
+ _query_decorators = [utils_1.boxedStep];
55
+ _tap_decorators = [utils_1.boxedStep];
56
+ __esDecorate(this, null, _query_decorators, { kind: "method", name: "query", static: false, private: false, access: { has: obj => "query" in obj, get: obj => obj.query }, metadata: _metadata }, null, _instanceExtraInitializers);
57
+ __esDecorate(this, null, _tap_decorators, { kind: "method", name: "tap", static: false, private: false, access: { has: obj => "tap" in obj, get: obj => obj.tap }, metadata: _metadata }, null, _instanceExtraInitializers);
58
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
59
+ }
60
+ device = __runInitializers(this, _instanceExtraInitializers);
61
+ webDriverClient;
62
+ constructor(device, webDriverClient) {
63
+ this.device = device;
64
+ this.webDriverClient = webDriverClient;
65
+ }
66
+ async query(prompt, options) {
67
+ test_1.default.skip(!process.env.OPENAI_API_KEY, "LLM vision based extract text is not enabled. Set the OPENAI_API_KEY environment variable to enable it");
68
+ let base64Screenshot = options?.screenshot;
69
+ if (!base64Screenshot) {
70
+ base64Screenshot = await this.webDriverClient.takeScreenshot();
71
+ }
72
+ return await (0, vision_1.query)(base64Screenshot, prompt, options);
73
+ }
74
+ async tap(prompt, options) {
75
+ test_1.default.skip(!process.env.EMPIRICAL_API_KEY, "LLM vision based tap is not enabled. Set the EMPIRICAL_API_KEY environment variable to enable it");
76
+ const base64Image = await this.webDriverClient.takeScreenshot();
77
+ const coordinates = await (0, point_1.getCoordinatesFor)(prompt, base64Image, options);
78
+ if (coordinates.annotatedImage) {
79
+ const random = Math.floor(1000 + Math.random() * 9000);
80
+ const file = test_1.default.info().outputPath(`${random}.png`);
81
+ await fs_1.default.promises.writeFile(file, Buffer.from(coordinates.annotatedImage, "base64"));
82
+ await test_1.default.info().attach(`${random}`, { path: file });
83
+ }
84
+ const driverSize = await this.webDriverClient.getWindowRect();
85
+ const { container: imageSize, x, y } = coordinates;
86
+ const scaleFactorWidth = imageSize.width / driverSize.width;
87
+ const scaleFactorHeight = imageSize.height / driverSize.height;
88
+ if (scaleFactorWidth !== scaleFactorHeight) {
89
+ logger_1.logger.warn(`Scale factors are different: ${scaleFactorWidth} vs ${scaleFactorHeight}`);
90
+ }
91
+ const tapTargetX = x / scaleFactorWidth;
92
+ // This uses the width scale factor because getWindowRect on LambdaTest returns a smaller
93
+ // height value than the screenshot height, which causes disproportionate scaling
94
+ // for width and height.
95
+ // For example, Pixel 8 screenshot is 1080 (w) x 2400 (h), but LambdaTest returns
96
+ // 1080 (w) x 2142 (h) for getWindowRect.
97
+ const tapTargetY = y / scaleFactorWidth;
98
+ await this.device.tap({
99
+ x: tapTargetX,
100
+ y: tapTargetY,
101
+ });
102
+ return { x: tapTargetX, y: tapTargetY };
103
+ }
104
+ };
105
+ })();
106
+ exports.VisionProvider = VisionProvider;
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@samsara-dev/appwright",
3
+ "version": "0.2.0",
4
+ "publishConfig": {
5
+ "registry": "https://registry.npmjs.org/",
6
+ "access": "public"
7
+ },
8
+ "main": "dist/index.js",
9
+ "engines": {
10
+ "node": ">=20.19.0"
11
+ },
12
+ "bin": {
13
+ "appwright": "dist/bin/index.js"
14
+ },
15
+ "scripts": {
16
+ "lint": "eslint .",
17
+ "test": "vitest --config ./src/tests/vitest.config.mts",
18
+ "build": "tsc --build",
19
+ "changeset": "changeset",
20
+ "clean": "tsc --build --clean",
21
+ "release": "changeset publish",
22
+ "build:doc": "typedoc --out api-references src"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/bheemreddy-samsara/appwright.git"
27
+ },
28
+ "keywords": [
29
+ "e2e",
30
+ "automation",
31
+ "ios",
32
+ "android",
33
+ "testing"
34
+ ],
35
+ "author": "bheem.reddy@samsara.com",
36
+ "license": "Apache-2.0",
37
+ "description": "E2E mobile app testing done right, with the Playwright test runner",
38
+ "dependencies": {
39
+ "@empiricalrun/llm": "^0.9.25",
40
+ "@ffmpeg-installer/ffmpeg": "^1.1.0",
41
+ "@playwright/test": "^1.56.1",
42
+ "appium": "^3.1.0",
43
+ "appium-uiautomator2-driver": "^5.0.7",
44
+ "appium-xcuitest-driver": "^10.2.2",
45
+ "async-retry": "^1.3.3",
46
+ "fluent-ffmpeg": "^2.1.3",
47
+ "form-data": "4.0.0",
48
+ "node-fetch": "^3.3.2",
49
+ "picocolors": "^1.1.0",
50
+ "webdriver": "^8.36.1"
51
+ },
52
+ "devDependencies": {
53
+ "@changesets/cli": "^2.27.8",
54
+ "@empiricalrun/eslint-config": "^0.4.1",
55
+ "@empiricalrun/typescript-config": "^0.3.0",
56
+ "@types/async-retry": "^1.4.8",
57
+ "@types/fluent-ffmpeg": "^2.1.26",
58
+ "@types/node": "^22.5.2",
59
+ "eslint": "8.57.0",
60
+ "typedoc": "0.26.7",
61
+ "vitest": "^2.1.3"
62
+ }
63
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": "@empiricalrun/typescript-config/base",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": [
8
+ "src/**/*"
9
+ ],
10
+ "exclude": [
11
+ "node_modules",
12
+ "dist",
13
+ "src/**/*.test.ts"
14
+ ]
15
+ }