-
-
Notifications
You must be signed in to change notification settings - Fork 3
Enhance Appium integration and screenshot handling #424
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
858e55b
2ea99e9
9451ff5
e007ce6
c60dbd9
c779ea7
39be475
6ad9590
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||||||||||||||||||||||||||||||||||||||
| const kill = require("tree-kill"); | ||||||||||||||||||||||||||||||||||||||||||
| const wdio = require("webdriverio"); | ||||||||||||||||||||||||||||||||||||||||||
| const os = require("os"); | ||||||||||||||||||||||||||||||||||||||||||
| const net = require("net"); | ||||||||||||||||||||||||||||||||||||||||||
| const { log, replaceEnvs } = require("./utils"); | ||||||||||||||||||||||||||||||||||||||||||
| const axios = require("axios"); | ||||||||||||||||||||||||||||||||||||||||||
| const { instantiateCursor } = require("./tests/moveTo"); | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -31,6 +32,7 @@ const { uploadChangedFiles } = require("./integrations"); | |||||||||||||||||||||||||||||||||||||||||
| exports.runSpecs = runSpecs; | ||||||||||||||||||||||||||||||||||||||||||
| exports.runViaApi = runViaApi; | ||||||||||||||||||||||||||||||||||||||||||
| exports.getRunner = getRunner; | ||||||||||||||||||||||||||||||||||||||||||
| exports.checkPortAvailable = checkPortAvailable; | ||||||||||||||||||||||||||||||||||||||||||
| // exports.appiumStart = appiumStart; | ||||||||||||||||||||||||||||||||||||||||||
| // exports.appiumIsReady = appiumIsReady; | ||||||||||||||||||||||||||||||||||||||||||
| // exports.driverStart = driverStart; | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -380,6 +382,7 @@ async function runSpecs({ resolvedTests }) { | |||||||||||||||||||||||||||||||||||||||||
| const availableApps = runnerDetails.availableApps; | ||||||||||||||||||||||||||||||||||||||||||
| const metaValues = { specs: {} }; | ||||||||||||||||||||||||||||||||||||||||||
| let appium; | ||||||||||||||||||||||||||||||||||||||||||
| let appiumHost; | ||||||||||||||||||||||||||||||||||||||||||
| const report = { | ||||||||||||||||||||||||||||||||||||||||||
| summary: { | ||||||||||||||||||||||||||||||||||||||||||
| specs: { | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -413,24 +416,52 @@ async function runSpecs({ resolvedTests }) { | |||||||||||||||||||||||||||||||||||||||||
| // Determine which apps are required | ||||||||||||||||||||||||||||||||||||||||||
| const appiumRequired = isAppiumRequired(specs); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Warm up Appium | ||||||||||||||||||||||||||||||||||||||||||
| if (appiumRequired) { | ||||||||||||||||||||||||||||||||||||||||||
| // Set Appium home directory | ||||||||||||||||||||||||||||||||||||||||||
| setAppiumHome(); | ||||||||||||||||||||||||||||||||||||||||||
| // Start Appium server | ||||||||||||||||||||||||||||||||||||||||||
| appium = spawn("npx", ["appium"], { | ||||||||||||||||||||||||||||||||||||||||||
| shell: true, | ||||||||||||||||||||||||||||||||||||||||||
| windowsHide: true, | ||||||||||||||||||||||||||||||||||||||||||
| cwd: path.join(__dirname, ".."), | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
| // Warm up Appium | ||||||||||||||||||||||||||||||||||||||||||
| if (appiumRequired) { | ||||||||||||||||||||||||||||||||||||||||||
| // Check port availability before spawning (probe both hosts like appiumIsReady does) | ||||||||||||||||||||||||||||||||||||||||||
| const hosts = ["127.0.0.1", "localhost"]; | ||||||||||||||||||||||||||||||||||||||||||
| let portAvailableOnAnyHost = false; | ||||||||||||||||||||||||||||||||||||||||||
| const hostStatuses = {}; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| for (const host of hosts) { | ||||||||||||||||||||||||||||||||||||||||||
| const isAvailable = await checkPortAvailable(4723, host); | ||||||||||||||||||||||||||||||||||||||||||
| hostStatuses[host] = isAvailable; | ||||||||||||||||||||||||||||||||||||||||||
| if (isAvailable) { | ||||||||||||||||||||||||||||||||||||||||||
| portAvailableOnAnyHost = true; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (!portAvailableOnAnyHost) { | ||||||||||||||||||||||||||||||||||||||||||
| let message = "Appium port 4723 is already in use. Stop the process using it or set a different port.\n"; | ||||||||||||||||||||||||||||||||||||||||||
| message += "Port availability check results:\n"; | ||||||||||||||||||||||||||||||||||||||||||
| for (const host of hosts) { | ||||||||||||||||||||||||||||||||||||||||||
| message += ` ${host}: ${hostStatuses[host] ? "available" : "unavailable"}\n`; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| log(config, "error", message); | ||||||||||||||||||||||||||||||||||||||||||
| throw new Error(message); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| // Set Appium home directory | ||||||||||||||||||||||||||||||||||||||||||
| setAppiumHome(); | ||||||||||||||||||||||||||||||||||||||||||
| // Start Appium server | ||||||||||||||||||||||||||||||||||||||||||
| appium = spawn("npx", ["appium"], { | ||||||||||||||||||||||||||||||||||||||||||
| shell: true, | ||||||||||||||||||||||||||||||||||||||||||
| windowsHide: true, | ||||||||||||||||||||||||||||||||||||||||||
| cwd: path.join(__dirname, ".."), | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
| appium.stdout.on("data", (data) => { | ||||||||||||||||||||||||||||||||||||||||||
| // console.log(`stdout: ${data}`); | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
| appium.stderr.on("data", (data) => { | ||||||||||||||||||||||||||||||||||||||||||
| // console.error(`stderr: ${data}`); | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
| await appiumIsReady(); | ||||||||||||||||||||||||||||||||||||||||||
| log(config, "debug", "Appium is ready."); | ||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||
| appiumHost = await appiumIsReady(); | ||||||||||||||||||||||||||||||||||||||||||
| log(config, "debug", `Appium is ready on ${appiumHost}.`); | ||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||
| // Clean up Appium process on timeout/failure | ||||||||||||||||||||||||||||||||||||||||||
| kill(appium.pid); | ||||||||||||||||||||||||||||||||||||||||||
| throw error; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Iterate specs | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -541,7 +572,7 @@ async function runSpecs({ resolvedTests }) { | |||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Instantiate driver | ||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||
| driver = await driverStart(caps); | ||||||||||||||||||||||||||||||||||||||||||
| driver = await driverStart(caps, appiumHost); | ||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||
| // If driver fails to start, try again as headless | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -552,15 +583,15 @@ async function runSpecs({ resolvedTests }) { | |||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| context.browser.headless = true; | ||||||||||||||||||||||||||||||||||||||||||
| caps = getDriverCapabilities({ | ||||||||||||||||||||||||||||||||||||||||||
| config: config, | ||||||||||||||||||||||||||||||||||||||||||
| runnerDetails: runnerDetails, | ||||||||||||||||||||||||||||||||||||||||||
| name: context.browser.name, | ||||||||||||||||||||||||||||||||||||||||||
| options: { | ||||||||||||||||||||||||||||||||||||||||||
| width: context.browser?.window?.width || 1200, | ||||||||||||||||||||||||||||||||||||||||||
| height: context.browser?.window?.height || 800, | ||||||||||||||||||||||||||||||||||||||||||
| headless: context.browser?.headless !== false, | ||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
| driver = await driverStart(caps); | ||||||||||||||||||||||||||||||||||||||||||
| driver = await driverStart(caps, appiumHost); | ||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||
| let errorMessage = `Failed to start context '${context.browser?.name}' on '${platform}'.`; | ||||||||||||||||||||||||||||||||||||||||||
| if (context.browser?.name === "safari") | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -939,36 +970,155 @@ async function runStep({ | |||||||||||||||||||||||||||||||||||||||||
| return actionResult; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Delay execution until Appium server is available. | ||||||||||||||||||||||||||||||||||||||||||
| async function appiumIsReady() { | ||||||||||||||||||||||||||||||||||||||||||
| let isReady = false; | ||||||||||||||||||||||||||||||||||||||||||
| while (!isReady) { | ||||||||||||||||||||||||||||||||||||||||||
| // Retry delay | ||||||||||||||||||||||||||||||||||||||||||
| // TODO: Add configurable retry delay | ||||||||||||||||||||||||||||||||||||||||||
| // TODO: Add configurable timeout duration | ||||||||||||||||||||||||||||||||||||||||||
| await new Promise((resolve) => setTimeout(resolve, 1000)); | ||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * Check if a port is available (not in use). | ||||||||||||||||||||||||||||||||||||||||||
| * @param {number} port - Port number to check | ||||||||||||||||||||||||||||||||||||||||||
| * @param {string} host - Host to check on (default: 127.0.0.1) | ||||||||||||||||||||||||||||||||||||||||||
| * @returns {Promise<boolean>} - True if port is available, false if in use | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| async function checkPortAvailable(port, host = "127.0.0.1") { | ||||||||||||||||||||||||||||||||||||||||||
| return new Promise((resolve) => { | ||||||||||||||||||||||||||||||||||||||||||
| const server = net.createServer(); | ||||||||||||||||||||||||||||||||||||||||||
| server.once("error", (err) => { | ||||||||||||||||||||||||||||||||||||||||||
| if (["EADDRINUSE", "EACCES", "EADDRNOTAVAIL"].includes(err.code)) { | ||||||||||||||||||||||||||||||||||||||||||
| resolve(false); // Port is not available | ||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
| } else { | |
| } else { | |
| server.close(); |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The retry loop sleeps before the first connection attempt (line 1018), adding an unnecessary 1-second delay at startup. This means the first connection attempt won't happen until 1 second after the function is called. Consider restructuring to attempt connection first, then sleep only before retries. This would make the function more responsive and reduce the minimum startup time by 1 second.
| while (Date.now() < deadline) { | |
| const remaining = deadline - Date.now(); | |
| if (remaining <= 0) break; | |
| // Retry delay - clamp to remaining time | |
| const sleepMs = Math.min(1000, remaining); | |
| await new Promise((resolve) => setTimeout(resolve, sleepMs)); | |
| let firstAttempt = true; | |
| while (Date.now() < deadline) { | |
| const remaining = deadline - Date.now(); | |
| if (remaining <= 0) break; | |
| // Retry delay - clamp to remaining time (skip before first attempt) | |
| if (!firstAttempt) { | |
| const sleepMs = Math.min(1000, remaining); | |
| await new Promise((resolve) => setTimeout(resolve, sleepMs)); | |
| } | |
| firstAttempt = false; |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment "Clamp to remaining time, no minimum" is slightly unclear. The code uses Math.min(5000, hostRemaining) which clamps the timeout to a maximum of 5 seconds OR the remaining time (whichever is smaller). The phrase "no minimum" might be confusing since there's effectively a minimum of 0 enforced by the remaining time check. Consider clarifying to "Clamp to minimum of 5 seconds and remaining time" or "Use 5 seconds or remaining time, whichever is smaller".
| timeout: Math.min(5000, hostRemaining), // Clamp to remaining time, no minimum | |
| timeout: Math.min(5000, hostRemaining), // Use 5 seconds or remaining time, whichever is smaller |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clamp axios timeout to the remaining budget (avoid minimum 100ms).
Math.max(100, hostRemaining) can overshoot the deadline, which the new tests try to enforce strictly. If the remaining budget is small, prefer using it as-is (or skip the request).
🛠️ Suggested fix
- const resp = await axios.get(`http://${host}:4723/status`, {
- timeout: Math.min(5000, Math.max(100, hostRemaining)), // Clamp to remaining time
- });
+ const resp = await axios.get(`http://${host}:4723/status`, {
+ timeout: Math.min(5000, hostRemaining), // Clamp to remaining time
+ });🤖 Prompt for AI Agents
In `@src/tests.js` around lines 1009 - 1012, The axios timeout clamp currently
uses Math.min(5000, Math.max(100, hostRemaining)) which forces a 100ms minimum
and can overshoot the overall deadline; update the try block where axios.get is
called so it uses a timeout of Math.min(5000, hostRemaining) and before making
the request skip/return if hostRemaining <= 0 (or otherwise treat very small
remaining budgets as a skip) to avoid exceeding the deadline; locate and modify
the axios.get call in src/tests.js (the try block that constructs the timeout)
to implement this change.
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The setTimeout delay calculation checkDeadline - Date.now() can result in a negative or zero value if time elapses between line 1051 and line 1056. A negative timeout will cause setTimeout to fire immediately, which may not cause an error but defeats the purpose of the timeout. Consider using Math.max(0, checkDeadline - Date.now()) to ensure a non-negative timeout value.
| setTimeout(() => reject(new Error("Port check timeout")), checkDeadline - Date.now()) | |
| setTimeout( | |
| () => reject(new Error("Port check timeout")), | |
| Math.max(0, checkDeadline - Date.now()) | |
| ) |
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -237,6 +237,32 @@ async function saveScreenshot({ config, step, driver }) { | |||||||||||||||||||||||||||||
| rect.width += padding.left + padding.right; | ||||||||||||||||||||||||||||||
| rect.height += padding.top + padding.bottom; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Get viewport dimensions for clamping | ||||||||||||||||||||||||||||||
| const viewport = await driver.execute(() => ({ | ||||||||||||||||||||||||||||||
| width: window.innerWidth, | ||||||||||||||||||||||||||||||
| height: window.innerHeight, | ||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Clamp coordinates to viewport bounds (prevent negative values and overflow) | ||||||||||||||||||||||||||||||
| if (rect.x < 0) { | ||||||||||||||||||||||||||||||
| rect.width += rect.x; // Reduce width by the amount we're clamping | ||||||||||||||||||||||||||||||
| rect.x = 0; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| if (rect.y < 0) { | ||||||||||||||||||||||||||||||
| rect.height += rect.y; // Reduce height by the amount we're clamping | ||||||||||||||||||||||||||||||
| rect.y = 0; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| if (rect.x + rect.width > viewport.width) { | ||||||||||||||||||||||||||||||
| rect.width = viewport.width - rect.x; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| if (rect.y + rect.height > viewport.height) { | ||||||||||||||||||||||||||||||
| rect.height = viewport.height - rect.y; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
| // After clamping, ensure the element is at least partially visible and the rect is valid | |
| const isNonPositiveSize = rect.width <= 0 || rect.height <= 0; | |
| const isCompletelyOutsideViewport = | |
| rect.x >= viewport.width || | |
| rect.y >= viewport.height || | |
| rect.x + rect.width <= 0 || | |
| rect.y + rect.height <= 0; | |
| if (isNonPositiveSize || isCompletelyOutsideViewport) { | |
| throw new Error( | |
| "Cannot capture screenshot: target element is not sufficiently visible within the viewport." | |
| ); | |
| } |
Uh oh!
There was an error while loading. Please reload this page.