testdriverai 7.2.28 ā 7.2.30
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 +1 -1
- package/docs/docs.json +1 -1
- package/docs/v7/quickstart.mdx +2 -2
- package/package.json +1 -1
- package/sdk.js +59 -65
- package/test/api-resilience.test.mjs +110 -0
- package/test/testdriver/windows-installer.test.mjs +9 -17
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ expect(result).toBeTruthy();
|
|
|
49
49
|
|
|
50
50
|
### Step 1: Create a TestDriver Account
|
|
51
51
|
|
|
52
|
-
<a href="https://
|
|
52
|
+
<a href="https://console.testdriver.ai/team"><img src="https://img.shields.io/badge/Sign_Up-Free_Account-blue?style=for-the-badge" alt="Sign Up"/></a>
|
|
53
53
|
|
|
54
54
|
*No credit card required!*
|
|
55
55
|
|
package/docs/docs.json
CHANGED
package/docs/v7/quickstart.mdx
CHANGED
|
@@ -22,7 +22,7 @@ TestDriver makes it easy to write automated computer-use tests for web browsers,
|
|
|
22
22
|
<Card
|
|
23
23
|
title="Sign Up for TestDriver"
|
|
24
24
|
icon="user-plus"
|
|
25
|
-
href="https://
|
|
25
|
+
href="https://console.testdriver.ai/team"
|
|
26
26
|
arrow
|
|
27
27
|
horizontal
|
|
28
28
|
>
|
|
@@ -67,7 +67,7 @@ TestDriver makes it easy to write automated computer-use tests for web browsers,
|
|
|
67
67
|
<Card
|
|
68
68
|
title="Sign Up for TestDriver"
|
|
69
69
|
icon="user-plus"
|
|
70
|
-
href="https://
|
|
70
|
+
href="https://console.testdriver.ai/team"
|
|
71
71
|
arrow
|
|
72
72
|
horizontal
|
|
73
73
|
>
|
package/package.json
CHANGED
package/sdk.js
CHANGED
|
@@ -1918,82 +1918,76 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
1918
1918
|
await this.dashcam.start();
|
|
1919
1919
|
}
|
|
1920
1920
|
|
|
1921
|
-
// Determine
|
|
1922
|
-
const urlObj = new URL(url);
|
|
1923
|
-
const detectedFilename = filename || urlObj.pathname.split('/').pop() || 'installer';
|
|
1924
|
-
|
|
1925
|
-
// Determine download directory and full path
|
|
1921
|
+
// Determine download directory
|
|
1926
1922
|
const downloadDir = this.os === 'windows'
|
|
1927
1923
|
? 'C:\\Users\\testdriver\\Downloads'
|
|
1928
1924
|
: '/tmp';
|
|
1929
|
-
const filePath = this.os === 'windows'
|
|
1930
|
-
? `${downloadDir}\\${detectedFilename}`
|
|
1931
|
-
: `${downloadDir}/${detectedFilename}`;
|
|
1932
1925
|
|
|
1933
1926
|
console.log(`[provision.installer] Downloading ${url}...`);
|
|
1934
1927
|
|
|
1935
|
-
|
|
1936
|
-
if (this.os === 'windows') {
|
|
1937
|
-
await this.exec(
|
|
1938
|
-
shell,
|
|
1939
|
-
`Invoke-WebRequest -Uri "${url}" -OutFile "${filePath}"`,
|
|
1940
|
-
300000, // 5 min timeout for download
|
|
1941
|
-
true
|
|
1942
|
-
);
|
|
1943
|
-
} else {
|
|
1944
|
-
await this.exec(
|
|
1945
|
-
shell,
|
|
1946
|
-
`curl -L -o "${filePath}" "${url}"`,
|
|
1947
|
-
300000,
|
|
1948
|
-
true
|
|
1949
|
-
);
|
|
1950
|
-
}
|
|
1928
|
+
let actualFilePath;
|
|
1951
1929
|
|
|
1952
|
-
//
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
if (!hasValidExtension && this.os === 'windows') {
|
|
1957
|
-
// On Windows, scan the download directory for .msi or .exe files
|
|
1958
|
-
console.log(`[provision.installer] Downloaded file has no extension, scanning for .msi or .exe files...`);
|
|
1959
|
-
const scanResult = await this.exec(
|
|
1960
|
-
shell,
|
|
1961
|
-
`Get-ChildItem -Path "${downloadDir}" -File | Where-Object { $_.Extension -match '\\.(msi|exe)$' } | Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName`,
|
|
1962
|
-
30000,
|
|
1963
|
-
true
|
|
1964
|
-
);
|
|
1930
|
+
// Download the file and get the actual filename (handles redirects)
|
|
1931
|
+
if (this.os === 'windows') {
|
|
1932
|
+
// Simple approach: download first, then get the actual filename from the response
|
|
1933
|
+
const tempFile = `${downloadDir}\\installer_temp_${Date.now()}`;
|
|
1965
1934
|
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1935
|
+
const downloadScript = `
|
|
1936
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
1937
|
+
$response = Invoke-WebRequest -Uri "${url}" -OutFile "${tempFile}" -PassThru -UseBasicParsing
|
|
1938
|
+
|
|
1939
|
+
# Try to get filename from Content-Disposition header
|
|
1940
|
+
$filename = $null
|
|
1941
|
+
if ($response.Headers['Content-Disposition']) {
|
|
1942
|
+
if ($response.Headers['Content-Disposition'] -match 'filename=\\"?([^\\"]+)\\"?') {
|
|
1943
|
+
$filename = $matches[1]
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
# If no filename from header, try to get from URL or use default
|
|
1948
|
+
if (-not $filename) {
|
|
1949
|
+
$uri = [System.Uri]"${url}"
|
|
1950
|
+
$filename = [System.IO.Path]::GetFileName($uri.LocalPath)
|
|
1951
|
+
if (-not $filename -or $filename -eq '') {
|
|
1952
|
+
$filename = "installer"
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
# Move temp file to final location with proper filename
|
|
1957
|
+
$finalPath = Join-Path "${downloadDir}" $filename
|
|
1958
|
+
Move-Item -Path "${tempFile}" -Destination $finalPath -Force
|
|
1959
|
+
Write-Output $finalPath
|
|
1960
|
+
`;
|
|
1961
|
+
|
|
1962
|
+
const result = await this.exec(shell, downloadScript, 300000, true);
|
|
1963
|
+
actualFilePath = result ? result.trim() : null;
|
|
1979
1964
|
|
|
1980
|
-
if (
|
|
1981
|
-
|
|
1982
|
-
console.log(`[provision.installer] Found installer: ${actualFilePath}`);
|
|
1965
|
+
if (!actualFilePath) {
|
|
1966
|
+
throw new Error('[provision.installer] Failed to download file');
|
|
1983
1967
|
}
|
|
1984
|
-
} else
|
|
1985
|
-
//
|
|
1986
|
-
|
|
1987
|
-
const
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
);
|
|
1968
|
+
} else {
|
|
1969
|
+
// Use curl with options to get the final filename
|
|
1970
|
+
const tempMarker = `installer_${Date.now()}`;
|
|
1971
|
+
const downloadScript = `
|
|
1972
|
+
cd "${downloadDir}"
|
|
1973
|
+
curl -L -J -O -w "%{filename_effective}" "${url}" 2>/dev/null || echo "${tempMarker}"
|
|
1974
|
+
`;
|
|
1975
|
+
|
|
1976
|
+
const result = await this.exec(shell, downloadScript, 300000, true);
|
|
1977
|
+
const downloadedFile = result ? result.trim() : null;
|
|
1993
1978
|
|
|
1994
|
-
if (
|
|
1995
|
-
actualFilePath =
|
|
1996
|
-
|
|
1979
|
+
if (downloadedFile && downloadedFile !== tempMarker) {
|
|
1980
|
+
actualFilePath = `${downloadDir}/${downloadedFile}`;
|
|
1981
|
+
} else {
|
|
1982
|
+
// Fallback: use curl without -J and specify output file
|
|
1983
|
+
const fallbackFilename = filename || 'installer';
|
|
1984
|
+
actualFilePath = `${downloadDir}/${fallbackFilename}`;
|
|
1985
|
+
await this.exec(
|
|
1986
|
+
shell,
|
|
1987
|
+
`curl -L -o "${actualFilePath}" "${url}"`,
|
|
1988
|
+
300000,
|
|
1989
|
+
true
|
|
1990
|
+
);
|
|
1997
1991
|
}
|
|
1998
1992
|
}
|
|
1999
1993
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TestDriver SDK - API Resilience Test
|
|
3
|
+
*
|
|
4
|
+
* This test verifies that TestDriver client can handle API restarts gracefully.
|
|
5
|
+
* It will:
|
|
6
|
+
* 1. Start a sandbox and browser
|
|
7
|
+
* 2. Make some API calls (TestDriver operations)
|
|
8
|
+
* 3. Kill the API (dev.sh)
|
|
9
|
+
* 4. Restart the API
|
|
10
|
+
* 5. Continue making API calls and verify they work
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* npm test -- test/api-resilience.test.mjs
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, expect, it } from "vitest";
|
|
17
|
+
import { TestDriver } from "../lib/vitest/hooks.mjs";
|
|
18
|
+
import { spawn, exec } from 'child_process';
|
|
19
|
+
import { promisify } from 'util';
|
|
20
|
+
|
|
21
|
+
const execAsync = promisify(exec);
|
|
22
|
+
|
|
23
|
+
describe("API Resilience Test", () => {
|
|
24
|
+
it("should continue working after API restart", async (context) => {
|
|
25
|
+
const testdriver = TestDriver(context, {
|
|
26
|
+
newSandbox: true,
|
|
27
|
+
headless: false
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
console.log("\nš Step 1: Provision Chrome and navigate to test page");
|
|
31
|
+
await testdriver.provision.chrome({
|
|
32
|
+
url: 'http://testdriver-sandbox.vercel.app/login',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
console.log("ā
Provisioned successfully");
|
|
36
|
+
|
|
37
|
+
console.log("\nš Step 2: Perform initial test operations");
|
|
38
|
+
const button1 = await testdriver.find("Sign In button");
|
|
39
|
+
console.log("ā
Found Sign In button:", button1.found());
|
|
40
|
+
expect(button1.found()).toBe(true);
|
|
41
|
+
|
|
42
|
+
const result1 = await testdriver.assert("I can see a login page");
|
|
43
|
+
console.log("ā
First assertion passed:", result1);
|
|
44
|
+
expect(result1).toBeTruthy();
|
|
45
|
+
|
|
46
|
+
console.log("\nš Step 3: Simulate API going down");
|
|
47
|
+
console.log("ā ļø Killing dev.sh process...");
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// Kill all node processes running app.js (the API server)
|
|
51
|
+
await execAsync("pkill -f 'node.*app.js'");
|
|
52
|
+
console.log("ā
API killed");
|
|
53
|
+
} catch (error) {
|
|
54
|
+
// pkill returns non-zero exit code if no processes found, which is okay
|
|
55
|
+
console.log("Note: No app.js processes found to kill (or already killed)");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Wait a bit to ensure API is down
|
|
59
|
+
console.log("ā³ Waiting 3 seconds to ensure API is down...");
|
|
60
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
61
|
+
|
|
62
|
+
console.log("\nš Step 4: Restart API");
|
|
63
|
+
console.log("š Starting dev.sh...");
|
|
64
|
+
|
|
65
|
+
// Start dev.sh in background
|
|
66
|
+
const apiProcess = spawn('bash', ['dev.sh'], {
|
|
67
|
+
cwd: '/Users/ianjennings/Development/api',
|
|
68
|
+
detached: true,
|
|
69
|
+
stdio: 'ignore'
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Unref so the process doesn't keep this test running
|
|
73
|
+
apiProcess.unref();
|
|
74
|
+
|
|
75
|
+
console.log("ā
API restarted (PID:", apiProcess.pid, ")");
|
|
76
|
+
|
|
77
|
+
// Wait for API to be ready
|
|
78
|
+
console.log("ā³ Waiting 10 seconds for API to initialize...");
|
|
79
|
+
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
80
|
+
|
|
81
|
+
console.log("\nš Step 5: Continue test operations after API restart");
|
|
82
|
+
console.log("š Attempting to find element again...");
|
|
83
|
+
|
|
84
|
+
const button2 = await testdriver.find("Sign In button");
|
|
85
|
+
console.log("ā
Found Sign In button again:", button2.found());
|
|
86
|
+
expect(button2.found()).toBe(true);
|
|
87
|
+
|
|
88
|
+
console.log("š Performing another assertion...");
|
|
89
|
+
const result2 = await testdriver.assert("I can see a login page");
|
|
90
|
+
console.log("ā
Second assertion passed:", result2);
|
|
91
|
+
expect(result2).toBeTruthy();
|
|
92
|
+
|
|
93
|
+
console.log("\nš Step 6: Perform additional operations to verify full functionality");
|
|
94
|
+
const emailInput = await testdriver.find("email input field");
|
|
95
|
+
console.log("ā
Found email input:", emailInput.found());
|
|
96
|
+
expect(emailInput.found()).toBe(true);
|
|
97
|
+
|
|
98
|
+
await emailInput.click();
|
|
99
|
+
await testdriver.type("test@example.com");
|
|
100
|
+
console.log("ā
Typed into email field");
|
|
101
|
+
|
|
102
|
+
const result3 = await testdriver.assert("the email field contains text");
|
|
103
|
+
console.log("ā
Final assertion passed:", result3);
|
|
104
|
+
expect(result3).toBeTruthy();
|
|
105
|
+
|
|
106
|
+
console.log("\nš Test completed successfully! API resilience verified.");
|
|
107
|
+
});
|
|
108
|
+
}, {
|
|
109
|
+
timeout: 120000 // 2 minute timeout for this test
|
|
110
|
+
});
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Run: TD_OS=windows npx vitest run examples/windows-installer.test.mjs
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { describe,
|
|
12
|
+
import { describe, it } from "vitest";
|
|
13
13
|
import { TestDriver } from "../../lib/vitest/hooks.mjs";
|
|
14
14
|
|
|
15
15
|
const isLinux = (process.env.TD_OS || "linux") === "linux";
|
|
@@ -19,7 +19,7 @@ describe("Windows App Installation", () => {
|
|
|
19
19
|
it.skipIf(isLinux)("should download, install, and launch GitButler on Windows", async (context) => {
|
|
20
20
|
// Alternative approach using provision.installer helper
|
|
21
21
|
const testdriver = TestDriver(context, {
|
|
22
|
-
|
|
22
|
+
reconnect: true,
|
|
23
23
|
os: 'windows'
|
|
24
24
|
});
|
|
25
25
|
|
|
@@ -29,21 +29,13 @@ describe("Windows App Installation", () => {
|
|
|
29
29
|
launch: false, // Don't auto-launch, we'll install manually
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
120000
|
|
40
|
-
);
|
|
41
|
-
} else {
|
|
42
|
-
await testdriver.exec('pwsh',
|
|
43
|
-
`Start-Process "${installerPath}" -ArgumentList "/S" -Wait`,
|
|
44
|
-
120000
|
|
45
|
-
);
|
|
46
|
-
}
|
|
32
|
+
console.log('Installer downloaded to:', installerPath);
|
|
33
|
+
|
|
34
|
+
// Install the MSI silently (the file might not have an extension, so we try MSI first)
|
|
35
|
+
await testdriver.exec('pwsh',
|
|
36
|
+
`Start-Process msiexec.exe -ArgumentList "/i \`"${installerPath}\`" /qn /norestart" -Wait`,
|
|
37
|
+
120000
|
|
38
|
+
);
|
|
47
39
|
|
|
48
40
|
// Verify installation by checking if executable exists
|
|
49
41
|
const verifyScript = `
|