@wdio/ocr-service 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/CHANGELOG.md +29 -0
- package/LICENSE +21 -0
- package/README.md +13 -0
- package/assets/target.png +0 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +165 -0
- package/dist/commands/ocrClickOnText.d.ts +3 -0
- package/dist/commands/ocrClickOnText.d.ts.map +1 -0
- package/dist/commands/ocrClickOnText.js +27 -0
- package/dist/commands/ocrGetElementPositionByText.d.ts +3 -0
- package/dist/commands/ocrGetElementPositionByText.d.ts.map +1 -0
- package/dist/commands/ocrGetElementPositionByText.js +46 -0
- package/dist/commands/ocrGetText.d.ts +3 -0
- package/dist/commands/ocrGetText.d.ts.map +1 -0
- package/dist/commands/ocrGetText.js +5 -0
- package/dist/commands/ocrSetValue.d.ts +3 -0
- package/dist/commands/ocrSetValue.d.ts.map +1 -0
- package/dist/commands/ocrSetValue.js +38 -0
- package/dist/commands/ocrWaitForTextDisplayed.d.ts +3 -0
- package/dist/commands/ocrWaitForTextDisplayed.d.ts.map +1 -0
- package/dist/commands/ocrWaitForTextDisplayed.js +8 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/service.d.ts +15 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +97 -0
- package/dist/types.d.ts +331 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils/constants.d.ts +67 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/constants.js +66 -0
- package/dist/utils/fuzzySearch.d.ts +3 -0
- package/dist/utils/fuzzySearch.d.ts.map +1 -0
- package/dist/utils/fuzzySearch.js +29 -0
- package/dist/utils/getData.d.ts +3 -0
- package/dist/utils/getData.d.ts.map +1 -0
- package/dist/utils/getData.js +71 -0
- package/dist/utils/getTextPositions.d.ts +3 -0
- package/dist/utils/getTextPositions.d.ts.map +1 -0
- package/dist/utils/getTextPositions.js +13 -0
- package/dist/utils/imageProcessing.d.ts +5 -0
- package/dist/utils/imageProcessing.d.ts.map +1 -0
- package/dist/utils/imageProcessing.js +73 -0
- package/dist/utils/index.d.ts +15 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +46 -0
- package/dist/utils/sendKeys.d.ts +2 -0
- package/dist/utils/sendKeys.d.ts.map +1 -0
- package/dist/utils/sendKeys.js +21 -0
- package/dist/utils/tesseract.d.ts +13 -0
- package/dist/utils/tesseract.d.ts.map +1 -0
- package/dist/utils/tesseract.js +210 -0
- package/package.json +41 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# @wdio/ocr-service
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- a924dfc: # 🚀 New Feature
|
|
8
|
+
|
|
9
|
+
Sometimes it can be hard to find an element in a mobile native app or desktop site, with an interactable Canvas, with the default [WebdriverIO selectors](https://webdriver.io/docs/selectors). In that case, it would be nice if you would be able to use something like OCR ([Optical Character Recognition](https://en.wikipedia.org/wiki/Optical_character_recognition)) to interact with elements on your device/screen.
|
|
10
|
+
|
|
11
|
+
The new `@wdio/ocr-service` service provides you with the option to interact with elements based on **visible text**. It will provide multiple commands to:
|
|
12
|
+
|
|
13
|
+
- wait
|
|
14
|
+
- search
|
|
15
|
+
- and interact
|
|
16
|
+
|
|
17
|
+
with an element, all based on text.
|
|
18
|
+
|
|
19
|
+
The following commands will be added
|
|
20
|
+
|
|
21
|
+
- `ocrGetText`
|
|
22
|
+
- `ocrGetElementPositionByText`
|
|
23
|
+
- `ocrWaitForTextDisplayed`
|
|
24
|
+
- `ocrClickOnText`
|
|
25
|
+
- `ocrSetValue`
|
|
26
|
+
|
|
27
|
+
A CLI command will also be provided to pre-check text received from the image. For a demo check this video
|
|
28
|
+
|
|
29
|
+
https://github.com/webdriverio/visual-testing/assets/11979740/6e4677ba-1463-4d6e-98b0-9a3bf70b702f
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 OpenJS Foundation
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# WebdriverIO OCR Service
|
|
2
|
+
|
|
3
|
+
> A WebdriverIO service for using OCR to interact with elements on a page.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
The easiest way is to keep `@wdio/ocr-service` as a dev-dependency in your `package.json`, via:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @wdio/ocr-service --save-dev
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Instructions on how to get started can be found in the [visual testing](https://webdriver.io/docs/visual-testing) docs on the WebdriverIO project page.
|
|
Binary file
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,mBAAmB,wHAI/B,CAAA"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import getData from './utils/getData.js';
|
|
5
|
+
import { createOcrDir } from './utils/index.js';
|
|
6
|
+
import { isSystemTesseractAvailable } from './utils/tesseract.js';
|
|
7
|
+
import { SUPPORTED_LANGUAGES } from './utils/constants.js';
|
|
8
|
+
export const CONFIG_HELPER_INTRO = `
|
|
9
|
+
=============================
|
|
10
|
+
🤖 WDIO OCR-Service Wizard 🧙
|
|
11
|
+
=============================
|
|
12
|
+
`;
|
|
13
|
+
const currentPath = process.cwd();
|
|
14
|
+
function convertFileToBase64(filePath) {
|
|
15
|
+
if (!existsSync(filePath)) {
|
|
16
|
+
throw new Error(`File does not exist at: "${filePath}"`);
|
|
17
|
+
}
|
|
18
|
+
const fileBuffer = readFileSync(filePath);
|
|
19
|
+
const base64String = fileBuffer.toString('base64');
|
|
20
|
+
return base64String;
|
|
21
|
+
}
|
|
22
|
+
function listFilesAndFolders(folderPath) {
|
|
23
|
+
const items = readdirSync(folderPath, { withFileTypes: true });
|
|
24
|
+
const choices = items.filter(item => {
|
|
25
|
+
if (item.isDirectory() && !item.name.startsWith('node_modules')) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return /\.(jpg|jpeg|png|gif)$/i.test(item.name);
|
|
29
|
+
}).map(item => {
|
|
30
|
+
const isDirectory = item.isDirectory();
|
|
31
|
+
const value = item.name + (isDirectory ? '/' : '');
|
|
32
|
+
return {
|
|
33
|
+
name: value,
|
|
34
|
+
value,
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
if (resolve(folderPath) !== resolve('/')) {
|
|
38
|
+
choices.push({
|
|
39
|
+
name: '\x1b[36m↩ Go Back\x1b[0m',
|
|
40
|
+
value: '..'
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return choices;
|
|
44
|
+
}
|
|
45
|
+
async function chooseFolder(currentPath) {
|
|
46
|
+
async function prompt(srcPath) {
|
|
47
|
+
const choices = listFilesAndFolders(srcPath);
|
|
48
|
+
const answers = await inquirer.prompt([{
|
|
49
|
+
type: 'list',
|
|
50
|
+
name: 'path',
|
|
51
|
+
message: `Please choose a folder (current folder: ${srcPath})`,
|
|
52
|
+
choices: choices
|
|
53
|
+
}]);
|
|
54
|
+
const newPath = join(srcPath, answers.path);
|
|
55
|
+
if (answers.path === '..') {
|
|
56
|
+
// Clears the previous line
|
|
57
|
+
process.stdout.write('\u001b[1A\u001b[2K');
|
|
58
|
+
return prompt(newPath);
|
|
59
|
+
}
|
|
60
|
+
if (answers.path.endsWith('/')) {
|
|
61
|
+
// Clears the previous line
|
|
62
|
+
process.stdout.write('\u001b[1A\u001b[2K');
|
|
63
|
+
return prompt(newPath);
|
|
64
|
+
}
|
|
65
|
+
return newPath;
|
|
66
|
+
}
|
|
67
|
+
return prompt(currentPath);
|
|
68
|
+
}
|
|
69
|
+
async function main() {
|
|
70
|
+
const ocrDir = createOcrDir('.tmp/');
|
|
71
|
+
const defaultOptions = {
|
|
72
|
+
contrast: 0.25,
|
|
73
|
+
isTesseractAvailable: isSystemTesseractAvailable(),
|
|
74
|
+
language: SUPPORTED_LANGUAGES.ENGLISH,
|
|
75
|
+
ocrImagesPath: ocrDir,
|
|
76
|
+
};
|
|
77
|
+
let filePath;
|
|
78
|
+
console.log(CONFIG_HELPER_INTRO);
|
|
79
|
+
const initialChoice = await inquirer.prompt({
|
|
80
|
+
type: 'list',
|
|
81
|
+
name: 'method',
|
|
82
|
+
message: 'How would you like to specify the file?',
|
|
83
|
+
choices: [
|
|
84
|
+
{ name: 'Use a "file explorer"', value: 'explore' },
|
|
85
|
+
{ name: 'Type the file path manually', value: 'type' },
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
if (initialChoice.method === 'explore') {
|
|
89
|
+
filePath = await chooseFolder(currentPath);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const pathInput = await inquirer.prompt({
|
|
93
|
+
type: 'input',
|
|
94
|
+
name: 'path',
|
|
95
|
+
message: 'Please enter the file path:',
|
|
96
|
+
});
|
|
97
|
+
filePath = pathInput.path;
|
|
98
|
+
}
|
|
99
|
+
const useHaystack = await inquirer.prompt({
|
|
100
|
+
type: 'confirm',
|
|
101
|
+
name: 'use',
|
|
102
|
+
message: 'Would you like to use a haystack?',
|
|
103
|
+
});
|
|
104
|
+
let haystack;
|
|
105
|
+
if (useHaystack.use) {
|
|
106
|
+
const { x } = await inquirer.prompt({
|
|
107
|
+
type: 'input',
|
|
108
|
+
name: 'x',
|
|
109
|
+
message: 'Enter the x coordinate:',
|
|
110
|
+
filter: (input) => Number(input),
|
|
111
|
+
});
|
|
112
|
+
const { y } = await inquirer.prompt({
|
|
113
|
+
type: 'input',
|
|
114
|
+
name: 'y',
|
|
115
|
+
message: 'Enter the y coordinate:',
|
|
116
|
+
filter: (input) => Number(input),
|
|
117
|
+
});
|
|
118
|
+
const { width } = await inquirer.prompt({
|
|
119
|
+
type: 'input',
|
|
120
|
+
name: 'width',
|
|
121
|
+
message: 'Enter the width:',
|
|
122
|
+
filter: (input) => Number(input),
|
|
123
|
+
});
|
|
124
|
+
const { height } = await inquirer.prompt({
|
|
125
|
+
type: 'input',
|
|
126
|
+
name: 'height',
|
|
127
|
+
message: 'Enter the height:',
|
|
128
|
+
filter: (input) => Number(input),
|
|
129
|
+
});
|
|
130
|
+
haystack = { x, y, width, height };
|
|
131
|
+
}
|
|
132
|
+
let options = {
|
|
133
|
+
...defaultOptions,
|
|
134
|
+
...{ haystack },
|
|
135
|
+
cliFile: convertFileToBase64(filePath),
|
|
136
|
+
};
|
|
137
|
+
const { useAdvanced } = await inquirer.prompt([{
|
|
138
|
+
type: 'confirm',
|
|
139
|
+
name: 'useAdvanced',
|
|
140
|
+
message: 'Do you want to use the advanced mode?',
|
|
141
|
+
}]);
|
|
142
|
+
if (useAdvanced) {
|
|
143
|
+
const advancedOptions = await inquirer.prompt([
|
|
144
|
+
{
|
|
145
|
+
type: 'input',
|
|
146
|
+
name: 'contrast',
|
|
147
|
+
message: 'Enter new contrast (0.0 - 1.0):',
|
|
148
|
+
default: defaultOptions.contrast.toString(),
|
|
149
|
+
validate: (input) => {
|
|
150
|
+
const value = parseFloat(input);
|
|
151
|
+
return (value >= 0.0 && value <= 1.0) || 'Please enter a contrast between 0.0 and 1.0';
|
|
152
|
+
},
|
|
153
|
+
filter: (input) => input ? parseFloat(input) : defaultOptions.contrast
|
|
154
|
+
},
|
|
155
|
+
]);
|
|
156
|
+
options = {
|
|
157
|
+
...options,
|
|
158
|
+
contrast: advancedOptions.contrast,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
console.log('\nProcessing image...\n');
|
|
162
|
+
await getData(options);
|
|
163
|
+
console.log('\nDone!');
|
|
164
|
+
}
|
|
165
|
+
main();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ocrClickOnText.d.ts","sourceRoot":"","sources":["../../src/commands/ocrClickOnText.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAGxD,wBAA8B,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B1F"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import ocrGetElementPositionByText from './ocrGetElementPositionByText.js';
|
|
2
|
+
import { determineClickPoint } from '../utils/index.js';
|
|
3
|
+
import { drawTarget } from '../utils/imageProcessing.js';
|
|
4
|
+
export default async function ocrClickOnText(options) {
|
|
5
|
+
const element = await ocrGetElementPositionByText(options);
|
|
6
|
+
let { x, y } = determineClickPoint({ rectangles: element.dprPosition });
|
|
7
|
+
const { relativePosition } = options;
|
|
8
|
+
// Adjust the x and y coordinates based on the relative position
|
|
9
|
+
if (relativePosition) {
|
|
10
|
+
const { above = 0, below = 0, left = 0, right = 0 } = relativePosition;
|
|
11
|
+
x = x - left + right;
|
|
12
|
+
y = y - above + below;
|
|
13
|
+
}
|
|
14
|
+
// Draw a target on the image so a user can see where the click will happen
|
|
15
|
+
await drawTarget({ filePath: element.filePath, targetX: x, targetY: y });
|
|
16
|
+
const actionType = browser.isMobile ? 'touch' : 'mouse';
|
|
17
|
+
const clickDuration = options.clickDuration ? options.clickDuration : 500;
|
|
18
|
+
await browser
|
|
19
|
+
.action('pointer', {
|
|
20
|
+
parameters: { pointerType: actionType }
|
|
21
|
+
})
|
|
22
|
+
.move({ duration: 0, x, y })
|
|
23
|
+
.down({ button: 0 })
|
|
24
|
+
.pause(clickDuration)
|
|
25
|
+
.up({ button: 0 })
|
|
26
|
+
.perform();
|
|
27
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { OcrGetElementPositionByText, OcrGetElementPositionByTextOptions } from '../types.js';
|
|
2
|
+
export default function ocrGetElementPositionByText(data: OcrGetElementPositionByTextOptions): Promise<OcrGetElementPositionByText>;
|
|
3
|
+
//# sourceMappingURL=ocrGetElementPositionByText.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ocrGetElementPositionByText.d.ts","sourceRoot":"","sources":["../../src/commands/ocrGetElementPositionByText.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAgB,2BAA2B,EAAE,kCAAkC,EAAE,MAAM,aAAa,CAAA;AAMhH,wBAA8B,2BAA2B,CACrD,IAAI,EAAE,kCAAkC,GACzC,OAAO,CAAC,2BAA2B,CAAC,CAuDtC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import logger from '@wdio/logger';
|
|
2
|
+
import getTextPositions from '../utils/getTextPositions.js';
|
|
3
|
+
import { fuzzyFind } from '../utils/fuzzySearch.js';
|
|
4
|
+
const log = logger('@wdio/ocr-service:ocrGetElementPositionByText');
|
|
5
|
+
export default async function ocrGetElementPositionByText(data) {
|
|
6
|
+
const { contrast, haystack, fuzzyFindOptions, isTesseractAvailable, language, ocrImagesPath, text, } = data;
|
|
7
|
+
const textPositions = await getTextPositions({
|
|
8
|
+
contrast,
|
|
9
|
+
haystack,
|
|
10
|
+
isTesseractAvailable,
|
|
11
|
+
language,
|
|
12
|
+
ocrImagesPath,
|
|
13
|
+
});
|
|
14
|
+
const matches = fuzzyFind({
|
|
15
|
+
pattern: text,
|
|
16
|
+
searchOptions: fuzzyFindOptions,
|
|
17
|
+
textArray: textPositions,
|
|
18
|
+
});
|
|
19
|
+
let matchedTextElements;
|
|
20
|
+
let score;
|
|
21
|
+
if (matches.length === 0) {
|
|
22
|
+
log.warn(`No matches were found based on the word "${text}"`);
|
|
23
|
+
throw new Error(`Invalid Text Selector Match. Visual OCR has failed to find the word '${text}' in the image`);
|
|
24
|
+
}
|
|
25
|
+
else if (matches.length > 1) {
|
|
26
|
+
matches.sort((a, b) => (a.score > b.score ? 1 : -1));
|
|
27
|
+
matchedTextElements = matches[0];
|
|
28
|
+
score = Number(((1 - matchedTextElements.score) * 100).toFixed(2));
|
|
29
|
+
const messageOne = `Multiple matches were found based on the word "${text}".`;
|
|
30
|
+
const messageTwo = `The match "${matchedTextElements.item.text}" with score "${score}%" will be used.`;
|
|
31
|
+
log.info(`${messageOne} ${messageTwo}`);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
matchedTextElements = matches[0];
|
|
35
|
+
score = Number(((1 - matchedTextElements.score) * 100).toFixed(2));
|
|
36
|
+
log.info(`We searched for the word "${text}" and found one match "${matchedTextElements.item.text}" with score "${score}%"`);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
dprPosition: matchedTextElements.item.dprPosition,
|
|
40
|
+
filePath: matchedTextElements.item.filePath,
|
|
41
|
+
matchedString: matchedTextElements.item.text,
|
|
42
|
+
originalPosition: matchedTextElements.item.originalPosition,
|
|
43
|
+
score,
|
|
44
|
+
searchValue: text,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ocrGetText.d.ts","sourceRoot":"","sources":["../../src/commands/ocrGetText.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAEpD,wBAA8B,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAIpF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ocrSetValue.d.ts","sourceRoot":"","sources":["../../src/commands/ocrSetValue.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAIrD,wBAA8B,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmDpF"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import sendKeys from '../utils/sendKeys.js';
|
|
2
|
+
import ocrClickOnText from './ocrClickOnText.js';
|
|
3
|
+
export default async function ocrSetValue(options) {
|
|
4
|
+
const { contrast, clickDuration, haystack, fuzzyFindOptions, isTesseractAvailable, language, ocrImagesPath, submitValue = false, text, value, } = options;
|
|
5
|
+
// 1. First click on the position of the text to make sure it is intractable
|
|
6
|
+
await ocrClickOnText({
|
|
7
|
+
contrast,
|
|
8
|
+
clickDuration,
|
|
9
|
+
haystack,
|
|
10
|
+
fuzzyFindOptions,
|
|
11
|
+
isTesseractAvailable,
|
|
12
|
+
language,
|
|
13
|
+
ocrImagesPath,
|
|
14
|
+
text,
|
|
15
|
+
});
|
|
16
|
+
// 2. If Mobile then a keyboard might be shown
|
|
17
|
+
if (driver.isMobile) {
|
|
18
|
+
try {
|
|
19
|
+
// Wait for 3 seconds for the keyboard to be shown
|
|
20
|
+
await driver.waitUntil(async () => driver.isKeyboardShown(), { timeout: 3 * 1000 });
|
|
21
|
+
}
|
|
22
|
+
catch (ign) {
|
|
23
|
+
// Keyboard is not shown
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// 3. Send the value to the active element
|
|
27
|
+
await sendKeys(value, submitValue);
|
|
28
|
+
// 4. If Mobile then hide the keyboard
|
|
29
|
+
if (driver.isMobile) {
|
|
30
|
+
try {
|
|
31
|
+
await driver.hideKeyboard();
|
|
32
|
+
await driver.waitUntil(async () => !(await driver.isKeyboardShown()), { timeout: 3 * 1000 });
|
|
33
|
+
}
|
|
34
|
+
catch (ign) {
|
|
35
|
+
// Keyboard is not present or not hidden
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ocrWaitForTextDisplayed.d.ts","sourceRoot":"","sources":["../../src/commands/ocrWaitForTextDisplayed.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,aAAa,CAAA;AAGjE,wBAA8B,uBAAuB,CACjD,OAAO,EAAE,8BAA8B,8DAW1C"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import ocrGetElementPositionByText from './ocrGetElementPositionByText.js';
|
|
2
|
+
export default async function ocrWaitForTextDisplayed(options) {
|
|
3
|
+
const { timeout, timeoutMsg } = options;
|
|
4
|
+
return driver.waitUntil(async () => ocrGetElementPositionByText(options), {
|
|
5
|
+
timeout: timeout || 180000,
|
|
6
|
+
timeoutMsg: timeoutMsg || `Could not find the text "${options.text}" within the requested time.`,
|
|
7
|
+
});
|
|
8
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import WdioOcrService from './service.js';
|
|
2
|
+
import type { ClickOnTextOptions, GetElementPositionByTextOptions, GetTextOptions, OcrGetElementPositionByText, SetValueOptions, WaitForTextDisplayedOptions } from './types.js';
|
|
3
|
+
declare global {
|
|
4
|
+
namespace WebdriverIO {
|
|
5
|
+
interface Browser {
|
|
6
|
+
/**
|
|
7
|
+
* Get the text of an image base on OCR
|
|
8
|
+
*/
|
|
9
|
+
ocrGetText(options?: GetTextOptions): Promise<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Get the text of an image base on OCR
|
|
12
|
+
*/
|
|
13
|
+
ocrGetElementPositionByText(options: GetElementPositionByTextOptions): Promise<OcrGetElementPositionByText>;
|
|
14
|
+
/**
|
|
15
|
+
* Click on an element based on text
|
|
16
|
+
*/
|
|
17
|
+
ocrClickOnText(options: ClickOnTextOptions): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Set value on an element based on text
|
|
20
|
+
*/
|
|
21
|
+
ocrSetValue(options: SetValueOptions): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Wait for the text to be displayed based on OCR
|
|
24
|
+
*/
|
|
25
|
+
ocrWaitForTextDisplayed(options: WaitForTextDisplayedOptions): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
interface Element {
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export default WdioOcrService;
|
|
32
|
+
export declare const SUPPORTED_OCR_LANGUAGES: {
|
|
33
|
+
AFRIKAANS: string;
|
|
34
|
+
AMHARIC: string;
|
|
35
|
+
ARABIC: string;
|
|
36
|
+
ASSANESE: string;
|
|
37
|
+
AZERBAIJANI: string;
|
|
38
|
+
BELARUSIAN: string;
|
|
39
|
+
BENGALI: string;
|
|
40
|
+
TIBETAN: string;
|
|
41
|
+
BOSNIAN: string;
|
|
42
|
+
BULGARIAN: string;
|
|
43
|
+
CATALAN: string;
|
|
44
|
+
CZECH: string;
|
|
45
|
+
CHINESE: string;
|
|
46
|
+
CORSICAN: string;
|
|
47
|
+
DANISH: string;
|
|
48
|
+
GERMAN: string;
|
|
49
|
+
GREEK: string;
|
|
50
|
+
ENGLISH: string;
|
|
51
|
+
MATH: string;
|
|
52
|
+
ESTONIAN: string;
|
|
53
|
+
FINNISH: string;
|
|
54
|
+
FRENCH: string;
|
|
55
|
+
GAELIC: string;
|
|
56
|
+
IRISH: string; /**
|
|
57
|
+
* Get the text of an image base on OCR
|
|
58
|
+
*/
|
|
59
|
+
HINDI: string;
|
|
60
|
+
CROATIAN: string;
|
|
61
|
+
HUNGARIAN: string;
|
|
62
|
+
ARMENIAN: string;
|
|
63
|
+
ITALIAN: string;
|
|
64
|
+
JAPANESE: string;
|
|
65
|
+
KANNADA: string;
|
|
66
|
+
KOREAN: string;
|
|
67
|
+
LATIN: string;
|
|
68
|
+
LATVIAN: string;
|
|
69
|
+
LITHUANIAN: string;
|
|
70
|
+
LUXEMBOURGISH: string;
|
|
71
|
+
MACEDONIAN: string;
|
|
72
|
+
DUTCH: string;
|
|
73
|
+
NORWEGIAN: string;
|
|
74
|
+
POLISH: string;
|
|
75
|
+
PORTUGUESE: string;
|
|
76
|
+
ROMANIAN: string;
|
|
77
|
+
RUSSIAN: string;
|
|
78
|
+
SLOVAK: string;
|
|
79
|
+
SLOVENIAN: string;
|
|
80
|
+
SPANISH: string;
|
|
81
|
+
ALBANIAN: string;
|
|
82
|
+
SERBIAN: string;
|
|
83
|
+
SWAHILI: string;
|
|
84
|
+
SWEDISH: string;
|
|
85
|
+
THAI: string;
|
|
86
|
+
TURKISH: string;
|
|
87
|
+
UKRAINIAN: string;
|
|
88
|
+
VIETNAMESE: string;
|
|
89
|
+
};
|
|
90
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,cAAc,MAAM,cAAc,CAAA;AACzC,OAAO,KAAK,EACR,kBAAkB,EAClB,+BAA+B,EAC/B,cAAc,EACd,2BAA2B,EAC3B,eAAe,EACf,2BAA2B,EAC9B,MAAM,YAAY,CAAA;AAGnB,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,WAAW,CAAC;QAClB,UAAU,OAAO;YACb;;eAEG;YACH,UAAU,CACN,OAAO,CAAC,EAAE,cAAc,GACzB,OAAO,CAAC,MAAM,CAAC,CAAC;YAEnB;;eAEG;YACH,2BAA2B,CACvB,OAAO,EAAE,+BAA+B,GACzC,OAAO,CAAC,2BAA2B,CAAC,CAAC;YAExC;;eAEG;YACH,cAAc,CACV,OAAO,EAAE,kBAAkB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAAC;YAEjB;;eAEG;YACH,WAAW,CACP,OAAO,EAAE,eAAe,GACzB,OAAO,CAAC,IAAI,CAAC,CAAC;YAEjB;;eAEG;YACH,uBAAuB,CACnB,OAAO,EAAE,2BAA2B,GACrC,OAAO,CAAC,IAAI,CAAC,CAAC;SACpB;QACD,UAAU,OAAO;SAAI;KACxB;CACJ;AAED,eAAe,cAAc,CAAA;AAC7B,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;mBAjCxB;;OAEG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+B2C,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { OcrOptions } from './types.js';
|
|
2
|
+
export default class WdioOcrService {
|
|
3
|
+
#private;
|
|
4
|
+
private _browser?;
|
|
5
|
+
private _ocrDir;
|
|
6
|
+
private _ocrLanguage;
|
|
7
|
+
private _ocrContrast;
|
|
8
|
+
constructor(options: OcrOptions);
|
|
9
|
+
/**
|
|
10
|
+
* Set up the service if users want to use it in standalone mode
|
|
11
|
+
*/
|
|
12
|
+
remoteSetup(browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser): Promise<void>;
|
|
13
|
+
before(capabilities: WebdriverIO.Capabilities, _specs: string[], browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAgB5C,MAAM,CAAC,OAAO,OAAO,cAAc;;IAC/B,OAAO,CAAC,QAAQ,CAAC,CAAsD;IACvE,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,YAAY,CAAQ;IAC5B,OAAO,CAAC,YAAY,CAAQ;gBAEhB,OAAO,EAAE,UAAU;IAM/B;;OAEG;IACG,WAAW,CAAC,OAAO,EAAE,WAAW,CAAC,OAAO,GAAG,WAAW,CAAC,kBAAkB;IAIzE,MAAM,CACR,YAAY,EAAE,WAAW,CAAC,YAAY,EACtC,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,EAAE,WAAW,CAAC,OAAO,GAAG,WAAW,CAAC,kBAAkB;CA2EpE"}
|
package/dist/service.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import logger from '@wdio/logger';
|
|
2
|
+
import { isSystemTesseractAvailable } from './utils/tesseract.js';
|
|
3
|
+
import { CONTRAST, DEFAULT_IMAGES_FOLDER, SUPPORTED_LANGUAGES } from './utils/constants.js';
|
|
4
|
+
import { createOcrDir } from './utils/index.js';
|
|
5
|
+
import ocrGetText from './commands/ocrGetText.js';
|
|
6
|
+
import ocrGetElementPositionByText from './commands/ocrGetElementPositionByText.js';
|
|
7
|
+
import ocrWaitForTextDisplayed from './commands/ocrWaitForTextDisplayed.js';
|
|
8
|
+
import ocrClickOnText from './commands/ocrClickOnText.js';
|
|
9
|
+
import ocrSetValue from './commands/ocrSetValue.js';
|
|
10
|
+
const log = logger('@wdio/ocr-service');
|
|
11
|
+
const ocrCommands = {
|
|
12
|
+
ocrGetText,
|
|
13
|
+
ocrGetElementPositionByText,
|
|
14
|
+
ocrWaitForTextDisplayed,
|
|
15
|
+
ocrClickOnText,
|
|
16
|
+
ocrSetValue,
|
|
17
|
+
};
|
|
18
|
+
export default class WdioOcrService {
|
|
19
|
+
_browser;
|
|
20
|
+
_ocrDir;
|
|
21
|
+
_ocrLanguage;
|
|
22
|
+
_ocrContrast;
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this._ocrDir = createOcrDir(options?.imagesFolder || DEFAULT_IMAGES_FOLDER);
|
|
25
|
+
this._ocrLanguage = options?.language || SUPPORTED_LANGUAGES.ENGLISH;
|
|
26
|
+
this._ocrContrast = options?.contrast || CONTRAST;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Set up the service if users want to use it in standalone mode
|
|
30
|
+
*/
|
|
31
|
+
async remoteSetup(browser) {
|
|
32
|
+
await this.before(browser.capabilities, [], browser);
|
|
33
|
+
}
|
|
34
|
+
async before(capabilities, _specs, browser) {
|
|
35
|
+
this._browser = browser;
|
|
36
|
+
if (!this._browser.isMultiremote) {
|
|
37
|
+
log.info('Adding commands to global browser');
|
|
38
|
+
await this.#addCommandsToBrowser(this._browser);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
await this.#extendMultiremoteBrowser(capabilities);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async #extendMultiremoteBrowser(capabilities) {
|
|
45
|
+
const browser = this._browser;
|
|
46
|
+
const browserNames = Object.keys(capabilities);
|
|
47
|
+
const self = this;
|
|
48
|
+
log.info(`Adding commands to Multi Browser: ${browserNames.join(', ')}`);
|
|
49
|
+
for (const browserName of browserNames) {
|
|
50
|
+
const multiremoteBrowser = browser;
|
|
51
|
+
const browserInstance = multiremoteBrowser.getInstance(browserName);
|
|
52
|
+
await this.#addCommandsToBrowser(browserInstance);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Add all OCR commands to the global browser object that will execute
|
|
56
|
+
* on each browser in the Multi Remote. This involves mapping through both the visualElementCommands,
|
|
57
|
+
* visualPageCommands, and ocrCommands.
|
|
58
|
+
*/
|
|
59
|
+
for (const command of Object.keys(ocrCommands)) {
|
|
60
|
+
browser.addCommand(command, async function (...args) {
|
|
61
|
+
const returnData = {};
|
|
62
|
+
if (typeof args[0] === 'object' && args[0] !== null) {
|
|
63
|
+
const options = args[0];
|
|
64
|
+
options.contrast = options.contrast || self._ocrContrast;
|
|
65
|
+
args[0] = options;
|
|
66
|
+
}
|
|
67
|
+
for (const browserName of browserNames) {
|
|
68
|
+
const multiremoteBrowser = browser;
|
|
69
|
+
const browserInstance = multiremoteBrowser.getInstance(browserName);
|
|
70
|
+
if (typeof browserInstance[command] === 'function') {
|
|
71
|
+
returnData[browserName] = await browserInstance[command].apply(browserInstance, args);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
throw new Error(`Command ${command} is not a function on the browser instance ${browserName}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return returnData;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async #addCommandsToBrowser(currentBrowser) {
|
|
82
|
+
const isTesseractAvailable = isSystemTesseractAvailable();
|
|
83
|
+
const self = this;
|
|
84
|
+
for (const [commandName, command] of Object.entries(ocrCommands)) {
|
|
85
|
+
log.info(`Adding browser command "${commandName}" to browser object`);
|
|
86
|
+
currentBrowser.addCommand(commandName, function (options) {
|
|
87
|
+
return command({
|
|
88
|
+
...options,
|
|
89
|
+
contrast: options.contrast || self._ocrContrast,
|
|
90
|
+
isTesseractAvailable,
|
|
91
|
+
language: options.language || self._ocrLanguage,
|
|
92
|
+
ocrImagesPath: self._ocrDir,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|