askui 0.10.1 → 0.10.2
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/dist/cjs/core/annotation/template.html +1 -11390
- package/dist/cjs/core/model/annotation-result/annotation-interface.d.ts +1 -1
- package/dist/cjs/core/model/custom-element-json.d.ts +75 -0
- package/dist/cjs/core/model/custom-element-json.js +2 -0
- package/dist/cjs/core/model/custom-element.d.ts +21 -0
- package/dist/cjs/core/model/custom-element.js +54 -0
- package/dist/cjs/core/reporting/default-reporter.d.ts +10 -0
- package/dist/cjs/core/reporting/default-reporter.js +12 -0
- package/dist/cjs/core/reporting/default-step.d.ts +24 -0
- package/dist/cjs/core/reporting/default-step.js +75 -0
- package/dist/cjs/core/reporting/index.d.ts +11 -0
- package/dist/cjs/core/reporting/index.js +23 -0
- package/dist/cjs/core/reporting/instruction.d.ts +7 -0
- package/dist/cjs/core/reporting/instruction.js +2 -0
- package/dist/cjs/core/reporting/reporter-config.d.ts +13 -0
- package/dist/cjs/core/reporting/reporter-config.js +2 -0
- package/dist/cjs/core/reporting/reporter.d.ts +14 -0
- package/dist/cjs/core/reporting/reporter.js +2 -0
- package/dist/cjs/core/reporting/snapshot-detail-level.d.ts +21 -0
- package/dist/cjs/core/reporting/snapshot-detail-level.js +2 -0
- package/dist/cjs/core/reporting/snapshot.d.ts +6 -0
- package/dist/cjs/core/reporting/snapshot.js +2 -0
- package/dist/cjs/core/reporting/step-reporter.d.ts +17 -0
- package/dist/cjs/core/reporting/step-reporter.js +64 -0
- package/dist/cjs/core/reporting/step-run.d.ts +9 -0
- package/dist/cjs/core/reporting/step-run.js +2 -0
- package/dist/cjs/core/reporting/step-status-end.d.ts +1 -0
- package/dist/cjs/core/reporting/step-status-end.js +2 -0
- package/dist/cjs/core/reporting/step-status.d.ts +13 -0
- package/dist/cjs/core/reporting/step-status.js +2 -0
- package/dist/cjs/core/reporting/step.d.ts +36 -0
- package/dist/cjs/core/reporting/step.js +2 -0
- package/dist/cjs/execution/dsl.d.ts +1 -1
- package/dist/cjs/execution/execution-runtime.d.ts +16 -9
- package/dist/cjs/execution/execution-runtime.js +79 -33
- package/dist/cjs/execution/inference-client.d.ts +1 -1
- package/dist/cjs/execution/reporter.d.ts +132 -0
- package/dist/cjs/execution/reporter.js +146 -0
- package/dist/cjs/execution/ui-control-client-dependency-builder.d.ts +14 -0
- package/dist/cjs/execution/ui-control-client-dependency-builder.js +72 -0
- package/dist/cjs/execution/ui-control-client.d.ts +27 -15
- package/dist/cjs/execution/ui-control-client.js +97 -67
- package/dist/cjs/execution/ui-controller-client-interface.d.ts +11 -2
- package/dist/cjs/execution/ui-controller-client.d.ts +4 -4
- package/dist/cjs/execution/ui-controller-client.js +6 -6
- package/dist/cjs/lib/ui-controller-facade.js +1 -1
- package/dist/cjs/main.d.ts +2 -1
- package/dist/cjs/main.js +2 -2
- package/dist/cjs/utils/proxy/proxy-builder.d.ts +1 -1
- package/dist/cjs/utils/proxy/proxy-builder.js +3 -3
- package/dist/esm/core/annotation/template.html +1 -11390
- package/dist/esm/core/model/annotation-result/annotation-interface.d.ts +1 -1
- package/dist/esm/core/model/custom-element-json.d.ts +75 -0
- package/dist/esm/core/model/custom-element-json.js +1 -0
- package/dist/esm/core/model/custom-element.d.ts +21 -0
- package/dist/esm/core/model/custom-element.js +50 -0
- package/dist/esm/core/reporting/default-reporter.d.ts +10 -0
- package/dist/esm/core/reporting/default-reporter.js +9 -0
- package/dist/esm/core/reporting/default-step.d.ts +24 -0
- package/dist/esm/core/reporting/default-step.js +71 -0
- package/dist/esm/core/reporting/index.d.ts +11 -0
- package/dist/esm/core/reporting/index.js +11 -0
- package/dist/esm/core/reporting/instruction.d.ts +7 -0
- package/dist/esm/core/reporting/instruction.js +1 -0
- package/dist/esm/core/reporting/reporter-config.d.ts +13 -0
- package/dist/esm/core/reporting/reporter-config.js +1 -0
- package/dist/esm/core/reporting/reporter.d.ts +14 -0
- package/dist/esm/core/reporting/reporter.js +1 -0
- package/dist/esm/core/reporting/snapshot-detail-level.d.ts +21 -0
- package/dist/esm/core/reporting/snapshot-detail-level.js +1 -0
- package/dist/esm/core/reporting/snapshot.d.ts +6 -0
- package/dist/esm/core/reporting/snapshot.js +1 -0
- package/dist/esm/core/reporting/step-reporter.d.ts +17 -0
- package/dist/esm/core/reporting/step-reporter.js +60 -0
- package/dist/esm/core/reporting/step-run.d.ts +9 -0
- package/dist/esm/core/reporting/step-run.js +1 -0
- package/dist/esm/core/reporting/step-status-end.d.ts +1 -0
- package/dist/esm/core/reporting/step-status-end.js +1 -0
- package/dist/esm/core/reporting/step-status.d.ts +13 -0
- package/dist/esm/core/reporting/step-status.js +1 -0
- package/dist/esm/core/reporting/step.d.ts +36 -0
- package/dist/esm/core/reporting/step.js +1 -0
- package/dist/esm/execution/dsl.d.ts +1 -1
- package/dist/esm/execution/execution-runtime.d.ts +16 -9
- package/dist/esm/execution/execution-runtime.js +77 -31
- package/dist/esm/execution/inference-client.d.ts +1 -1
- package/dist/esm/execution/reporter.d.ts +132 -0
- package/dist/esm/execution/reporter.js +142 -0
- package/dist/esm/execution/ui-control-client-dependency-builder.d.ts +14 -0
- package/dist/esm/execution/ui-control-client-dependency-builder.js +68 -0
- package/dist/esm/execution/ui-control-client.d.ts +27 -15
- package/dist/esm/execution/ui-control-client.js +96 -66
- package/dist/esm/execution/ui-controller-client-interface.d.ts +11 -2
- package/dist/esm/execution/ui-controller-client.d.ts +4 -4
- package/dist/esm/execution/ui-controller-client.js +6 -6
- package/dist/esm/lib/ui-controller-facade.js +2 -2
- package/dist/esm/main.d.ts +2 -1
- package/dist/esm/main.js +1 -1
- package/dist/esm/utils/proxy/proxy-builder.d.ts +1 -1
- package/dist/esm/utils/proxy/proxy-builder.js +1 -1
- package/dist/example_projects_templates/typescript_jest/test/my-first-askui-test-suite.test.ts +9 -2
- package/package.json +1 -1
|
@@ -11,22 +11,42 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.ExecutionRuntime = void 0;
|
|
13
13
|
const ui_control_commands_1 = require("../core/ui-control-commands");
|
|
14
|
-
const
|
|
14
|
+
const custom_element_1 = require("../core/model/custom-element");
|
|
15
15
|
const repeat_error_1 = require("./repeat-error");
|
|
16
16
|
const misc_1 = require("./misc");
|
|
17
17
|
const control_command_error_1 = require("./control-command-error");
|
|
18
18
|
const logger_1 = require("../lib/logger");
|
|
19
19
|
const base_64_image_1 = require("../utils/base_64_image/base-64-image");
|
|
20
20
|
class ExecutionRuntime {
|
|
21
|
-
constructor(uiControllerClient, inferenceClient) {
|
|
21
|
+
constructor(uiControllerClient, inferenceClient, stepReporter) {
|
|
22
22
|
this.uiControllerClient = uiControllerClient;
|
|
23
23
|
this.inferenceClient = inferenceClient;
|
|
24
|
+
this.stepReporter = stepReporter;
|
|
24
25
|
this.EXEC_REPETITION_COUNT = 25;
|
|
25
26
|
this.PREDICT_COMMAND_RETRY_COUNT = 2;
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
+
connect() {
|
|
28
29
|
return __awaiter(this, void 0, void 0, function* () {
|
|
29
|
-
|
|
30
|
+
return this.uiControllerClient.connect();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
disconnect() {
|
|
34
|
+
this.uiControllerClient.disconnect();
|
|
35
|
+
}
|
|
36
|
+
startVideoRecording() {
|
|
37
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
38
|
+
yield this.uiControllerClient.startVideoRecording();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
stopVideoRecording() {
|
|
42
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
43
|
+
yield this.uiControllerClient.stopVideoRecording();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
readVideoRecording() {
|
|
47
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
48
|
+
const response = yield this.uiControllerClient.readVideoRecording();
|
|
49
|
+
return response.data.video;
|
|
30
50
|
});
|
|
31
51
|
}
|
|
32
52
|
requestControl(controlCommand) {
|
|
@@ -34,25 +54,22 @@ class ExecutionRuntime {
|
|
|
34
54
|
yield this.uiControllerClient.requestControl(controlCommand);
|
|
35
55
|
});
|
|
36
56
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
*/
|
|
40
|
-
executeCommand(step) {
|
|
41
|
-
var _a;
|
|
57
|
+
executeInstruction(instruction) {
|
|
58
|
+
var _a, _b, _c;
|
|
42
59
|
return __awaiter(this, void 0, void 0, function* () {
|
|
43
|
-
const controlCommand = yield this.predictCommandWithRetry(
|
|
60
|
+
const controlCommand = yield this.predictCommandWithRetry(instruction);
|
|
44
61
|
if (controlCommand.code === ui_control_commands_1.ControlCommandCode.OK) {
|
|
45
62
|
return this.requestControl(controlCommand);
|
|
46
63
|
}
|
|
47
64
|
if (controlCommand.tryToRepeat) {
|
|
48
65
|
yield this.requestControl(controlCommand);
|
|
49
|
-
return this.executeCommandRepeatedly(
|
|
66
|
+
return this.executeCommandRepeatedly(instruction);
|
|
50
67
|
}
|
|
51
|
-
throw new control_command_error_1.ControlCommandError(((_a = controlCommand.actions[0]) === null ||
|
|
68
|
+
throw new control_command_error_1.ControlCommandError((_c = (_b = (_a = controlCommand.actions) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.text) !== null && _c !== void 0 ? _c : '');
|
|
52
69
|
});
|
|
53
70
|
}
|
|
54
|
-
executeCommandRepeatedly(
|
|
55
|
-
var _a;
|
|
71
|
+
executeCommandRepeatedly(instruction) {
|
|
72
|
+
var _a, _b, _c;
|
|
56
73
|
return __awaiter(this, void 0, void 0, function* () {
|
|
57
74
|
/* eslint-disable no-await-in-loop */
|
|
58
75
|
for (let repeatCount = this.EXEC_REPETITION_COUNT; repeatCount >= 0; repeatCount -= 1) {
|
|
@@ -61,7 +78,7 @@ class ExecutionRuntime {
|
|
|
61
78
|
+ 'from a single test step reached');
|
|
62
79
|
}
|
|
63
80
|
logger_1.logger.debug('Repeat command execution....');
|
|
64
|
-
const controlCommand = yield this.predictCommandWithRetry(
|
|
81
|
+
const controlCommand = yield this.predictCommandWithRetry(instruction);
|
|
65
82
|
if (controlCommand.code === ui_control_commands_1.ControlCommandCode.OK) {
|
|
66
83
|
break;
|
|
67
84
|
}
|
|
@@ -69,7 +86,7 @@ class ExecutionRuntime {
|
|
|
69
86
|
yield this.requestControl(controlCommand);
|
|
70
87
|
}
|
|
71
88
|
else {
|
|
72
|
-
throw new control_command_error_1.ControlCommandError(((_a = controlCommand.actions[0]) === null ||
|
|
89
|
+
throw new control_command_error_1.ControlCommandError((_c = (_b = (_a = controlCommand.actions) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.text) !== null && _c !== void 0 ? _c : '');
|
|
73
90
|
}
|
|
74
91
|
}
|
|
75
92
|
});
|
|
@@ -79,9 +96,10 @@ class ExecutionRuntime {
|
|
|
79
96
|
* --> retry with linear back-off
|
|
80
97
|
*/
|
|
81
98
|
/* eslint-disable-next-line consistent-return */
|
|
82
|
-
predictCommandWithRetry(
|
|
99
|
+
predictCommandWithRetry(instruction) {
|
|
100
|
+
var _a, _b, _c;
|
|
83
101
|
return __awaiter(this, void 0, void 0, function* () {
|
|
84
|
-
let command = yield this.predictCommand(
|
|
102
|
+
let command = yield this.predictCommand(instruction);
|
|
85
103
|
/* eslint-disable no-await-in-loop */
|
|
86
104
|
for (let k = 0; k < this.PREDICT_COMMAND_RETRY_COUNT; k += 1) {
|
|
87
105
|
if (command.code === ui_control_commands_1.ControlCommandCode.OK) {
|
|
@@ -90,28 +108,56 @@ class ExecutionRuntime {
|
|
|
90
108
|
const msUntilRetry = k * 1000;
|
|
91
109
|
logger_1.logger.debug(`Wait ${msUntilRetry} and retry predicting command...`);
|
|
92
110
|
yield (0, misc_1.delay)(msUntilRetry);
|
|
93
|
-
command = yield this.predictCommand(
|
|
111
|
+
command = yield this.predictCommand(instruction, new control_command_error_1.ControlCommandError((_c = (_b = (_a = command.actions) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.text) !== null && _c !== void 0 ? _c : ''));
|
|
94
112
|
}
|
|
95
113
|
/* eslint-enable no-await-in-loop */
|
|
96
114
|
return command;
|
|
97
115
|
});
|
|
98
116
|
}
|
|
99
|
-
|
|
117
|
+
isImageRequiredByConfig() {
|
|
118
|
+
return this.stepReporter.config.withScreenshots === 'begin'
|
|
119
|
+
|| this.stepReporter.config.withScreenshots === 'always';
|
|
120
|
+
}
|
|
121
|
+
isImageRequired(instruction) {
|
|
100
122
|
return __awaiter(this, void 0, void 0, function* () {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
123
|
+
if (this.isImageRequiredByConfig())
|
|
124
|
+
return Promise.resolve(true);
|
|
125
|
+
return this.inferenceClient.isImageRequired(instruction);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
isAnnotationRequired() {
|
|
129
|
+
return this.stepReporter.config.withDetectedElements === 'begin'
|
|
130
|
+
|| this.stepReporter.config.withDetectedElements === 'always';
|
|
131
|
+
}
|
|
132
|
+
getScreenshot() {
|
|
133
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
134
|
+
const requestScreenshotResponse = yield this.uiControllerClient.requestScreenshot();
|
|
135
|
+
return requestScreenshotResponse.data.image;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
buildSnapshot(instruction) {
|
|
139
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
140
|
+
const createdAt = new Date();
|
|
141
|
+
const screenshot = (yield this.isImageRequired(instruction))
|
|
142
|
+
? yield this.getScreenshot() : undefined;
|
|
143
|
+
const annotation = this.isAnnotationRequired() ? yield this.annotateImage() : undefined;
|
|
144
|
+
return {
|
|
145
|
+
createdAt,
|
|
146
|
+
screenshot,
|
|
147
|
+
detectedElements: annotation === null || annotation === void 0 ? void 0 : annotation.detected_elements,
|
|
148
|
+
};
|
|
107
149
|
});
|
|
108
150
|
}
|
|
109
|
-
predictCommand(
|
|
151
|
+
predictCommand(instruction, retryError) {
|
|
110
152
|
return __awaiter(this, void 0, void 0, function* () {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
153
|
+
const snapshot = yield this.buildSnapshot(instruction.value);
|
|
154
|
+
if (retryError !== undefined)
|
|
155
|
+
this.stepReporter.onStepRetry(snapshot, retryError);
|
|
156
|
+
else
|
|
157
|
+
this.stepReporter.onStepBegin(snapshot);
|
|
158
|
+
const controlCommand = yield this.inferenceClient.predictControlCommand(instruction.value, instruction.customElements, snapshot.screenshot);
|
|
159
|
+
if (instruction.secretText !== undefined) {
|
|
160
|
+
controlCommand.setTextToBeTyped(instruction.secretText);
|
|
115
161
|
}
|
|
116
162
|
return controlCommand;
|
|
117
163
|
});
|
|
@@ -140,7 +186,7 @@ class ExecutionRuntime {
|
|
|
140
186
|
let customElements = [];
|
|
141
187
|
const base64Image = yield this.takeScreenshotIfImageisNotProvided();
|
|
142
188
|
if (customElementJson !== undefined) {
|
|
143
|
-
customElements = yield
|
|
189
|
+
customElements = yield custom_element_1.CustomElement.fromJsonListWithImagePathOrImage(customElementJson);
|
|
144
190
|
}
|
|
145
191
|
return this.inferenceClient.getDetectedElements(instruction, base64Image, customElements);
|
|
146
192
|
});
|
|
@@ -150,7 +196,7 @@ class ExecutionRuntime {
|
|
|
150
196
|
let customElements = [];
|
|
151
197
|
const base64Image = yield this.takeScreenshotIfImageisNotProvided(imagePath);
|
|
152
198
|
if (customElementJson !== undefined) {
|
|
153
|
-
customElements = yield
|
|
199
|
+
customElements = yield custom_element_1.CustomElement.fromJsonListWithImagePathOrImage(customElementJson);
|
|
154
200
|
}
|
|
155
201
|
return this.inferenceClient.predictImageAnnotation(base64Image, customElements);
|
|
156
202
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HttpClientGot } from '../utils/http/http-client-got';
|
|
2
2
|
import { ControlCommand } from '../core/ui-control-commands';
|
|
3
|
-
import { CustomElement } from '../core/model/
|
|
3
|
+
import { CustomElement } from '../core/model/custom-element';
|
|
4
4
|
import { Annotation } from '../core/annotation/annotation';
|
|
5
5
|
import { DetectedElement } from '../core/model/annotation-result/detected-element';
|
|
6
6
|
import { ModelCompositionBranch } from './model-composition-branch';
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { DetectedElement } from "@/core/model/annotation-result/detected-element";
|
|
2
|
+
import { CustomElement } from "@/core/model/custom-element";
|
|
3
|
+
export interface Snapshot {
|
|
4
|
+
readonly createdAt: Date;
|
|
5
|
+
readonly screenshot?: string | undefined;
|
|
6
|
+
readonly detectedElements?: Readonly<Readonly<DetectedElement>>[] | undefined;
|
|
7
|
+
}
|
|
8
|
+
export interface Instruction {
|
|
9
|
+
readonly value: string;
|
|
10
|
+
readonly valueHumanReadable: string;
|
|
11
|
+
readonly customElements?: Readonly<Readonly<CustomElement>>[];
|
|
12
|
+
readonly secretText?: string | undefined;
|
|
13
|
+
}
|
|
14
|
+
export interface Step {
|
|
15
|
+
readonly instruction: Readonly<Instruction>;
|
|
16
|
+
readonly status: StepStatus;
|
|
17
|
+
/**
|
|
18
|
+
* A snapshot of the state of the screen before the step is ran which is nearly immediately after the call to `.exec()`
|
|
19
|
+
*/
|
|
20
|
+
readonly begin?: Snapshot | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* The duration of the step in milliseconds based on the start and end time. If the step is still running, this property is undefined.
|
|
23
|
+
*/
|
|
24
|
+
readonly duration?: number | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* A snapshot of the state of the screen after the step has been run. If the step is still running or still pending, this property is undefined.
|
|
27
|
+
*/
|
|
28
|
+
readonly end?: Snapshot | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* When a step failed or is erroneous, this property contains the error.
|
|
31
|
+
*/
|
|
32
|
+
readonly error?: Error | undefined;
|
|
33
|
+
readonly runs: Readonly<StepRun[]>;
|
|
34
|
+
readonly retries: Readonly<StepRun[]>;
|
|
35
|
+
readonly retryCount: number;
|
|
36
|
+
readonly firstRun?: StepRun | undefined;
|
|
37
|
+
readonly lastRun?: StepRun | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* retryCount > 0.
|
|
40
|
+
*/
|
|
41
|
+
readonly flaky: boolean;
|
|
42
|
+
}
|
|
43
|
+
export interface StepRun {
|
|
44
|
+
readonly status: StepStatus;
|
|
45
|
+
readonly begin?: Snapshot | undefined;
|
|
46
|
+
readonly end?: Snapshot | undefined;
|
|
47
|
+
readonly duration?: number | undefined;
|
|
48
|
+
readonly error?: Error | undefined;
|
|
49
|
+
}
|
|
50
|
+
declare class askuiStep implements Step {
|
|
51
|
+
instruction: Instruction;
|
|
52
|
+
runs: Readonly<StepRun[]>;
|
|
53
|
+
constructor(instruction: Instruction);
|
|
54
|
+
get status(): StepStatus;
|
|
55
|
+
get begin(): Snapshot | undefined;
|
|
56
|
+
get end(): Snapshot | undefined;
|
|
57
|
+
get error(): Error | undefined;
|
|
58
|
+
get retries(): StepRun[];
|
|
59
|
+
get retryCount(): number;
|
|
60
|
+
get firstRun(): StepRun | undefined;
|
|
61
|
+
get lastRun(): StepRun | undefined;
|
|
62
|
+
get flaky(): boolean;
|
|
63
|
+
get duration(): number | undefined;
|
|
64
|
+
onBegin(snapshot: Snapshot): askuiStep;
|
|
65
|
+
onRetry(snapshot: Snapshot, error: Error): askuiStep;
|
|
66
|
+
onEnd(snapshot: Snapshot, error?: Error): askuiStep;
|
|
67
|
+
private determineLastRunStatus;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* - `passed`: the step passed
|
|
71
|
+
* - `failed`: the step failed because of a failed (implicit/explicit) assertion, e.g., an element could not be found
|
|
72
|
+
* - `pending`: the step is waiting for a previous step to pass
|
|
73
|
+
* - `running`: the step is currently running (including retries)
|
|
74
|
+
* - `skipped`: the step was skipped, e.g., because a previous step failed or because workflow was skipped
|
|
75
|
+
* - `erroneous`: the step could not be run because of a runtime error, e.g., the user has no usage left, response of inference backend cannot be processed, e.g., because lib version is outdated, etc. (currently not supported, everything "failed")
|
|
76
|
+
*/
|
|
77
|
+
export declare type StepStatus = 'passed' | 'failed' | 'pending' | 'running' | 'erroneous';
|
|
78
|
+
export declare type StepStatusEnd = 'passed' | 'failed' | 'erroneous';
|
|
79
|
+
/**
|
|
80
|
+
* Can be used to report on a step run.
|
|
81
|
+
*
|
|
82
|
+
* The reporter is an interface (instead of a class) so that the it may be implemented by a class that already extends another class, e.g., the reporter of a test framework.
|
|
83
|
+
*/
|
|
84
|
+
export interface Reporter {
|
|
85
|
+
config?: ReporterConfig;
|
|
86
|
+
onStepBegin?(step: Step): Promise<void>;
|
|
87
|
+
onStepRetry?(step: Step): Promise<void>;
|
|
88
|
+
onStepEnd?(step: Step): Promise<void>;
|
|
89
|
+
}
|
|
90
|
+
export declare type SnapshotDetailLevel =
|
|
91
|
+
/**
|
|
92
|
+
* Details of snapshot, e.g., screenshot or detected elements, may or may not be available depending on if they are required by the step. There are not guarantees made.
|
|
93
|
+
*/
|
|
94
|
+
'required' |
|
|
95
|
+
/**
|
|
96
|
+
* Details are available when the step fails, e.g., for debugging. Includes everything of required.
|
|
97
|
+
*/
|
|
98
|
+
'onFailure' |
|
|
99
|
+
/**
|
|
100
|
+
* Details are available also when the command is started, e.g., for detecting why a certain element was interacted with. Includes everything of onFailure.
|
|
101
|
+
*/
|
|
102
|
+
'begin' |
|
|
103
|
+
/**
|
|
104
|
+
* Details are available always, e.g., before and after a step has been run no matter if it failed or not for debugging.
|
|
105
|
+
*/
|
|
106
|
+
'always';
|
|
107
|
+
export interface ReporterConfig {
|
|
108
|
+
withScreenshots?: SnapshotDetailLevel;
|
|
109
|
+
withDetectedElements?: SnapshotDetailLevel;
|
|
110
|
+
}
|
|
111
|
+
export declare const DEFAULT_REPORTER: {
|
|
112
|
+
config: {
|
|
113
|
+
readonly withScreenshots: "onFailure";
|
|
114
|
+
readonly withDetectedElements: "onFailure";
|
|
115
|
+
};
|
|
116
|
+
onStepBegin: (_step: Step) => Promise<void>;
|
|
117
|
+
onStepRetry: (_step: Step) => Promise<void>;
|
|
118
|
+
onStepEnd: (_step: Step) => Promise<void>;
|
|
119
|
+
};
|
|
120
|
+
export declare class StepReporter {
|
|
121
|
+
private reporter;
|
|
122
|
+
currentStep?: askuiStep | undefined;
|
|
123
|
+
constructor(reporter: Required<Reporter> & {
|
|
124
|
+
config: Required<ReporterConfig>;
|
|
125
|
+
});
|
|
126
|
+
get config(): Required<ReporterConfig>;
|
|
127
|
+
resetStep(instruction: Instruction): void;
|
|
128
|
+
onStepBegin(snapshot: Snapshot): Promise<void>;
|
|
129
|
+
onStepRetry(snapshot: Snapshot, error: Error): Promise<void>;
|
|
130
|
+
onStepEnd(snapshot: Snapshot, error?: Error): Promise<void>;
|
|
131
|
+
}
|
|
132
|
+
export {};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.StepReporter = exports.DEFAULT_REPORTER = void 0;
|
|
13
|
+
// retries could be modeled as totally new steps only that we set the retry no at the beginning, it is undefined if there is no retry or 0 --> user could filter out the retries for the report and has to get the last retry to find out when the step actually ended and the snapshot at that point in time
|
|
14
|
+
// Where do I save the error of the first step, what state does the first step have?
|
|
15
|
+
// We could point back from retry to original step, but then we have to save the original step in the retry step, which is not nice
|
|
16
|
+
// retry should be handled like completely new step but the state of the whole state is not failed --> if a passed step can have an error, then this is fine
|
|
17
|
+
// how do we determine if it is erroneous or failed?
|
|
18
|
+
class askuiStep {
|
|
19
|
+
constructor(instruction) {
|
|
20
|
+
this.instruction = instruction;
|
|
21
|
+
this.runs = [];
|
|
22
|
+
}
|
|
23
|
+
get status() {
|
|
24
|
+
var _a, _b;
|
|
25
|
+
return (_b = (_a = this.lastRun) === null || _a === void 0 ? void 0 : _a.status) !== null && _b !== void 0 ? _b : 'pending';
|
|
26
|
+
}
|
|
27
|
+
get begin() {
|
|
28
|
+
var _a;
|
|
29
|
+
return (_a = this.firstRun) === null || _a === void 0 ? void 0 : _a.begin;
|
|
30
|
+
}
|
|
31
|
+
get end() {
|
|
32
|
+
var _a;
|
|
33
|
+
return (_a = this.lastRun) === null || _a === void 0 ? void 0 : _a.end;
|
|
34
|
+
}
|
|
35
|
+
get error() {
|
|
36
|
+
var _a;
|
|
37
|
+
return (_a = this.lastRun) === null || _a === void 0 ? void 0 : _a.error;
|
|
38
|
+
}
|
|
39
|
+
get retries() {
|
|
40
|
+
var _a, _b;
|
|
41
|
+
return (_b = (_a = this.runs) === null || _a === void 0 ? void 0 : _a.slice(1)) !== null && _b !== void 0 ? _b : [];
|
|
42
|
+
}
|
|
43
|
+
get retryCount() {
|
|
44
|
+
var _a, _b;
|
|
45
|
+
return (_b = (_a = this.retries) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0;
|
|
46
|
+
}
|
|
47
|
+
get firstRun() {
|
|
48
|
+
var _a;
|
|
49
|
+
return (_a = this.runs) === null || _a === void 0 ? void 0 : _a[0];
|
|
50
|
+
}
|
|
51
|
+
get lastRun() {
|
|
52
|
+
var _a;
|
|
53
|
+
return (_a = this.runs) === null || _a === void 0 ? void 0 : _a[this.runs.length - 1];
|
|
54
|
+
}
|
|
55
|
+
get flaky() {
|
|
56
|
+
return this.retryCount > 0;
|
|
57
|
+
}
|
|
58
|
+
get duration() {
|
|
59
|
+
if (this.begin !== undefined && this.end !== undefined) {
|
|
60
|
+
return this.end.createdAt.getTime() - this.begin.createdAt.getTime();
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
onBegin(snapshot) {
|
|
65
|
+
this.runs = [...this.runs, {
|
|
66
|
+
status: 'running',
|
|
67
|
+
begin: snapshot,
|
|
68
|
+
}];
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
onRetry(snapshot, error) {
|
|
72
|
+
this.onEnd(snapshot, error);
|
|
73
|
+
this.onBegin(snapshot);
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
onEnd(snapshot, error) {
|
|
77
|
+
this.runs = [...this.runs.slice(0, -1), Object.assign(Object.assign({}, this.lastRun), { status: this.determineLastRunStatus(error), end: snapshot, error, duration: snapshot.createdAt.getTime() - this.lastRun.begin.createdAt.getTime() })];
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
determineLastRunStatus(error) {
|
|
81
|
+
if (error !== undefined) {
|
|
82
|
+
return 'failed';
|
|
83
|
+
}
|
|
84
|
+
return 'passed';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
exports.DEFAULT_REPORTER = {
|
|
88
|
+
config: {
|
|
89
|
+
withScreenshots: 'onFailure',
|
|
90
|
+
withDetectedElements: 'onFailure',
|
|
91
|
+
},
|
|
92
|
+
onStepBegin: (_step) => {
|
|
93
|
+
return Promise.resolve();
|
|
94
|
+
},
|
|
95
|
+
onStepRetry: (_step) => {
|
|
96
|
+
return Promise.resolve();
|
|
97
|
+
},
|
|
98
|
+
onStepEnd: (_step) => {
|
|
99
|
+
return Promise.resolve();
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
class StepReporter {
|
|
103
|
+
constructor(reporter) {
|
|
104
|
+
this.reporter = reporter;
|
|
105
|
+
}
|
|
106
|
+
get config() {
|
|
107
|
+
return this.reporter.config;
|
|
108
|
+
}
|
|
109
|
+
resetStep(instruction) {
|
|
110
|
+
this.currentStep = new askuiStep(instruction);
|
|
111
|
+
}
|
|
112
|
+
onStepBegin(snapshot) {
|
|
113
|
+
var _a, _b;
|
|
114
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
115
|
+
if (this.currentStep === undefined)
|
|
116
|
+
throw new Error('Cannot begin step if step is undefined.');
|
|
117
|
+
if (this.currentStep.status !== 'pending')
|
|
118
|
+
throw new Error('Cannot begin step that is not pending.');
|
|
119
|
+
this.currentStep.onBegin(snapshot);
|
|
120
|
+
yield ((_b = (_a = this.reporter).onStepBegin) === null || _b === void 0 ? void 0 : _b.call(_a, this.currentStep));
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
onStepRetry(snapshot, error) {
|
|
124
|
+
var _a, _b;
|
|
125
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
126
|
+
if (this.currentStep === undefined)
|
|
127
|
+
throw new Error('Cannot retry step if step is undefined.');
|
|
128
|
+
if (this.currentStep.status !== 'running')
|
|
129
|
+
throw new Error('Cannot retry step that has not been running.');
|
|
130
|
+
this.currentStep.onRetry(snapshot, error);
|
|
131
|
+
yield ((_b = (_a = this.reporter).onStepRetry) === null || _b === void 0 ? void 0 : _b.call(_a, this.currentStep));
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
onStepEnd(snapshot, error) {
|
|
135
|
+
var _a, _b;
|
|
136
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
137
|
+
if (this.currentStep === undefined)
|
|
138
|
+
throw new Error('Cannot end step if step is undefined.');
|
|
139
|
+
if (this.currentStep.status !== 'running')
|
|
140
|
+
throw new Error('Cannot end step that has not been running.');
|
|
141
|
+
this.currentStep.onEnd(snapshot, error);
|
|
142
|
+
yield ((_b = (_a = this.reporter).onStepEnd) === null || _b === void 0 ? void 0 : _b.call(_a, this.currentStep));
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
exports.StepReporter = StepReporter;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ClientArgs, ClientArgsWithDefaults } from './ui-controller-client-interface';
|
|
2
|
+
import { ExecutionRuntime } from './execution-runtime';
|
|
3
|
+
import { StepReporter } from '../core/reporting';
|
|
4
|
+
export declare class UiControlClientDependencyBuilder {
|
|
5
|
+
private static buildHttpClient;
|
|
6
|
+
private static buildInferenceClient;
|
|
7
|
+
private static buildUiControllerClient;
|
|
8
|
+
static build(clientArgs: ClientArgsWithDefaults): Promise<{
|
|
9
|
+
executionRuntime: ExecutionRuntime;
|
|
10
|
+
stepReporter: StepReporter;
|
|
11
|
+
}>;
|
|
12
|
+
private static buildReporter;
|
|
13
|
+
static getClientArgsWithDefaults(clientArgs: ClientArgs): Promise<ClientArgsWithDefaults>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.UiControlClientDependencyBuilder = void 0;
|
|
13
|
+
const http_client_got_1 = require("../utils/http/http-client-got");
|
|
14
|
+
const ui_controller_client_1 = require("./ui-controller-client");
|
|
15
|
+
const inference_client_1 = require("./inference-client");
|
|
16
|
+
const read_environment_credentials_1 = require("./read-environment-credentials");
|
|
17
|
+
const analytics_1 = require("../utils/analytics");
|
|
18
|
+
const proxy_builder_1 = require("../utils/proxy/proxy-builder");
|
|
19
|
+
const annotation_level_1 = require("./annotation-level");
|
|
20
|
+
const execution_runtime_1 = require("./execution-runtime");
|
|
21
|
+
const reporting_1 = require("../core/reporting");
|
|
22
|
+
class UiControlClientDependencyBuilder {
|
|
23
|
+
static buildHttpClient(clientArgs) {
|
|
24
|
+
var _a;
|
|
25
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
26
|
+
const analytics = new analytics_1.Analytics();
|
|
27
|
+
const analyticsHeaders = yield analytics.getAnalyticsHeaders();
|
|
28
|
+
const analyticsCookies = yield analytics.getAnalyticsCookies();
|
|
29
|
+
return new http_client_got_1.HttpClientGot((_a = clientArgs.credentials) === null || _a === void 0 ? void 0 : _a.token, analyticsHeaders, analyticsCookies, clientArgs.proxyAgents);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
static buildInferenceClient(clientArgs) {
|
|
33
|
+
var _a;
|
|
34
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
35
|
+
const httpClient = yield UiControlClientDependencyBuilder.buildHttpClient(clientArgs);
|
|
36
|
+
return new inference_client_1.InferenceClient(clientArgs.inferenceServerUrl, httpClient, clientArgs.resize, (_a = clientArgs.credentials) === null || _a === void 0 ? void 0 : _a.workspaceId, clientArgs.modelComposition);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
static buildUiControllerClient(clientArgs) {
|
|
40
|
+
return new ui_controller_client_1.UiControllerClient(clientArgs.uiControllerUrl);
|
|
41
|
+
}
|
|
42
|
+
static build(clientArgs) {
|
|
43
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
44
|
+
const uiControllerClient = UiControlClientDependencyBuilder.buildUiControllerClient(clientArgs);
|
|
45
|
+
const inferenceClient = yield UiControlClientDependencyBuilder.buildInferenceClient(clientArgs);
|
|
46
|
+
const stepReporter = new reporting_1.StepReporter(clientArgs.reporter);
|
|
47
|
+
return {
|
|
48
|
+
executionRuntime: new execution_runtime_1.ExecutionRuntime(uiControllerClient, inferenceClient, stepReporter),
|
|
49
|
+
stepReporter,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
static buildReporter(reporterArg) {
|
|
54
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
55
|
+
return {
|
|
56
|
+
config: {
|
|
57
|
+
withScreenshots: (_b = (_a = reporterArg === null || reporterArg === void 0 ? void 0 : reporterArg.config) === null || _a === void 0 ? void 0 : _a.withScreenshots) !== null && _b !== void 0 ? _b : reporting_1.DEFAULT_REPORTER.config.withScreenshots,
|
|
58
|
+
withDetectedElements: (_d = (_c = reporterArg === null || reporterArg === void 0 ? void 0 : reporterArg.config) === null || _c === void 0 ? void 0 : _c.withDetectedElements) !== null && _d !== void 0 ? _d : reporting_1.DEFAULT_REPORTER.config.withDetectedElements,
|
|
59
|
+
},
|
|
60
|
+
onStepBegin: (_f = (_e = reporterArg === null || reporterArg === void 0 ? void 0 : reporterArg.onStepBegin) === null || _e === void 0 ? void 0 : _e.bind(reporterArg)) !== null && _f !== void 0 ? _f : reporting_1.DEFAULT_REPORTER.onStepBegin.bind(reporting_1.DEFAULT_REPORTER),
|
|
61
|
+
onStepRetry: (_h = (_g = reporterArg === null || reporterArg === void 0 ? void 0 : reporterArg.onStepRetry) === null || _g === void 0 ? void 0 : _g.bind(reporterArg)) !== null && _h !== void 0 ? _h : reporting_1.DEFAULT_REPORTER.onStepRetry.bind(reporting_1.DEFAULT_REPORTER),
|
|
62
|
+
onStepEnd: (_k = (_j = reporterArg === null || reporterArg === void 0 ? void 0 : reporterArg.onStepEnd) === null || _j === void 0 ? void 0 : _j.bind(reporterArg)) !== null && _k !== void 0 ? _k : reporting_1.DEFAULT_REPORTER.onStepEnd.bind(reporting_1.DEFAULT_REPORTER),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
static getClientArgsWithDefaults(clientArgs) {
|
|
66
|
+
var _a, _b, _c, _d, _e;
|
|
67
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
68
|
+
return Object.assign(Object.assign({}, clientArgs), { uiControllerUrl: (_a = clientArgs.uiControllerUrl) !== null && _a !== void 0 ? _a : 'http://127.0.0.1:6769', inferenceServerUrl: (_b = clientArgs.inferenceServerUrl) !== null && _b !== void 0 ? _b : 'https://inference.askui.com', annotationLevel: (_c = clientArgs.annotationLevel) !== null && _c !== void 0 ? _c : annotation_level_1.AnnotationLevel.DISABLED, credentials: (_d = clientArgs.credentials) !== null && _d !== void 0 ? _d : (0, read_environment_credentials_1.envCredentials)(), proxyAgents: (_e = clientArgs.proxyAgents) !== null && _e !== void 0 ? _e : yield (0, proxy_builder_1.envProxyAgents)(), reporter: UiControlClientDependencyBuilder.buildReporter(clientArgs.reporter) });
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.UiControlClientDependencyBuilder = UiControlClientDependencyBuilder;
|
|
@@ -1,28 +1,44 @@
|
|
|
1
|
-
import { CustomElementJson } from '../core/model/
|
|
1
|
+
import { CustomElementJson } from '../core/model/custom-element-json';
|
|
2
2
|
import { Exec, Executable, FluentFilters, ApiCommands } from './dsl';
|
|
3
3
|
import { UiControllerClientConnectionState } from './ui-controller-client-connection-state';
|
|
4
4
|
import { Annotation } from '../core/annotation/annotation';
|
|
5
5
|
import { AnnotationRequest } from '../core/model/annotation-result/annotation-interface';
|
|
6
|
-
import { ClientArgs } from './ui-controller-client-interface';
|
|
7
6
|
import { DetectedElement } from '../core/model/annotation-result/detected-element';
|
|
7
|
+
import { ClientArgs } from './ui-controller-client-interface';
|
|
8
8
|
export declare class UiControlClient extends ApiCommands {
|
|
9
|
-
private
|
|
10
|
-
private
|
|
11
|
-
private
|
|
12
|
-
private _uiControllerClient?;
|
|
9
|
+
private config;
|
|
10
|
+
private executionRuntime;
|
|
11
|
+
private stepReporter;
|
|
13
12
|
private constructor();
|
|
14
13
|
static build(clientArgs?: ClientArgs): Promise<UiControlClient>;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
private annotateByDefault;
|
|
14
|
+
/**
|
|
15
|
+
* Connects to the askui UI Controller.
|
|
16
|
+
*/
|
|
19
17
|
connect(): Promise<UiControllerClientConnectionState>;
|
|
18
|
+
/**
|
|
19
|
+
* Disconnects from the askui UI Controller.
|
|
20
|
+
*/
|
|
21
|
+
disconnect(): void;
|
|
22
|
+
/**
|
|
23
|
+
* Disconnects from the askui UI Controller.
|
|
24
|
+
*
|
|
25
|
+
* @deprecated Use {@link disconnect} instead.
|
|
26
|
+
*/
|
|
27
|
+
close(): void;
|
|
28
|
+
startVideoRecording(): Promise<void>;
|
|
29
|
+
stopVideoRecording(): Promise<void>;
|
|
30
|
+
readVideoRecording(): Promise<string>;
|
|
31
|
+
private shouldWriteAnntotationAfterCommandExecution;
|
|
32
|
+
private shouldAnnotateAfterCommandExecution;
|
|
33
|
+
private afterCommandExecution;
|
|
20
34
|
annotate(annotationRequest?: AnnotationRequest): Promise<Annotation>;
|
|
21
35
|
annotateInteractively(): Promise<void>;
|
|
22
36
|
private escapeSeparatorString;
|
|
23
|
-
|
|
37
|
+
private buildInstruction;
|
|
38
|
+
fluentCommandExecutor(instructionString: string, customElementJson?: CustomElementJson[]): Promise<void>;
|
|
24
39
|
getterExecutor(instruction: string, customElementJson?: CustomElementJson[]): Promise<DetectedElement[]>;
|
|
25
40
|
private secretText;
|
|
41
|
+
private getAndResetSecretText;
|
|
26
42
|
/**
|
|
27
43
|
* Types a text inside the filtered element.
|
|
28
44
|
*
|
|
@@ -76,8 +92,4 @@ export declare class UiControlClient extends ApiCommands {
|
|
|
76
92
|
* @return {Executable}
|
|
77
93
|
*/
|
|
78
94
|
waitFor(delayInMs: number): Executable;
|
|
79
|
-
/**
|
|
80
|
-
* Closes the connection to the askui UI Controller
|
|
81
|
-
*/
|
|
82
|
-
close(): void;
|
|
83
95
|
}
|