@loadmill/droid-cua 2.9.0 → 2.10.1-rc.8

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 (31) hide show
  1. package/README.md +0 -2
  2. package/build/index.js +24 -18
  3. package/build/src/brand/loadmill-mark.js +20 -0
  4. package/build/src/browser/playwright/connection.js +148 -11
  5. package/build/src/browser/playwright/welcome-page.js +161 -0
  6. package/build/src/cli/headless-execution-config.js +16 -24
  7. package/build/src/cli/headless-target.js +0 -9
  8. package/build/src/commands/help.js +3 -5
  9. package/build/src/commands/run.js +1 -1
  10. package/build/src/core/aim-assist.js +5 -0
  11. package/build/src/core/execution-engine.js +266 -505
  12. package/build/src/core/prompts.js +1 -1
  13. package/build/src/core/runtime-config.js +1 -2
  14. package/build/src/core/screenshot-store.js +153 -0
  15. package/build/src/core/tool-handlers/assert.js +155 -0
  16. package/build/src/core/tool-handlers/loadmill.js +29 -0
  17. package/build/src/core/tool-handlers/navigate.js +110 -0
  18. package/build/src/core/tool-handlers/preview-click.js +112 -0
  19. package/build/src/core/tool-handlers/test-design-complete.js +93 -0
  20. package/build/src/core/tool-handlers/utils.js +80 -0
  21. package/build/src/device/openai.js +103 -4
  22. package/build/src/modes/design-mode-ink.js +17 -5
  23. package/build/src/modes/design-mode.js +17 -5
  24. package/build/src/modes/execution-mode.js +42 -234
  25. package/build/src/prompts/base.js +44 -31
  26. package/build/src/prompts/design.js +55 -15
  27. package/build/src/prompts/editor.js +1 -1
  28. package/build/src/prompts/execution.js +15 -10
  29. package/build/src/utils/cua-debug-tracer.js +3 -1
  30. package/package.json +3 -4
  31. package/build/src/device/screenshot-resolution.js +0 -33
