appium-uiwatchers-plugin 1.0.0
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/.c8rc.json +12 -0
- package/.github/workflows/npm-publish.yml +28 -0
- package/.husky/pre-commit +4 -0
- package/.lintstagedrc.json +4 -0
- package/.mocharc.json +10 -0
- package/.prettierignore +6 -0
- package/.prettierrc +11 -0
- package/README.md +376 -0
- package/eslint.config.js +65 -0
- package/package.json +114 -0
- package/src/commands/clear.ts +28 -0
- package/src/commands/list.ts +23 -0
- package/src/commands/register.ts +47 -0
- package/src/commands/toggle.ts +43 -0
- package/src/commands/unregister.ts +43 -0
- package/src/config.ts +30 -0
- package/src/element-cache.ts +262 -0
- package/src/plugin.ts +437 -0
- package/src/types.ts +207 -0
- package/src/utils.ts +47 -0
- package/src/validators.ts +131 -0
- package/src/watcher-checker.ts +113 -0
- package/src/watcher-store.ts +210 -0
- package/test/e2e/config.e2e.spec.cjs +420 -0
- package/test/e2e/plugin.e2e.spec.cjs +312 -0
- package/test/unit/element-cache.spec.js +269 -0
- package/test/unit/plugin.spec.js +52 -0
- package/test/unit/utils.spec.js +85 -0
- package/test/unit/validators.spec.js +246 -0
- package/test/unit/watcher-checker.spec.js +274 -0
- package/test/unit/watcher-store.spec.js +405 -0
- package/tsconfig.json +31 -0
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { BasePlugin } from '@appium/base-plugin';
|
|
2
|
+
import { logger } from '@appium/support';
|
|
3
|
+
import { WatcherStore } from './watcher-store.js';
|
|
4
|
+
import { ElementReferenceCache } from './element-cache.js';
|
|
5
|
+
import { registerUIWatcher } from './commands/register.js';
|
|
6
|
+
import { unregisterUIWatcher } from './commands/unregister.js';
|
|
7
|
+
import { clearAllUIWatchers } from './commands/clear.js';
|
|
8
|
+
import { listUIWatchers } from './commands/list.js';
|
|
9
|
+
import { disableUIWatchers, enableUIWatchers } from './commands/toggle.js';
|
|
10
|
+
import { checkWatchers } from './watcher-checker.js';
|
|
11
|
+
import { DefaultPluginConfig, type PluginConfig } from './config.js';
|
|
12
|
+
import { extractElementId } from './utils.js';
|
|
13
|
+
|
|
14
|
+
const log = logger.getLogger('AppiumUIWatchers');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Element action commands to intercept for StaleElement recovery
|
|
18
|
+
* These are Appium command names that operate on elements
|
|
19
|
+
*/
|
|
20
|
+
const ELEMENT_ACTION_COMMANDS = [
|
|
21
|
+
'click',
|
|
22
|
+
'getText',
|
|
23
|
+
'getAttribute',
|
|
24
|
+
'elementDisplayed',
|
|
25
|
+
'elementEnabled',
|
|
26
|
+
'elementSelected',
|
|
27
|
+
'getName',
|
|
28
|
+
'getLocation',
|
|
29
|
+
'getSize',
|
|
30
|
+
'setValue',
|
|
31
|
+
'setValueImmediate',
|
|
32
|
+
'clear',
|
|
33
|
+
'getElementScreenshot',
|
|
34
|
+
'getElementRect',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* UIWatchersPlugin - Appium plugin for automatic UI element handling
|
|
39
|
+
*
|
|
40
|
+
* This plugin provides automatic detection and handling of unexpected UI elements
|
|
41
|
+
* (popups, banners, dialogs) during test execution without explicit waits.
|
|
42
|
+
*/
|
|
43
|
+
class UIWatchersPlugin extends BasePlugin {
|
|
44
|
+
/** Per-session watcher storage (keyed by sessionId) */
|
|
45
|
+
private stores: Map<string, WatcherStore>;
|
|
46
|
+
|
|
47
|
+
/** Per-session element reference cache (keyed by sessionId) */
|
|
48
|
+
private caches: Map<string, ElementReferenceCache>;
|
|
49
|
+
|
|
50
|
+
/** Plugin configuration */
|
|
51
|
+
private config: Required<PluginConfig>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Creates an instance of UIWatchersPlugin
|
|
55
|
+
* @param pluginName - The name of the plugin
|
|
56
|
+
* @param cliArgs - Optional CLI arguments passed to the plugin
|
|
57
|
+
*/
|
|
58
|
+
constructor(pluginName: string, cliArgs?: Record<string, unknown>) {
|
|
59
|
+
super(pluginName, cliArgs);
|
|
60
|
+
|
|
61
|
+
this.config = Object.assign({}, DefaultPluginConfig, cliArgs as PluginConfig);
|
|
62
|
+
|
|
63
|
+
this.stores = new Map();
|
|
64
|
+
this.caches = new Map();
|
|
65
|
+
log.info('[UIWatchers] Plugin initialized with config:', this.config);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get or create WatcherStore for a specific session
|
|
70
|
+
* @param driver - The driver instance containing sessionId
|
|
71
|
+
* @returns The WatcherStore for this session
|
|
72
|
+
* @throws Error if no sessionId is available
|
|
73
|
+
*/
|
|
74
|
+
private getStore(driver: any): WatcherStore {
|
|
75
|
+
const sessionId = driver.sessionId;
|
|
76
|
+
if (!sessionId) {
|
|
77
|
+
throw new Error('No session ID available');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!this.stores.has(sessionId)) {
|
|
81
|
+
this.stores.set(sessionId, new WatcherStore(this.config));
|
|
82
|
+
log.info(`[UIWatchers] Created watcher store for session ${sessionId}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return this.stores.get(sessionId)!;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get or create ElementReferenceCache for a specific session
|
|
90
|
+
* @param driver - The driver instance containing sessionId
|
|
91
|
+
* @returns The ElementReferenceCache for this session
|
|
92
|
+
* @throws Error if no sessionId is available
|
|
93
|
+
*/
|
|
94
|
+
private getCache(driver: any): ElementReferenceCache {
|
|
95
|
+
const sessionId = driver.sessionId;
|
|
96
|
+
if (!sessionId) {
|
|
97
|
+
throw new Error('No session ID available');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!this.caches.has(sessionId)) {
|
|
101
|
+
this.caches.set(sessionId, new ElementReferenceCache(this.config));
|
|
102
|
+
log.info(`[UIWatchers] Created element cache for session ${sessionId}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return this.caches.get(sessionId)!;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if error is a StaleElementReferenceException
|
|
110
|
+
*/
|
|
111
|
+
private isStaleElementException(error: any): boolean {
|
|
112
|
+
const errorName = error?.name || error?.error || '';
|
|
113
|
+
return (
|
|
114
|
+
errorName.includes('StaleElementReference') ||
|
|
115
|
+
error?.message?.includes('stale element reference') ||
|
|
116
|
+
error?.message?.includes('element is not attached')
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Override execute command to handle mobile: commands
|
|
122
|
+
*/
|
|
123
|
+
async execute(next: () => Promise<any>, driver: any, script: string, args: any[]): Promise<any> {
|
|
124
|
+
const params = args && args.length > 0 ? args[0] : null;
|
|
125
|
+
const store = this.getStore(driver);
|
|
126
|
+
|
|
127
|
+
switch (script) {
|
|
128
|
+
case 'mobile: registerUIWatcher':
|
|
129
|
+
return await registerUIWatcher(store, params);
|
|
130
|
+
case 'mobile: unregisterUIWatcher':
|
|
131
|
+
return await unregisterUIWatcher(store, params.name);
|
|
132
|
+
case 'mobile: clearAllUIWatchers':
|
|
133
|
+
return await clearAllUIWatchers(store);
|
|
134
|
+
case 'mobile: listUIWatchers':
|
|
135
|
+
return await listUIWatchers(store);
|
|
136
|
+
case 'mobile: disableUIWatchers':
|
|
137
|
+
return await disableUIWatchers(store);
|
|
138
|
+
case 'mobile: enableUIWatchers':
|
|
139
|
+
return await enableUIWatchers(store);
|
|
140
|
+
default:
|
|
141
|
+
return await next();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Intercept findElement to check watchers on exceptions and cache on success
|
|
147
|
+
*/
|
|
148
|
+
async findElement(next: () => Promise<any>, driver: any, ...args: any[]): Promise<any> {
|
|
149
|
+
const [using, value] = args;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
// Try the original findElement
|
|
153
|
+
const result = await next();
|
|
154
|
+
|
|
155
|
+
// On success, cache the element reference for stale element recovery
|
|
156
|
+
if (result && using && value) {
|
|
157
|
+
const elementId = extractElementId(result);
|
|
158
|
+
if (elementId) {
|
|
159
|
+
const cache = this.getCache(driver);
|
|
160
|
+
cache.cacheElement(elementId, using, value);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return result;
|
|
165
|
+
} catch (error: any) {
|
|
166
|
+
// Check if this is NoSuchElementException or StaleElementReferenceException
|
|
167
|
+
const errorName = error.name || error.error || '';
|
|
168
|
+
const shouldCheckWatchers =
|
|
169
|
+
errorName.includes('NoSuchElement') ||
|
|
170
|
+
errorName.includes('StaleElementReference') ||
|
|
171
|
+
error.message?.includes('An element could not be located');
|
|
172
|
+
|
|
173
|
+
if (shouldCheckWatchers) {
|
|
174
|
+
log.debug('[UIWatchers] findElement failed, checking watchers');
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const store = this.getStore(driver);
|
|
178
|
+
// Check all watchers
|
|
179
|
+
await checkWatchers(driver, store);
|
|
180
|
+
|
|
181
|
+
// Retry findElement once
|
|
182
|
+
log.debug('[UIWatchers] Retrying findElement after watcher checking');
|
|
183
|
+
const result = await next();
|
|
184
|
+
|
|
185
|
+
// Cache the successful retry result
|
|
186
|
+
if (result && using && value) {
|
|
187
|
+
const elementId = extractElementId(result);
|
|
188
|
+
if (elementId) {
|
|
189
|
+
const cache = this.getCache(driver);
|
|
190
|
+
cache.cacheElement(elementId, using, value);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return result;
|
|
195
|
+
} catch {
|
|
196
|
+
// If retry also fails, throw the original exception
|
|
197
|
+
log.debug('[UIWatchers] Retry failed, throwing original exception');
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Not a handled exception type, re-throw
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Intercept findElements to check watchers on empty results and cache on success
|
|
209
|
+
*/
|
|
210
|
+
async findElements(next: () => Promise<any>, driver: any, ...args: any[]): Promise<any> {
|
|
211
|
+
const [using, value] = args;
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
// Try the original findElements
|
|
215
|
+
const result = await next();
|
|
216
|
+
|
|
217
|
+
// Check if result is empty array
|
|
218
|
+
if (Array.isArray(result) && result.length === 0) {
|
|
219
|
+
log.debug('[UIWatchers] findElements returned empty array, checking watchers');
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const store = this.getStore(driver);
|
|
223
|
+
// Check all watchers
|
|
224
|
+
await checkWatchers(driver, store);
|
|
225
|
+
|
|
226
|
+
// Retry findElements once
|
|
227
|
+
log.debug('[UIWatchers] Retrying findElements after watcher checking');
|
|
228
|
+
const retryResult = await next();
|
|
229
|
+
|
|
230
|
+
// Cache successful retry results
|
|
231
|
+
if (Array.isArray(retryResult) && retryResult.length > 0 && using && value) {
|
|
232
|
+
const cache = this.getCache(driver);
|
|
233
|
+
cache.cacheElements(retryResult, using, value);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return retryResult;
|
|
237
|
+
} catch {
|
|
238
|
+
// If retry fails, return empty array (original result)
|
|
239
|
+
log.debug('[UIWatchers] Retry failed, returning empty array');
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Non-empty result, cache all elements and return
|
|
245
|
+
if (Array.isArray(result) && result.length > 0 && using && value) {
|
|
246
|
+
const cache = this.getCache(driver);
|
|
247
|
+
cache.cacheElements(result, using, value);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return result;
|
|
251
|
+
} catch (error: any) {
|
|
252
|
+
// Exception thrown by findElements
|
|
253
|
+
log.debug('[UIWatchers] findElements threw exception, checking watchers');
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const store = this.getStore(driver);
|
|
257
|
+
// Check all watchers
|
|
258
|
+
await checkWatchers(driver, store);
|
|
259
|
+
|
|
260
|
+
// Retry findElements once
|
|
261
|
+
log.debug('[UIWatchers] Retrying findElements after watcher checking');
|
|
262
|
+
const result = await next();
|
|
263
|
+
|
|
264
|
+
// Cache successful retry results
|
|
265
|
+
if (Array.isArray(result) && result.length > 0 && using && value) {
|
|
266
|
+
const cache = this.getCache(driver);
|
|
267
|
+
cache.cacheElements(result, using, value);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return result;
|
|
271
|
+
} catch {
|
|
272
|
+
// If retry also fails, throw the original exception
|
|
273
|
+
log.debug('[UIWatchers] Retry failed, throwing original exception');
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Handle method intercepts all Appium commands not specifically handled by named methods
|
|
281
|
+
* We use this to intercept element action commands (click, getText, etc.) for StaleElement recovery
|
|
282
|
+
*/
|
|
283
|
+
async handle(
|
|
284
|
+
next: () => Promise<any>,
|
|
285
|
+
driver: any,
|
|
286
|
+
cmdName: string,
|
|
287
|
+
...args: any[]
|
|
288
|
+
): Promise<any> {
|
|
289
|
+
// Only intercept element action commands
|
|
290
|
+
if (!ELEMENT_ACTION_COMMANDS.includes(cmdName)) {
|
|
291
|
+
return await next();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Extract elementId from args (first argument for element commands)
|
|
295
|
+
const elementId = args[0];
|
|
296
|
+
if (!elementId) {
|
|
297
|
+
return await next();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return this.interceptAction(next, driver, elementId, cmdName, args);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Intercept element action commands for StaleElement recovery
|
|
305
|
+
*/
|
|
306
|
+
private async interceptAction(
|
|
307
|
+
next: () => Promise<any>,
|
|
308
|
+
driver: any,
|
|
309
|
+
elementId: string,
|
|
310
|
+
cmdName: string,
|
|
311
|
+
args: any[]
|
|
312
|
+
): Promise<any> {
|
|
313
|
+
const cache = this.getCache(driver);
|
|
314
|
+
|
|
315
|
+
// Check for mapped ID first (from previous recovery)
|
|
316
|
+
const actualId = cache.getMappedId(elementId) || elementId;
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
return await next();
|
|
320
|
+
} catch (error: any) {
|
|
321
|
+
// Only handle StaleElementReferenceException
|
|
322
|
+
if (!this.isStaleElementException(error)) {
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
log.debug(`[UIWatchers] StaleElement on ${cmdName}, checking watchers`);
|
|
327
|
+
|
|
328
|
+
// Step 1: Check watchers FIRST
|
|
329
|
+
const store = this.getStore(driver);
|
|
330
|
+
const watchersTriggered = await checkWatchers(driver, store);
|
|
331
|
+
|
|
332
|
+
// Step 2: If no watchers handled, throw original exception
|
|
333
|
+
if (!watchersTriggered) {
|
|
334
|
+
log.debug('[UIWatchers] No watchers triggered, throwing original exception');
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Step 3: Lookup cached reference (only if watchers handled something)
|
|
339
|
+
const ref = cache.getRef(actualId);
|
|
340
|
+
if (!ref) {
|
|
341
|
+
log.error(`[UIWatchers] Element reference not found in cache for ${actualId}`);
|
|
342
|
+
throw new Error(
|
|
343
|
+
'Element reference not found in cache. Unable to recover from StaleElementReferenceException.'
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
log.debug(
|
|
348
|
+
`[UIWatchers] Attempting to recover element using cached locator (${ref.using}=${ref.value})`
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
// Step 4: Re-find element
|
|
352
|
+
let newElement: any;
|
|
353
|
+
if (ref.source === 'findElement') {
|
|
354
|
+
newElement = await driver.findElement(ref.using, ref.value);
|
|
355
|
+
} else {
|
|
356
|
+
// findElements - need to get element at specific index
|
|
357
|
+
const elements = await driver.findElements(ref.using, ref.value);
|
|
358
|
+
if (elements.length <= ref.index) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
`Element at index ${ref.index} not found in findElements result. Expected at least ${ref.index + 1} elements.`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
newElement = elements[ref.index];
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Step 5: Create mapping with transitive update
|
|
367
|
+
const newId = extractElementId(newElement);
|
|
368
|
+
if (newId) {
|
|
369
|
+
cache.setMapping(elementId, newId);
|
|
370
|
+
log.debug(`[UIWatchers] Created element ID mapping: ${elementId} → ${newId}`);
|
|
371
|
+
|
|
372
|
+
// Step 6: Retry action with new element ID
|
|
373
|
+
// Replace elementId in args with newId, then call driver command
|
|
374
|
+
const newArgs = [newId, ...args.slice(1)];
|
|
375
|
+
log.debug(`[UIWatchers] Retrying ${cmdName} with recovered element`);
|
|
376
|
+
return await driver[cmdName](...newArgs);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// If we couldn't extract newId, throw original error
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Cleanup watcher store and element cache when session ends normally
|
|
386
|
+
* Appium lifecycle hook called when deleteSession is invoked
|
|
387
|
+
*/
|
|
388
|
+
async deleteSession(next: () => Promise<any>, driver: any, ..._args: any[]): Promise<any> {
|
|
389
|
+
const sessionId = driver.sessionId;
|
|
390
|
+
|
|
391
|
+
// Call the original deleteSession first
|
|
392
|
+
const result = await next();
|
|
393
|
+
|
|
394
|
+
// Clean up our session-specific store and cache
|
|
395
|
+
if (sessionId) {
|
|
396
|
+
if (this.stores.has(sessionId)) {
|
|
397
|
+
this.stores.delete(sessionId);
|
|
398
|
+
log.info(`[UIWatchers] Cleaned up watcher store for session ${sessionId}`);
|
|
399
|
+
}
|
|
400
|
+
if (this.caches.has(sessionId)) {
|
|
401
|
+
this.caches.get(sessionId)!.clear();
|
|
402
|
+
this.caches.delete(sessionId);
|
|
403
|
+
log.info(`[UIWatchers] Cleaned up element cache for session ${sessionId}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Cleanup watcher store and element cache when session ends unexpectedly (crash, timeout, etc.)
|
|
412
|
+
* Appium lifecycle hook called on unexpected shutdown
|
|
413
|
+
*/
|
|
414
|
+
async onUnexpectedShutdown(driver: any, cause: string): Promise<void> {
|
|
415
|
+
const sessionId = driver.sessionId;
|
|
416
|
+
|
|
417
|
+
if (sessionId) {
|
|
418
|
+
if (this.stores.has(sessionId)) {
|
|
419
|
+
this.stores.delete(sessionId);
|
|
420
|
+
log.info(
|
|
421
|
+
`[UIWatchers] Cleaned up watcher store for session ${sessionId} (cause: ${cause})`
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
if (this.caches.has(sessionId)) {
|
|
425
|
+
this.caches.get(sessionId)!.clear();
|
|
426
|
+
this.caches.delete(sessionId);
|
|
427
|
+
log.info(
|
|
428
|
+
`[UIWatchers] Cleaned up element cache for session ${sessionId} (cause: ${cause})`
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Export as both default and named export for compatibility
|
|
436
|
+
export { UIWatchersPlugin };
|
|
437
|
+
export default UIWatchersPlugin;
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for Appium UI Watchers Plugin
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Appium locator strategy definition
|
|
7
|
+
* Represents a standard Appium element locator
|
|
8
|
+
*/
|
|
9
|
+
export interface Locator {
|
|
10
|
+
/** Locator strategy (e.g., 'id', 'xpath', 'accessibility id') */
|
|
11
|
+
using: string;
|
|
12
|
+
/** Locator value/selector */
|
|
13
|
+
value: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* UI Watcher registration parameters
|
|
18
|
+
* These are the parameters passed when registering a new watcher
|
|
19
|
+
*/
|
|
20
|
+
export interface UIWatcher {
|
|
21
|
+
/** Unique identifier for the watcher */
|
|
22
|
+
name: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Priority level for watcher execution (higher values checked first)
|
|
26
|
+
* @default 0
|
|
27
|
+
*/
|
|
28
|
+
priority?: number;
|
|
29
|
+
|
|
30
|
+
/** Locator for the element to watch for (e.g., popup, banner) */
|
|
31
|
+
referenceLocator: Locator;
|
|
32
|
+
|
|
33
|
+
/** Locator for the element to click when reference is found */
|
|
34
|
+
actionLocator: Locator;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Time in milliseconds before watcher auto-expires from registration time
|
|
38
|
+
* Must be ≤ 60000ms (1 minute)
|
|
39
|
+
*/
|
|
40
|
+
duration: number;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* If true, deactivate watcher after first successful trigger
|
|
44
|
+
* @default false
|
|
45
|
+
*/
|
|
46
|
+
stopOnFound?: boolean;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Cooldown period in milliseconds after successful action click
|
|
50
|
+
* Watcher skipped during cooldown. Ignored if stopOnFound=true
|
|
51
|
+
* @default 0
|
|
52
|
+
*/
|
|
53
|
+
cooldownMs?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Internal watcher state representation
|
|
58
|
+
* Used for managing watcher lifecycle and tracking trigger information
|
|
59
|
+
*/
|
|
60
|
+
export interface WatcherState {
|
|
61
|
+
/** Unique identifier for the watcher */
|
|
62
|
+
name: string;
|
|
63
|
+
|
|
64
|
+
/** Priority level for watcher execution */
|
|
65
|
+
priority: number;
|
|
66
|
+
|
|
67
|
+
/** Locator for the element to watch for */
|
|
68
|
+
referenceLocator: Locator;
|
|
69
|
+
|
|
70
|
+
/** Locator for the element to click when reference is found */
|
|
71
|
+
actionLocator: Locator;
|
|
72
|
+
|
|
73
|
+
/** Duration in milliseconds before auto-expiry */
|
|
74
|
+
duration: number;
|
|
75
|
+
|
|
76
|
+
/** If true, deactivate after first trigger */
|
|
77
|
+
stopOnFound: boolean;
|
|
78
|
+
|
|
79
|
+
/** Cooldown period in milliseconds */
|
|
80
|
+
cooldownMs: number;
|
|
81
|
+
|
|
82
|
+
/** Timestamp when watcher was registered (milliseconds since epoch) */
|
|
83
|
+
registeredAt: number;
|
|
84
|
+
|
|
85
|
+
/** Timestamp when watcher expires (milliseconds since epoch) */
|
|
86
|
+
expiresAt: number;
|
|
87
|
+
|
|
88
|
+
/** Current watcher status */
|
|
89
|
+
status: 'active' | 'inactive';
|
|
90
|
+
|
|
91
|
+
/** Number of times watcher has been triggered */
|
|
92
|
+
triggerCount: number;
|
|
93
|
+
|
|
94
|
+
/** Timestamp of last trigger (null if never triggered) */
|
|
95
|
+
lastTriggeredAt: number | null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Result returned by registerUIWatcher command
|
|
100
|
+
*/
|
|
101
|
+
export interface RegisterWatcherResult {
|
|
102
|
+
/** Operation success status */
|
|
103
|
+
success: boolean;
|
|
104
|
+
|
|
105
|
+
/** Watcher information after registration */
|
|
106
|
+
watcher: {
|
|
107
|
+
name: string;
|
|
108
|
+
priority: number;
|
|
109
|
+
registeredAt: number;
|
|
110
|
+
expiresAt: number;
|
|
111
|
+
status: 'active' | 'inactive';
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Result returned by unregisterUIWatcher command
|
|
117
|
+
*/
|
|
118
|
+
export interface UnregisterWatcherResult {
|
|
119
|
+
/** Operation success status */
|
|
120
|
+
success: boolean;
|
|
121
|
+
|
|
122
|
+
/** Name of the removed watcher */
|
|
123
|
+
removed: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Result returned by clearAllUIWatchers command
|
|
128
|
+
*/
|
|
129
|
+
export interface ClearAllWatchersResult {
|
|
130
|
+
/** Operation success status */
|
|
131
|
+
success: boolean;
|
|
132
|
+
|
|
133
|
+
/** Number of watchers removed */
|
|
134
|
+
removedCount: number;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Result returned by listUIWatchers command
|
|
139
|
+
*/
|
|
140
|
+
export interface ListWatchersResult {
|
|
141
|
+
/** Operation success status */
|
|
142
|
+
success: boolean;
|
|
143
|
+
|
|
144
|
+
/** List of all active watchers with their state */
|
|
145
|
+
watchers: WatcherState[];
|
|
146
|
+
|
|
147
|
+
/** Total count of watchers */
|
|
148
|
+
totalCount: number;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Result returned by disableUIWatchers/enableUIWatchers commands
|
|
153
|
+
*/
|
|
154
|
+
export interface ToggleWatchersResult {
|
|
155
|
+
/** Operation success status */
|
|
156
|
+
success: boolean;
|
|
157
|
+
|
|
158
|
+
/** Status message */
|
|
159
|
+
message: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ============================================================================
|
|
163
|
+
// Element Reference Caching Types (for StaleElement recovery)
|
|
164
|
+
// ============================================================================
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Cached reference for element from findElement
|
|
168
|
+
*/
|
|
169
|
+
export interface CachedElementRef {
|
|
170
|
+
/** Locator strategy used to find the element */
|
|
171
|
+
using: string;
|
|
172
|
+
|
|
173
|
+
/** Locator value used to find the element */
|
|
174
|
+
value: string;
|
|
175
|
+
|
|
176
|
+
/** Source operation that found this element */
|
|
177
|
+
source: 'findElement';
|
|
178
|
+
|
|
179
|
+
/** Timestamp when this reference was cached */
|
|
180
|
+
createdAt: number;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Cached reference for element from findElements
|
|
185
|
+
* Includes the index position in the result array
|
|
186
|
+
*/
|
|
187
|
+
export interface CachedElementsRef {
|
|
188
|
+
/** Locator strategy used to find the elements */
|
|
189
|
+
using: string;
|
|
190
|
+
|
|
191
|
+
/** Locator value used to find the elements */
|
|
192
|
+
value: string;
|
|
193
|
+
|
|
194
|
+
/** Index position of this element in the findElements result array */
|
|
195
|
+
index: number;
|
|
196
|
+
|
|
197
|
+
/** Source operation that found this element */
|
|
198
|
+
source: 'findElements';
|
|
199
|
+
|
|
200
|
+
/** Timestamp when this reference was cached */
|
|
201
|
+
createdAt: number;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Union type for cached element references
|
|
206
|
+
*/
|
|
207
|
+
export type CachedRef = CachedElementRef | CachedElementsRef;
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for UI Watchers Plugin
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* W3C WebDriver Element Identifier key
|
|
7
|
+
* This is the standard key used in W3C WebDriver protocol to identify elements
|
|
8
|
+
*/
|
|
9
|
+
export const W3C_ELEMENT_KEY = 'element-6066-11e4-a52e-4f735466cecf';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Element object type that supports both W3C and JSONWP formats
|
|
13
|
+
*/
|
|
14
|
+
export interface ElementObject {
|
|
15
|
+
ELEMENT?: string;
|
|
16
|
+
[W3C_ELEMENT_KEY]?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extract element ID from element object
|
|
21
|
+
* Handles W3C format, JSONWP format, and direct string IDs
|
|
22
|
+
*
|
|
23
|
+
* @param element - Element object or string ID
|
|
24
|
+
* @returns The element ID string, or undefined if not found
|
|
25
|
+
*/
|
|
26
|
+
export function extractElementId(
|
|
27
|
+
element: ElementObject | string | null | undefined
|
|
28
|
+
): string | undefined {
|
|
29
|
+
if (!element) return undefined;
|
|
30
|
+
|
|
31
|
+
// Direct string ID
|
|
32
|
+
if (typeof element === 'string') {
|
|
33
|
+
return element;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// W3C format
|
|
37
|
+
if (element[W3C_ELEMENT_KEY]) {
|
|
38
|
+
return element[W3C_ELEMENT_KEY];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// JSONWP format
|
|
42
|
+
if (element.ELEMENT) {
|
|
43
|
+
return element.ELEMENT;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|