Skip to content

Commit f203fa4

Browse files
committed
feat(phase3): pattern-first interaction primitives
- .NET UIA host: 4 new JSONL commands (setValue, scroll, expandCollapse, getText) with ResolveElement + GetPatternNames helpers - Node bridge: 4 new UIAHost convenience methods - pattern-actions.js: setElementValue, scrollElement, expandElement, collapseElement, toggleExpandCollapse, getElementText - high-level.js: fillField tries ValuePattern first, selectDropdownItem tries ExpandCollapsePattern first, both fall back gracefully - Pattern name normalization (handles both ProgrammaticName + short formats) - Barrel exports wired through interactions/index.js + ui-automation/index.js - 152 smoke assertions (41 new), 0 failures
1 parent fd4638c commit f203fa4

File tree

9 files changed

+659
-7
lines changed

9 files changed

+659
-7
lines changed

.github/hooks/logs/tool-audit.jsonl

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,3 +515,62 @@
515515
{"timestamp":"2026-02-27T11:38:08.110Z","tool":null,"result":null}
516516
{"timestamp":"2026-02-27T11:38:11.530Z","tool":null,"result":null}
517517
{"timestamp":"2026-02-27T11:38:17.798Z","tool":null,"result":null}
518+
{"timestamp":"2026-02-27T11:39:24.374Z","tool":null,"result":null}
519+
{"timestamp":"2026-02-27T11:39:30.052Z","tool":null,"result":null}
520+
{"timestamp":"2026-02-27T11:39:46.608Z","tool":null,"result":null}
521+
{"timestamp":"2026-02-27T11:39:52.686Z","tool":null,"result":null}
522+
{"timestamp":"2026-02-27T11:42:33.440Z","tool":null,"result":null}
523+
{"timestamp":"2026-02-27T11:42:37.356Z","tool":null,"result":null}
524+
{"timestamp":"2026-02-27T11:47:14.525Z","tool":null,"result":null}
525+
{"timestamp":"2026-02-27T11:47:42.270Z","tool":null,"result":null}
526+
{"timestamp":"2026-02-27T11:47:42.281Z","tool":null,"result":null}
527+
{"timestamp":"2026-02-27T11:47:42.374Z","tool":null,"result":null}
528+
{"timestamp":"2026-02-27T11:47:42.375Z","tool":null,"result":null}
529+
{"timestamp":"2026-02-27T11:47:42.427Z","tool":null,"result":null}
530+
{"timestamp":"2026-02-27T11:47:42.485Z","tool":null,"result":null}
531+
{"timestamp":"2026-02-27T11:47:42.487Z","tool":null,"result":null}
532+
{"timestamp":"2026-02-27T11:47:51.332Z","tool":null,"result":null}
533+
{"timestamp":"2026-02-27T11:47:51.354Z","tool":null,"result":null}
534+
{"timestamp":"2026-02-27T11:47:51.370Z","tool":null,"result":null}
535+
{"timestamp":"2026-02-27T11:47:57.011Z","tool":null,"result":null}
536+
{"timestamp":"2026-02-27T11:47:57.047Z","tool":null,"result":null}
537+
{"timestamp":"2026-02-27T11:48:06.604Z","tool":null,"result":null}
538+
{"timestamp":"2026-02-27T11:48:06.618Z","tool":null,"result":null}
539+
{"timestamp":"2026-02-27T11:48:06.668Z","tool":null,"result":null}
540+
{"timestamp":"2026-02-27T11:48:06.671Z","tool":null,"result":null}
541+
{"timestamp":"2026-02-27T11:49:27.613Z","tool":null,"result":null}
542+
{"timestamp":"2026-02-27T11:49:32.032Z","tool":null,"result":null}
543+
{"timestamp":"2026-02-27T11:49:39.222Z","tool":null,"result":null}
544+
{"timestamp":"2026-02-27T11:49:44.242Z","tool":null,"result":null}
545+
{"timestamp":"2026-02-27T11:49:48.749Z","tool":null,"result":null}
546+
{"timestamp":"2026-02-27T11:49:52.864Z","tool":null,"result":null}
547+
{"timestamp":"2026-02-27T11:49:57.063Z","tool":null,"result":null}
548+
{"timestamp":"2026-02-27T11:50:00.719Z","tool":null,"result":null}
549+
{"timestamp":"2026-02-27T11:50:22.446Z","tool":null,"result":null}
550+
{"timestamp":"2026-02-27T11:50:57.663Z","tool":null,"result":null}
551+
{"timestamp":"2026-02-27T11:51:42.648Z","tool":null,"result":null}
552+
{"timestamp":"2026-02-27T11:51:49.352Z","tool":null,"result":null}
553+
{"timestamp":"2026-02-27T11:52:34.093Z","tool":null,"result":null}
554+
{"timestamp":"2026-02-27T11:52:45.587Z","tool":null,"result":null}
555+
{"timestamp":"2026-02-27T11:53:50.953Z","tool":null,"result":null}
556+
{"timestamp":"2026-02-27T11:53:58.498Z","tool":null,"result":null}
557+
{"timestamp":"2026-02-27T11:54:03.722Z","tool":null,"result":null}
558+
{"timestamp":"2026-02-27T11:54:12.692Z","tool":null,"result":null}
559+
{"timestamp":"2026-02-27T11:54:17.546Z","tool":null,"result":null}
560+
{"timestamp":"2026-02-27T11:54:50.053Z","tool":null,"result":null}
561+
{"timestamp":"2026-02-27T11:55:21.749Z","tool":null,"result":null}
562+
{"timestamp":"2026-02-27T11:55:28.768Z","tool":null,"result":null}
563+
{"timestamp":"2026-02-27T11:55:35.472Z","tool":null,"result":null}
564+
{"timestamp":"2026-02-27T11:55:42.258Z","tool":null,"result":null}
565+
{"timestamp":"2026-02-27T11:55:46.379Z","tool":null,"result":null}
566+
{"timestamp":"2026-02-27T11:56:09.266Z","tool":null,"result":null}
567+
{"timestamp":"2026-02-27T11:57:38.260Z","tool":null,"result":null}
568+
{"timestamp":"2026-02-27T11:57:42.633Z","tool":null,"result":null}
569+
{"timestamp":"2026-02-27T11:57:46.835Z","tool":null,"result":null}
570+
{"timestamp":"2026-02-27T11:57:50.540Z","tool":null,"result":null}
571+
{"timestamp":"2026-02-27T11:57:55.254Z","tool":null,"result":null}
572+
{"timestamp":"2026-02-27T11:57:58.845Z","tool":null,"result":null}
573+
{"timestamp":"2026-02-27T11:58:02.916Z","tool":null,"result":null}
574+
{"timestamp":"2026-02-27T11:58:07.819Z","tool":null,"result":null}
575+
{"timestamp":"2026-02-27T11:58:11.826Z","tool":null,"result":null}
576+
{"timestamp":"2026-02-27T11:58:21.660Z","tool":null,"result":null}

