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
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { remote: wdio } = require('webdriverio');
|
|
3
|
+
const { pluginE2EHarness } = require('@appium/plugin-test-support');
|
|
4
|
+
|
|
5
|
+
const THIS_PLUGIN_DIR = path.join(__dirname, '..', '..');
|
|
6
|
+
const TEST_HOST = '127.0.0.1';
|
|
7
|
+
const TEST_PORT = 4723;
|
|
8
|
+
const TEST_FAKE_APP = path.join(
|
|
9
|
+
THIS_PLUGIN_DIR,
|
|
10
|
+
'node_modules',
|
|
11
|
+
'@appium',
|
|
12
|
+
'fake-driver',
|
|
13
|
+
'test',
|
|
14
|
+
'fixtures',
|
|
15
|
+
'app.xml'
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const TEST_CAPS = {
|
|
19
|
+
platformName: 'Fake',
|
|
20
|
+
'appium:automationName': 'Fake',
|
|
21
|
+
'appium:deviceName': 'Fake',
|
|
22
|
+
'appium:app': TEST_FAKE_APP,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const WDIO_OPTS = {
|
|
26
|
+
hostname: TEST_HOST,
|
|
27
|
+
port: TEST_PORT,
|
|
28
|
+
connectionRetryCount: 0,
|
|
29
|
+
capabilities: TEST_CAPS,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
describe('UIWatchers Plugin E2E', function () {
|
|
33
|
+
let driver;
|
|
34
|
+
|
|
35
|
+
pluginE2EHarness({
|
|
36
|
+
before,
|
|
37
|
+
after,
|
|
38
|
+
port: TEST_PORT,
|
|
39
|
+
host: TEST_HOST,
|
|
40
|
+
appiumHome: THIS_PLUGIN_DIR,
|
|
41
|
+
driverName: 'fake',
|
|
42
|
+
driverSource: 'npm',
|
|
43
|
+
driverSpec: '@appium/fake-driver',
|
|
44
|
+
pluginName: 'uiwatchers',
|
|
45
|
+
pluginSource: 'local',
|
|
46
|
+
pluginSpec: THIS_PLUGIN_DIR,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
beforeEach(async function () {
|
|
50
|
+
driver = await wdio(WDIO_OPTS);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterEach(async function () {
|
|
54
|
+
if (driver) {
|
|
55
|
+
await driver.deleteSession();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('Plugin activation', function () {
|
|
60
|
+
it('should start session with plugin active', async function () {
|
|
61
|
+
const session = await driver.getSession();
|
|
62
|
+
session.should.exist;
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('mobile: registerUIWatcher', function () {
|
|
67
|
+
it('should register a new watcher successfully', async function () {
|
|
68
|
+
const result = await driver.executeScript('mobile: registerUIWatcher', [
|
|
69
|
+
{
|
|
70
|
+
name: 'test-watcher',
|
|
71
|
+
referenceLocator: { using: 'id', value: 'popup' },
|
|
72
|
+
actionLocator: { using: 'id', value: 'close' },
|
|
73
|
+
duration: 30000,
|
|
74
|
+
},
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
result.should.have.property('success', true);
|
|
78
|
+
result.should.have.property('watcher');
|
|
79
|
+
result.watcher.should.have.property('name', 'test-watcher');
|
|
80
|
+
result.watcher.should.have.property('priority', 0);
|
|
81
|
+
result.watcher.should.have.property('status', 'active');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should register watcher with optional parameters', async function () {
|
|
85
|
+
const result = await driver.executeScript('mobile: registerUIWatcher', [
|
|
86
|
+
{
|
|
87
|
+
name: 'priority-watcher',
|
|
88
|
+
referenceLocator: { using: 'id', value: 'banner' },
|
|
89
|
+
actionLocator: { using: 'id', value: 'dismiss' },
|
|
90
|
+
duration: 60000,
|
|
91
|
+
priority: 10,
|
|
92
|
+
stopOnFound: true,
|
|
93
|
+
cooldownMs: 5000,
|
|
94
|
+
},
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
result.should.have.property('success', true);
|
|
98
|
+
result.watcher.should.have.property('priority', 10);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should throw error for duration > 60000', async function () {
|
|
102
|
+
try {
|
|
103
|
+
await driver.executeScript('mobile: registerUIWatcher', [
|
|
104
|
+
{
|
|
105
|
+
name: 'long-duration',
|
|
106
|
+
referenceLocator: { using: 'id', value: 'popup' },
|
|
107
|
+
actionLocator: { using: 'id', value: 'close' },
|
|
108
|
+
duration: 70000,
|
|
109
|
+
},
|
|
110
|
+
]);
|
|
111
|
+
throw new Error('Should have thrown an error');
|
|
112
|
+
} catch (error) {
|
|
113
|
+
error.message.should.match(/60 seconds/);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('mobile: listUIWatchers', function () {
|
|
119
|
+
beforeEach(async function () {
|
|
120
|
+
// Clear any existing watchers
|
|
121
|
+
await driver.executeScript('mobile: clearAllUIWatchers', []);
|
|
122
|
+
|
|
123
|
+
// Register a few watchers
|
|
124
|
+
await driver.executeScript('mobile: registerUIWatcher', [
|
|
125
|
+
{
|
|
126
|
+
name: 'list-test-1',
|
|
127
|
+
referenceLocator: { using: 'id', value: 'popup1' },
|
|
128
|
+
actionLocator: { using: 'id', value: 'close1' },
|
|
129
|
+
duration: 30000,
|
|
130
|
+
priority: 10,
|
|
131
|
+
},
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
await driver.executeScript('mobile: registerUIWatcher', [
|
|
135
|
+
{
|
|
136
|
+
name: 'list-test-2',
|
|
137
|
+
referenceLocator: { using: 'id', value: 'popup2' },
|
|
138
|
+
actionLocator: { using: 'id', value: 'close2' },
|
|
139
|
+
duration: 30000,
|
|
140
|
+
priority: 5,
|
|
141
|
+
},
|
|
142
|
+
]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should list all registered watchers', async function () {
|
|
146
|
+
const result = await driver.executeScript('mobile: listUIWatchers', []);
|
|
147
|
+
|
|
148
|
+
result.should.have.property('success', true);
|
|
149
|
+
result.should.have.property('watchers');
|
|
150
|
+
result.should.have.property('totalCount');
|
|
151
|
+
result.watchers.should.be.an('array');
|
|
152
|
+
result.totalCount.should.be.at.least(2);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should include complete watcher state', async function () {
|
|
156
|
+
const result = await driver.executeScript('mobile: listUIWatchers', []);
|
|
157
|
+
|
|
158
|
+
const watcher = result.watchers.find((w) => w.name === 'list-test-1');
|
|
159
|
+
watcher.should.exist;
|
|
160
|
+
watcher.should.have.property('name');
|
|
161
|
+
watcher.should.have.property('priority');
|
|
162
|
+
watcher.should.have.property('referenceLocator');
|
|
163
|
+
watcher.should.have.property('actionLocator');
|
|
164
|
+
watcher.should.have.property('status');
|
|
165
|
+
watcher.should.have.property('triggerCount');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('mobile: unregisterUIWatcher', function () {
|
|
170
|
+
beforeEach(async function () {
|
|
171
|
+
await driver.executeScript('mobile: clearAllUIWatchers', []);
|
|
172
|
+
await driver.executeScript('mobile: registerUIWatcher', [
|
|
173
|
+
{
|
|
174
|
+
name: 'to-remove',
|
|
175
|
+
referenceLocator: { using: 'id', value: 'popup' },
|
|
176
|
+
actionLocator: { using: 'id', value: 'close' },
|
|
177
|
+
duration: 30000,
|
|
178
|
+
},
|
|
179
|
+
]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should unregister an existing watcher', async function () {
|
|
183
|
+
const result = await driver.executeScript('mobile: unregisterUIWatcher', [
|
|
184
|
+
{ name: 'to-remove' },
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
result.should.have.property('success', true);
|
|
188
|
+
result.should.have.property('removed', 'to-remove');
|
|
189
|
+
|
|
190
|
+
// Verify it's actually removed
|
|
191
|
+
const listResult = await driver.executeScript('mobile: listUIWatchers', []);
|
|
192
|
+
const found = listResult.watchers.find((w) => w.name === 'to-remove');
|
|
193
|
+
(found === undefined).should.be.true;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should throw error for non-existent watcher', async function () {
|
|
197
|
+
try {
|
|
198
|
+
await driver.executeScript('mobile: unregisterUIWatcher', [{ name: 'non-existent' }]);
|
|
199
|
+
throw new Error('Should have thrown an error');
|
|
200
|
+
} catch (error) {
|
|
201
|
+
error.message.should.match(/not found/);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('mobile: clearAllUIWatchers', function () {
|
|
207
|
+
beforeEach(async function () {
|
|
208
|
+
await driver.executeScript('mobile: clearAllUIWatchers', []);
|
|
209
|
+
await driver.executeScript('mobile: registerUIWatcher', [
|
|
210
|
+
{
|
|
211
|
+
name: 'clear-test-1',
|
|
212
|
+
referenceLocator: { using: 'id', value: 'popup1' },
|
|
213
|
+
actionLocator: { using: 'id', value: 'close1' },
|
|
214
|
+
duration: 30000,
|
|
215
|
+
},
|
|
216
|
+
]);
|
|
217
|
+
|
|
218
|
+
await driver.executeScript('mobile: registerUIWatcher', [
|
|
219
|
+
{
|
|
220
|
+
name: 'clear-test-2',
|
|
221
|
+
referenceLocator: { using: 'id', value: 'popup2' },
|
|
222
|
+
actionLocator: { using: 'id', value: 'close2' },
|
|
223
|
+
duration: 30000,
|
|
224
|
+
},
|
|
225
|
+
]);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should clear all watchers', async function () {
|
|
229
|
+
const result = await driver.executeScript('mobile: clearAllUIWatchers', []);
|
|
230
|
+
|
|
231
|
+
result.should.have.property('success', true);
|
|
232
|
+
result.should.have.property('removedCount');
|
|
233
|
+
result.removedCount.should.be.at.least(2);
|
|
234
|
+
|
|
235
|
+
// Verify all are removed
|
|
236
|
+
const listResult = await driver.executeScript('mobile: listUIWatchers', []);
|
|
237
|
+
listResult.totalCount.should.equal(0);
|
|
238
|
+
listResult.watchers.should.be.empty;
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe('mobile: disableUIWatchers / enableUIWatchers', function () {
|
|
243
|
+
it('should disable watcher checking', async function () {
|
|
244
|
+
const result = await driver.executeScript('mobile: disableUIWatchers', []);
|
|
245
|
+
|
|
246
|
+
result.should.have.property('success', true);
|
|
247
|
+
result.should.have.property('message');
|
|
248
|
+
result.message.should.match(/disabled/);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should enable watcher checking', async function () {
|
|
252
|
+
const result = await driver.executeScript('mobile: enableUIWatchers', []);
|
|
253
|
+
|
|
254
|
+
result.should.have.property('success', true);
|
|
255
|
+
result.should.have.property('message');
|
|
256
|
+
result.message.should.match(/enabled/);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe('Maximum watcher limit', function () {
|
|
261
|
+
beforeEach(async function () {
|
|
262
|
+
await driver.executeScript('mobile: clearAllUIWatchers', []);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should allow up to 5 watchers', async function () {
|
|
266
|
+
for (let i = 1; i <= 5; i++) {
|
|
267
|
+
const result = await driver.executeScript('mobile: registerUIWatcher', [
|
|
268
|
+
{
|
|
269
|
+
name: `limit-test-${i}`,
|
|
270
|
+
referenceLocator: { using: 'id', value: `popup-${i}` },
|
|
271
|
+
actionLocator: { using: 'id', value: `close-${i}` },
|
|
272
|
+
duration: 30000,
|
|
273
|
+
},
|
|
274
|
+
]);
|
|
275
|
+
result.success.should.be.true;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Verify all 5 are registered
|
|
279
|
+
const listResult = await driver.executeScript('mobile: listUIWatchers', []);
|
|
280
|
+
listResult.totalCount.should.equal(5);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('should throw error when adding 6th watcher', async function () {
|
|
284
|
+
// First add 5 watchers
|
|
285
|
+
for (let i = 1; i <= 5; i++) {
|
|
286
|
+
await driver.executeScript('mobile: registerUIWatcher', [
|
|
287
|
+
{
|
|
288
|
+
name: `limit-test-${i}`,
|
|
289
|
+
referenceLocator: { using: 'id', value: `popup-${i}` },
|
|
290
|
+
actionLocator: { using: 'id', value: `close-${i}` },
|
|
291
|
+
duration: 30000,
|
|
292
|
+
},
|
|
293
|
+
]);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Try to add 6th
|
|
297
|
+
try {
|
|
298
|
+
await driver.executeScript('mobile: registerUIWatcher', [
|
|
299
|
+
{
|
|
300
|
+
name: 'limit-test-6',
|
|
301
|
+
referenceLocator: { using: 'id', value: 'popup-6' },
|
|
302
|
+
actionLocator: { using: 'id', value: 'close-6' },
|
|
303
|
+
duration: 30000,
|
|
304
|
+
},
|
|
305
|
+
]);
|
|
306
|
+
throw new Error('Should have thrown an error');
|
|
307
|
+
} catch (error) {
|
|
308
|
+
error.message.should.match(/Maximum 5/);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
});
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { describe, it, beforeEach } from 'mocha';
|
|
2
|
+
import { expect } from 'chai';
|
|
3
|
+
import { ElementReferenceCache } from '../../lib/element-cache.js';
|
|
4
|
+
|
|
5
|
+
describe('ElementReferenceCache', function () {
|
|
6
|
+
let cache;
|
|
7
|
+
|
|
8
|
+
const defaultConfig = {
|
|
9
|
+
maxWatchers: 5,
|
|
10
|
+
maxDurationMs: 60000,
|
|
11
|
+
maxCacheEntries: 50,
|
|
12
|
+
elementTtlMs: 60000,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
beforeEach(function () {
|
|
16
|
+
cache = new ElementReferenceCache(defaultConfig);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('cacheElement', function () {
|
|
20
|
+
it('should cache element from findElement', function () {
|
|
21
|
+
cache.cacheElement('elem-123', 'id', 'submit-btn');
|
|
22
|
+
|
|
23
|
+
const ref = cache.getRef('elem-123');
|
|
24
|
+
expect(ref).to.not.be.undefined;
|
|
25
|
+
expect(ref.using).to.equal('id');
|
|
26
|
+
expect(ref.value).to.equal('submit-btn');
|
|
27
|
+
expect(ref.source).to.equal('findElement');
|
|
28
|
+
expect(ref.createdAt).to.be.a('number');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should increment cache size when adding elements', function () {
|
|
32
|
+
expect(cache.size).to.equal(0);
|
|
33
|
+
|
|
34
|
+
cache.cacheElement('elem-1', 'id', 'btn1');
|
|
35
|
+
expect(cache.size).to.equal(1);
|
|
36
|
+
|
|
37
|
+
cache.cacheElement('elem-2', 'id', 'btn2');
|
|
38
|
+
expect(cache.size).to.equal(2);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('cacheElements', function () {
|
|
43
|
+
it('should cache elements from findElements with correct indices', function () {
|
|
44
|
+
const elements = [
|
|
45
|
+
{ 'element-6066-11e4-a52e-4f735466cecf': 'elem-1' },
|
|
46
|
+
{ 'element-6066-11e4-a52e-4f735466cecf': 'elem-2' },
|
|
47
|
+
{ 'element-6066-11e4-a52e-4f735466cecf': 'elem-3' },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
cache.cacheElements(elements, 'css selector', '.item');
|
|
51
|
+
|
|
52
|
+
const ref1 = cache.getRef('elem-1');
|
|
53
|
+
expect(ref1.source).to.equal('findElements');
|
|
54
|
+
expect(ref1.index).to.equal(0);
|
|
55
|
+
|
|
56
|
+
const ref2 = cache.getRef('elem-2');
|
|
57
|
+
expect(ref2.source).to.equal('findElements');
|
|
58
|
+
expect(ref2.index).to.equal(1);
|
|
59
|
+
|
|
60
|
+
const ref3 = cache.getRef('elem-3');
|
|
61
|
+
expect(ref3.source).to.equal('findElements');
|
|
62
|
+
expect(ref3.index).to.equal(2);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should handle JSONWP element format', function () {
|
|
66
|
+
const elements = [{ ELEMENT: 'jsonwp-elem-1' }, { ELEMENT: 'jsonwp-elem-2' }];
|
|
67
|
+
|
|
68
|
+
cache.cacheElements(elements, 'xpath', '//div');
|
|
69
|
+
|
|
70
|
+
const ref1 = cache.getRef('jsonwp-elem-1');
|
|
71
|
+
expect(ref1).to.not.be.undefined;
|
|
72
|
+
expect(ref1.index).to.equal(0);
|
|
73
|
+
|
|
74
|
+
const ref2 = cache.getRef('jsonwp-elem-2');
|
|
75
|
+
expect(ref2).to.not.be.undefined;
|
|
76
|
+
expect(ref2.index).to.equal(1);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('getRef', function () {
|
|
81
|
+
it('should return undefined for non-existent element', function () {
|
|
82
|
+
const ref = cache.getRef('non-existent');
|
|
83
|
+
expect(ref).to.be.undefined;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should return cached reference for existing element', function () {
|
|
87
|
+
cache.cacheElement('elem-123', 'id', 'myBtn');
|
|
88
|
+
|
|
89
|
+
const ref = cache.getRef('elem-123');
|
|
90
|
+
expect(ref).to.not.be.undefined;
|
|
91
|
+
expect(ref.using).to.equal('id');
|
|
92
|
+
expect(ref.value).to.equal('myBtn');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('LRU eviction', function () {
|
|
97
|
+
it('should evict oldest entry when max entries reached', function () {
|
|
98
|
+
// Use small cache for testing
|
|
99
|
+
const smallCache = new ElementReferenceCache({
|
|
100
|
+
...defaultConfig,
|
|
101
|
+
maxCacheEntries: 3,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
smallCache.cacheElement('elem-1', 'id', 'btn1');
|
|
105
|
+
smallCache.cacheElement('elem-2', 'id', 'btn2');
|
|
106
|
+
smallCache.cacheElement('elem-3', 'id', 'btn3');
|
|
107
|
+
|
|
108
|
+
expect(smallCache.size).to.equal(3);
|
|
109
|
+
|
|
110
|
+
// Adding 4th should evict elem-1 (oldest)
|
|
111
|
+
smallCache.cacheElement('elem-4', 'id', 'btn4');
|
|
112
|
+
|
|
113
|
+
expect(smallCache.size).to.equal(3);
|
|
114
|
+
expect(smallCache.getRef('elem-1')).to.be.undefined;
|
|
115
|
+
expect(smallCache.getRef('elem-2')).to.not.be.undefined;
|
|
116
|
+
expect(smallCache.getRef('elem-3')).to.not.be.undefined;
|
|
117
|
+
expect(smallCache.getRef('elem-4')).to.not.be.undefined;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should update LRU order when accessing element', function () {
|
|
121
|
+
const smallCache = new ElementReferenceCache({
|
|
122
|
+
...defaultConfig,
|
|
123
|
+
maxCacheEntries: 3,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
smallCache.cacheElement('elem-1', 'id', 'btn1');
|
|
127
|
+
smallCache.cacheElement('elem-2', 'id', 'btn2');
|
|
128
|
+
smallCache.cacheElement('elem-3', 'id', 'btn3');
|
|
129
|
+
|
|
130
|
+
// Access elem-1 to make it most recently used
|
|
131
|
+
smallCache.getRef('elem-1');
|
|
132
|
+
|
|
133
|
+
// Adding elem-4 should evict elem-2 (now the oldest)
|
|
134
|
+
smallCache.cacheElement('elem-4', 'id', 'btn4');
|
|
135
|
+
|
|
136
|
+
expect(smallCache.getRef('elem-1')).to.not.be.undefined; // Was accessed, not evicted
|
|
137
|
+
expect(smallCache.getRef('elem-2')).to.be.undefined; // Evicted
|
|
138
|
+
expect(smallCache.getRef('elem-3')).to.not.be.undefined;
|
|
139
|
+
expect(smallCache.getRef('elem-4')).to.not.be.undefined;
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('TTL expiry', function () {
|
|
144
|
+
it('should return undefined for expired entries', function () {
|
|
145
|
+
// Use very short TTL for testing
|
|
146
|
+
const shortTtlCache = new ElementReferenceCache({
|
|
147
|
+
...defaultConfig,
|
|
148
|
+
elementTtlMs: 10, // 10ms TTL
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
shortTtlCache.cacheElement('elem-1', 'id', 'btn1');
|
|
152
|
+
|
|
153
|
+
// Wait for TTL to expire
|
|
154
|
+
return new Promise((resolve) => {
|
|
155
|
+
setTimeout(() => {
|
|
156
|
+
const ref = shortTtlCache.getRef('elem-1');
|
|
157
|
+
expect(ref).to.be.undefined;
|
|
158
|
+
resolve();
|
|
159
|
+
}, 20);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('setMapping', function () {
|
|
165
|
+
it('should create element ID mapping', function () {
|
|
166
|
+
cache.setMapping('old-id', 'new-id');
|
|
167
|
+
|
|
168
|
+
const mapped = cache.getMappedId('old-id');
|
|
169
|
+
expect(mapped).to.equal('new-id');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should return undefined for unmapped ID', function () {
|
|
173
|
+
const mapped = cache.getMappedId('unknown-id');
|
|
174
|
+
expect(mapped).to.be.undefined;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should increment mappings count', function () {
|
|
178
|
+
expect(cache.mappingsCount).to.equal(0);
|
|
179
|
+
|
|
180
|
+
cache.setMapping('old-1', 'new-1');
|
|
181
|
+
expect(cache.mappingsCount).to.equal(1);
|
|
182
|
+
|
|
183
|
+
cache.setMapping('old-2', 'new-2');
|
|
184
|
+
expect(cache.mappingsCount).to.equal(2);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('transitive mapping update', function () {
|
|
189
|
+
it('should update existing mappings when creating new mapping', function () {
|
|
190
|
+
// Initial mapping: abc → efg
|
|
191
|
+
cache.setMapping('abc', 'efg');
|
|
192
|
+
expect(cache.getMappedId('abc')).to.equal('efg');
|
|
193
|
+
|
|
194
|
+
// New mapping: efg → hij
|
|
195
|
+
// Should also update abc → hij
|
|
196
|
+
cache.setMapping('efg', 'hij');
|
|
197
|
+
|
|
198
|
+
expect(cache.getMappedId('abc')).to.equal('hij'); // Transitive update
|
|
199
|
+
expect(cache.getMappedId('efg')).to.equal('hij'); // Direct mapping
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should handle multiple transitive chains', function () {
|
|
203
|
+
// Create chain: a → b, c → b
|
|
204
|
+
cache.setMapping('a', 'b');
|
|
205
|
+
cache.setMapping('c', 'b');
|
|
206
|
+
|
|
207
|
+
expect(cache.getMappedId('a')).to.equal('b');
|
|
208
|
+
expect(cache.getMappedId('c')).to.equal('b');
|
|
209
|
+
|
|
210
|
+
// Now b → d should update both a and c
|
|
211
|
+
cache.setMapping('b', 'd');
|
|
212
|
+
|
|
213
|
+
expect(cache.getMappedId('a')).to.equal('d');
|
|
214
|
+
expect(cache.getMappedId('c')).to.equal('d');
|
|
215
|
+
expect(cache.getMappedId('b')).to.equal('d');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should handle deep transitive chains', function () {
|
|
219
|
+
// Create chain: x → y → z
|
|
220
|
+
cache.setMapping('x', 'y');
|
|
221
|
+
cache.setMapping('y', 'z');
|
|
222
|
+
|
|
223
|
+
expect(cache.getMappedId('x')).to.equal('z');
|
|
224
|
+
expect(cache.getMappedId('y')).to.equal('z');
|
|
225
|
+
|
|
226
|
+
// Now z → w should update all
|
|
227
|
+
cache.setMapping('z', 'w');
|
|
228
|
+
|
|
229
|
+
expect(cache.getMappedId('x')).to.equal('w');
|
|
230
|
+
expect(cache.getMappedId('y')).to.equal('w');
|
|
231
|
+
expect(cache.getMappedId('z')).to.equal('w');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('cleanup', function () {
|
|
236
|
+
it('should remove expired entries', function () {
|
|
237
|
+
const shortTtlCache = new ElementReferenceCache({
|
|
238
|
+
...defaultConfig,
|
|
239
|
+
elementTtlMs: 10, // 10ms TTL
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
shortTtlCache.cacheElement('elem-1', 'id', 'btn1');
|
|
243
|
+
|
|
244
|
+
return new Promise((resolve) => {
|
|
245
|
+
setTimeout(() => {
|
|
246
|
+
shortTtlCache.cleanup();
|
|
247
|
+
expect(shortTtlCache.size).to.equal(0);
|
|
248
|
+
resolve();
|
|
249
|
+
}, 20);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('clear', function () {
|
|
255
|
+
it('should remove all entries', function () {
|
|
256
|
+
cache.cacheElement('elem-1', 'id', 'btn1');
|
|
257
|
+
cache.cacheElement('elem-2', 'id', 'btn2');
|
|
258
|
+
cache.setMapping('old', 'new');
|
|
259
|
+
|
|
260
|
+
expect(cache.size).to.equal(2);
|
|
261
|
+
expect(cache.mappingsCount).to.equal(1);
|
|
262
|
+
|
|
263
|
+
cache.clear();
|
|
264
|
+
|
|
265
|
+
expect(cache.size).to.equal(0);
|
|
266
|
+
expect(cache.mappingsCount).to.equal(0);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import sinon from 'sinon';
|
|
2
|
+
import plugin from '../../lib/plugin.js';
|
|
3
|
+
import { BasePlugin } from '@appium/base-plugin';
|
|
4
|
+
|
|
5
|
+
const UIWatchersPlugin = plugin.default || plugin;
|
|
6
|
+
|
|
7
|
+
describe('UIWatchersPlugin', function () {
|
|
8
|
+
let sandbox;
|
|
9
|
+
|
|
10
|
+
before(async function () {
|
|
11
|
+
const chai = await import('chai');
|
|
12
|
+
const chaiAsPromised = await import('chai-as-promised');
|
|
13
|
+
chai.use(chaiAsPromised.default);
|
|
14
|
+
chai.should();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
beforeEach(function () {
|
|
18
|
+
sandbox = sinon.createSandbox();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(function () {
|
|
22
|
+
sandbox.restore();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('constructor', function () {
|
|
26
|
+
it('should create a new plugin instance', function () {
|
|
27
|
+
const pluginInstance = new UIWatchersPlugin('uiwatchers');
|
|
28
|
+
pluginInstance.should.exist;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should extend BasePlugin', function () {
|
|
32
|
+
const pluginInstance = new UIWatchersPlugin('uiwatchers');
|
|
33
|
+
pluginInstance.should.be.instanceOf(BasePlugin);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should have the correct class name', function () {
|
|
37
|
+
const pluginInstance = new UIWatchersPlugin('uiwatchers');
|
|
38
|
+
pluginInstance.constructor.name.should.equal('UIWatchersPlugin');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should accept optional CLI arguments', function () {
|
|
42
|
+
const cliArgs = { verbose: true };
|
|
43
|
+
const pluginInstance = new UIWatchersPlugin('uiwatchers', cliArgs);
|
|
44
|
+
pluginInstance.should.exist;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should initialize without CLI arguments', function () {
|
|
48
|
+
const pluginInstance = new UIWatchersPlugin('uiwatchers');
|
|
49
|
+
pluginInstance.should.exist;
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|