suitest-js-api 3.12.2 → 3.14.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/SuitestAutomation/suitest-js-api/blob/master/LICENSE)
4
4
  [![npm version](https://img.shields.io/npm/v/suitest-js-api.svg?style=flat)](https://www.npmjs.com/package/suitest-js-api)
5
- [![CircleCI](https://circleci.com/gh/SuitestAutomation/suitest-js-api.svg?style=shield&circle-token=4aced774267b69829bea6d617c873c40010b1a8b)](https://circleci.com/gh/SuitestAutomation/suitest-js-api)
5
+ [![CircleCI](https://circleci.com/gh/SuitestAutomation/suitest-js-api.svg?style=shield)](https://circleci.com/gh/SuitestAutomation/suitest-js-api)
6
6
  [![Test Coverage](https://api.codeclimate.com/v1/badges/02661808dc132b86710d/test_coverage)](https://codeclimate.com/github/SuitestAutomation/suitest-js-api/test_coverage)
7
7
  [![Maintainability](https://api.codeclimate.com/v1/badges/02661808dc132b86710d/maintainability)](https://codeclimate.com/github/SuitestAutomation/suitest-js-api/maintainability)
8
8
 
package/index.d.ts CHANGED
@@ -47,6 +47,9 @@ import {OcrReadAs} from './typeDefinition/constants/OcrReadAs';
47
47
  import {StringPropComparators} from './typeDefinition/constants/PropComparators';
48
48
  import {ValueOf} from './typeDefinition/utils';
49
49
  import {OcrColor} from './typeDefinition/constants/OcrColor';
50
+ import {ImageChain} from './typeDefinition/ImageChain';
51
+ import {Accuracy} from './typeDefinition/constants/Accuracy';
52
+ import {Lang} from './typeDefinition/constants/Langs';
50
53
 
51
54
  // --------------- Suitest Interface ---------------------- //
52
55
 
@@ -126,6 +129,8 @@ declare namespace suitest {
126
129
  saveScreenshot(fileName?: string): TakeScreenshotChain<void>;
127
130
  openDeepLink(deepLink?: string): OpenDeepLinkChain;
128
131
  ocr(comparators: OcrCommonItem[]): OcrChain;
132
+ image(imageData: ImageData): ImageChain;
133
+ image(apiId: string): ImageChain;
129
134
 
130
135
  getPairedDevice(): null | {
131
136
  deviceId: string,
@@ -165,6 +170,8 @@ declare namespace suitest {
165
170
  COOKIE_PROP: CookieProp;
166
171
  OCR_READ_AS: OcrReadAs;
167
172
  OCR_COLOR: OcrColor;
173
+ ACCURACY: Accuracy;
174
+ LANG: Lang;
168
175
 
169
176
  authContext: AuthContext;
170
177
  appContext: Context;
@@ -212,6 +219,8 @@ declare namespace suitest {
212
219
  setScreenOrientation(orientation: ScreenOrientationValues): SetScreenOrientationChain;
213
220
  openDeepLink(deepLink?: string): OpenDeepLinkChain;
214
221
  ocr(comparators: OcrCommonItem[]): OcrChain;
222
+ image(imageData: ImageData): ImageChain;
223
+ image(apiId: string): ImageChain;
215
224
  }
216
225
 
217
226
  type NetworkLogEvent = {
@@ -354,6 +363,21 @@ declare namespace suitest {
354
363
  color?: ValueOf<OcrColor>,
355
364
  whitelist?: string,
356
365
  blacklist?: string,
357
- region?: [x: number, y: number, width: number, height: number],
366
+ region?: [left: number, top: number, width: number, height: number],
358
367
  }
368
+
369
+ type ImageData =
370
+ | {
371
+ // image api id from suitest image repository
372
+ apiId: string,
373
+ }
374
+ | {
375
+ // url to image somewhere in internet
376
+ url: string,
377
+ }
378
+ | {
379
+ // os path to image somewhere on a disc
380
+ filepath: string,
381
+ }
382
+
359
383
  }
@@ -15,6 +15,7 @@ const {handleProgress} = require('../utils/progressHandler');
15
15
  const {getInfoErrorMessage} = require('../utils/socketErrorMessages');
16
16
  const {translateNotStartedReason} = require('../utils/translateResults');
17
17
  const logLevels = require('../../lib/constants/logLevels');
18
+ const {createBufferFromSocketMessage} = require('../utils/socketChainHelper');
18
19
 
19
20
  /**
20
21
  * @description print log message that comes from BE side with proper log level
@@ -281,20 +282,26 @@ const webSocketsFactory = (self) => {
281
282
 
282
283
  /**
283
284
  * Send ws message
284
- * @param {Object} content
285
+ * @param {Object | [Object, Buffer]} socketMessage
285
286
  * @returns {Promise}
286
287
  */
287
- function send(content) {
288
+ function send(socketMessage) {
288
289
  if (ws) {
289
290
  const messageId = uuid();
290
291
 
292
+ const withBinaryData = Array.isArray(socketMessage);
293
+ const content = withBinaryData ? socketMessage[0] : socketMessage;
291
294
  const msg = JSON.stringify({
292
295
  messageId,
293
296
  content,
294
297
  });
295
298
 
296
299
  logger.debug('Sending message:', msg);
297
- ws.send(msg);
300
+ if (withBinaryData) {
301
+ ws.send(createBufferFromSocketMessage([msg, socketMessage[1]]));
302
+ } else {
303
+ ws.send(msg);
304
+ }
298
305
 
299
306
  return handleRequest(messageId, content.type);
300
307
  }
@@ -0,0 +1,168 @@
1
+ const fs = require('fs');
2
+ const {isNil} = require('ramda');
3
+ const {
4
+ makeToStringComposer,
5
+ makeThenComposer,
6
+ makeToJSONComposer,
7
+ cloneComposer,
8
+ assertComposer,
9
+ abandonComposer,
10
+ timeoutComposer,
11
+ visibleComposer,
12
+ notComposer,
13
+ inRegionComposer,
14
+ accuracyComposer,
15
+ } = require('../composers');
16
+ const makeChain = require('../utils/makeChain');
17
+ const {getRequestType} = require('../utils/socketChainHelper');
18
+ const {validate, validators} = require('../validation');
19
+ const {invalidInputMessage, imageMalformed} = require('../texts');
20
+ const {applyTimeout, applyNegation} = require('../utils/chainUtils');
21
+ const SuitestError = require('../utils/SuitestError');
22
+ const {fetch} = require('../utils/fetch');
23
+ const {getAccuracy} = require('../composers/accuracyComposer');
24
+
25
+ /**
26
+ * @param {import('../../index.d.ts').ISuitest} classInstance
27
+ */
28
+ const imageChainFactory = (classInstance) => {
29
+ const toJSON = (data) => {
30
+ if (isNil(data.comparator)) {
31
+ throw new SuitestError(imageMalformed(), SuitestError.INVALID_INPUT);
32
+ }
33
+
34
+ const socketMessage = {
35
+ type: getRequestType(data, false),
36
+ request: applyTimeout(
37
+ {
38
+ type: 'assert',
39
+ condition: {
40
+ subject: {
41
+ type: 'image',
42
+ },
43
+ type: applyNegation(data.comparator.type, data),
44
+ },
45
+ },
46
+ data,
47
+ classInstance.config.defaultTimeout,
48
+ ),
49
+ };
50
+
51
+ const condition = socketMessage.request.condition;
52
+ const accuracy = getAccuracy(data);
53
+
54
+ if (accuracy) {
55
+ condition.accuracy = accuracy;
56
+ }
57
+
58
+ if (data.imageData.apiId) {
59
+ condition.subject.apiId = data.imageData.apiId;
60
+ } else if (data.imageData.url) {
61
+ condition.subject.url = data.imageData.url;
62
+ } else if (data.imageData.filepath) {
63
+ condition.subject.filepath = data.imageData.filepath;
64
+ }
65
+
66
+ if (data.region) {
67
+ condition.region = data.region;
68
+ }
69
+
70
+ return socketMessage;
71
+ };
72
+
73
+ const toStringComposer = makeToStringComposer(toJSON);
74
+
75
+ const thenComposer = makeThenComposer(async(data) => {
76
+ const socketMessage = toJSON(data);
77
+
78
+ if (data.imageData.apiId) {
79
+ return socketMessage;
80
+ }
81
+
82
+ if (data.imageData.filepath) {
83
+ const imageBuffer = await fs.promises.readFile(data.imageData.filepath);
84
+
85
+ return [socketMessage, imageBuffer];
86
+ }
87
+
88
+ if (data.imageData.url) {
89
+ const imageBuffer = await (await fetch(data.imageData.url)).buffer();
90
+
91
+ return [socketMessage, imageBuffer];
92
+ }
93
+
94
+ return socketMessage;
95
+ });
96
+
97
+ const toJSONComposer = makeToJSONComposer(toJSON);
98
+
99
+ const getComposers = (data) => {
100
+ const output = [
101
+ toStringComposer,
102
+ thenComposer,
103
+ cloneComposer,
104
+ toJSONComposer,
105
+ ];
106
+
107
+ if (!data.isAssert) {
108
+ output.push(assertComposer);
109
+ }
110
+
111
+ if (!data.isAbandoned) {
112
+ output.push(abandonComposer);
113
+ }
114
+
115
+ if (!data.isNegated) {
116
+ output.push(notComposer);
117
+ }
118
+
119
+ if (!data.timeout) {
120
+ output.push(timeoutComposer);
121
+ }
122
+
123
+ if (!data.region) {
124
+ output.push(inRegionComposer);
125
+ }
126
+
127
+ if (!data.comparator) {
128
+ output.push(visibleComposer);
129
+ }
130
+
131
+ if (!data.accuracy) {
132
+ output.push(accuracyComposer);
133
+ }
134
+
135
+ return output;
136
+ };
137
+
138
+ const makeImageChain = (imageData) => {
139
+ const unifiedImageData = typeof imageData === 'string'
140
+ ? {apiId: imageData}
141
+ : imageData;
142
+
143
+ return makeChain(
144
+ classInstance,
145
+ getComposers,
146
+ {
147
+ type: 'image',
148
+ imageData:
149
+ validate(
150
+ validators.IMAGE_DATA,
151
+ unifiedImageData,
152
+ invalidInputMessage('image', 'Image data'),
153
+ ),
154
+ },
155
+ );
156
+ };
157
+
158
+ return {
159
+ image: makeImageChain,
160
+ imageAssert: (data) => makeImageChain(data).toAssert(),
161
+
162
+ // for unit tests
163
+ getComposers,
164
+ toJSON,
165
+ };
166
+ };
167
+
168
+ module.exports = imageChainFactory;
@@ -6,6 +6,7 @@ const {
6
6
  assertComposer,
7
7
  abandonComposer,
8
8
  timeoutComposer,
9
+ languageComposer,
9
10
  } = require('../composers');
10
11
  const makeChain = require('../utils/makeChain');
11
12
  const {getRequestType} = require('../utils/socketChainHelper');
@@ -63,6 +64,9 @@ const ocrFactory = (classInstance) => {
63
64
  if (data.ocrItems) {
64
65
  socketMessage.subject.options = data.ocrItems;
65
66
  }
67
+ if (data.language) {
68
+ socketMessage.subject.language = data.language;
69
+ }
66
70
  } else {
67
71
  socketMessage.request = applyTimeout(
68
72
  {
@@ -75,6 +79,9 @@ const ocrFactory = (classInstance) => {
75
79
  data,
76
80
  classInstance.config.defaultTimeout,
77
81
  );
82
+ if (data.language) {
83
+ socketMessage.request.condition.language = data.language;
84
+ }
78
85
  if (data.ocrItems) {
79
86
  socketMessage.request.condition.comparators = validateOcrComparators(data.ocrItems);
80
87
  }
@@ -107,6 +114,10 @@ const ocrFactory = (classInstance) => {
107
114
  output.push(timeoutComposer);
108
115
  }
109
116
 
117
+ if (!data.language) {
118
+ output.push(languageComposer);
119
+ }
120
+
110
121
  return output;
111
122
  };
112
123
 
@@ -0,0 +1,24 @@
1
+ const {makeModifierComposer} = require('../utils/makeComposer');
2
+ const composers = require('../constants/composer');
3
+ const {validate, validators} = require('../validation');
4
+ const {invalidInputMessage} = require('../texts');
5
+
6
+ const accuracyComposer = makeModifierComposer(composers.ACCURACY, ['accuracy'], (_, meta, accuracy) => {
7
+ return {
8
+ ...meta,
9
+ accuracy: validate(
10
+ validators.ACCURACY,
11
+ accuracy,
12
+ invalidInputMessage('accuracy', 'Accuracy'),
13
+ ),
14
+ };
15
+ });
16
+
17
+ const getAccuracy = (meta) => {
18
+ return meta.accuracy;
19
+ };
20
+
21
+ module.exports = {
22
+ accuracyComposer,
23
+ getAccuracy,
24
+ };
@@ -0,0 +1,17 @@
1
+ const {makeModifierComposer} = require('../utils/makeComposer');
2
+ const composers = require('../constants/composer');
3
+ const {validate, validators} = require('../validation');
4
+ const {invalidInputMessage} = require('../texts');
5
+
6
+ const inRegionComposer = makeModifierComposer(composers.IN_REGION, ['inRegion'], (_, meta, region) => {
7
+ return {
8
+ ...meta,
9
+ region: validate(
10
+ validators.REGION,
11
+ region,
12
+ invalidInputMessage('region', 'Region'),
13
+ ),
14
+ };
15
+ });
16
+
17
+ module.exports = inRegionComposer;
@@ -46,6 +46,9 @@ const attributesComposer = require('./attributesComposer');
46
46
  const cssPropsComposer = require('./cssPropsComposer');
47
47
  const deepLinkComposer = require('./deepLinkComposer');
48
48
  const withPropertiesComposer = require('./withPropertiesComposer');
49
+ const inRegionComposer = require('./inRegionComposer');
50
+ const languageComposer = require('./languageComposer');
51
+ const {accuracyComposer} = require('./accuracyComposer');
49
52
 
50
53
  module.exports = {
51
54
  abandonComposer,
@@ -98,4 +101,7 @@ module.exports = {
98
101
  cssPropsComposer,
99
102
  deepLinkComposer,
100
103
  withPropertiesComposer,
104
+ inRegionComposer,
105
+ languageComposer,
106
+ accuracyComposer,
101
107
  };
@@ -0,0 +1,18 @@
1
+ const {makeModifierComposer} = require('../utils/makeComposer');
2
+ const composers = require('../constants/composer');
3
+ const {validate, validators} = require('../validation');
4
+ const {invalidInputMessage} = require('../texts');
5
+
6
+ /**
7
+ * Defines language method
8
+ */
9
+ const languageComposer = makeModifierComposer(composers.LANGUAGE, ['language'], (_, meta, value) => ({
10
+ ...meta,
11
+ language: validate(
12
+ validators.LANGUAGE,
13
+ value,
14
+ invalidInputMessage('language', 'Language'),
15
+ ),
16
+ }));
17
+
18
+ module.exports = languageComposer;
@@ -21,8 +21,12 @@ const makeThenComposer = (getSocketMessage, callback, beforeSend) => makeMethodC
21
21
  if (promiseMap.has(data)) {
22
22
  promise = promiseMap.get(data);
23
23
  } else {
24
- const socketMessage = getSocketMessage(data);
25
- let dataToTranslate = socketMessage;
24
+ /** @type {Object | [Object, Buffer]} */
25
+ const message = await getSocketMessage(data);
26
+ const withBinaryData = Array.isArray(message);
27
+ const jsonSocketMessage = withBinaryData ? message[0] : message;
28
+
29
+ let dataToTranslate = jsonSocketMessage;
26
30
  let snippets;
27
31
 
28
32
  if (dataToTranslate.type === 'takeScreenshot') {
@@ -45,16 +49,21 @@ const makeThenComposer = (getSocketMessage, callback, beforeSend) => makeMethodC
45
49
  logger.log(translation);
46
50
  }
47
51
  }
48
- promise = authContext.authorizeWs(socketMessage, data.type)
49
- .then(() => webSockets.send(socketMessage).then(result => {
50
- return callback
51
- ? callback(result, data, socketMessage)
52
- : processServerResponse(logger, config.logLevel)(result, data, socketMessage, snippets);
53
- }, error => {
54
- return callback
55
- ? callback(error, data, socketMessage)
56
- : processServerResponse(logger, config.logLevel)(error, data, socketMessage, snippets);
57
- }))
52
+
53
+ promise = authContext.authorizeWs(jsonSocketMessage, data.type)
54
+ .then(() => {
55
+ const defaultResponseHandler = processServerResponse(logger, config.logLevel);
56
+
57
+ return webSockets.send(message).then(result => {
58
+ return callback
59
+ ? callback(result, data, jsonSocketMessage)
60
+ : defaultResponseHandler(result, data, jsonSocketMessage, snippets);
61
+ }, error => {
62
+ return callback
63
+ ? callback(error, data, jsonSocketMessage)
64
+ : defaultResponseHandler(error, data, jsonSocketMessage, snippets);
65
+ });
66
+ })
58
67
  .catch(err => {
59
68
  if (err instanceof SuitestError && err.code === SuitestError.AUTH_NOT_ALLOWED) {
60
69
  logger.error(connectionNotEstablished());
@@ -0,0 +1,9 @@
1
+ const ACCURACY = {
2
+ HIGH: 'high',
3
+ MEDIUM: 'medium',
4
+ LOW: 'low',
5
+ };
6
+
7
+ Object.freeze(ACCURACY);
8
+
9
+ module.exports = ACCURACY;
@@ -45,7 +45,10 @@ const composers = {
45
45
  ATTRIBUTES: Symbol('getAttributes'),
46
46
  CSS_PROPS: Symbol('getCssProp'),
47
47
  DEEP_LINK: Symbol('deepLink'),
48
- WITH_PROPERTIES: Symbol('withProperties'),
48
+ WITH_PROPERTIES: Symbol('withProperties'),
49
+ IN_REGION: Symbol('inRegion'),
50
+ LANGUAGE: Symbol('language'),
51
+ ACCURACY: Symbol('accuracy'),
49
52
  };
50
53
 
51
54
  Object.freeze(composers);
@@ -0,0 +1,13 @@
1
+ const LANG = {
2
+ ENGLISH: 'eng',
3
+ GERMAN: 'deu',
4
+ FRENCH: 'fra',
5
+ ITALIAN: 'ita',
6
+ DUTCH: 'nld',
7
+ SPANISH: 'spa',
8
+ POLISH: 'pol',
9
+ };
10
+
11
+ Object.freeze(LANG);
12
+
13
+ module.exports = LANG;
@@ -35,6 +35,10 @@ const validationKeys = {
35
35
  COOKIE_PROPS: Symbol('cookieProps'),
36
36
  OCR_COMPARATORS: Symbol('ocrComparators'),
37
37
  OCR_OPTIONS: Symbol('ocrOptions'),
38
+ IMAGE_DATA: Symbol('imageData'),
39
+ REGION: Symbol('region'),
40
+ LANGUAGE: Symbol('language'),
41
+ ACCURACY: Symbol('accuracy'),
38
42
  };
39
43
 
40
44
  Object.freeze(validationKeys);
package/lib/texts.js CHANGED
@@ -78,6 +78,7 @@ module.exports = {
78
78
  positionIsMalformed: () => 'position command is malformed',
79
79
  relativePositionIsMalformed: () => 'relative position is malformed',
80
80
  assertOcrMalformed: () => 'Assert ocr line is malformed - comparators are missing',
81
+ imageMalformed: () => 'Image line is malformed',
81
82
 
82
83
  unusedLeaves: leaves => `Some of your Suitest chains were not executed.
83
84
  Put an "await" in front of those chains or call .abandon() to suppress these warnings.
@@ -1,5 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const {Buffer} = require('buffer');
3
4
  const {isNil} = require('ramda');
4
5
  const assert = require('assert');
5
6
  const SuitestError = require('./SuitestError');
@@ -213,7 +214,31 @@ function getResponseForError(res) {
213
214
  return responseForError;
214
215
  }
215
216
 
217
+ /**
218
+ * @description concat socket message and binary data pair into single binary data
219
+ * protocol is:
220
+ * | protocol number (1 byte) | size of json socket message (32-bit unsigned integer) | socket message | binary data |
221
+ * example:
222
+ * | 0x00 | 0x000001c(28) | Buffer.from(JSON.stringify({type: 'eval', request: {}})) | binaryData |
223
+ * @param {Object|string} socketMessage
224
+ * @param {Buffer} binaryData
225
+ * @returns {Buffer}
226
+ */
227
+ function createBufferFromSocketMessage([socketMessage, binaryData]) {
228
+ const protocolNumber = 0x00;
229
+ const socketMessageSizeMaxBytes = 4;
230
+ const socketMessageBuffer = Buffer.from(typeof socketMessage === 'string' ? socketMessage : JSON.stringify(socketMessage));
231
+ const sizeSocketMessage = Buffer.alloc(socketMessageSizeMaxBytes);
232
+
233
+ sizeSocketMessage.writeUInt32BE(socketMessageBuffer.length);
234
+
235
+ const header = Buffer.concat([Buffer.from([protocolNumber]), sizeSocketMessage]);
236
+
237
+ return Buffer.concat([header, socketMessageBuffer, binaryData]);
238
+ }
239
+
216
240
  module.exports = {
217
241
  processServerResponse,
218
242
  getRequestType,
243
+ createBufferFromSocketMessage,
219
244
  };
@@ -5,6 +5,9 @@ const {stripAnsiChars} = require('../stringUtils');
5
5
 
6
6
  const bySymbol = (a, b) => a.toString() > b.toString() ? 1 : -1;
7
7
  const getComposerTypes = comps => comps.map(c => c.composerType).sort(bySymbol);
8
+ const excludeComposer = (composers, toExclude) => {
9
+ return composers.filter((composer) => composer !== toExclude);
10
+ };
8
11
 
9
12
  const assertBeforeSendMsg = curry((beforeSendMsgFn, logStub, chainData, substring) => {
10
13
  logStub.resetHistory();
@@ -13,12 +16,13 @@ const assertBeforeSendMsg = curry((beforeSendMsgFn, logStub, chainData, substrin
13
16
 
14
17
  assert.ok(
15
18
  beforeSendMsgLog.includes(substring),
16
- `Substring "${substring}" should be found in beforeSendMsg log: "${beforeSendMsgLog}"`
19
+ `Substring "${substring}" should be found in beforeSendMsg log: "${beforeSendMsgLog}"`,
17
20
  );
18
21
  });
19
22
 
20
23
  module.exports = {
21
24
  bySymbol,
22
25
  getComposerTypes,
26
+ excludeComposer,
23
27
  assertBeforeSendMsg,
24
28
  };
@@ -13,5 +13,30 @@ const commonTestInputError = curry((asserter, fn, args = [], expect = {}, msg) =
13
13
  }, msg || `Invalid error if ${args.length ? args.map(JSON.stringify).join(', ') : 'no args'}`);
14
14
  });
15
15
 
16
+ const suitestInvalidInputError = (message) => {
17
+ return new SuitestError(message, SuitestError.INVALID_INPUT);
18
+ };
19
+
20
+ /**
21
+ * @deprecated use assert.rejects and suitestInvalidInputError https://nodejs.org/api/assert.html#assertrejectsasyncfn-error-message
22
+ * @example
23
+ * // Before:
24
+ * await testInputErrorAsync(testingFunc, ['some arg']);
25
+ * await testInputErrorAsync(testingFunc, ['some arg'] {message: 'Error message'});
26
+ * // After:
27
+ * await assert.rejects(() => testingFunc('some arg'));
28
+ * await assert.rejects(() => testingFunc('some arg'), suitestInvalidInputError('Error message'));
29
+ */
16
30
  module.exports.testInputErrorAsync = commonTestInputError(assertThrowsAsync);
31
+ /**
32
+ * @deprecated use assert.throws instead https://nodejs.org/api/assert.html#assertthrowsfn-error-message
33
+ * @example
34
+ * // Before:
35
+ * await testInputErrorSync(testingFunc, ['some arg']);
36
+ * await testInputErrorSync(testingFunc, ['some arg'], {message: 'Error message'});
37
+ * // After:
38
+ * await assert.throws(() => testingFunc('some arg'));
39
+ * await assert.throws(() => testingFunc('some arg'), suitestInvalidInputError('Error message'));
40
+ */
17
41
  module.exports.testInputErrorSync = commonTestInputError(assert.throws);
42
+ module.exports.suitestInvalidInputError = suitestInvalidInputError;
@@ -20,6 +20,8 @@ const LAUNCH_MODE = require('../constants/launchMode');
20
20
  const COOKIE_PROP = require('../constants/cookieProp');
21
21
  const OCR_READ_AS = require('../constants/ocrReadAs');
22
22
  const OCR_COLOR = require('../constants/ocrColor');
23
+ const ACCURACY = require('../constants/accuracy');
24
+ const LANG = require('../constants/lang');
23
25
  const schemas = {};
24
26
 
25
27
  const nonNegativeNumber = () => ({
@@ -32,6 +34,21 @@ const positiveNumber = () => ({
32
34
  exclusiveMinimum: 0,
33
35
  });
34
36
 
37
+ /**
38
+ * @description schema for validation region for OCR and Image assertions
39
+ */
40
+ const regionSchema = () => ({
41
+ 'type': 'array',
42
+ 'items': [
43
+ {...nonNegativeNumber(), maximum: 100},
44
+ {...nonNegativeNumber(), maximum: 100},
45
+ {...positiveNumber(), maximum: 100},
46
+ {...positiveNumber(), maximum: 100},
47
+ ],
48
+ 'minItems': 4,
49
+ 'additionalItems': false,
50
+ });
51
+
35
52
  const CONFIG_OVERRIDE_PROPERTIES = {
36
53
  'url': {'type': 'string'},
37
54
  'suitestify': {'type': 'boolean'},
@@ -535,17 +552,7 @@ schemas[validationKeys.OCR_COMPARATORS] = {
535
552
  },
536
553
  'whitelist': schemas[validationKeys.NON_EMPTY_STRING],
537
554
  'blacklist': schemas[validationKeys.NON_EMPTY_STRING],
538
- 'region': {
539
- 'type': 'array',
540
- 'items': [
541
- {...nonNegativeNumber(), maximum: 100},
542
- {...nonNegativeNumber(), maximum: 100},
543
- {...positiveNumber(), maximum: 100},
544
- {...positiveNumber(), maximum: 100},
545
- ],
546
- 'minItems': 4,
547
- 'additionalItems': false,
548
- },
555
+ 'region': regionSchema(),
549
556
  },
550
557
  },
551
558
  };
@@ -575,21 +582,43 @@ schemas[validationKeys.OCR_OPTIONS] = {
575
582
  },
576
583
  'whitelist': schemas[validationKeys.NON_EMPTY_STRING],
577
584
  'blacklist': schemas[validationKeys.NON_EMPTY_STRING],
578
- 'region': {
579
- 'type': 'array',
580
- 'items': [
581
- {...nonNegativeNumber(), maximum: 100},
582
- {...nonNegativeNumber(), maximum: 100},
583
- {...positiveNumber(), maximum: 100},
584
- {...positiveNumber(), maximum: 100},
585
- ],
586
- 'minItems': 4,
587
- 'additionalItems': false,
588
- },
585
+ 'region': regionSchema(),
589
586
  },
590
587
  },
591
588
  };
592
589
 
590
+ schemas[validationKeys.REGION] = {
591
+ 'schemaId': validationKeys.REGION,
592
+ ...regionSchema(),
593
+ };
594
+
595
+ schemas[validationKeys.IMAGE_DATA] = {
596
+ 'schemaId': validationKeys.IMAGE_DATA,
597
+ 'type': 'object',
598
+ 'properties': {
599
+ 'url': {'type': 'string'},
600
+ 'filepath': {'type': 'string'},
601
+ 'apiId': {'type': 'string'},
602
+ },
603
+ 'anyOf': [
604
+ {'required': ['url']},
605
+ {'required': ['filepath']},
606
+ {'required': ['apiId']},
607
+ ],
608
+ };
609
+
610
+ schemas[validationKeys.LANGUAGE] = {
611
+ 'schemaId': validationKeys.LANGUAGE,
612
+ 'type': 'string',
613
+ 'enum': Object.values(LANG),
614
+ };
615
+
616
+ schemas[validationKeys.ACCURACY] = {
617
+ 'schemaId': validationKeys.ACCURACY,
618
+ 'type': 'string',
619
+ 'enum': Object.values(ACCURACY),
620
+ };
621
+
593
622
  Object.freeze(schemas);
594
623
 
595
624
  module.exports = schemas;
@@ -245,6 +245,7 @@ const allowedUntilConditionChainTypes = [
245
245
  'network',
246
246
  'video',
247
247
  'psVideo',
248
+ 'image',
248
249
  ];
249
250
 
250
251
  const allowedUntilConditionChainNames = [
@@ -256,6 +257,7 @@ const allowedUntilConditionChainNames = [
256
257
  'networkRequest',
257
258
  'video',
258
259
  'psVideo',
260
+ 'image',
259
261
  ];
260
262
 
261
263
  const getSubjectType = path(['request', 'condition', 'subject', 'type']);
@@ -111,6 +111,18 @@ const validatorsMap = {
111
111
  [validationKeys.OCR_OPTIONS]: (value, text) => {
112
112
  return validators.validateJsonSchema(validationKeys.OCR_OPTIONS, value, text);
113
113
  },
114
+ [validationKeys.IMAGE_DATA]: (value, text) => {
115
+ return validators.validateJsonSchema(validationKeys.IMAGE_DATA, value, text);
116
+ },
117
+ [validationKeys.REGION]: (value, text) => {
118
+ return validators.validateJsonSchema(validationKeys.REGION, value, text);
119
+ },
120
+ [validationKeys.LANGUAGE]: (value, text) => {
121
+ return validators.validateJsonSchema(validationKeys.LANGUAGE, value, text);
122
+ },
123
+ [validationKeys.ACCURACY]: (value, text) => {
124
+ return validators.validateJsonSchema(validationKeys.ACCURACY, value, text);
125
+ },
114
126
  };
115
127
 
116
128
  Object.freeze(validatorsMap);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suitest-js-api",
3
- "version": "3.12.2",
3
+ "version": "3.14.0",
4
4
  "main": "index.js",
5
5
  "repository": "git@github.com:SuitestAutomation/suitest-js-api.git",
6
6
  "author": "Suitest <hello@suite.st>",
@@ -25,7 +25,10 @@
25
25
  "XClass TV testing",
26
26
  "Xumo testing",
27
27
  "Smart TV test automation",
28
- "Fire TV test automation"
28
+ "Fire TV test automation",
29
+ "visual testing",
30
+ "image template matching",
31
+ "ocr"
29
32
  ],
30
33
  "homepage": "https://suite.st/",
31
34
  "license": "MIT",
@@ -91,7 +94,7 @@
91
94
  },
92
95
  "dependencies": {
93
96
  "@suitest/smst-to-text": "^4.13.0",
94
- "@suitest/translate": "^4.15.0",
97
+ "@suitest/translate": "^4.16.0",
95
98
  "@types/node": "^14.0.10",
96
99
  "ajv": "^6.12.2",
97
100
  "ansi-regex": "^5.0.0",
package/suitest.js CHANGED
@@ -40,6 +40,7 @@ const playstationVideoFactory = require('./lib/chains/playstationVideoChain');
40
40
  const setScreenOrientationFactory = require('./lib/chains/setScreenOrientationChain');
41
41
  const openDeepLinkFactory = require('./lib/chains/openDeepLinkChain');
42
42
  const ocrFactory = require('./lib/chains/ocrChain');
43
+ const imageFactory = require('./lib/chains/imageChain');
43
44
 
44
45
  // Constants
45
46
  const {ELEMENT_PROP, VALUE} = require('./lib/constants/element');
@@ -62,6 +63,8 @@ const SCREEN_ORIENTATION = require('./lib/constants/screenOrientation');
62
63
  const COOKIE_PROP = require('./lib/constants/cookieProp');
63
64
  const OCR_READ_AS = require('./lib/constants/ocrReadAs');
64
65
  const OCR_COLOR = require('./lib/constants/ocrColor');
66
+ const ACCURACY = require('./lib/constants/accuracy');
67
+ const LANG = require('./lib/constants/lang');
65
68
 
66
69
  // Network
67
70
  const webSocketsFactory = require('./lib/api/webSockets');
@@ -139,6 +142,7 @@ class SUITEST_API extends EventEmitter {
139
142
  const {setScreenOrientation, setScreenOrientationAssert} = setScreenOrientationFactory(this);
140
143
  const {openDeepLink, openDeepLinkAssert} = openDeepLinkFactory(this);
141
144
  const {ocr, ocrAssert} = ocrFactory(this);
145
+ const {image, imageAssert} = imageFactory(this);
142
146
 
143
147
  this.openApp = openApp;
144
148
  this.closeApp = closeApp;
@@ -167,6 +171,7 @@ class SUITEST_API extends EventEmitter {
167
171
  this.setScreenOrientation = setScreenOrientation;
168
172
  this.openDeepLink = openDeepLink;
169
173
  this.ocr = ocr;
174
+ this.image = image;
170
175
 
171
176
  this.PROP = ELEMENT_PROP;
172
177
  this.COMP = PROP_COMPARATOR;
@@ -190,6 +195,8 @@ class SUITEST_API extends EventEmitter {
190
195
  this.COOKIE_PROP = COOKIE_PROP;
191
196
  this.OCR_READ_AS = OCR_READ_AS;
192
197
  this.OCR_COLOR = OCR_COLOR;
198
+ this.ACCURACY = ACCURACY;
199
+ this.LANG = LANG;
193
200
 
194
201
  this.assert = {
195
202
  application: applicationAssert,
@@ -218,6 +225,7 @@ class SUITEST_API extends EventEmitter {
218
225
  setScreenOrientation: setScreenOrientationAssert,
219
226
  openDeepLink: openDeepLinkAssert,
220
227
  ocr: ocrAssert,
228
+ image: imageAssert,
221
229
  };
222
230
 
223
231
  // Listen to process events to trigger websocket termination and dump warnings, if any
@@ -0,0 +1,47 @@
1
+ import {
2
+ AbstractChain,
3
+ Timeout,
4
+ InRegionModifier,
5
+ VisibleModifier,
6
+ AccuracyModifier,
7
+ Negatable,
8
+ Assertable,
9
+ BaseEmptyChain,
10
+ } from './modifiers';
11
+ import {
12
+ AccuracyModifierNames,
13
+ AssertableMethodsNames,
14
+ InRegionMethodsNames,
15
+ NegatableMethodsNames,
16
+ TimeoutMethodsNames,
17
+ VisibleMethodsNames,
18
+ Chainable,
19
+ ChainWithoutMethods,
20
+ } from './utils';
21
+
22
+ interface ImageBase extends
23
+ // not(), doesNot(), isNot()
24
+ Negatable<ChainWithoutMethods<ImageBase, NegatableMethodsNames>>,
25
+ // timeout(...)
26
+ Timeout<ChainWithoutMethods<ImageBase, TimeoutMethodsNames>>,
27
+ // visible()
28
+ VisibleModifier<ChainWithoutMethods<ImageBase, VisibleMethodsNames>>,
29
+ // inRegion(...)
30
+ InRegionModifier<ChainWithoutMethods<ImageBase, InRegionMethodsNames>>,
31
+ // accuracy(...)
32
+ AccuracyModifier<ChainWithoutMethods<ImageBase, AccuracyModifierNames>>,
33
+ // toAssert(...)
34
+ Assertable<ChainWithoutMethods<ImageBase, AssertableMethodsNames>>,
35
+ // toString(), then(), abandon(), clone()
36
+ ImageBaseEvalChain<ImageBase>
37
+ {}
38
+
39
+ type ImageChain = Chainable<ImageBase>;
40
+
41
+ interface ImageBaseEvalChain<TSelf> extends
42
+ BaseEmptyChain<TSelf, ImageEvalResult, ImageAbandonedChain>
43
+ {}
44
+
45
+ interface ImageAbandonedChain extends AbstractChain {}
46
+
47
+ type ImageEvalResult = boolean;
@@ -3,17 +3,32 @@ import {
3
3
  Assertable,
4
4
  Timeout,
5
5
  BaseEmptyChain,
6
+ Language,
6
7
  } from './modifiers';
7
8
 
9
+ // +language +timeout
8
10
  export interface OcrChain extends
9
11
  OcrBaseQueryChain<OcrChain>,
10
- Timeout<OcrWithoutTimeout>
12
+ Timeout<OcrWithoutTimeout>,
13
+ Language<OcrWithoutLanguage>
11
14
  {}
12
15
 
16
+ // -language +timeout
17
+ interface OcrWithoutLanguage extends
18
+ Timeout<OcrEmptyChain>,
19
+ OcrBaseQueryChain<OcrWithoutLanguage>
20
+ {}
21
+
22
+ // + language -timeout
13
23
  interface OcrWithoutTimeout extends
24
+ Language<OcrEmptyChain>,
14
25
  OcrBaseQueryChain<OcrWithoutTimeout>
15
26
  {}
16
27
 
28
+ interface OcrEmptyChain extends
29
+ OcrBaseQueryChain<OcrEmptyChain>
30
+ {}
31
+
17
32
  interface OcrBaseQueryChain<TSelf> extends
18
33
  BaseEmptyChain<TSelf, OcrQueryResult, OcrAbandonedChain>,
19
34
  Assertable<TSelf>
@@ -0,0 +1,5 @@
1
+ export type Accuracy = {
2
+ HIGH: 'high',
3
+ MEDIUM: 'medium',
4
+ LOW: 'low',
5
+ };
@@ -0,0 +1,9 @@
1
+ export type Lang = {
2
+ ENGLISH: 'eng',
3
+ GERMAN: 'deu',
4
+ FRENCH: 'fra',
5
+ ITALIAN: 'ita',
6
+ DUTCH: 'nld',
7
+ SPANISH: 'spa',
8
+ POLISH: 'pol',
9
+ };
@@ -1,4 +1,7 @@
1
+ import {ValueOf} from './utils';
1
2
  import {LaunchModeValues} from './constants/LaunchMode';
3
+ import {Lang} from './constants/Langs';
4
+ import {Accuracy} from './constants/Accuracy';
2
5
 
3
6
  export interface Thenable <R> {
4
7
  then <U> (onFulfilled?: (value: R) => U | Thenable<U>, onRejected?: (error: any) => U | Thenable<U>): Thenable<U>;
@@ -264,3 +267,15 @@ export interface HandleModifier<T> {
264
267
  export interface GetAttributesModifier<T> {
265
268
  getAttributes(attributes?: string[]): T;
266
269
  }
270
+
271
+ export interface InRegionModifier<T> {
272
+ inRegion(region: [number, number, number, number]): T;
273
+ }
274
+
275
+ export interface Language<T> {
276
+ language(lang: ValueOf<Lang>): T;
277
+ }
278
+
279
+ export interface AccuracyModifier<T> {
280
+ accuracy(accuracy: ValueOf<Accuracy>): T;
281
+ }
@@ -1,6 +1,56 @@
1
+ import {AccuracyModifier, Assertable, InRegionModifier, Negatable, Timeout, VisibleModifier} from '../modifiers';
2
+
1
3
  export type ValueOf<T> = T[keyof T];
2
4
 
3
5
  /**
4
6
  * @description utility type for checking that TObject is satisfies TSubject (aka satisfies operator)
5
7
  */
6
8
  export type Satisfies<TObject extends TSubject, TSubject> = TObject;
9
+
10
+ /**
11
+ * @description utility type for converting given interface for describe suitest chains
12
+ * @example
13
+ *
14
+ * interface ExampleInterface {
15
+ * method1: () => ChainWithoutMethods<ExampleInterface, 'method1'>;
16
+ * method2: () => ChainWithoutMethods<ExampleInterface, 'method2'>;
17
+ * method3: () => string;
18
+ * }
19
+ *
20
+ * type ChainableExample = Chainable<ExampleInterface>;
21
+ *
22
+ * declare const example: ChainableExample;
23
+ *
24
+ * example
25
+ * .method1()
26
+ * .method3(); // -> string will be returned
27
+ *
28
+ * example
29
+ * .method1()
30
+ * .method1(); // -> will be displayed error since method1 already called and no longer presents in chain
31
+ *
32
+ * example
33
+ * .method1()
34
+ * .method2()
35
+ * .method2(); // -> will be displayed error since method2 already called and no longer presents in chain;
36
+ */
37
+ export type Chainable<T> = {
38
+ [K in keyof T]: T[K] extends (...args: any[]) => ChainWithoutMethods<T, infer ExcludeMethods>
39
+ // if field is function than return new chain without specified methods names
40
+ ? (...args: Parameters<T[K]>) => Chainable<WithoutMethods<T, ExcludeMethods>>
41
+ // will be returned as it is, if field not function or function which not return MethodToOmit helper type
42
+ : T[K];
43
+ };
44
+
45
+ export type WithoutMethods<T, K extends keyof T> = Omit<T, K>;
46
+
47
+ export type ChainWithoutMethods<T, ExcludeMethods extends keyof T> = {
48
+ __excludeMethods: ExcludeMethods,
49
+ }
50
+
51
+ export type NegatableMethodsNames = keyof Negatable<any>;
52
+ export type TimeoutMethodsNames = keyof Timeout<any>;
53
+ export type VisibleMethodsNames = keyof VisibleModifier<any>;
54
+ export type InRegionMethodsNames = keyof InRegionModifier<any>;
55
+ export type AssertableMethodsNames = keyof Assertable<any>;
56
+ export type AccuracyModifierNames = keyof AccuracyModifier<any>;