@samsara-dev/appwright 0.6.0 → 0.6.1
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/AGENTS.md +48 -63
- package/CHANGELOG.md +7 -0
- package/dist/providers/browserstack/index.d.ts.map +1 -1
- package/dist/providers/browserstack/index.js +47 -32
- package/dist/providers/browserstack/s3.d.ts +11 -0
- package/dist/providers/browserstack/s3.d.ts.map +1 -0
- package/dist/providers/browserstack/s3.js +102 -0
- package/dist/tests/browserstack.s3.spec.d.ts +2 -0
- package/dist/tests/browserstack.s3.spec.d.ts.map +1 -0
- package/dist/tests/browserstack.s3.spec.js +78 -0
- package/package.json +2 -1
package/AGENTS.md
CHANGED
|
@@ -1,68 +1,53 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Appwright Agent Guide
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- **Package name:** `@samsara-dev/appwright`
|
|
8
|
-
- **Language:** TypeScript targeting Node.js ≥ 20.19
|
|
9
|
-
- **Distribution entry:** `dist/index.js` (compiled via `npm run build`)
|
|
10
|
-
- **Primary domains:** Mobile app end-to-end automation on real devices or device farms (BrowserStack, LambdaTest, local emulators)
|
|
11
|
-
|
|
12
|
-
## Repository Layout
|
|
13
|
-
|
|
14
|
-
- `src/` – TypeScript sources (single source of truth)
|
|
15
|
-
- `dist/` – Generated JavaScript and type declarations; never edit manually
|
|
16
|
-
- `example/` – Sample usage and fixtures
|
|
17
|
-
- `docs/` – Generated documentation assets
|
|
18
|
-
- `.changeset/` – Release notes authored via Changesets
|
|
19
|
-
- `node_modules/` – Managed dependencies (do not commit edits)
|
|
20
|
-
|
|
21
|
-
## Core Concepts & Types
|
|
22
|
-
|
|
23
|
-
- `Device` class wraps a WebDriver client for mobile-specific flows and is exported from `src/index.ts`.
|
|
24
|
-
- `Locator` utilities hold find strategies and timeout behavior derived from `TimeoutOptions`.
|
|
25
|
-
- Providers (`src/providers/*`) bootstrap sessions for BrowserStack, LambdaTest, local, and emulator contexts.
|
|
26
|
-
- Vision utilities in `src/vision` offer computer-vision-assisted interactions.
|
|
27
|
-
|
|
28
|
-
## Key Commands
|
|
3
|
+
## 1. Project Snapshot
|
|
4
|
+
- **Repo type:** Single-package TypeScript library (no workspaces)
|
|
5
|
+
- **Stack:** Node.js ≥20, TypeScript, Playwright fixtures, Vitest, ESLint (`@empiricalrun`)
|
|
6
|
+
- **Docs:** Each major directory ships its own `AGENTS.md`; nearest file wins
|
|
29
7
|
|
|
8
|
+
## 2. Root Setup Commands
|
|
30
9
|
```bash
|
|
31
|
-
npm
|
|
32
|
-
npm
|
|
33
|
-
|
|
34
|
-
npm run
|
|
35
|
-
npm run changeset
|
|
10
|
+
npm ci # install dependencies (use npm install for incremental updates)
|
|
11
|
+
npm run lint # ESLint with @empiricalrun rules
|
|
12
|
+
npm run build # tsc --build (typecheck + emit to dist/)
|
|
13
|
+
npm test -- --run # Vitest single pass (avoids interactive watch mode)
|
|
14
|
+
npm run changeset # prepare release notes when changes ship
|
|
36
15
|
```
|
|
37
16
|
|
|
38
|
-
##
|
|
39
|
-
|
|
40
|
-
-
|
|
41
|
-
- `
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
17
|
+
## 3. Universal Conventions
|
|
18
|
+
- Share a short plan with maintainers **before editing** (hard requirement)
|
|
19
|
+
- Author new code in `src/**`; never modify generated `dist/**`
|
|
20
|
+
- Rely on `src/logger.ts` instead of raw `console` (exceptions stream subprocess output only)
|
|
21
|
+
- Follow Conventional Commits (`feat:`, `fix:`, `chore:`) and branch from `main`
|
|
22
|
+
- Keep APIs backward compatible; prefer additive options over breaking changes
|
|
23
|
+
- Always generate a Changeset that captures the user-facing impact of the feature you worked on
|
|
24
|
+
|
|
25
|
+
## 4. Security & Secrets
|
|
26
|
+
- Never commit API keys, BrowserStack creds, or AWS secrets; load via environment variables
|
|
27
|
+
- BrowserStack needs `BROWSERSTACK_USERNAME` / `BROWSERSTACK_ACCESS_KEY`
|
|
28
|
+
- Remote build downloads require AWS credentials (`AWS_REGION` plus standard SDK env vars)
|
|
29
|
+
- Avoid storing PII or test artifacts in the repo; use external storage for logs/videos
|
|
30
|
+
|
|
31
|
+
## 5. JIT Index (what to open, not what to paste)
|
|
32
|
+
|
|
33
|
+
### Package Structure
|
|
34
|
+
- Core library: `src/` → [see src/AGENTS.md](src/AGENTS.md)
|
|
35
|
+
- Device runtime: `src/device/` → [see src/device/AGENTS.md](src/device/AGENTS.md)
|
|
36
|
+
- Providers (BrowserStack, LambdaTest, emulator, local): `src/providers/` → [see src/providers/AGENTS.md](src/providers/AGENTS.md)
|
|
37
|
+
- Vision utilities: `src/vision/` → [see src/vision/AGENTS.md](src/vision/AGENTS.md)
|
|
38
|
+
- Visual trace capture: `src/visualTrace/` → [see src/visualTrace/AGENTS.md](src/visualTrace/AGENTS.md)
|
|
39
|
+
- Test suite: `src/tests/` → [see src/tests/AGENTS.md](src/tests/AGENTS.md)
|
|
40
|
+
- Example consumer app: `example/` → [see example/AGENTS.md](example/AGENTS.md)
|
|
41
|
+
|
|
42
|
+
### Quick Find Commands
|
|
43
|
+
- Locate a public export: `rg -n "export .*" src/index.ts src/types` (fallback: `grep -rn "export .*" src/index.ts src/types`)
|
|
44
|
+
- Discover provider hooks: `rg -n "DeviceProvider" src/providers` (fallback: `grep -rn "DeviceProvider" src/providers`)
|
|
45
|
+
- Inspect Playwright fixtures: `rg -n "extend<TestLevelFixtures" src/fixture` (fallback: `grep -rn "extend<TestLevelFixtures" src/fixture`)
|
|
46
|
+
- Track vision helpers: `rg -n "AppwrightVision" src/vision` (fallback: `grep -rn "AppwrightVision" src/vision`)
|
|
47
|
+
- Find targeted tests: `rg -n "\\.spec\\.ts" src/tests` (fallback: `grep -rn "\.spec\.ts" src/tests`)
|
|
48
|
+
|
|
49
|
+
## 6. Definition of Done
|
|
50
|
+
- `npm run lint && npm run build && npm test -- --run` must all pass locally
|
|
51
|
+
- Add a Changeset entry for user-facing changes
|
|
52
|
+
- Ensure documentation in relevant `AGENTS.md` files reflects the update
|
|
53
|
+
- Confirm the plan was shared and acknowledged before merging
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# appwright
|
|
2
2
|
|
|
3
|
+
## 0.6.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 40979ac: - add BrowserStack S3 buildPath support with local download helper and coverage
|
|
8
|
+
- restructure hierarchical AGENTS.md guidance and reminders for contributors
|
|
9
|
+
|
|
3
10
|
## 0.6.0
|
|
4
11
|
|
|
5
12
|
### Minor Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/providers/browserstack/index.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,eAAe,EACf,cAAc,EAGf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/providers/browserstack/index.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,eAAe,EACf,cAAc,EAGf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAsDtC,qBAAa,0BAA2B,YAAW,cAAc;IAC/D,OAAO,CAAC,cAAc,CAAC,CAA6B;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,OAAO,CAA+B;gBAG5C,OAAO,EAAE,WAAW,CAAC,eAAe,CAAC,EACrC,WAAW,EAAE,MAAM,GAAG,SAAS;IAU3B,WAAW;IA0EX,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC;IAMlC,OAAO,CAAC,cAAc;YASR,YAAY;YAiBZ,iBAAiB;YAKjB,yBAAyB;WAK1B,aAAa,CACxB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAuFlD,eAAe,CAAC,OAAO,EAAE;QAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf;IA2BD,OAAO,CAAC,YAAY;CAuFrB"}
|
|
@@ -11,6 +11,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
11
11
|
const types_1 = require("../../types");
|
|
12
12
|
const device_1 = require("../../device");
|
|
13
13
|
const logger_1 = require("../../logger");
|
|
14
|
+
const s3_1 = require("./s3");
|
|
14
15
|
const API_BASE_URL = "https://api-cloud.browserstack.com/app-automate";
|
|
15
16
|
const envVarKeyForBuild = (projectName) => `BROWSERSTACK_APP_URL_${projectName.toUpperCase()}`;
|
|
16
17
|
function getAuthHeader() {
|
|
@@ -50,45 +51,59 @@ class BrowserStackDeviceProvider {
|
|
|
50
51
|
throw new Error("BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY are required environment variables for this device provider.");
|
|
51
52
|
}
|
|
52
53
|
const buildPath = this.project.use.buildPath;
|
|
53
|
-
const
|
|
54
|
+
const isS3Url = (0, s3_1.isS3Uri)(buildPath);
|
|
55
|
+
const isHttpUrl = !isS3Url && buildPath.startsWith("http");
|
|
54
56
|
const isBrowserStackUrl = buildPath.startsWith("bs://");
|
|
55
57
|
let appUrl = undefined;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// Upload the file to BrowserStack and get the appUrl
|
|
61
|
-
let body;
|
|
62
|
-
let headers = {
|
|
63
|
-
Authorization: getAuthHeader(),
|
|
64
|
-
};
|
|
65
|
-
if (isHttpUrl) {
|
|
66
|
-
body = new URLSearchParams({
|
|
67
|
-
url: buildPath,
|
|
68
|
-
});
|
|
58
|
+
let downloadedArtifact;
|
|
59
|
+
try {
|
|
60
|
+
if (isBrowserStackUrl) {
|
|
61
|
+
appUrl = buildPath;
|
|
69
62
|
}
|
|
70
63
|
else {
|
|
71
|
-
|
|
72
|
-
|
|
64
|
+
// Upload the file to BrowserStack and get the appUrl
|
|
65
|
+
let body;
|
|
66
|
+
let headers = {
|
|
67
|
+
Authorization: getAuthHeader(),
|
|
68
|
+
};
|
|
69
|
+
let uploadSource = buildPath;
|
|
70
|
+
if (isS3Url) {
|
|
71
|
+
logger_1.logger.log(`Downloading build from S3: ${buildPath}`);
|
|
72
|
+
downloadedArtifact = await (0, s3_1.downloadS3Artifact)(buildPath);
|
|
73
|
+
uploadSource = downloadedArtifact.filePath;
|
|
74
|
+
}
|
|
75
|
+
if (isHttpUrl) {
|
|
76
|
+
body = new URLSearchParams({
|
|
77
|
+
url: buildPath,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
if (!fs_1.default.existsSync(uploadSource)) {
|
|
82
|
+
throw new Error(`Build file not found: ${uploadSource}`);
|
|
83
|
+
}
|
|
84
|
+
const form = new form_data_1.default();
|
|
85
|
+
form.append("file", fs_1.default.createReadStream(uploadSource));
|
|
86
|
+
headers = { ...headers, ...form.getHeaders() };
|
|
87
|
+
body = form;
|
|
88
|
+
}
|
|
89
|
+
const fetch = (await import("node-fetch")).default;
|
|
90
|
+
logger_1.logger.log(`Uploading build to BrowserStack: ${uploadSource}`);
|
|
91
|
+
const response = await fetch(`${API_BASE_URL}/upload`, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers,
|
|
94
|
+
body,
|
|
95
|
+
});
|
|
96
|
+
const data = await response.json();
|
|
97
|
+
appUrl = data.app_url;
|
|
98
|
+
if (!appUrl) {
|
|
99
|
+
logger_1.logger.error("Uploading the build failed:", data);
|
|
100
|
+
throw new Error(`Failed to upload build to BrowserStack: ${JSON.stringify(data)}`);
|
|
73
101
|
}
|
|
74
|
-
const form = new form_data_1.default();
|
|
75
|
-
form.append("file", fs_1.default.createReadStream(buildPath));
|
|
76
|
-
headers = { ...headers, ...form.getHeaders() };
|
|
77
|
-
body = form;
|
|
78
|
-
}
|
|
79
|
-
const fetch = (await import("node-fetch")).default;
|
|
80
|
-
logger_1.logger.log(`Uploading: ${buildPath}`);
|
|
81
|
-
const response = await fetch(`${API_BASE_URL}/upload`, {
|
|
82
|
-
method: "POST",
|
|
83
|
-
headers,
|
|
84
|
-
body,
|
|
85
|
-
});
|
|
86
|
-
const data = await response.json();
|
|
87
|
-
appUrl = data.app_url;
|
|
88
|
-
if (!appUrl) {
|
|
89
|
-
logger_1.logger.error("Uploading the build failed:", data);
|
|
90
102
|
}
|
|
91
103
|
}
|
|
104
|
+
finally {
|
|
105
|
+
await downloadedArtifact?.cleanup();
|
|
106
|
+
}
|
|
92
107
|
process.env[envVarKeyForBuild(this.project.name)] = appUrl;
|
|
93
108
|
}
|
|
94
109
|
async getDevice() {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type DownloadedS3Artifact = {
|
|
2
|
+
filePath: string;
|
|
3
|
+
cleanup: () => Promise<void>;
|
|
4
|
+
};
|
|
5
|
+
export declare function isS3Uri(value: string): boolean;
|
|
6
|
+
export declare function parseS3Uri(uri: string): {
|
|
7
|
+
bucket: string;
|
|
8
|
+
key: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function downloadS3Artifact(uri: string): Promise<DownloadedS3Artifact>;
|
|
11
|
+
//# sourceMappingURL=s3.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../../../src/providers/browserstack/s3.ts"],"names":[],"mappings":"AAeA,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAEF,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE9C;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAoBvE;AAgDD,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,oBAAoB,CAAC,CA8B/B"}
|
|
@@ -0,0 +1,102 @@
|
|
|
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.isS3Uri = isS3Uri;
|
|
7
|
+
exports.parseS3Uri = parseS3Uri;
|
|
8
|
+
exports.downloadS3Artifact = downloadS3Artifact;
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const promises_2 = require("stream/promises");
|
|
14
|
+
const stream_1 = require("stream");
|
|
15
|
+
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
16
|
+
const S3_SCHEME = "s3://";
|
|
17
|
+
const TEMP_PREFIX = "appwright-s3-";
|
|
18
|
+
function isS3Uri(value) {
|
|
19
|
+
return value.startsWith(S3_SCHEME);
|
|
20
|
+
}
|
|
21
|
+
function parseS3Uri(uri) {
|
|
22
|
+
if (!isS3Uri(uri)) {
|
|
23
|
+
throw new Error(`Invalid S3 URI: ${uri}`);
|
|
24
|
+
}
|
|
25
|
+
const remainder = uri.slice(S3_SCHEME.length);
|
|
26
|
+
const firstSlash = remainder.indexOf("/");
|
|
27
|
+
if (firstSlash === -1) {
|
|
28
|
+
throw new Error(`S3 URI must be in the format s3://bucket/key. Received: ${uri}`);
|
|
29
|
+
}
|
|
30
|
+
const bucket = remainder.slice(0, firstSlash);
|
|
31
|
+
const key = remainder.slice(firstSlash + 1);
|
|
32
|
+
if (!bucket || !key) {
|
|
33
|
+
throw new Error(`S3 URI must include both bucket and key: ${uri}`);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
bucket,
|
|
37
|
+
key: decodeURIComponent(key),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async function writeBodyToFile(body, destination) {
|
|
41
|
+
if (!body) {
|
|
42
|
+
throw new Error("Received empty S3 object body");
|
|
43
|
+
}
|
|
44
|
+
if (body instanceof stream_1.Readable) {
|
|
45
|
+
await (0, promises_2.pipeline)(body, fs_1.default.createWriteStream(destination));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (typeof body.transformToByteArray === "function") {
|
|
49
|
+
const bytes = await body.transformToByteArray();
|
|
50
|
+
await promises_1.default.writeFile(destination, Buffer.from(bytes));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (typeof body.arrayBuffer === "function") {
|
|
54
|
+
const buffer = Buffer.from(await body.arrayBuffer());
|
|
55
|
+
await promises_1.default.writeFile(destination, buffer);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (Symbol.asyncIterator in Object(body)) {
|
|
59
|
+
const chunks = [];
|
|
60
|
+
const iterable = body;
|
|
61
|
+
for await (const chunk of iterable) {
|
|
62
|
+
if (typeof chunk === "string") {
|
|
63
|
+
chunks.push(Buffer.from(chunk));
|
|
64
|
+
}
|
|
65
|
+
else if (chunk instanceof Uint8Array) {
|
|
66
|
+
chunks.push(Buffer.from(chunk));
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
chunks.push(Buffer.from(String(chunk)));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
await promises_1.default.writeFile(destination, Buffer.concat(chunks));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
throw new Error("Unsupported S3 response body type");
|
|
76
|
+
}
|
|
77
|
+
async function downloadS3Artifact(uri) {
|
|
78
|
+
const { bucket, key } = parseS3Uri(uri);
|
|
79
|
+
const region = process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION;
|
|
80
|
+
if (!region) {
|
|
81
|
+
throw new Error("Set AWS_REGION or AWS_DEFAULT_REGION to download builds from S3.");
|
|
82
|
+
}
|
|
83
|
+
const client = new client_s3_1.S3Client({ region });
|
|
84
|
+
const command = new client_s3_1.GetObjectCommand({ Bucket: bucket, Key: key });
|
|
85
|
+
const response = await client.send(command);
|
|
86
|
+
const tmpDir = await promises_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), TEMP_PREFIX));
|
|
87
|
+
const fileName = key.split("/").filter(Boolean).pop() ?? "artifact";
|
|
88
|
+
const destination = path_1.default.join(tmpDir, fileName);
|
|
89
|
+
try {
|
|
90
|
+
await writeBodyToFile(response.Body, destination);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
await promises_1.default.rm(tmpDir, { recursive: true, force: true });
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
filePath: destination,
|
|
98
|
+
cleanup: async () => {
|
|
99
|
+
await promises_1.default.rm(tmpDir, { recursive: true, force: true });
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browserstack.s3.spec.d.ts","sourceRoot":"","sources":["../../src/tests/browserstack.s3.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
const vitest_1 = require("vitest");
|
|
7
|
+
const stream_1 = require("stream");
|
|
8
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
|
+
const sendMock = vitest_1.vi.fn();
|
|
10
|
+
const capturedConfigs = [];
|
|
11
|
+
vitest_1.vi.mock("@aws-sdk/client-s3", () => {
|
|
12
|
+
class MockS3Client {
|
|
13
|
+
constructor(options) {
|
|
14
|
+
capturedConfigs.push(options);
|
|
15
|
+
}
|
|
16
|
+
send = sendMock;
|
|
17
|
+
}
|
|
18
|
+
class MockGetObjectCommand {
|
|
19
|
+
input;
|
|
20
|
+
constructor(input) {
|
|
21
|
+
this.input = input;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
S3Client: MockS3Client,
|
|
26
|
+
GetObjectCommand: MockGetObjectCommand,
|
|
27
|
+
__esModule: true,
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
let browserstackS3;
|
|
31
|
+
(0, vitest_1.beforeAll)(async () => {
|
|
32
|
+
browserstackS3 = await import("../providers/browserstack/s3.js");
|
|
33
|
+
});
|
|
34
|
+
(0, vitest_1.afterEach)(async () => {
|
|
35
|
+
sendMock.mockReset();
|
|
36
|
+
capturedConfigs.length = 0;
|
|
37
|
+
delete process.env.AWS_REGION;
|
|
38
|
+
delete process.env.AWS_DEFAULT_REGION;
|
|
39
|
+
});
|
|
40
|
+
(0, vitest_1.describe)("browserstack S3 helpers", () => {
|
|
41
|
+
(0, vitest_1.test)("parseS3Uri extracts bucket and decoded key", () => {
|
|
42
|
+
const result = browserstackS3.parseS3Uri("s3://my-bucket/builds/app%20v2.ipa");
|
|
43
|
+
(0, vitest_1.expect)(result).toEqual({
|
|
44
|
+
bucket: "my-bucket",
|
|
45
|
+
key: "builds/app v2.ipa",
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
(0, vitest_1.test)("downloadS3Artifact saves file locally and cleans up", async () => {
|
|
49
|
+
process.env.AWS_REGION = "us-west-2";
|
|
50
|
+
sendMock.mockResolvedValueOnce({
|
|
51
|
+
Body: stream_1.Readable.from(["test-binary"]),
|
|
52
|
+
});
|
|
53
|
+
const artifact = await browserstackS3.downloadS3Artifact("s3://test-bucket/apps/mobile.apk");
|
|
54
|
+
const fileContents = await promises_1.default.readFile(artifact.filePath, "utf-8");
|
|
55
|
+
(0, vitest_1.expect)(fileContents).toBe("test-binary");
|
|
56
|
+
(0, vitest_1.expect)(sendMock).toHaveBeenCalledTimes(1);
|
|
57
|
+
(0, vitest_1.expect)(sendMock.mock.calls[0]?.[0]).toMatchObject({
|
|
58
|
+
input: { Bucket: "test-bucket", Key: "apps/mobile.apk" },
|
|
59
|
+
});
|
|
60
|
+
(0, vitest_1.expect)(capturedConfigs[0]).toMatchObject({ region: "us-west-2" });
|
|
61
|
+
await artifact.cleanup();
|
|
62
|
+
await (0, vitest_1.expect)(promises_1.default.stat(artifact.filePath)).rejects.toMatchObject({
|
|
63
|
+
code: "ENOENT",
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
(0, vitest_1.test)("downloadS3Artifact throws when region is missing", async () => {
|
|
67
|
+
sendMock.mockResolvedValueOnce({
|
|
68
|
+
Body: stream_1.Readable.from(["unused"]),
|
|
69
|
+
});
|
|
70
|
+
await (0, vitest_1.expect)(browserstackS3.downloadS3Artifact("s3://bucket/key")).rejects.toThrow(/AWS_REGION/);
|
|
71
|
+
(0, vitest_1.expect)(sendMock).not.toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
(0, vitest_1.test)("isS3Uri differentiates schemes", () => {
|
|
74
|
+
(0, vitest_1.expect)(browserstackS3.isS3Uri("s3://bucket/key")).toBe(true);
|
|
75
|
+
(0, vitest_1.expect)(browserstackS3.isS3Uri("https://example.com/app.apk")).toBe(false);
|
|
76
|
+
(0, vitest_1.expect)(browserstackS3.isS3Uri("bs://sample-app")).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@samsara-dev/appwright",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"registry": "https://registry.npmjs.org/",
|
|
6
6
|
"access": "public"
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"license": "Apache-2.0",
|
|
37
37
|
"description": "E2E mobile app testing done right, with the Playwright test runner",
|
|
38
38
|
"dependencies": {
|
|
39
|
+
"@aws-sdk/client-s3": "^3.932.0",
|
|
39
40
|
"@empiricalrun/llm": "^0.9.25",
|
|
40
41
|
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
|
41
42
|
"@playwright/test": "^1.56.1",
|