Skip to content

Commit d456f16

Browse files
committed
Calibration with QR codes
1 parent baacca7 commit d456f16

File tree

16 files changed

+894
-303
lines changed

16 files changed

+894
-303
lines changed

components/assistive-playwright-client/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ export {
4141
CalibrationError,
4242
CalibrationResult,
4343
CalibrationOptions,
44-
Color,
4544
playwrightCalibrate
4645
} from "./vm/calibrate";
4746
export { MouseButton, Key } from "vm-providers";

components/assistive-playwright-client/src/vm/calibrate.ts

Lines changed: 55 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ import {
2222
VM,
2323
ScreenPosition,
2424
SimplePosition,
25-
findRectangle
25+
calibrationQRCodesGenerate,
26+
calibrationQRCodesScan,
27+
pngToDataURI,
28+
CalibrationQRCodesConfig,
29+
MouseButton
2630
} from "vm-providers";
2731
import { ElementHandle, Frame, Page } from "playwright-core";
2832
import { createWriteStream } from "fs";
@@ -44,45 +48,14 @@ export class CalibrationError {
4448
* to give details allowing to understand why the calibration failed.
4549
*
4650
* @param screenshot - Cf {@link CalibrationError.screenshot | screenshot}
47-
* @param color - Cf {@link CalibrationError.color | color}
48-
* @param expectedWidth - Cf {@link CalibrationError.expectedWidth | expectedWidth}
49-
* @param expectedHeight - Cf {@link CalibrationError.expectedHeight | expectedHeight}
50-
* @param colorTolerance - Cf {@link CalibrationError.colorTolerance | colorTolerance}
5151
*/
5252
constructor(
5353
/**
5454
* Screenshot of the virtual machine, in which the browser viewport could not be found.
5555
*/
56-
public screenshot: PNG,
57-
/**
58-
* Color of the rectangle displayed in the viewport.
59-
*/
60-
public color: Color,
61-
/**
62-
* Expected width (in pixels) of the rectangle that was looked for in the screenshot.
63-
*/
64-
public expectedWidth: number,
65-
/**
66-
* Expected height (in pixels) of the rectangle that was looked for in the screenshot.
67-
*/
68-
public expectedHeight: number,
69-
/**
70-
* Tolerance on the color that was used (as configured in {@link CalibrationOptions.colorTolerance}).
71-
*/
72-
public colorTolerance: number
56+
public screenshot: PNG
7357
) {}
7458

75-
/**
76-
* Error message.
77-
*/
78-
get message(): string {
79-
return `Could not find the ${this.expectedWidth}x${
80-
this.expectedHeight
81-
} rectangle filled with color ${JSON.stringify(this.color)} (tolerance ${
82-
this.colorTolerance
83-
})`;
84-
}
85-
8659
/**
8760
* Saves the {@link CalibrationError.screenshot | screenshot} as a file.
8861
* @param fileName - Full path (or path relative to the current directory) where to store the screenshot.
@@ -181,56 +154,15 @@ export class CalibrationResult implements ScreenPosition {
181154
}
182155
}
183156

184-
/**
185-
* Color, expressed as an array of three numbers between 0 and 255,
186-
* representing the red, green and blue parts
187-
* @public
188-
*/
189-
export type Color = [number, number, number];
190-
const rgb = (color: Color) => `rgb(${color[0]},${color[1]},${color[2]})`;
191-
192157
/**
193158
* Options for the calibration, passed to {@link playwrightCalibrate}.
194159
* @public
195160
*/
196-
export interface CalibrationOptions {
197-
/**
198-
* Color of the rectangle to display in the viewport.
199-
* Defaults to `[255, 0, 0]` (red)
200-
* @defaultValue [255, 0, 0]
201-
*/
202-
calibrationColor?: Color;
161+
export interface CalibrationOptions extends CalibrationQRCodesConfig {
203162
/**
204-
* Color of the border of the rectangle to be displayed in the viewport.
205-
* Defaults to `[100, 100, 100]`
206-
* @defaultValue [100, 100, 100]
163+
* Whether to skip the click done during calibration.
207164
*/
208-
borderColor?: Color;
209-
/**
210-
* Width (in pixels) of the border of the rectangle to be displayed in the viewport.
211-
* Defaults to `30`.
212-
* @defaultValue 30
213-
*/
214-
borderWidth?: number;
215-
/**
216-
* Allowed difference between the color in the screenshot and {@link CalibrationOptions.calibrationColor | calibrationColor}, 0 meaning no difference.
217-
* The difference is computed as the sum of the absolute value of the difference for each red, green and blue parts.
218-
* Defaults to `[255, 0, 0]`.
219-
* @defaultValue [255, 0, 0]
220-
*/
221-
colorTolerance?: number;
222-
/**
223-
* Extra horizontal space to add at the right of the colored rectangle.
224-
* Defaults to `0`.
225-
* @defaultValue 0
226-
*/
227-
estimatedXMargin?: number;
228-
/**
229-
* Extra vertical space to add at the bottom of the colored rectangle.
230-
* Defaults to `0`.
231-
* @defaultValue 0
232-
*/
233-
estimatedYMargin?: number;
165+
skipClick?: boolean;
234166
}
235167

236168
/**
@@ -248,16 +180,7 @@ export async function playwrightCalibrate(
248180
frame: Page | Frame,
249181
options: CalibrationOptions = {}
250182
): Promise<CalibrationResult> {
251-
const {
252-
calibrationColor = [255, 0, 0],
253-
borderColor = [100, 100, 100],
254-
borderWidth = 30,
255-
colorTolerance = 50,
256-
estimatedXMargin = 0,
257-
estimatedYMargin = 0
258-
} = options;
259-
const rgbCalibrationColor = rgb(calibrationColor);
260-
const rgbBorderColor = rgb(borderColor);
183+
const { skipClick, ...qrCodeOptions } = options;
261184
const elementId = JSON.stringify(createUUIDv4());
262185
const displayRectangleResult: {
263186
width: number;
@@ -269,9 +192,7 @@ export async function playwrightCalibrate(
269192
} = await frame.evaluate(`(() => {
270193
const div = document.createElement("div");
271194
div.setAttribute("id", ${elementId});
272-
div.style.cssText = "display:block;position:fixed;background-color:${rgbCalibrationColor};border:${borderWidth}px solid ${rgbBorderColor};left:0px;top:0px;right:0px;bottom:0px;cursor:none;z-index:999999;";
273-
div.style.maxWidth = (screen.availWidth - window.screenX - ${estimatedXMargin}) + "px";
274-
div.style.maxHeight = (screen.availHeight - window.screenY - ${estimatedYMargin}) + "px";
195+
div.style.cssText = "display:block;position:fixed;left:0px;top:0px;right:0px;bottom:0px;cursor:none;z-index:999999;";
275196
document.body.appendChild(div);
276197
return {
277198
width: div.clientWidth,
@@ -282,7 +203,17 @@ return {
282203
screenHeight: window.screen.height
283204
};})()`);
284205
try {
285-
// moves the mouse out of the colored zone
206+
const viewportImage = calibrationQRCodesGenerate(
207+
displayRectangleResult.width,
208+
displayRectangleResult.height,
209+
qrCodeOptions
210+
);
211+
await frame.evaluate(
212+
`(() => {const calibrationDIV = document.getElementById(${elementId}); calibrationDIV.style.backgroundImage = ${JSON.stringify(
213+
`url("${await pngToDataURI(viewportImage)}")`
214+
)};})()`
215+
);
216+
// moves the mouse out of the way
286217
await vm.sendMouseMoveEvent({
287218
x: displayRectangleResult.screenX,
288219
y: displayRectangleResult.screenY,
@@ -291,30 +222,41 @@ return {
291222
});
292223
await wait(1000);
293224
const image = await vm.takePNGScreenshot();
294-
const calibrationResult = findRectangle(
295-
image,
296-
[...calibrationColor, 255],
297-
displayRectangleResult.width,
298-
displayRectangleResult.height,
299-
colorTolerance
300-
);
301-
if (!calibrationResult) {
302-
throw new CalibrationError(
303-
image,
304-
calibrationColor,
305-
displayRectangleResult.width,
306-
displayRectangleResult.height,
307-
colorTolerance
225+
try {
226+
const result = calibrationQRCodesScan(image, qrCodeOptions);
227+
if (!skipClick) {
228+
// click on the detected QR code:
229+
await vm.sendMouseMoveEvent({
230+
x: Math.floor(result.qrCode.x + result.qrCode.width / 2),
231+
y: Math.floor(result.qrCode.y + result.qrCode.height / 2),
232+
screenWidth: image.width,
233+
screenHeight: image.height
234+
});
235+
await wait(100);
236+
await vm.sendMouseDownEvent(MouseButton.LEFT);
237+
await wait(50);
238+
await vm.sendMouseUpEvent(MouseButton.LEFT);
239+
await wait(100);
240+
// moves again the mouse out of the way
241+
await vm.sendMouseMoveEvent({
242+
x: displayRectangleResult.screenX,
243+
y: displayRectangleResult.screenY,
244+
screenWidth: image.width,
245+
screenHeight: image.height
246+
});
247+
await wait(100);
248+
}
249+
return new CalibrationResult(
250+
result.viewport.x - displayRectangleResult.screenX,
251+
result.viewport.y - displayRectangleResult.screenY,
252+
image.width,
253+
image.height,
254+
vm,
255+
frame
308256
);
257+
} catch (error) {
258+
throw new CalibrationError(image);
309259
}
310-
return new CalibrationResult(
311-
calibrationResult.x - borderWidth - displayRectangleResult.screenX,
312-
calibrationResult.y - borderWidth - displayRectangleResult.screenY,
313-
image.width,
314-
image.height,
315-
vm,
316-
frame
317-
);
318260
} finally {
319261
await frame.evaluate(
320262
`(() => {const calibrationDIV = document.getElementById(${elementId}); calibrationDIV.parentNode.removeChild(calibrationDIV);})()`

components/assistive-webdriver/jest.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,6 @@ module.exports = {
2626
"ts-jest": {
2727
tsconfig: "tsconfig.test.json"
2828
}
29-
}
29+
},
30+
testTimeout: 10000
3031
};

components/assistive-webdriver/src/server/calibration.ts

Lines changed: 59 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@ import {
2323
VM,
2424
ScreenPosition,
2525
wait,
26-
findRectangle,
26+
calibrationQRCodesGenerate,
27+
calibrationQRCodesScan,
28+
pngToDataURI,
2729
createSubLogFunction,
28-
LogFunction
30+
LogFunction,
31+
MouseButton
2932
} from "vm-providers";
3033
import { PublicError } from "./publicError";
3134

@@ -57,20 +60,16 @@ export async function webdriverCalibrate(
5760
log
5861
);
5962
}
60-
const color: [number, number, number, number] = [255, 0, 0, 255];
61-
const rgbColor = `rgb(${color[0]},${color[1]},${color[2]});`;
62-
const border = 30;
63-
const colorTolerance = 50;
64-
const displayRectangleResult = await request(
63+
const sizeInfo = await request(
6564
`${sessionUrl}/execute/sync`,
6665
{
6766
body: {
6867
script: `var div = document.createElement("div"); div.setAttribute("id", "calibrationDIV");
69-
div.style.cssText = "display:block;position:absolute;background-color:${rgbColor};border:${border}px solid rgb(100, 100, 100);left:0px;top:0px;right:0px;bottom:0px;cursor:none;z-index:999999;";
68+
div.style.cssText = "display:block;position:absolute;left:0px;top:0px;right:0px;bottom:0px;cursor:none;z-index:999999;";
7069
document.body.appendChild(div);
7170
return {
72-
width: div.offsetWidth - ${2 * border},
73-
height: div.offsetHeight - ${2 * border},
71+
width: div.offsetWidth,
72+
height: div.offsetHeight,
7473
screenX: window.screenX,
7574
screenY: window.screenY
7675
};`,
@@ -79,27 +78,32 @@ return {
7978
},
8079
log
8180
);
81+
const viewportImage = calibrationQRCodesGenerate(
82+
sizeInfo.value.width,
83+
sizeInfo.value.height
84+
);
85+
await request(
86+
`${sessionUrl}/execute/sync`,
87+
{
88+
body: {
89+
script: `var calibrationDIV = document.getElementById("calibrationDIV"); calibrationDIV.style.backgroundImage = ${JSON.stringify(
90+
`url("${await pngToDataURI(viewportImage)}")`
91+
)};`,
92+
args: []
93+
}
94+
},
95+
log
96+
);
8297
log({
8398
message: "displayed",
84-
rectangle: displayRectangleResult.value
99+
result: sizeInfo.value
85100
});
86101
await wait(1000);
87102
const image = await vm.takePNGScreenshot();
88-
const calibrationResult = findRectangle(
89-
image,
90-
color,
91-
displayRectangleResult.value.width,
92-
displayRectangleResult.value.height,
93-
colorTolerance
94-
);
95-
if (!calibrationResult) {
96-
const errorMessage = `could not find the ${
97-
displayRectangleResult.value.width
98-
}x${
99-
displayRectangleResult.value.height
100-
} rectangle filled with color ${JSON.stringify(
101-
color
102-
)} (tolerance ${colorTolerance})`;
103+
let calibrationResult;
104+
try {
105+
calibrationResult = calibrationQRCodesScan(image);
106+
} catch (error) {
103107
if (failedCalibrationFileName) {
104108
await new Promise<void>((resolve, reject) =>
105109
pipeline(
@@ -109,16 +113,40 @@ return {
109113
)
110114
);
111115
throw new Error(
112-
`${errorMessage}, screenshot recorded as ${failedCalibrationFileName}`
116+
`${error}\nScreenshot recorded as ${failedCalibrationFileName}`
113117
);
114118
} else {
115-
throw new Error(`${errorMessage}, screenshot was not saved.`);
119+
throw new Error(`${error}\nScreenshot was not saved.`);
116120
}
117121
}
118122
log({
119123
message: "success",
120-
rectangle: calibrationResult
124+
result: calibrationResult
125+
});
126+
// click on the detected QR code:
127+
await vm.sendMouseMoveEvent({
128+
x: Math.floor(
129+
calibrationResult.qrCode.x + calibrationResult.qrCode.width / 2
130+
),
131+
y: Math.floor(
132+
calibrationResult.qrCode.y + calibrationResult.qrCode.height / 2
133+
),
134+
screenWidth: image.width,
135+
screenHeight: image.height
136+
});
137+
await wait(100);
138+
await vm.sendMouseDownEvent(MouseButton.LEFT);
139+
await wait(50);
140+
await vm.sendMouseUpEvent(MouseButton.LEFT);
141+
await wait(100);
142+
// moves again the mouse out of the way
143+
await vm.sendMouseMoveEvent({
144+
x: sizeInfo.value.screenX,
145+
y: sizeInfo.value.screenY,
146+
screenWidth: image.width,
147+
screenHeight: image.height
121148
});
149+
await wait(100);
122150
await request(
123151
`${sessionUrl}/execute/sync`,
124152
{
@@ -130,8 +158,8 @@ return {
130158
log
131159
);
132160
return {
133-
x: calibrationResult.x - border - displayRectangleResult.value.screenX,
134-
y: calibrationResult.y - border - displayRectangleResult.value.screenY,
161+
x: calibrationResult.viewport.x - sizeInfo.value.screenX,
162+
y: calibrationResult.viewport.y - sizeInfo.value.screenY,
135163
screenWidth: image.width,
136164
screenHeight: image.height
137165
};

0 commit comments

Comments
 (0)