scripts/smoke-command-system.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,81 @@ console.log('\n\x1b[1m[15] Phase 2: element-from-point + stable identity\x1b[0m'
379379
assert('detectRegions maps clickPoint from .NET or PS', isContent.includes('e.clickPoint'));
380380
}
381381

382+
// ── [16] Phase 3: Pattern-first interaction primitives ───────────────────
383+
{
384+
console.log('\n\x1b[1m[16] Phase 3 \u2013 Pattern-first interaction primitives\x1b[0m');
385+
386+
// .NET host has all 4 new handlers
387+
const dotnetPath = path.join(ROOT, 'src', 'native', 'windows-uia-dotnet', 'Program.cs');
388+
const dotnet = fs.readFileSync(dotnetPath, 'utf-8');
389+
assert('.NET host handles setValue command', dotnet.includes('case "setValue"'));
390+
assert('.NET host handles scroll command', dotnet.includes('case "scroll"'));
391+
assert('.NET host handles expandCollapse command', dotnet.includes('case "expandCollapse"'));
392+
assert('.NET host handles getText command', dotnet.includes('case "getText"'));
393+
assert('.NET HandleSetValue method', dotnet.includes('HandleSetValue'));
394+
assert('.NET HandleScroll method', dotnet.includes('HandleScroll'));
395+
assert('.NET HandleExpandCollapse method', dotnet.includes('HandleExpandCollapse'));
396+
assert('.NET HandleGetText method', dotnet.includes('HandleGetText'));
397+
assert('.NET ResolveElement helper', dotnet.includes('ResolveElement'));
398+
assert('.NET GetPatternNames helper', dotnet.includes('GetPatternNames'));
399+
400+
// Node bridge convenience methods
401+
const hostPath = path.join(ROOT, 'src', 'main', 'ui-automation', 'core', 'uia-host.js');
402+
const host = fs.readFileSync(hostPath, 'utf-8');
403+
assert('UIAHost.setValue bridge method', host.includes('async setValue'));
404+
assert('UIAHost.scroll bridge method', host.includes('async scroll'));
405+
assert('UIAHost.expandCollapse bridge method', host.includes('async expandCollapse'));
406+
assert('UIAHost.getText bridge method', host.includes('async getText'));
407+
408+
// pattern-actions.js exists with all functions
409+
const paPath = path.join(ROOT, 'src', 'main', 'ui-automation', 'interactions', 'pattern-actions.js');
410+
assert('pattern-actions.js exists', fs.existsSync(paPath));
411+
const pa = fs.readFileSync(paPath, 'utf-8');
412+
assert('normalizePatternName helper', pa.includes('function normalizePatternName'));
413+
assert('hasPattern helper', pa.includes('function hasPattern'));
414+
assert('setElementValue function', pa.includes('async function setElementValue'));
415+
assert('scrollElement function', pa.includes('async function scrollElement'));
416+
assert('expandElement function', pa.includes('async function expandElement'));
417+
assert('collapseElement function', pa.includes('async function collapseElement'));
418+
assert('toggleExpandCollapse function', pa.includes('async function toggleExpandCollapse'));
419+
assert('getElementText function', pa.includes('async function getElementText'));
420+
assert('pattern-actions exports all public functions',
421+
pa.includes('setElementValue') && pa.includes('scrollElement') &&
422+
pa.includes('expandElement') && pa.includes('collapseElement') &&
423+
pa.includes('getElementText') && pa.includes('normalizePatternName'));
424+
assert('pattern-actions returns patternUnsupported flag', pa.includes('patternUnsupported'));
425+
426+
// high-level.js upgraded with pattern-first strategies
427+
const hlPath = path.join(ROOT, 'src', 'main', 'ui-automation', 'interactions', 'high-level.js');
428+
const hl = fs.readFileSync(hlPath, 'utf-8');
429+
assert('fillField imports setElementValue from pattern-actions', hl.includes("require('./pattern-actions')"));
430+
assert('fillField tries ValuePattern first', hl.includes('setElementValue') && hl.includes('preferPattern'));
431+
assert('selectDropdownItem tries ExpandCollapsePattern first', hl.includes('expandElement') && hl.includes('ExpandCollapsePattern'));
432+
433+
// Barrel re-exports from interactions/index.js
434+
const intIdx = fs.readFileSync(path.join(ROOT, 'src', 'main', 'ui-automation', 'interactions', 'index.js'), 'utf-8');
435+
assert('interactions/index re-exports setElementValue', intIdx.includes('setElementValue'));
436+
assert('interactions/index re-exports scrollElement', intIdx.includes('scrollElement'));
437+
assert('interactions/index re-exports expandElement', intIdx.includes('expandElement'));
438+
assert('interactions/index re-exports collapseElement', intIdx.includes('collapseElement'));
439+
assert('interactions/index re-exports toggleExpandCollapse', intIdx.includes('toggleExpandCollapse'));
440+
assert('interactions/index re-exports getElementText', intIdx.includes('getElementText'));
441+
442+
// Main barrel exports
443+
const mainIdx = fs.readFileSync(path.join(ROOT, 'src', 'main', 'ui-automation', 'index.js'), 'utf-8');
444+
assert('main barrel exports setElementValue', mainIdx.includes('setElementValue'));
445+
assert('main barrel exports scrollElement', mainIdx.includes('scrollElement'));
446+
assert('main barrel exports expandElement', mainIdx.includes('expandElement'));
447+
assert('main barrel exports getElementText', mainIdx.includes('getElementText'));
448+
assert('main barrel exports normalizePatternName', mainIdx.includes('normalizePatternName'));
449+
assert('main barrel exports hasPattern', mainIdx.includes('hasPattern'));
450+
451+
// element-click.js handles both pattern name formats
452+
const ecPath = path.join(ROOT, 'src', 'main', 'ui-automation', 'interactions', 'element-click.js');
453+
const ec = fs.readFileSync(ecPath, 'utf-8');
454+
assert('clickElement handles short pattern name format', ec.includes("'Invoke'"));
455+
}
456+
382457
// ── Cleanup & Summary ────────────────────────────────────────────────────
383458
cleanup();
384459
// Also remove any screenshot artifacts from root