@@ -27,16 +27,6 @@ function appendCustomSections(prompt, sections = []) {
27
27
  ${customSection}
28
28
  `;
29
29
  }
30
- function buildStrictModeRuntimeSection(runtimeOptions = {}) {
31
- if (runtimeOptions?.strictMode !== true) {
32
- return "";
33
- }
34
- return `STRICT MODE - Observation-First Runtime:
35
- - The runtime may execute only the first meaningful action from any multi-step action chain you propose.
36
- - After that first action, it will re-observe the ${runtimeOptions?.platform === "browser" ? "browser" : "device"} and continue from a fresh screenshot.
37
- - If you propose multiple steps, assume only the first may happen before the next turn.
38
- - Do not assume later proposed actions succeeded unless the next screenshot confirms them.`;
39
- }
40
30
  function isBrowserTarget(deviceInfo = {}) {
41
31
  return typeof deviceInfo.platform === "string" && deviceInfo.platform.trim().toLowerCase() === "browser";
42
32
  }
@@ -55,7 +45,41 @@ function describeControlledTarget(deviceInfo = {}) {
55
45
  }
56
46
  return "a mobile device";
57
47
  }
58
- function buildBrowserBaseSystemPrompt(deviceInfo) {
48
+ function buildAimAssistGuidance(targetKind, aimAssistMode = "always") {
49
+ if (aimAssistMode === "off") {
50
+ return [
51
+ "- Click directly based on your best visual estimate.",
52
+ "- If a click has no effect, treat it as a likely misclick and try a meaningfully different coordinate.",
53
+ "- Click precisely based on your visual estimate.",
54
+ ].join("\n ");
55
+ }
56
+ if (aimAssistMode === "smart") {
57
+ return [
58
+ "- Before every click action, call `preview_click` with the exact coordinates you intend to click.",
59
+ "- Review the returned annotated screenshot before clicking.",
60
+ `- Click if the marker is clearly inside the intended ${targetKind === "browser" ? "clickable target" : "tappable control"}.${targetKind === "browser" ? "" : " Text inside a button, field, row, or bottom bar is okay."}`,
61
+ `- Adjust and preview again only if the marker is outside the target, on the ${targetKind === "browser" ? "edge" : "target border/edge"}, or closer to a neighboring target than the intended one.`,
62
+ "- If you preview more than once, the next click should use the most recent accepted preview coordinates unless the screen changed.",
63
+ "- If the click does not cause the expected UI change, treat it as a likely misclick and try a meaningfully different coordinate.",
64
+ "- Click precisely based on your visual estimate.",
65
+ "- You may skip preview only when the target is broad, isolated, stable, and unambiguous, and you are confident the click point is clearly inside the intended target.",
66
+ "- Good skip examples are large empty text fields, large search fields, large CTA buttons with generous padding, launcher icons, and wide rows with generous spacing and no nearby competing actions.",
67
+ "- Always use preview for dropdowns, expanded option lists, popovers, select menus, dense lists or search results, bottom sheets or cards, modals, overlays, date or time pickers, calendar cells, wheels, segmented selectors, and small inline controls.",
68
+ "- Do not skip preview when the target is one of many similar options, close to neighboring controls, in a keyboard-driven flow, after a recent screen change, or whenever you are not confident.",
69
+ ].join("\n ");
70
+ }
71
+ return [
72
+ "- Before every click action, call `preview_click` with the exact coordinates you intend to click.",
73
+ "- Review the returned annotated screenshot before clicking.",
74
+ `- Click if the marker is clearly inside the intended ${targetKind === "browser" ? "clickable target" : "tappable control"}.${targetKind === "browser" ? "" : " Text inside a button, field, row, or bottom bar is okay."}`,
75
+ `- Adjust and preview again only if the marker is outside the target, on the ${targetKind === "browser" ? "edge" : "target border/edge"}, or closer to a neighboring target than the intended one.`,
76
+ "- If you preview more than once, the next click should use the most recent accepted preview coordinates unless the screen changed.",
77
+ "- If the click does not cause the expected UI change, treat it as a likely misclick and try a meaningfully different coordinate.",
78
+ "- Click precisely based on your visual estimate.",
79
+ ].join("\n ");
80
+ }
81
+ function buildBrowserBaseSystemPrompt(deviceInfo, options = {}) {
82
+ const aimAssistMode = typeof options.aimAssistMode === "string" ? options.aimAssistMode : "always";
59
83
  return `
60
84
  You are controlling ${describeControlledTarget(deviceInfo)} in a sandboxed testing environment.
61
85
  Follow the user's instructions to interact with the browser.
@@ -67,13 +91,7 @@ function buildBrowserBaseSystemPrompt(deviceInfo) {
67
91
 
68
92
  When aiming for visual targets:
69
93
  - Reason carefully about the approximate pixel position.
70
- - Before every click action, call \`preview_click\` with the exact coordinates you intend to click.
71
- - Review the returned annotated screenshot before clicking.
72
- - Click if the marker is clearly inside the intended clickable target.
73
- - Adjust and preview again only if the marker is outside the target, on the edge, or closer to a neighboring target than the intended one.
74
- - If you preview more than once, the next click should use the most recent accepted preview coordinates unless the screen changed.
75
- - If the click does not cause the expected UI change, treat it as a likely misclick and try a meaningfully different coordinate.
76
- - Click precisely based on your visual estimate.
94
+ ${buildAimAssistGuidance("browser", aimAssistMode)}
77
95
 
78
96
  Available actions: click, scroll, type, keypress, wait, screenshot.
79
97
 
@@ -122,14 +140,15 @@ function buildBrowserBaseSystemPrompt(deviceInfo) {
122
140
 
123
141
  Browser-Specific Notes:
124
142
  - You are operating a desktop browser, not a phone UI
125
- - Direct URL navigation is handled by runner-owned \`navigate:\` lines when available; do not try to reach the browser address bar or fake URL entry with keyboard shortcuts
143
+ - Use the \`navigate\` tool for direct URL changes when available; do not try to reach the browser address bar or fake URL entry with keyboard shortcuts
126
144
  - ESC dismisses many overlays and dialogs
127
145
  - Cookie or consent banners may block the main task and may need dismissal first
128
146
  - Vertical scrolling is the default way to discover content below the fold
129
147
  - CTRL/CMD/ALT/OPTION/SHIFT may be used when they are the most reliable path
130
148
  `;
131
149
  }
132
- function buildMobileBaseSystemPrompt(deviceInfo) {
150
+ function buildMobileBaseSystemPrompt(deviceInfo, options = {}) {
151
+ const aimAssistMode = typeof options.aimAssistMode === "string" ? options.aimAssistMode : "always";
133
152
  return `
134
153
  You are controlling ${describeControlledTarget(deviceInfo)} in a sandboxed testing environment.
135
154
  Follow the user's instructions to interact with the device.
@@ -142,13 +161,7 @@ function buildMobileBaseSystemPrompt(deviceInfo) {
142
161
 
143
162
  When aiming for visual targets:
144
163
  - Reason carefully about the approximate pixel position.
145
- - Before every click action, call \`preview_click\` with the exact coordinates you intend to click.
146
- - Review the returned annotated screenshot before clicking.
147
- - Click if the marker is clearly inside the intended tappable control. Text inside a button, field, row, or bottom bar is okay.
148
- - Adjust and preview again only if the marker is outside the target, on the target border/edge, or closer to a neighboring target than the intended one.
149
- - If you preview more than once, the next click should use the most recent accepted preview coordinates unless the screen changed.
150
- - If the click does not cause the expected UI change, treat it as a likely misclick and try a meaningfully different coordinate.
151
- - Click precisely based on your visual estimate.
164
+ ${buildAimAssistGuidance("mobile", aimAssistMode)}
152
165
 
153
166
  Available actions: click, scroll, type, keypress, wait, screenshot.
154
167
 
@@ -209,9 +222,9 @@ function buildMobileBaseSystemPrompt(deviceInfo) {
209
222
  - Never use CTRL, CMD, ALT, OPTION, or SHIFT in a keypress action
210
223
  `;
211
224
  }
212
- function buildBaseSystemPrompt(deviceInfo) {
225
+ function buildBaseSystemPrompt(deviceInfo, options = {}) {
213
226
  return isBrowserTarget(deviceInfo)
214
- ? buildBrowserBaseSystemPrompt(deviceInfo)
215
- : buildMobileBaseSystemPrompt(deviceInfo);
227
+ ? buildBrowserBaseSystemPrompt(deviceInfo, options)
228
+ : buildMobileBaseSystemPrompt(deviceInfo, options);
216
229
  }
217
- export { appendCustomSections, buildBaseSystemPrompt, buildCustomInstructionsSection, describeControlledTarget, isBrowserTarget, buildStrictModeRuntimeSection, };
230
+ export { appendCustomSections, buildBaseSystemPrompt, buildCustomInstructionsSection, describeControlledTarget, isBrowserTarget, };
@@ -1,8 +1,7 @@
1
- import { appendCustomSections, buildBaseSystemPrompt, buildStrictModeRuntimeSection, isBrowserTarget } from "./base.js";
2
- export function buildDesignModePrompt(deviceInfo, customInstructions = {}, runtimeOptions = {}) {
1
+ import { appendCustomSections, buildBaseSystemPrompt, isBrowserTarget } from "./base.js";
2
+ export function buildDesignModePrompt(deviceInfo, customInstructions = {}) {
3
3
  const designCustomText = typeof customInstructions.designModeInstructions === "string" ? customInstructions.designModeInstructions.trim() : "";
4
4
  const basePrompt = buildBaseSystemPrompt(deviceInfo, customInstructions);
5
- const strictModeSection = buildStrictModeRuntimeSection(runtimeOptions);
6
5
  const browserTarget = isBrowserTarget(deviceInfo);
7
6
  const prompt = `${basePrompt}
8
7
 
@@ -14,15 +13,21 @@ Your task:
14
13
  1. Understand what the user wants to test from their initial instruction
15
14
  2. Explore the ${browserTarget ? "website" : "app"} autonomously to understand the flows
16
15
  3. Take screenshots and interact as needed to discover the UI and behavior
17
- 4. Once you've successfully completed the user's requested flow, immediately generate the test script
16
+ 4. Once you've successfully completed the user's requested flow, call \`test_design_complete\` with a structured summary of what was proven
18
17
 
19
18
  CRITICAL - After Completing the Task:
20
19
  - DO NOT navigate back or away from the final screen
21
20
  - The final screen state is what matters for verification
22
- - Generate the test script immediately showing the current state
21
+ - Call \`test_design_complete\` immediately once the requested flow has been proven
23
22
  - Use assertions to verify state, not navigation
24
23
  - "Check that it changed" means verify the current visual state, not navigate elsewhere
25
- - If the target validation state is visible (including expected error states), STOP actions and immediately output the final test script
24
+ - If the target validation state is visible (including expected error states), STOP actions and call \`test_design_complete\`
25
+ - If an assertion or validation PASS confirms the user goal, treat the task as complete
26
+ - After a passing assertion confirms the goal, your very next action should be calling \`test_design_complete\`
27
+ - Do NOT repeat the same assertion or validation on an unchanged screen after it has already passed
28
+ - Do NOT keep browsing, screenshotting, or re-checking once the goal has been confirmed
29
+ - Do NOT output the final test script before calling \`test_design_complete\`
30
+ - After calling \`test_design_complete\`, stop exploring and wait for the final script-generation turn
26
31
 
27
32
  CRITICAL - Recognizing When You Are Stuck:
28
33
  If you find yourself:
@@ -40,7 +45,7 @@ Example:
40
45
  "Chrome is open but I don't see a search bar or new tab button. Should I open a new tab, or is there a specific way you'd like me to navigate?"
41
46
 
42
47
  DO NOT continue brute-forcing the UI when stuck. The user prefers being asked over watching repeated failed attempts.
43
- DO NOT ask if the user wants a script after successfully completing the flow - just generate it automatically.
48
+ DO NOT ask if the user wants a script after successfully completing the flow - complete the handoff automatically.
44
49
 
45
50
  CRITICAL - Off-Screen Element Discovery:
46
51
  - If a required element is not visible, assume it may be off-screen before changing strategy
@@ -53,11 +58,11 @@ CRITICAL - Off-Screen Element Discovery:
53
58
  CRITICAL - Test Script Format Rules:
54
59
  - One simple instruction per line (NO numbers, NO bullets)
55
60
  - Use imperative commands: "Open X", "Click Y", "Type Z"
56
- - Include "assert: <condition>" lines to validate expected behavior
57
- - Normalize validation wording into assertions:
58
- - Convert "check", "verify", "ensure", "fetch", and "compare" intent into explicit "assert: ..." lines
59
- - Do not leave standalone "Check ..." or "Verify ..." lines in the final script
60
- - Merge duplicate or near-duplicate validation lines into one clear assertion
61
+ - Use plain-language verification lines such as "Verify ..." or "Validate ..."
62
+ - Normalize validation wording into plain-language verification:
63
+ - Convert "check", "assert", "ensure", "fetch", and "compare" intent into clear "Verify ..." or "Validate ..." lines
64
+ - Do not leave vague standalone "Check ..." lines in the final script
65
+ - Merge duplicate or near-duplicate verification lines into one clear validation step
61
66
  - End with "exit"
62
67
  - Keep it simple and executable
63
68
  - When you generate the final result, include a suggested test name before the script
@@ -75,7 +80,7 @@ CORRECT Example:
75
80
  Suggested test name: ${browserTarget ? "docs-search" : "calculator-addition"}
76
81
 
77
82
  \`\`\`
78
- ${browserTarget ? "navigate: https://docs.example.com\nType \"pricing\" in the search field\nPress Enter\nwait\nClick \"Pricing\"\nassert: Pricing page is visible" : "Open Calculator app\nassert: Calculator app is visible\nType \"2\"\nClick the plus button\nType \"3\"\nClick the equals button\nassert: result shows 5"}
83
+ ${browserTarget ? "Go to https://docs.example.com\nType \"pricing\" in the search field\nPress Enter\nwait\nClick \"Pricing\"\nVerify the Pricing page is visible" : "Open Calculator app\nVerify the Calculator app is visible\nType \"2\"\nClick the plus button\nType \"3\"\nClick the equals button\nVerify the result shows 5"}
79
84
  exit
80
85
  \`\`\`
81
86
 
@@ -87,7 +92,7 @@ WRONG Example (DON'T DO THIS):
87
92
  \`\`\`
88
93
 
89
94
  Remember: You are autonomous. Explore confidently. Generate simple, executable test scripts.
90
- ` + (strictModeSection ? `\n\n${strictModeSection}` : "");
95
+ `;
91
96
  return appendCustomSections(prompt, [
92
97
  { title: "Base Prompt Instructions", text: customInstructions.basePromptInstructions },
93
98
  { title: "Design Mode Instructions", text: designCustomText }
@@ -106,5 +111,40 @@ ${transcript}
106
111
  Original objective:
107
112
  ${objective ?? "(not provided)"}
108
113
 
109
- If the objective is already completed, generate the final test script now.`;
114
+ If the objective is already completed, or a recent passing validation already confirmed the goal, call \`test_design_complete\` now.
115
+ Do not repeat the same assertion on an unchanged screen.`;
116
+ }
117
+ function formatStringList(items = []) {
118
+ const normalized = Array.isArray(items)
119
+ ? items
120
+ .map((item) => (typeof item === "string" ? item.trim() : ""))
121
+ .filter(Boolean)
122
+ : [];
123
+ if (normalized.length === 0) {
124
+ return "- (none provided)";
125
+ }
126
+ return normalized.map((item) => `- ${item}`).join("\n");
127
+ }
128
+ export function buildDesignScriptGenerationPrompt(summary = {}) {
129
+ const suggestedTestName = typeof summary.suggestedTestName === "string" ? summary.suggestedTestName.trim() : "";
130
+ const goalSummary = typeof summary.goalSummary === "string" ? summary.goalSummary.trim() : "";
131
+ const finalState = typeof summary.finalState === "string" ? summary.finalState.trim() : "";
132
+ const notesForScript = typeof summary.notesForScript === "string" ? summary.notesForScript.trim() : "";
133
+ return `AUTHORING MODE:
134
+ Test design exploration is complete. Use the validated completion summary below and the conversation so far to produce the final test script now.
135
+
136
+ Do not call tools.
137
+ Do not continue exploring.
138
+ Do not add explanation.
139
+ Output only the final result in the required design-mode format.
140
+
141
+ Completion summary:
142
+ Suggested test name: ${suggestedTestName || "(choose a short kebab-case name)"}
143
+ Goal summary: ${goalSummary || "(not provided)"}
144
+ Final state: ${finalState || "(not provided)"}
145
+ Important steps:
146
+ ${formatStringList(summary.importantSteps)}
147
+ Verification points:
148
+ ${formatStringList(summary.verificationPoints)}
149
+ ${notesForScript ? `Notes for script:\n${notesForScript}\n` : ""}`;
110
150
  }
@@ -12,7 +12,7 @@ Apply the user's changes and output the revised test script.
12
12
  FORMAT RULES:
13
13
  - One simple instruction per line (NO numbers, NO bullets)
14
14
  - Use imperative commands: "Open X", "Click Y", "Type Z"
15
- - Include "assert: <condition>" lines to validate expected behavior
15
+ - Use plain-language verification lines such as "Verify ..." or "Validate ..." for expected behavior
16
16
  - End with "exit"
17
17
 
18
18
  Output only the revised test script, nothing else.`;
@@ -1,4 +1,5 @@
1
- import { appendCustomSections, buildBaseSystemPrompt, buildStrictModeRuntimeSection, isBrowserTarget } from "./base.js";
1
+ import { DEFAULT_AIM_ASSIST_MODE } from "../core/aim-assist.js";
2
+ import { appendCustomSections, buildBaseSystemPrompt, isBrowserTarget } from "./base.js";
2
3
  export function buildAppContextSection(text) {
3
4
  const trimmed = typeof text === "string" ? text.trim() : "";
4
5
  if (!trimmed) {
@@ -10,11 +11,11 @@ Use this to understand screen layouts, terminology, navigation, and expected beh
10
11
 
11
12
  ${trimmed}`;
12
13
  }
13
- export function buildExecutionModePrompt(deviceInfo, customInstructions = {}, appContextText = "", runtimeOptions = {}) {
14
+ export function buildExecutionModePrompt(deviceInfo, customInstructions = {}, appContextText = "", options = {}) {
14
15
  const executionCustomText = typeof customInstructions.executionModeInstructions === "string" ? customInstructions.executionModeInstructions.trim() : "";
15
- const basePrompt = buildBaseSystemPrompt(deviceInfo, customInstructions);
16
+ const aimAssistMode = typeof options.aimAssistMode === "string" ? options.aimAssistMode : DEFAULT_AIM_ASSIST_MODE;
17
+ const basePrompt = buildBaseSystemPrompt(deviceInfo, { aimAssistMode });
16
18
  const appContextSection = buildAppContextSection(appContextText);
17
- const strictModeSection = buildStrictModeRuntimeSection(runtimeOptions);
18
19
  const browserTarget = isBrowserTarget(deviceInfo);
19
20
  const prompt = `${basePrompt}
20
21
 
@@ -38,14 +39,18 @@ CRITICAL RULES:
38
39
 
39
40
  Your process:
40
41
  1. Read the instruction
41
- 2. Execute the required actions
42
- 3. Before tapping a mutating action, dismiss the keyboard if it is open and not needed
43
- 4. After a mutating action, inspect the resulting screen for success cues such as cleared fields, reset buttons, changed status, refreshed content, or navigation
44
- 5. Stop as soon as success is visible
45
- 6. Stop immediately - no commentary, no questions
42
+ 2. Choose the right tool when the instruction is really a higher-level capability:
43
+ - Use \`assert\` for assertions and verification intent, including legacy \`assert:\` lines and natural language like verify, validate, ensure, or confirm
44
+ - Use \`navigate\` for direct browser navigation instead of trying to manipulate the address bar
45
+ - Use \`loadmill\` only when the instruction explicitly references the word "loadmill"
46
+ 3. Execute the required actions
47
+ 4. Before tapping a mutating action, dismiss the keyboard if it is open and not needed
48
+ 5. After a mutating action, inspect the resulting screen for success cues such as cleared fields, reset buttons, changed status, refreshed content, or navigation
49
+ 6. Stop as soon as success is visible
50
+ 7. Stop immediately - no commentary, no questions
46
51
 
47
52
  Each instruction is independent. Do not reference previous instructions or ask about next steps.
48
- ${appContextSection ? `\n\n${appContextSection}` : ""}` + (strictModeSection ? `\n\n${strictModeSection}` : "");
53
+ ${appContextSection ? `\n\n${appContextSection}` : ""}`;
49
54
  return appendCustomSections(prompt, [
50
55
  { title: "Base Prompt Instructions", text: customInstructions.basePromptInstructions },
51
56
  { title: "Execution Mode Instructions", text: executionCustomText }
@@ -191,7 +191,9 @@ export class CuaDebugTracer {
191
191
  ...(Number.isInteger(debugContext?.instructionIndex) ? { instructionIndex: debugContext.instructionIndex } : {})
192
192
  };
193
193
  const chainId = scope === "design" ? ids.sessionId : ids.runId;
194
- const chainKey = `${scope}:${chainId || "unknown"}`;
194
+ const chainKey = typeof debugContext?.chainId === "string" && debugContext.chainId.trim()
195
+ ? debugContext.chainId.trim()
196
+ : `${scope}:${chainId || "unknown"}`;
195
197
  const previousPendingSafetyChecks = Array.isArray(this.lastPendingSafetyChecksByChain.get(chainKey))
196
198
  ? this.lastPendingSafetyChecksByChain.get(chainKey)
197
199
  : [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loadmill/droid-cua",
3
- "version": "2.9.0",
3
+ "version": "2.10.1-rc.8",
4
4
  "description": "AI-powered Android testing agent using OpenAI's computer-use model and ADB",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
@@ -23,9 +23,8 @@
23
23
  "test": "node --test unit-tests/*.test.js",
24
24
  "build": "tsc",
25
25
  "clean": "rm -rf build",
26
- "prerelease": "npm run build",
27
- "release:patch": "npm version patch && npm publish",
28
- "postversion": "git push && git push --tags"
26
+ "typecheck": "tsc --noEmit",
27
+ "prepublishOnly": "npm run build"
29
28
  },
30
29
  "repository": {
31
30
  "type": "git",
@@ -1,33 +0,0 @@
1
- export const TARGET_SCALED_WIDTH = 400;
2
- export const SCREENSHOT_RESOLUTION_MODE_DOWNSCALED = "downscaled";
3
- export const SCREENSHOT_RESOLUTION_MODE_NATIVE = "native";
4
- export function normalizeScreenshotResolutionMode(value) {
5
- return value === SCREENSHOT_RESOLUTION_MODE_NATIVE
6
- ? SCREENSHOT_RESOLUTION_MODE_NATIVE
7
- : SCREENSHOT_RESOLUTION_MODE_DOWNSCALED;
8
- }
9
- export function validateScreenshotResolutionMode(value, label) {
10
- if (typeof value !== "string") {
11
- throw new Error(`${label} must be one of: downscaled, native.`);
12
- }
13
- const normalized = normalizeScreenshotResolutionMode(value);
14
- if (normalized !== value) {
15
- throw new Error(`${label} must be one of: downscaled, native.`);
16
- }
17
- return normalized;
18
- }
19
- export function buildResolutionAwareDeviceInfo({ width, height, screenshotResolutionMode, }) {
20
- const normalizedMode = normalizeScreenshotResolutionMode(screenshotResolutionMode);
21
- const scale = normalizedMode === SCREENSHOT_RESOLUTION_MODE_NATIVE || width <= TARGET_SCALED_WIDTH
22
- ? 1.0
23
- : TARGET_SCALED_WIDTH / width;
24
- return {
25
- scaled_width: Math.round(width * scale),
26
- scaled_height: Math.round(height * scale),
27
- scale,
28
- screenshot_resolution_mode: normalizedMode,
29
- };
30
- }
31
- export function readScreenshotResolutionModeFromEnv() {
32
- return normalizeScreenshotResolutionMode(process.env.DROID_CUA_SCREENSHOT_RESOLUTION_MODE);
33
- }