@appium/fake-driver 6.0.2 → 6.1.1
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/LICENSE +201 -0
- package/build/lib/commands/alert.d.ts +10 -13
- package/build/lib/commands/alert.d.ts.map +1 -1
- package/build/lib/commands/alert.js +39 -46
- package/build/lib/commands/alert.js.map +1 -1
- package/build/lib/commands/contexts.d.ts +9 -13
- package/build/lib/commands/contexts.d.ts.map +1 -1
- package/build/lib/commands/contexts.js +55 -62
- package/build/lib/commands/contexts.js.map +1 -1
- package/build/lib/commands/element.d.ts +23 -26
- package/build/lib/commands/element.d.ts.map +1 -1
- package/build/lib/commands/element.js +100 -89
- package/build/lib/commands/element.js.map +1 -1
- package/build/lib/commands/find.d.ts +14 -17
- package/build/lib/commands/find.d.ts.map +1 -1
- package/build/lib/commands/find.js +66 -63
- package/build/lib/commands/find.js.map +1 -1
- package/build/lib/commands/general.d.ts +25 -28
- package/build/lib/commands/general.d.ts.map +1 -1
- package/build/lib/commands/general.js +90 -75
- package/build/lib/commands/general.js.map +1 -1
- package/build/lib/desired-caps.d.ts +14 -0
- package/build/lib/desired-caps.d.ts.map +1 -0
- package/build/lib/desired-caps.js +16 -0
- package/build/lib/desired-caps.js.map +1 -0
- package/build/lib/doctor/common.d.ts +11 -0
- package/build/lib/doctor/common.d.ts.map +1 -0
- package/build/lib/doctor/common.js +25 -0
- package/build/lib/doctor/common.js.map +1 -0
- package/build/lib/doctor/fake1.d.ts +3 -0
- package/build/lib/doctor/fake1.d.ts.map +1 -0
- package/build/lib/doctor/fake1.js +6 -0
- package/build/lib/doctor/fake1.js.map +1 -0
- package/build/lib/doctor/fake2.d.ts +3 -0
- package/build/lib/doctor/fake2.d.ts.map +1 -0
- package/build/lib/doctor/fake2.js +6 -0
- package/build/lib/doctor/fake2.js.map +1 -0
- package/build/lib/driver.d.ts +128 -156
- package/build/lib/driver.d.ts.map +1 -1
- package/build/lib/driver.js +146 -154
- package/build/lib/driver.js.map +1 -1
- package/build/lib/fake-app.d.ts +37 -45
- package/build/lib/fake-app.d.ts.map +1 -1
- package/build/lib/fake-app.js +57 -34
- package/build/lib/fake-app.js.map +1 -1
- package/build/lib/fake-driver-schema.d.ts +16 -11
- package/build/lib/fake-driver-schema.d.ts.map +1 -1
- package/build/lib/fake-driver-schema.js +2 -17
- package/build/lib/fake-driver-schema.js.map +1 -1
- package/build/lib/fake-element.d.ts +27 -26
- package/build/lib/fake-element.d.ts.map +1 -1
- package/build/lib/fake-element.js +19 -19
- package/build/lib/fake-element.js.map +1 -1
- package/build/lib/index.d.ts +2 -3
- package/build/lib/index.d.ts.map +1 -1
- package/build/lib/index.js +2 -6
- package/build/lib/index.js.map +1 -1
- package/build/lib/logger.d.ts +1 -2
- package/build/lib/logger.d.ts.map +1 -1
- package/build/lib/logger.js +2 -2
- package/build/lib/logger.js.map +1 -1
- package/build/lib/scripts/fake-error.d.ts.map +1 -1
- package/build/lib/scripts/fake-error.js.map +1 -1
- package/build/lib/scripts/fake-stdin.d.ts.map +1 -1
- package/build/lib/scripts/fake-stdin.js.map +1 -1
- package/build/lib/scripts/fake-success.d.ts.map +1 -1
- package/build/lib/scripts/fake-success.js +3 -6
- package/build/lib/scripts/fake-success.js.map +1 -1
- package/build/lib/server.d.ts +2 -1
- package/build/lib/server.d.ts.map +1 -1
- package/build/lib/server.js +3 -5
- package/build/lib/server.js.map +1 -1
- package/build/lib/types.d.ts +1 -1
- package/build/lib/types.d.ts.map +1 -1
- package/lib/commands/alert.ts +31 -61
- package/lib/commands/contexts.ts +50 -73
- package/lib/commands/element.ts +122 -135
- package/lib/commands/find.ts +100 -115
- package/lib/commands/general.ts +122 -127
- package/lib/desired-caps.ts +16 -0
- package/lib/doctor/common.ts +26 -0
- package/lib/doctor/fake1.ts +3 -0
- package/lib/doctor/fake2.ts +3 -0
- package/lib/driver.ts +321 -0
- package/lib/fake-app.ts +234 -0
- package/lib/fake-driver-schema.ts +43 -0
- package/lib/fake-element.ts +128 -0
- package/lib/{index.js → index.ts} +5 -9
- package/lib/logger.ts +3 -0
- package/lib/scripts/{fake-success.js → fake-success.ts} +1 -1
- package/lib/{server.js → server.ts} +3 -4
- package/lib/types.ts +1 -1
- package/package.json +11 -14
- package/tsconfig.json +2 -1
- package/build/lib/commands/index.d.ts +0 -14
- package/build/lib/commands/index.d.ts.map +0 -1
- package/build/lib/commands/index.js +0 -16
- package/build/lib/commands/index.js.map +0 -1
- package/build/lib/commands/mixin.d.ts +0 -11
- package/build/lib/commands/mixin.d.ts.map +0 -1
- package/build/lib/commands/mixin.js +0 -16
- package/build/lib/commands/mixin.js.map +0 -1
- package/lib/commands/index.ts +0 -14
- package/lib/commands/mixin.ts +0 -13
- package/lib/driver.js +0 -356
- package/lib/fake-app.js +0 -190
- package/lib/fake-driver-schema.js +0 -35
- package/lib/fake-element.js +0 -117
- package/lib/logger.js +0 -5
- package/test/fixtures/app.xml +0 -38
- /package/lib/scripts/{fake-error.js → fake-error.ts} +0 -0
- /package/lib/scripts/{fake-stdin.js → fake-stdin.ts} +0 -0
package/lib/driver.ts
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import {sleep} from 'asyncbox';
|
|
2
|
+
import type {Express, Request, Response} from 'express';
|
|
3
|
+
import type {Server as HttpServer} from 'node:http';
|
|
4
|
+
import {BaseDriver, errors} from 'appium/driver';
|
|
5
|
+
import type {DriverData, InitialOpts} from '@appium/types';
|
|
6
|
+
import {desiredCapConstraints} from './desired-caps';
|
|
7
|
+
import type {FakeDriverConstraints} from './desired-caps';
|
|
8
|
+
import type {FakeDriverCaps, W3CFakeDriverCaps} from './types';
|
|
9
|
+
import {FakeApp} from './fake-app';
|
|
10
|
+
import type {FakeElement} from './fake-element';
|
|
11
|
+
import * as alertCommands from './commands/alert';
|
|
12
|
+
import * as contextsCommands from './commands/contexts';
|
|
13
|
+
import * as elementCommands from './commands/element';
|
|
14
|
+
import * as findCommands from './commands/find';
|
|
15
|
+
import * as generalCommands from './commands/general';
|
|
16
|
+
|
|
17
|
+
export type {FakeDriverConstraints};
|
|
18
|
+
export type {Orientation} from '@appium/types';
|
|
19
|
+
|
|
20
|
+
/** Driver supporting a generic "fake thing" value (getFakeThing / setFakeThing). */
|
|
21
|
+
export class FakeDriver<Thing = unknown> extends BaseDriver<FakeDriverConstraints> {
|
|
22
|
+
readonly desiredCapConstraints = desiredCapConstraints;
|
|
23
|
+
|
|
24
|
+
curContext: string;
|
|
25
|
+
readonly appModel: FakeApp;
|
|
26
|
+
_proxyActive: boolean;
|
|
27
|
+
shook: boolean;
|
|
28
|
+
focusedElId: string | null;
|
|
29
|
+
fakeThing: Thing | null;
|
|
30
|
+
/** Next numeric id for new elements; keys in elMap are stringified. */
|
|
31
|
+
maxElId: number;
|
|
32
|
+
/** Map of element id (string) to FakeElement for this session. */
|
|
33
|
+
elMap: Record<string, FakeElement>;
|
|
34
|
+
/** If set, Bidi connections are proxied to this URL instead of handling locally. */
|
|
35
|
+
private _bidiProxyUrl: string | null;
|
|
36
|
+
private _clockRunning = false;
|
|
37
|
+
/** Current document URL; set by bidiNavigate, returned by getUrl. */
|
|
38
|
+
url: string = '';
|
|
39
|
+
|
|
40
|
+
// Alert commands
|
|
41
|
+
assertNoAlert = alertCommands.assertNoAlert;
|
|
42
|
+
assertAlert = alertCommands.assertAlert;
|
|
43
|
+
getAlertText = alertCommands.getAlertText;
|
|
44
|
+
setAlertText = alertCommands.setAlertText;
|
|
45
|
+
postAcceptAlert = alertCommands.postAcceptAlert;
|
|
46
|
+
postDismissAlert = alertCommands.postDismissAlert;
|
|
47
|
+
|
|
48
|
+
// Context commands
|
|
49
|
+
getRawContexts = contextsCommands.getRawContexts;
|
|
50
|
+
assertWebviewContext = contextsCommands.assertWebviewContext;
|
|
51
|
+
getCurrentContext = contextsCommands.getCurrentContext;
|
|
52
|
+
getContexts = contextsCommands.getContexts;
|
|
53
|
+
setContext = contextsCommands.setContext;
|
|
54
|
+
setFrame = contextsCommands.setFrame;
|
|
55
|
+
|
|
56
|
+
// Element commands
|
|
57
|
+
getElements = elementCommands.getElements;
|
|
58
|
+
getElement = elementCommands.getElement;
|
|
59
|
+
getName = elementCommands.getName;
|
|
60
|
+
elementDisplayed = elementCommands.elementDisplayed;
|
|
61
|
+
elementEnabled = elementCommands.elementEnabled;
|
|
62
|
+
elementSelected = elementCommands.elementSelected;
|
|
63
|
+
setValue = elementCommands.setValue;
|
|
64
|
+
getText = elementCommands.getText;
|
|
65
|
+
clear = elementCommands.clear;
|
|
66
|
+
click = elementCommands.click;
|
|
67
|
+
getAttribute = elementCommands.getAttribute;
|
|
68
|
+
getElementRect = elementCommands.getElementRect;
|
|
69
|
+
getSize = elementCommands.getSize;
|
|
70
|
+
equalsElement = elementCommands.equalsElement;
|
|
71
|
+
getCssProperty = elementCommands.getCssProperty;
|
|
72
|
+
getLocation = elementCommands.getLocation;
|
|
73
|
+
getLocationInView = elementCommands.getLocationInView;
|
|
74
|
+
|
|
75
|
+
// Find commands
|
|
76
|
+
getExistingElementForNode = findCommands.getExistingElementForNode;
|
|
77
|
+
wrapNewEl = findCommands.wrapNewEl;
|
|
78
|
+
findElOrEls = findCommands.findElOrEls;
|
|
79
|
+
findElement = findCommands.findElement;
|
|
80
|
+
findElements = findCommands.findElements;
|
|
81
|
+
findElementFromElement = findCommands.findElementFromElement;
|
|
82
|
+
findElementsFromElement = findCommands.findElementsFromElement;
|
|
83
|
+
|
|
84
|
+
// General commands
|
|
85
|
+
title = generalCommands.title;
|
|
86
|
+
keys = generalCommands.keys;
|
|
87
|
+
setGeoLocation = generalCommands.setGeoLocation;
|
|
88
|
+
getGeoLocation = generalCommands.getGeoLocation;
|
|
89
|
+
getPageSource = generalCommands.getPageSource;
|
|
90
|
+
getOrientation = generalCommands.getOrientation;
|
|
91
|
+
setOrientation = generalCommands.setOrientation;
|
|
92
|
+
getScreenshot = generalCommands.getScreenshot;
|
|
93
|
+
getWindowSize = generalCommands.getWindowSize;
|
|
94
|
+
getWindowRect = generalCommands.getWindowRect;
|
|
95
|
+
performActions = generalCommands.performActions;
|
|
96
|
+
releaseActions = generalCommands.releaseActions;
|
|
97
|
+
getLog = generalCommands.getLog;
|
|
98
|
+
mobileShake = generalCommands.mobileShake;
|
|
99
|
+
doubleClick = generalCommands.doubleClick;
|
|
100
|
+
execute = generalCommands.execute;
|
|
101
|
+
fakeAddition = generalCommands.fakeAddition;
|
|
102
|
+
getUrl = generalCommands.getUrl;
|
|
103
|
+
bidiNavigate = generalCommands.bidiNavigate;
|
|
104
|
+
|
|
105
|
+
constructor(opts: InitialOpts = {} as InitialOpts, shouldValidateCaps = true) {
|
|
106
|
+
super(opts, shouldValidateCaps);
|
|
107
|
+
this.curContext = 'NATIVE_APP';
|
|
108
|
+
this.elMap = {};
|
|
109
|
+
this.focusedElId = null;
|
|
110
|
+
this.maxElId = 0;
|
|
111
|
+
this.fakeThing = null;
|
|
112
|
+
this._proxyActive = false;
|
|
113
|
+
this.shook = false;
|
|
114
|
+
this.appModel = new FakeApp();
|
|
115
|
+
this._bidiProxyUrl = null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
proxyActive(): boolean {
|
|
119
|
+
return this._proxyActive;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
canProxy(): boolean {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
get bidiProxyUrl(): string | null {
|
|
127
|
+
return this._bidiProxyUrl;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
proxyReqRes(req: Request, res: Response): void {
|
|
131
|
+
res.set('content-type', 'application/json');
|
|
132
|
+
const resBodyObj: {value: string; sessionId: string | null} = {
|
|
133
|
+
value: 'proxied via proxyReqRes',
|
|
134
|
+
sessionId: null,
|
|
135
|
+
};
|
|
136
|
+
const match = req.originalUrl.match(/\/session\/([^/]+)/);
|
|
137
|
+
resBodyObj.sessionId = match ? match[1] : null;
|
|
138
|
+
res.status(200).send(JSON.stringify(resBodyObj));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async proxyCommand<T = unknown>(): Promise<T> {
|
|
142
|
+
return 'proxied via proxyCommand' as T;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Create session and load fake app XML from caps.app.
|
|
147
|
+
* Starts clock event emitter if caps.runClock is true.
|
|
148
|
+
*/
|
|
149
|
+
override async createSession(
|
|
150
|
+
w3cCapabilities1: W3CFakeDriverCaps,
|
|
151
|
+
w3cCapabilities2?: W3CFakeDriverCaps,
|
|
152
|
+
w3cCapabilities3?: W3CFakeDriverCaps,
|
|
153
|
+
driverData: DriverData[] = []
|
|
154
|
+
): Promise<[string, FakeDriverCaps]> {
|
|
155
|
+
for (const d of driverData) {
|
|
156
|
+
if (d.isUnique) {
|
|
157
|
+
throw new errors.SessionNotCreatedError(
|
|
158
|
+
'Cannot start session; another ' +
|
|
159
|
+
'unique session is in progress that requires all resources'
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const [sessionId, caps] = await super.createSession(
|
|
165
|
+
w3cCapabilities1,
|
|
166
|
+
w3cCapabilities2,
|
|
167
|
+
w3cCapabilities3,
|
|
168
|
+
driverData
|
|
169
|
+
) as [string, FakeDriverCaps];
|
|
170
|
+
this.caps = caps;
|
|
171
|
+
await this.appModel.loadApp(caps.app);
|
|
172
|
+
if (this.caps.runClock) {
|
|
173
|
+
this.startClock();
|
|
174
|
+
}
|
|
175
|
+
return [sessionId, caps];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
override async deleteSession(sessionId?: string): Promise<void> {
|
|
179
|
+
this.stopClock();
|
|
180
|
+
return await super.deleteSession(sessionId);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async getWindowHandle(): Promise<string> {
|
|
184
|
+
return '1';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async getWindowHandles(): Promise<string[]> {
|
|
188
|
+
return ['1'];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
override get driverData(): {isUnique: boolean} {
|
|
192
|
+
return {
|
|
193
|
+
isUnique: !!this.caps.uniqueApp,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async getFakeThing(): Promise<Thing | null> {
|
|
198
|
+
await sleep(1);
|
|
199
|
+
return this.fakeThing;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async setFakeThing(thing: Thing): Promise<null> {
|
|
203
|
+
await sleep(1);
|
|
204
|
+
this.fakeThing = thing;
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async getFakeDriverArgs(): Promise<typeof this.cliArgs> {
|
|
209
|
+
await sleep(1);
|
|
210
|
+
return this.cliArgs;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** TODO: track deprecated commands when called and return their names. */
|
|
214
|
+
async getDeprecatedCommandsCalled(): Promise<string[]> {
|
|
215
|
+
await sleep(1);
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async callDeprecatedCommand(): Promise<void> {
|
|
220
|
+
await sleep(1);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async doSomeMath(num1: number, num2: number): Promise<number> {
|
|
224
|
+
await sleep(1);
|
|
225
|
+
return num1 + num2;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async doSomeMath2(num1: number, num2: number): Promise<number> {
|
|
229
|
+
await sleep(1);
|
|
230
|
+
return num1 + num2;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private async startClock(): Promise<void> {
|
|
234
|
+
this._clockRunning = true;
|
|
235
|
+
while (this._clockRunning) {
|
|
236
|
+
await sleep(500);
|
|
237
|
+
this.eventEmitter.emit('bidiEvent', {
|
|
238
|
+
method: 'appium:clock.currentTime',
|
|
239
|
+
params: {time: Date.now()},
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private stopClock(): void {
|
|
245
|
+
this._clockRunning = false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
static newBidiCommands = {
|
|
249
|
+
'appium:fake': {
|
|
250
|
+
getFakeThing: {
|
|
251
|
+
command: 'getFakeThing',
|
|
252
|
+
},
|
|
253
|
+
setFakeThing: {
|
|
254
|
+
command: 'setFakeThing',
|
|
255
|
+
params: {
|
|
256
|
+
required: ['thing'],
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
doSomeMath: {
|
|
260
|
+
command: 'doSomeMath',
|
|
261
|
+
params: {
|
|
262
|
+
required: ['num1', 'num2'],
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
doSomeMath2: {
|
|
266
|
+
command: 'doSomeMath2',
|
|
267
|
+
params: {
|
|
268
|
+
required: ['num1', 'num2'],
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
} as const;
|
|
273
|
+
|
|
274
|
+
static newMethodMap = {
|
|
275
|
+
'/session/:sessionId/fakedriver': {
|
|
276
|
+
GET: {command: 'getFakeThing'},
|
|
277
|
+
POST: {command: 'setFakeThing', payloadParams: {required: ['thing'] as const}},
|
|
278
|
+
},
|
|
279
|
+
'/session/:sessionId/fakedriverargs': {
|
|
280
|
+
GET: {command: 'getFakeDriverArgs'},
|
|
281
|
+
},
|
|
282
|
+
'/session/:sessionId/deprecated': {
|
|
283
|
+
POST: {command: 'callDeprecatedCommand', deprecated: true},
|
|
284
|
+
},
|
|
285
|
+
'/session/:sessionId/doubleclick': {
|
|
286
|
+
POST: {command: 'doubleClick'},
|
|
287
|
+
},
|
|
288
|
+
} as const;
|
|
289
|
+
|
|
290
|
+
static executeMethodMap = {
|
|
291
|
+
'fake: addition': {
|
|
292
|
+
command: 'fakeAddition',
|
|
293
|
+
params: {required: ['num1', 'num2'], optional: ['num3']},
|
|
294
|
+
},
|
|
295
|
+
'fake: getThing': {
|
|
296
|
+
command: 'getFakeThing',
|
|
297
|
+
},
|
|
298
|
+
'fake: setThing': {
|
|
299
|
+
command: 'setFakeThing',
|
|
300
|
+
params: {required: ['thing']},
|
|
301
|
+
},
|
|
302
|
+
'fake: getDeprecatedCommandsCalled': {
|
|
303
|
+
command: 'getDeprecatedCommandsCalled',
|
|
304
|
+
},
|
|
305
|
+
} as const;
|
|
306
|
+
|
|
307
|
+
static fakeRoute(req: Request, res: Response): void {
|
|
308
|
+
res.send(JSON.stringify({fakedriver: 'fakeResponse'}));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
static async updateServer(
|
|
312
|
+
expressApp: Express,
|
|
313
|
+
httpServer: HttpServer,
|
|
314
|
+
cliArgs: Record<string, unknown>
|
|
315
|
+
): Promise<void> {
|
|
316
|
+
expressApp.all('/fakedriver', FakeDriver.fakeRoute);
|
|
317
|
+
expressApp.all('/fakedriverCliArgs', (req: Request, res: Response) => {
|
|
318
|
+
res.send(JSON.stringify(cliArgs));
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
package/lib/fake-app.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import {fs} from 'appium/support';
|
|
2
|
+
import {readFileSync} from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import XMLDom from '@xmldom/xmldom';
|
|
5
|
+
import * as xpath from 'xpath';
|
|
6
|
+
import {log} from './logger';
|
|
7
|
+
import _ from 'lodash';
|
|
8
|
+
import {FakeElement, type XmlNodeLike} from './fake-element';
|
|
9
|
+
import type {ActionSequence, Location, Orientation} from '@appium/types';
|
|
10
|
+
import type {Document as XMLDocument, Node as XMLNode} from '@xmldom/xmldom';
|
|
11
|
+
|
|
12
|
+
const SCREENSHOT = path.join(__dirname, 'screen.png');
|
|
13
|
+
|
|
14
|
+
export interface FakeWebView {
|
|
15
|
+
node: XmlNodeLike;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** In-memory app model: XML DOM, webviews, alerts, geo, orientation, actions log. */
|
|
19
|
+
export class FakeApp {
|
|
20
|
+
dom: XMLDocument | null;
|
|
21
|
+
activeDom: XMLDocument | null;
|
|
22
|
+
activeWebview: FakeWebView | null;
|
|
23
|
+
activeFrame: XMLDocument | null;
|
|
24
|
+
activeAlert: FakeElement | null;
|
|
25
|
+
lat: number;
|
|
26
|
+
long: number;
|
|
27
|
+
private _width: number | null;
|
|
28
|
+
private _height: number | null;
|
|
29
|
+
rawXml: string;
|
|
30
|
+
currentOrientation: Orientation;
|
|
31
|
+
actionLog: ActionSequence[][];
|
|
32
|
+
|
|
33
|
+
constructor() {
|
|
34
|
+
this.dom = null;
|
|
35
|
+
this.activeDom = null;
|
|
36
|
+
this.activeWebview = null;
|
|
37
|
+
this.activeFrame = null;
|
|
38
|
+
this.activeAlert = null;
|
|
39
|
+
this.lat = 0;
|
|
40
|
+
this.long = 0;
|
|
41
|
+
this._width = null;
|
|
42
|
+
this._height = null;
|
|
43
|
+
this.rawXml = '';
|
|
44
|
+
this.currentOrientation = 'PORTRAIT';
|
|
45
|
+
this.actionLog = [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get title(): string {
|
|
49
|
+
const nodes = this.xpathQuery('//title');
|
|
50
|
+
if (!_.isArray(nodes) || nodes.length < 1) {
|
|
51
|
+
throw new Error('No title!');
|
|
52
|
+
}
|
|
53
|
+
const node = nodes[0];
|
|
54
|
+
const firstChild = node.firstChild as unknown as {data: string} | null;
|
|
55
|
+
return firstChild?.data ?? '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get currentGeoLocation(): Location {
|
|
59
|
+
return {
|
|
60
|
+
latitude: this.lat,
|
|
61
|
+
longitude: this.long,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get orientation(): Orientation {
|
|
66
|
+
return this.currentOrientation;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
set orientation(o: Orientation) {
|
|
70
|
+
this.currentOrientation = o;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get width(): number {
|
|
74
|
+
if (this._width === null) {
|
|
75
|
+
this.setDims();
|
|
76
|
+
}
|
|
77
|
+
const w = this._width;
|
|
78
|
+
if (w === null) {
|
|
79
|
+
throw new Error('Cannot fetch app dimensions');
|
|
80
|
+
}
|
|
81
|
+
return w;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get height(): number {
|
|
85
|
+
if (this._height === null) {
|
|
86
|
+
this.setDims();
|
|
87
|
+
}
|
|
88
|
+
const h = this._height;
|
|
89
|
+
if (h === null) {
|
|
90
|
+
throw new Error('Cannot fetch app dimensions');
|
|
91
|
+
}
|
|
92
|
+
return h;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private setDims(): void {
|
|
96
|
+
const nodes = this.xpathQuery('//app');
|
|
97
|
+
if (!_.isArray(nodes)) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
'Cannot fetch app dimensions because no corresponding node has been found in the source'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const app = new FakeElement(nodes[0] as unknown as XmlNodeLike, this);
|
|
103
|
+
this._width = parseInt(app.nodeAttrs.width, 10);
|
|
104
|
+
this._height = parseInt(app.nodeAttrs.height, 10);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async loadApp(appPath: string): Promise<void> {
|
|
108
|
+
log.info(`Loading Mock app model at ${appPath}`);
|
|
109
|
+
const data = await fs.readFile(appPath);
|
|
110
|
+
log.info('Parsing Mock app XML');
|
|
111
|
+
this.rawXml = data.toString();
|
|
112
|
+
this.rawXml = this.rawXml.replace('<app ', '<AppiumAUT><app ');
|
|
113
|
+
this.rawXml = this.rawXml.replace('<app>', '<AppiumAUT><app>');
|
|
114
|
+
this.rawXml = this.rawXml.replace('</app>', '</app></AppiumAUT>');
|
|
115
|
+
this.dom = new XMLDom.DOMParser().parseFromString(
|
|
116
|
+
this.rawXml,
|
|
117
|
+
XMLDom.MIME_TYPE.XML_TEXT
|
|
118
|
+
) as XMLDocument;
|
|
119
|
+
this.activeDom = this.dom;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
getWebviews(): FakeWebView[] {
|
|
123
|
+
const nodes = this.xpathQuery('//MockWebView/*[1]');
|
|
124
|
+
return _.isArray(nodes) ? nodes.map((n) => new FakeWebViewImpl(n as unknown as XmlNodeLike)) : [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
activateWebview(wv: FakeWebView): void {
|
|
128
|
+
this.activeWebview = wv;
|
|
129
|
+
const fragment = new XMLDom.XMLSerializer().serializeToString(
|
|
130
|
+
wv.node as unknown as XMLNode
|
|
131
|
+
);
|
|
132
|
+
this.activeDom = new XMLDom.DOMParser().parseFromString(
|
|
133
|
+
fragment,
|
|
134
|
+
XMLDom.MIME_TYPE.XML_TEXT
|
|
135
|
+
) as XMLDocument;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
deactivateWebview(): void {
|
|
139
|
+
this.activeWebview = null;
|
|
140
|
+
this.activeDom = this.dom;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
activateFrame(frame: XMLDocument): void {
|
|
144
|
+
this.activeFrame = frame;
|
|
145
|
+
const fragment = new XMLDom.XMLSerializer().serializeToString(
|
|
146
|
+
frame as unknown as XMLNode
|
|
147
|
+
);
|
|
148
|
+
this.activeDom = new XMLDom.DOMParser().parseFromString(
|
|
149
|
+
fragment,
|
|
150
|
+
XMLDom.MIME_TYPE.XML_TEXT
|
|
151
|
+
) as XMLDocument;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
deactivateFrame(): void {
|
|
155
|
+
this.activeFrame = null;
|
|
156
|
+
if (this.activeWebview) {
|
|
157
|
+
this.activateWebview(this.activeWebview);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
xpathQuery(sel: string, ctx?: XMLDocument | null): xpath.SelectedValue {
|
|
162
|
+
const node = ctx ?? this.activeDom;
|
|
163
|
+
return xpath.select(
|
|
164
|
+
sel,
|
|
165
|
+
node as unknown as Node
|
|
166
|
+
) as xpath.SelectedValue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
idQuery(id: string, ctx?: XMLDocument | null): xpath.SelectedValue {
|
|
170
|
+
return this.xpathQuery(`//*[@id="${id}"]`, ctx);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
classQuery(className: string, ctx?: XMLDocument | null): xpath.SelectedValue {
|
|
174
|
+
return this.xpathQuery(`//${className}`, ctx);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
cssQuery(css: string, ctx?: XMLDocument | null): xpath.SelectedValue {
|
|
178
|
+
if (css.startsWith('#')) {
|
|
179
|
+
return this.idQuery(css.slice(1), ctx);
|
|
180
|
+
}
|
|
181
|
+
if (css.startsWith('.')) {
|
|
182
|
+
return this.classQuery(css.slice(1), ctx);
|
|
183
|
+
}
|
|
184
|
+
return this.classQuery(css, ctx);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
hasAlert(): boolean {
|
|
188
|
+
return this.activeAlert !== null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
setAlertText(text: string): void {
|
|
192
|
+
if (!this.activeAlert?.hasPrompt()) {
|
|
193
|
+
throw new Error('No prompt to set text of');
|
|
194
|
+
}
|
|
195
|
+
this.activeAlert?.setAttr('prompt', text);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
showAlert(alertId: string): void {
|
|
199
|
+
const nodes = this.xpathQuery(`//alert[@id="${alertId}"]`);
|
|
200
|
+
if (!_.isArray(nodes) || _.isEmpty(nodes)) {
|
|
201
|
+
throw new Error(`Alert ${alertId} doesn't exist!`);
|
|
202
|
+
}
|
|
203
|
+
this.activeAlert = new FakeElement(nodes[0] as unknown as XmlNodeLike, this);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Alert text from prompt attr or node text attr (e.g. <alert text="Fake Alert">). */
|
|
207
|
+
alertText(): string {
|
|
208
|
+
const prompt = this.activeAlert?.getAttr('prompt');
|
|
209
|
+
if (prompt) {
|
|
210
|
+
return prompt;
|
|
211
|
+
}
|
|
212
|
+
const fromAttrs = this.activeAlert?.nodeAttrs?.text;
|
|
213
|
+
if (fromAttrs) {
|
|
214
|
+
return fromAttrs;
|
|
215
|
+
}
|
|
216
|
+
const node = this.activeAlert?.node as {getAttribute?(name: string): string | null} | undefined;
|
|
217
|
+
return node?.getAttribute?.('text') ?? '';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
handleAlert(): void {
|
|
221
|
+
this.activeAlert = null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
getScreenshot(): string {
|
|
225
|
+
return readFileSync(SCREENSHOT, 'base64');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export class FakeWebViewImpl implements FakeWebView {
|
|
230
|
+
readonly node: XmlNodeLike;
|
|
231
|
+
constructor(node: XmlNodeLike) {
|
|
232
|
+
this.node = node;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Schema for Fake Driver CLI/server arguments.
|
|
3
|
+
* Could be a .json file; kept as TS for consistency and type export.
|
|
4
|
+
*/
|
|
5
|
+
export interface FakeDriverSchema {
|
|
6
|
+
type: 'object';
|
|
7
|
+
title: string;
|
|
8
|
+
description: string;
|
|
9
|
+
properties: {
|
|
10
|
+
'silly-web-server-port'?: {
|
|
11
|
+
type: 'integer';
|
|
12
|
+
minimum: number;
|
|
13
|
+
maximum: number;
|
|
14
|
+
description: string;
|
|
15
|
+
};
|
|
16
|
+
sillyWebServerHost?: {
|
|
17
|
+
type: 'string';
|
|
18
|
+
description: string;
|
|
19
|
+
default?: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const schema: FakeDriverSchema = {
|
|
25
|
+
type: 'object',
|
|
26
|
+
title: 'Fake Driver Configuration',
|
|
27
|
+
description: 'A schema for Fake Driver arguments',
|
|
28
|
+
properties: {
|
|
29
|
+
'silly-web-server-port': {
|
|
30
|
+
type: 'integer',
|
|
31
|
+
minimum: 1,
|
|
32
|
+
maximum: 65535,
|
|
33
|
+
description: 'The port to use for the fake web server',
|
|
34
|
+
},
|
|
35
|
+
sillyWebServerHost: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
description: 'The host to use for the fake web server',
|
|
38
|
+
default: 'sillyhost',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default schema;
|