@samsara-dev/appwright 0.9.13 → 0.9.16
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/CHANGELOG.md +24 -0
- package/dist/gptDriver/index.d.ts +2 -1
- package/dist/gptDriver/index.d.ts.map +1 -1
- package/dist/gptDriver/index.js +17 -1
- package/dist/reporter.d.ts +10 -0
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +81 -25
- package/dist/tests/gptDriver.spec.d.ts +2 -0
- package/dist/tests/gptDriver.spec.d.ts.map +1 -0
- package/dist/tests/gptDriver.spec.js +131 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# appwright
|
|
2
2
|
|
|
3
|
+
## 0.9.16
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7e73c82: Fix persistent device video download race condition that caused the process to hang when a worker had only one test (common with retries). When the 5-second delay wasn't enough for endTime to be written, the code misclassified the last test as intermediate and waited for a file that only the endTime download path would create. The intermediate test branch now polls for both the video file on disk and endTime being set, and all downloads go through a per-session lock to prevent concurrent writes.
|
|
8
|
+
|
|
9
|
+
## 0.9.15
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 7e70815: Fix stale testId in GPT Driver when using persistent device fixtures
|
|
14
|
+
|
|
15
|
+
Update testId on the cached gpt-driver-node instance before every API call
|
|
16
|
+
so persistent device fixtures (scope: "worker") report the correct test in
|
|
17
|
+
GPT Driver sessions. Previously the testId was baked in on first use and
|
|
18
|
+
never updated, causing all subsequent tests and resetToHome teardowns to
|
|
19
|
+
report the same stale testId on the MobileBoost dashboard.
|
|
20
|
+
|
|
21
|
+
## 0.9.14
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- c7b7722: Switch GPT Driver cachingMode from FULL_SCREEN to INTERACTION_REGION
|
|
26
|
+
|
|
3
27
|
## 0.9.13
|
|
4
28
|
|
|
5
29
|
### Patch Changes
|
|
@@ -2,7 +2,7 @@ import type { Client as WebDriverClient } from "webdriver";
|
|
|
2
2
|
import type { GptDriverConfig } from "../types";
|
|
3
3
|
/**
|
|
4
4
|
* Options for aiExecute. Intentionally exposes only appiumHandler;
|
|
5
|
-
* cachingMode is set globally at provider init (
|
|
5
|
+
* cachingMode is set globally at provider init (INTERACTION_REGION),
|
|
6
6
|
* and useSmartLoop is not yet surfaced.
|
|
7
7
|
*/
|
|
8
8
|
export interface AiExecuteOptions {
|
|
@@ -32,6 +32,7 @@ export declare class GptDriverProvider implements GptDriverApi {
|
|
|
32
32
|
private webDriverClient;
|
|
33
33
|
private options?;
|
|
34
34
|
private driver;
|
|
35
|
+
private testIdWarned;
|
|
35
36
|
constructor(webDriverClient: WebDriverClient, options?: GptDriverConfig | undefined);
|
|
36
37
|
private getAppiumUrl;
|
|
37
38
|
private getDriver;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/gptDriver/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,IAAI,eAAe,EAAE,MAAM,WAAW,CAAC;AAG3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAIhD;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1E,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAClE,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;CAC9D;AAED;;;;;;;;;GASG;AACH,qBAAa,iBAAkB,YAAW,YAAY;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/gptDriver/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,IAAI,eAAe,EAAE,MAAM,WAAW,CAAC;AAG3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAIhD;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1E,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAClE,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;CAC9D;AAED;;;;;;;;;GASG;AACH,qBAAa,iBAAkB,YAAW,YAAY;IAKlD,OAAO,CAAC,eAAe;IACvB,OAAO,CAAC,OAAO,CAAC;IALlB,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,YAAY,CAAS;gBAGnB,eAAe,EAAE,eAAe,EAChC,OAAO,CAAC,EAAE,eAAe,YAAA;IAGnC,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,SAAS;IA0BjB,OAAO,CAAC,aAAa;IA8BrB,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,kBAAkB;IAS1B,OAAO,CAAC,aAAa;IAMf,SAAS,CACb,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,IAAI,CAAC;IAkBV,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAaxC,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAa/C,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAajE,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAWnE"}
|
package/dist/gptDriver/index.js
CHANGED
|
@@ -77,6 +77,7 @@ let GptDriverProvider = (() => {
|
|
|
77
77
|
webDriverClient = __runInitializers(this, _instanceExtraInitializers);
|
|
78
78
|
options;
|
|
79
79
|
driver = null;
|
|
80
|
+
testIdWarned = false;
|
|
80
81
|
constructor(webDriverClient, options) {
|
|
81
82
|
this.webDriverClient = webDriverClient;
|
|
82
83
|
this.options = options;
|
|
@@ -100,7 +101,7 @@ let GptDriverProvider = (() => {
|
|
|
100
101
|
// gpt-driver-node expects webdriverio Browser type but webdriver Client is API-compatible at runtime
|
|
101
102
|
driver: this.webDriverClient,
|
|
102
103
|
serverConfig: { url: this.getAppiumUrl() },
|
|
103
|
-
cachingMode: "
|
|
104
|
+
cachingMode: "INTERACTION_REGION",
|
|
104
105
|
testId: test_1.default.info()?.testId ?? `test-${Date.now()}`,
|
|
105
106
|
...(this.options?.additionalUserContext != null && {
|
|
106
107
|
additionalUserContext: this.options.additionalUserContext,
|
|
@@ -115,6 +116,21 @@ let GptDriverProvider = (() => {
|
|
|
115
116
|
test_1.default.skip(true, "GPT Driver not configured. Set GPT_DRIVER_API_KEY environment variable.");
|
|
116
117
|
throw new Error("GPT Driver not configured");
|
|
117
118
|
}
|
|
119
|
+
// Update testId to current test context so persistent device fixtures
|
|
120
|
+
// report the correct test in GPT Driver API calls.
|
|
121
|
+
// gpt-driver-node declares testId as private in TS but it's a plain
|
|
122
|
+
// JS property at runtime — safe to mutate directly.
|
|
123
|
+
// TODO: Replace with public setTestId() if gpt-driver-node exposes one.
|
|
124
|
+
const currentTestId = test_1.default.info()?.testId;
|
|
125
|
+
if (currentTestId && "testId" in driver) {
|
|
126
|
+
driver.testId = currentTestId;
|
|
127
|
+
}
|
|
128
|
+
else if (currentTestId && !this.testIdWarned) {
|
|
129
|
+
console.warn("[GptDriver] Cannot update testId — property not found on driver instance. " +
|
|
130
|
+
"GPT Driver sessions may be attributed to the wrong test. " +
|
|
131
|
+
"Check if gpt-driver-node renamed or removed the testId field.");
|
|
132
|
+
this.testIdWarned = true;
|
|
133
|
+
}
|
|
118
134
|
return driver;
|
|
119
135
|
}
|
|
120
136
|
validateInstruction(instruction, methodName) {
|
package/dist/reporter.d.ts
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import type { Reporter, TestCase, TestResult } from "@playwright/test/reporter";
|
|
2
2
|
declare class VideoDownloader implements Reporter {
|
|
3
3
|
private downloadPromises;
|
|
4
|
+
private sessionDownloadLocks;
|
|
4
5
|
onBegin(): void;
|
|
5
6
|
onTestBegin(test: TestCase, result: TestResult): void;
|
|
6
7
|
onTestEnd(test: TestCase, result: TestResult): void;
|
|
7
8
|
onEnd(): Promise<void>;
|
|
8
9
|
private trimAndAttachPersistentDeviceVideo;
|
|
10
|
+
/**
|
|
11
|
+
* Ensures exactly one download per session. The first caller triggers the
|
|
12
|
+
* download; concurrent callers await the same promise. Returns the
|
|
13
|
+
* downloaded file path, or null if the download failed.
|
|
14
|
+
*
|
|
15
|
+
* Keyed by sessionId (not workerIndex) because Playwright can reuse
|
|
16
|
+
* workerIndex across worker restarts with different BrowserStack sessions.
|
|
17
|
+
*/
|
|
18
|
+
private ensureWorkerVideoDownloaded;
|
|
9
19
|
private downloadAndAttachDeviceVideo;
|
|
10
20
|
private providerSupportsVideo;
|
|
11
21
|
}
|
package/dist/reporter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAUhF,cAAM,eAAgB,YAAW,QAAQ;IACvC,OAAO,CAAC,gBAAgB,CAAsB;
|
|
1
|
+
{"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAUhF,cAAM,eAAgB,YAAW,QAAQ;IACvC,OAAO,CAAC,gBAAgB,CAAsB;IAK9C,OAAO,CAAC,oBAAoB,CAA6C;IAEzE,OAAO;IAQP,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU;IAU9C,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU;IAgItC,KAAK;YAKG,kCAAkC;IA6ChD;;;;;;;OAOG;YACW,2BAA2B;IAqBzC,OAAO,CAAC,4BAA4B;IAyBpC,OAAO,CAAC,qBAAqB;CAU9B;AA0ID,eAAe,eAAe,CAAC"}
|
package/dist/reporter.js
CHANGED
|
@@ -13,6 +13,11 @@ const utils_1 = require("./utils");
|
|
|
13
13
|
const workerInfo_1 = require("./fixture/workerInfo");
|
|
14
14
|
class VideoDownloader {
|
|
15
15
|
downloadPromises = [];
|
|
16
|
+
// Ensures only one download per session — prevents concurrent downloads
|
|
17
|
+
// when multiple tests on the same worker resolve endTime simultaneously.
|
|
18
|
+
// Keyed by sessionId (not workerIndex) because Playwright can reuse
|
|
19
|
+
// workerIndex across worker restarts with different sessions.
|
|
20
|
+
sessionDownloadLocks = new Map();
|
|
16
21
|
onBegin() {
|
|
17
22
|
if (fs_1.default.existsSync((0, utils_1.basePath)())) {
|
|
18
23
|
fs_1.default.rmSync((0, utils_1.basePath)(), {
|
|
@@ -53,7 +58,7 @@ class VideoDownloader {
|
|
|
53
58
|
type: "workerInfo",
|
|
54
59
|
description: `Ran on worker #${workerIndex}.`,
|
|
55
60
|
});
|
|
56
|
-
// The `onTestEnd`
|
|
61
|
+
// The `onTestEnd` method is called before the worker ends and
|
|
57
62
|
// the worker's `endTime` is saved to disk. We add a 5 secs delay
|
|
58
63
|
// to prevent a harmful race condition.
|
|
59
64
|
const workerDownload = getWorkerInfo(workerIndex)
|
|
@@ -77,21 +82,37 @@ class VideoDownloader {
|
|
|
77
82
|
return; // Nothing to do here
|
|
78
83
|
}
|
|
79
84
|
const workerVideoBaseName = `worker-${workerIndex}-${sessionId}-video`;
|
|
85
|
+
const expectedWorkerVideoPath = path_1.default.join((0, utils_1.basePath)(), `${workerVideoBaseName}.mp4`);
|
|
80
86
|
if (endTime) {
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
if (!
|
|
87
|
+
// Last test in the worker — download through the lock
|
|
88
|
+
// so concurrent endTime resolutions don't race.
|
|
89
|
+
const downloadedPath = await this.ensureWorkerVideoDownloaded(sessionId, providerName, workerVideoBaseName);
|
|
90
|
+
if (!downloadedPath) {
|
|
85
91
|
return;
|
|
86
92
|
}
|
|
87
|
-
return this.trimAndAttachPersistentDeviceVideo(test, result,
|
|
93
|
+
return this.trimAndAttachPersistentDeviceVideo(test, result, downloadedPath);
|
|
88
94
|
}
|
|
89
95
|
else {
|
|
90
|
-
// This
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
// This looks like an intermediate test, but endTime may not
|
|
97
|
+
// have been written yet (race condition — common when a worker
|
|
98
|
+
// has only one test, e.g. retries). Poll for EITHER the video
|
|
99
|
+
// file on disk (written by the last test's download) OR
|
|
100
|
+
// endTime appearing (meaning we need to download ourselves).
|
|
101
|
+
const resolved = await waitForFileOrEndTime(expectedWorkerVideoPath, workerIndex);
|
|
102
|
+
if (resolved === "file") {
|
|
103
|
+
return this.trimAndAttachPersistentDeviceVideo(test, result, expectedWorkerVideoPath);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
// endTime was set but file doesn't exist yet. Use the
|
|
107
|
+
// per-session lock so only one test triggers the download;
|
|
108
|
+
// others await the same promise.
|
|
109
|
+
const downloadedPath = await this.ensureWorkerVideoDownloaded(sessionId, providerName, workerVideoBaseName);
|
|
110
|
+
if (!downloadedPath) {
|
|
111
|
+
logger_1.logger.error(`Video download returned null for session ${sessionId}, skipping attachment`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
return this.trimAndAttachPersistentDeviceVideo(test, result, downloadedPath);
|
|
115
|
+
}
|
|
95
116
|
}
|
|
96
117
|
});
|
|
97
118
|
})
|
|
@@ -146,6 +167,25 @@ class VideoDownloader {
|
|
|
146
167
|
name: "video",
|
|
147
168
|
});
|
|
148
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Ensures exactly one download per session. The first caller triggers the
|
|
172
|
+
* download; concurrent callers await the same promise. Returns the
|
|
173
|
+
* downloaded file path, or null if the download failed.
|
|
174
|
+
*
|
|
175
|
+
* Keyed by sessionId (not workerIndex) because Playwright can reuse
|
|
176
|
+
* workerIndex across worker restarts with different BrowserStack sessions.
|
|
177
|
+
*/
|
|
178
|
+
async ensureWorkerVideoDownloaded(sessionId, providerName, workerVideoBaseName) {
|
|
179
|
+
if (!this.sessionDownloadLocks.has(sessionId)) {
|
|
180
|
+
const downloadPromise = (async () => {
|
|
181
|
+
const provider = (0, providers_1.getProviderClass)(providerName);
|
|
182
|
+
const downloaded = await provider.downloadVideo(sessionId, (0, utils_1.basePath)(), workerVideoBaseName);
|
|
183
|
+
return downloaded?.path ?? null;
|
|
184
|
+
})();
|
|
185
|
+
this.sessionDownloadLocks.set(sessionId, downloadPromise);
|
|
186
|
+
}
|
|
187
|
+
return this.sessionDownloadLocks.get(sessionId);
|
|
188
|
+
}
|
|
149
189
|
downloadAndAttachDeviceVideo(test, result, providerClass, sessionId) {
|
|
150
190
|
const videoFileName = `${sessionId}-${test.id}`;
|
|
151
191
|
if (!providerClass.downloadVideo) {
|
|
@@ -174,21 +214,37 @@ class VideoDownloader {
|
|
|
174
214
|
return !!provider.downloadVideo;
|
|
175
215
|
}
|
|
176
216
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
217
|
+
/**
|
|
218
|
+
* Poll until the video file appears on disk OR the worker's endTime is set.
|
|
219
|
+
*
|
|
220
|
+
* When a worker has only one test (common with retries), the 5-second delay
|
|
221
|
+
* before checking endTime may not be enough — the worker teardown can take
|
|
222
|
+
* longer than 5 seconds. In that case onTestEnd
|
|
223
|
+
* incorrectly treats the last test as "intermediate" and waits for a video file
|
|
224
|
+
* that will never be created. This function resolves the race by checking both
|
|
225
|
+
* conditions in parallel.
|
|
226
|
+
*
|
|
227
|
+
* Uses validateMp4 instead of fs.existsSync to ensure the file is fully
|
|
228
|
+
* written (has a valid moov atom) before returning.
|
|
229
|
+
*/
|
|
230
|
+
async function waitForFileOrEndTime(filePath, workerIndex, timeout = 5 * 60 * 1000) {
|
|
231
|
+
const deadline = Date.now() + timeout;
|
|
232
|
+
while (Date.now() < deadline) {
|
|
233
|
+
if (fs_1.default.existsSync(filePath) && validateMp4(filePath)) {
|
|
234
|
+
return "file";
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
const info = await getWorkerInfo(workerIndex);
|
|
238
|
+
if (info?.endTime) {
|
|
239
|
+
return "endTime";
|
|
189
240
|
}
|
|
190
|
-
}
|
|
191
|
-
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
// getWorkerInfo can fail transiently; keep polling
|
|
244
|
+
}
|
|
245
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
246
|
+
}
|
|
247
|
+
throw new Error(`Timed out waiting for video file or worker ${workerIndex} endTime`);
|
|
192
248
|
}
|
|
193
249
|
/**
|
|
194
250
|
* Validate that an MP4 file has a valid moov atom (metadata header).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gptDriver.spec.d.ts","sourceRoot":"","sources":["../../src/tests/gptDriver.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,131 @@
|
|
|
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 test_1 = __importDefault(require("@playwright/test"));
|
|
8
|
+
// --- Mock gpt-driver-node ---
|
|
9
|
+
// vi.hoisted runs before vi.mock hoisting, so variables are available in the factory
|
|
10
|
+
const { MockGptDriver, mockAiExecute } = vitest_1.vi.hoisted(() => {
|
|
11
|
+
const mockAiExecute = vitest_1.vi.fn().mockResolvedValue(undefined);
|
|
12
|
+
// Must use function (not arrow) so it can be called with `new`
|
|
13
|
+
const MockGptDriver = vitest_1.vi.fn(function (config) {
|
|
14
|
+
this.aiExecute = mockAiExecute;
|
|
15
|
+
this.assert = vitest_1.vi.fn().mockResolvedValue(undefined);
|
|
16
|
+
this.assertBulk = vitest_1.vi.fn().mockResolvedValue(undefined);
|
|
17
|
+
this.checkBulk = vitest_1.vi.fn().mockResolvedValue({});
|
|
18
|
+
this.extract = vitest_1.vi.fn().mockResolvedValue({});
|
|
19
|
+
this.testId = config.testId;
|
|
20
|
+
});
|
|
21
|
+
return { MockGptDriver, mockAiExecute };
|
|
22
|
+
});
|
|
23
|
+
vitest_1.vi.mock("gpt-driver-node", () => ({ default: MockGptDriver }));
|
|
24
|
+
// --- Mock Playwright test helpers ---
|
|
25
|
+
let currentTestId = "test-initial";
|
|
26
|
+
test_1.default.step = vitest_1.vi.fn(async (_name, body) => await body());
|
|
27
|
+
test_1.default.info = () => currentTestId ? { testId: currentTestId } : undefined;
|
|
28
|
+
test_1.default.skip = vitest_1.vi.fn();
|
|
29
|
+
const gptDriver_1 = require("../gptDriver");
|
|
30
|
+
function createProvider() {
|
|
31
|
+
return new gptDriver_1.GptDriverProvider({
|
|
32
|
+
options: {
|
|
33
|
+
protocol: "http",
|
|
34
|
+
hostname: "localhost",
|
|
35
|
+
port: 4723,
|
|
36
|
+
path: "/wd/hub",
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
(0, vitest_1.describe)("GptDriverProvider", () => {
|
|
41
|
+
(0, vitest_1.beforeEach)(() => {
|
|
42
|
+
vitest_1.vi.clearAllMocks();
|
|
43
|
+
currentTestId = "test-initial";
|
|
44
|
+
process.env.GPT_DRIVER_API_KEY = "fake-key";
|
|
45
|
+
});
|
|
46
|
+
(0, vitest_1.afterEach)(() => {
|
|
47
|
+
delete process.env.GPT_DRIVER_API_KEY;
|
|
48
|
+
});
|
|
49
|
+
(0, vitest_1.describe)("testId tracks current test context", () => {
|
|
50
|
+
(0, vitest_1.test)("initializes testId from first call's test context", async () => {
|
|
51
|
+
const provider = createProvider();
|
|
52
|
+
currentTestId = "test-first";
|
|
53
|
+
await provider.aiExecute("login");
|
|
54
|
+
const driver = provider.driver;
|
|
55
|
+
(0, vitest_1.expect)(driver.testId).toBe("test-first");
|
|
56
|
+
});
|
|
57
|
+
(0, vitest_1.test)("updates testId when a different test uses the same device", async () => {
|
|
58
|
+
const provider = createProvider();
|
|
59
|
+
// Test A runs
|
|
60
|
+
currentTestId = "test-aaa";
|
|
61
|
+
await provider.aiExecute("tap the submit button");
|
|
62
|
+
(0, vitest_1.expect)(provider.driver.testId).toBe("test-aaa");
|
|
63
|
+
// Test B runs on the same persistent device
|
|
64
|
+
currentTestId = "test-bbb";
|
|
65
|
+
await provider.aiExecute("verify the results screen");
|
|
66
|
+
(0, vitest_1.expect)(provider.driver.testId).toBe("test-bbb");
|
|
67
|
+
});
|
|
68
|
+
(0, vitest_1.test)("fixture teardown uses the current test's ID, not the first", async () => {
|
|
69
|
+
const provider = createProvider();
|
|
70
|
+
// Test A runs
|
|
71
|
+
currentTestId = "test-aaa";
|
|
72
|
+
await provider.aiExecute("perform action");
|
|
73
|
+
// Fixture teardown runs in Test A's context
|
|
74
|
+
await provider.aiExecute("navigate back to home");
|
|
75
|
+
(0, vitest_1.expect)(provider.driver.testId).toBe("test-aaa");
|
|
76
|
+
// Test B starts with new context
|
|
77
|
+
currentTestId = "test-bbb";
|
|
78
|
+
await provider.aiExecute("perform another action");
|
|
79
|
+
(0, vitest_1.expect)(provider.driver.testId).toBe("test-bbb");
|
|
80
|
+
// Fixture teardown runs in Test B's context
|
|
81
|
+
await provider.aiExecute("navigate back to home");
|
|
82
|
+
(0, vitest_1.expect)(provider.driver.testId).toBe("test-bbb");
|
|
83
|
+
});
|
|
84
|
+
(0, vitest_1.test)("preserves testId when test.info() returns undefined", async () => {
|
|
85
|
+
const provider = createProvider();
|
|
86
|
+
currentTestId = "test-known";
|
|
87
|
+
await provider.aiExecute("do something");
|
|
88
|
+
(0, vitest_1.expect)(provider.driver.testId).toBe("test-known");
|
|
89
|
+
// Worker teardown — no test context
|
|
90
|
+
currentTestId = undefined;
|
|
91
|
+
await provider.aiExecute("cleanup");
|
|
92
|
+
(0, vitest_1.expect)(provider.driver.testId).toBe("test-known");
|
|
93
|
+
});
|
|
94
|
+
(0, vitest_1.test)("warns once if testId property is missing from driver", async () => {
|
|
95
|
+
const provider = createProvider();
|
|
96
|
+
const warnSpy = vitest_1.vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
97
|
+
currentTestId = "test-aaa";
|
|
98
|
+
await provider.aiExecute("initialize");
|
|
99
|
+
// Remove testId to simulate SDK rename/removal
|
|
100
|
+
delete provider.driver.testId;
|
|
101
|
+
currentTestId = "test-bbb";
|
|
102
|
+
await provider.aiExecute("second call");
|
|
103
|
+
(0, vitest_1.expect)(warnSpy).toHaveBeenCalledTimes(1);
|
|
104
|
+
(0, vitest_1.expect)(warnSpy).toHaveBeenCalledWith(vitest_1.expect.stringContaining("Cannot update testId"));
|
|
105
|
+
// Should not warn again on subsequent calls
|
|
106
|
+
currentTestId = "test-ccc";
|
|
107
|
+
await provider.aiExecute("third call");
|
|
108
|
+
(0, vitest_1.expect)(warnSpy).toHaveBeenCalledTimes(1);
|
|
109
|
+
warnSpy.mockRestore();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
(0, vitest_1.describe)("lazy singleton", () => {
|
|
113
|
+
(0, vitest_1.test)("creates GptDriver instance only once", async () => {
|
|
114
|
+
const provider = createProvider();
|
|
115
|
+
currentTestId = "test-1";
|
|
116
|
+
await provider.aiExecute("first");
|
|
117
|
+
currentTestId = "test-2";
|
|
118
|
+
await provider.aiExecute("second");
|
|
119
|
+
currentTestId = "test-3";
|
|
120
|
+
await provider.aiExecute("third");
|
|
121
|
+
(0, vitest_1.expect)(MockGptDriver).toHaveBeenCalledTimes(1);
|
|
122
|
+
});
|
|
123
|
+
(0, vitest_1.test)("does not create GptDriver when API key is missing", async () => {
|
|
124
|
+
delete process.env.GPT_DRIVER_API_KEY;
|
|
125
|
+
const provider = createProvider();
|
|
126
|
+
// Should call test.skip and throw
|
|
127
|
+
await (0, vitest_1.expect)(provider.aiExecute("anything")).rejects.toThrow("GPT Driver not configured");
|
|
128
|
+
(0, vitest_1.expect)(MockGptDriver).not.toHaveBeenCalled();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|