src/main/ui-automation/core/uia-host.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,34 @@ class UIAHost extends EventEmitter {
9898
return resp.tree;
9999
}
100100

101+
/** Set value on element at (x,y) using ValuePattern. */
102+
async setValue(x, y, value) {
103+
const resp = await this.send({ cmd: 'setValue', x, y, value });
104+
if (!resp.ok) throw new Error(resp.error || 'setValue failed');
105+
return resp;
106+
}
107+
108+
/** Scroll element at (x,y) using ScrollPattern. direction: up|down|left|right. amount: percent (0-100) or -1 for small increment. */
109+
async scroll(x, y, direction = 'down', amount = -1) {
110+
const resp = await this.send({ cmd: 'scroll', x, y, direction, amount });
111+
if (!resp.ok) throw new Error(resp.error || 'scroll failed');
112+
return resp;
113+
}
114+
115+
/** Expand/collapse element at (x,y). action: expand|collapse|toggle. */
116+
async expandCollapse(x, y, action = 'toggle') {
117+
const resp = await this.send({ cmd: 'expandCollapse', x, y, action });
118+
if (!resp.ok) throw new Error(resp.error || 'expandCollapse failed');
119+
return resp;
120+
}
121+
122+
/** Get text from element at (x,y) using TextPattern → ValuePattern → Name fallback. */
123+
async getText(x, y) {
124+
const resp = await this.send({ cmd: 'getText', x, y });
125+
if (!resp.ok) throw new Error(resp.error || 'getText failed');
126+
return resp;
127+
}
128+
101129
/** Gracefully shut down the host process. */
102130
async stop() {
103131
if (!this._alive || !this._proc) return;

src/main/ui-automation/index.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,15 @@ const {
9292
waitAndClick,
9393
clickAndWaitFor,
9494
selectFromDropdown,
95+
// Pattern-based interactions (Phase 3)
96+
normalizePatternName,
97+
hasPattern,
98+
setElementValue,
99+
scrollElement,
100+
expandElement,
101+
collapseElement,
102+
toggleExpandCollapse,
103+
getElementText,
95104
} = require('./interactions');
96105

97106
// Screenshot
@@ -168,6 +177,16 @@ module.exports = {
168177
clickAndWaitFor,
169178
selectFromDropdown,
170179

180+
// Pattern-based interactions (Phase 3)
181+
normalizePatternName,
182+
hasPattern,
183+
setElementValue,
184+
scrollElement,
185+
expandElement,
186+
collapseElement,
187+
toggleExpandCollapse,
188+
getElementText,
189+
171190
// Screenshot
172191
screenshot,
173192
screenshotActiveWindow,

src/main/ui-automation/interactions/element-click.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ async function clickElement(element, options = {}) {
136136
const centerY = bounds.y + bounds.height / 2;
137137

138138
// Strategy 1: Try Invoke pattern for buttons
139-
if (useInvoke && element.patterns?.includes('InvokePatternIdentifiers.Pattern')) {
139+
if (useInvoke && (element.patterns?.includes('InvokePatternIdentifiers.Pattern') || element.patterns?.includes('Invoke'))) {
140140
log(`Attempting Invoke pattern for "${element.name}"`);
141141
const invokeResult = await invokeElement(element);
142142
if (invokeResult.success) {

src/main/ui-automation/interactions/high-level.js

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
const { findElement, findElements, waitForElement } = require('../elements');
99
const { click, clickByText } = require('./element-click');
10+
const { setElementValue, expandElement } = require('./pattern-actions');
1011
const { typeText, sendKeys } = require('../keyboard');
1112
const { focusWindow, findWindows } = require('../window');
1213
const { log, sleep } = require('../core/helpers');
@@ -21,9 +22,18 @@ const { log, sleep } = require('../core/helpers');
2122
* @returns {Promise<{success: boolean}>}
2223
*/
2324
async function fillField(criteria, text, options = {}) {
24-
const { clear = true } = options;
25+
const { clear = true, preferPattern = true } = options;
26+
27+
// Strategy 1: Try ValuePattern (fast, no focus/click needed)
28+
if (preferPattern) {
29+
const patternResult = await setElementValue(criteria, text);
30+
if (patternResult.success) {
31+
log(`fillField: ValuePattern succeeded for "${text.slice(0, 30)}"`);
32+
return { success: true, method: 'ValuePattern' };
33+
}
34+
}
2535

26-
// Click the field
36+
// Strategy 2: Click + type (fallback)
2737
const clickResult = await click(criteria);
2838
if (!clickResult.success) {
2939
return { success: false };
@@ -39,7 +49,7 @@ async function fillField(criteria, text, options = {}) {
3949

4050
// Type text
4151
const typeResult = await typeText(text);
42-
return { success: typeResult.success };
52+
return { success: typeResult.success, method: 'sendKeys' };
4353
}
4454

4555
/**
@@ -52,9 +62,21 @@ async function fillField(criteria, text, options = {}) {
5262
* @returns {Promise<{success: boolean}>}
5363
*/
5464
async function selectDropdownItem(dropdownCriteria, itemCriteria, options = {}) {
55-
const { itemWait = 1000 } = options;
65+
const { itemWait = 1000, preferPattern = true } = options;
66+
67+
// Strategy 1: Try ExpandCollapsePattern to open
68+
if (preferPattern) {
69+
const expandResult = await expandElement(dropdownCriteria);
70+
if (expandResult.success) {
71+
log(`selectDropdownItem: ExpandCollapsePattern expanded (${expandResult.stateBefore}${expandResult.stateAfter})`);
72+
await sleep(itemWait);
73+
const itemQuery = typeof itemCriteria === 'string' ? { text: itemCriteria } : itemCriteria;
74+
const itemResult = await click(itemQuery);
75+
return { success: itemResult.success, method: 'ExpandCollapsePattern' };
76+
}
77+
}
5678

57-
// Click dropdown to open
79+
// Strategy 2: Click to open (fallback)
5880
const openResult = await click(dropdownCriteria);
5981
if (!openResult.success) {
6082
log('selectDropdownItem: Failed to open dropdown', 'warn');
@@ -69,7 +91,7 @@ async function selectDropdownItem(dropdownCriteria, itemCriteria, options = {})
6991
: itemCriteria;
7092

7193
const itemResult = await click(itemQuery);
72-
return { success: itemResult.success };
94+
return { success: itemResult.success, method: 'click' };
7395
}
7496

7597
/**

src/main/ui-automation/interactions/index.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,17 @@ const {
2525
selectFromDropdown,
2626
} = require('./high-level');
2727

28+
const {
29+
normalizePatternName,
30+
hasPattern,
31+
setElementValue,
32+
scrollElement,
33+
expandElement,
34+
collapseElement,
35+
toggleExpandCollapse,
36+
getElementText,
37+
} = require('./pattern-actions');
38+
2839
module.exports = {
2940
// Element clicks
3041
click,
@@ -44,4 +55,14 @@ module.exports = {
4455
waitAndClick,
4556
clickAndWaitFor,
4657
selectFromDropdown,
58+
59+
// Pattern-based interactions (Phase 3)
60+
normalizePatternName,
61+
hasPattern,
62+
setElementValue,
63+
scrollElement,
64+
expandElement,
65+
collapseElement,
66+
toggleExpandCollapse,
67+
getElementText,
4768
};

0 commit comments

Comments
 (0)