Skip to content
This repository was archived by the owner on Apr 18, 2024. It is now read-only.

Commit 250aaac

Browse files
authored
Merge pull request #105 from heartexlabs/bugfix/simple-image-objects
Add test for all simple objects on Image
2 parents 007fb6b + ccc3047 commit 250aaac

File tree

2 files changed

+310
-1
lines changed

2 files changed

+310
-1
lines changed

e2e/tests/helpers.js

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,73 @@ const initLabelStudio = async ({ config, data, completions = [{ result: [] }], p
2525
done();
2626
};
2727

28+
/**
29+
* Wait for the main Image object to be loaded
30+
* @param {function} done codecept async success handler
31+
*/
32+
const waitForImage = async done => {
33+
const img = document.querySelector("[alt=LS]");
34+
if (!img || img.complete) return done();
35+
img.onload = done;
36+
};
37+
38+
/**
39+
* Float numbers can't be compared strictly, so convert any numbers or structures with numbers
40+
* to same structures but with rounded numbers (int for ints, fixed(2) for floats)
41+
* @param {*} data
42+
*/
43+
const convertToFixed = data => {
44+
if (["string", "number"].includes(typeof data)) {
45+
const n = Number(data);
46+
return Number.isInteger(n) ? n : +Number(n).toFixed(2);
47+
}
48+
if (Array.isArray(data)) {
49+
return data.map(n => convertToFixed(n));
50+
}
51+
if (typeof data === "object") {
52+
const result = {};
53+
for (let key in data) {
54+
result[key] = convertToFixed(data[key]);
55+
}
56+
return result;
57+
}
58+
return data;
59+
};
60+
61+
/**
62+
* Create convertor for any measures to relative form on image with given dimensions
63+
* Accepts numbers, arrays ([x, y] treated as a special coords array) and hash objects
64+
* With [706, 882] given as image sizes:
65+
* assert.equal(convertToImageSize(123), 17.42);
66+
* assert.deepEqual(convertToImageSize([123, 123]), [17.42, 13.95]);
67+
* assert.deepEqual(
68+
* convertToImageSize({ width: 123, height: 123, radiusY: 123, coords: [123, 123] }),
69+
* { width: 17.42, height: 13.95, radiusY: 13.95, coords: [17.42, 13.95] }
70+
* );
71+
* @param {number} width
72+
* @param {number} height
73+
*/
74+
const getSizeConvertor = (width, height) =>
75+
function convert(data, size = width) {
76+
if (typeof data === "number") return convertToFixed((data * 100) / size);
77+
if (Array.isArray(data)) {
78+
if (data.length === 2) return [convert(data[0]), convert(data[1], height)];
79+
return data.map(n => convert(n));
80+
}
81+
if (typeof data === "object") {
82+
const result = {};
83+
for (let key in data) {
84+
if (key === "rotation") result[key] = data[key];
85+
else if (key.startsWith("height") || key === "y" || key.endsWith("Y")) result[key] = convert(data[key], height);
86+
else result[key] = convert(data[key]);
87+
}
88+
return result;
89+
}
90+
return data;
91+
};
92+
93+
const delay = n => new Promise(resolve => setTimeout(resolve, n));
94+
2895
// good idea, but it doesn't work :(
2996
const emulateClick = source => {
3097
const event = document.createEvent("CustomEvent");
@@ -40,6 +107,93 @@ const clickRect = () => {
40107
rect.fire("click", { clientX: 10, clientY: 10 });
41108
};
42109

110+
/**
111+
* Click once on the main Stage
112+
* @param {number} x
113+
* @param {number} y
114+
* @param {function} done
115+
*/
116+
const clickKonva = (x, y, done) => {
117+
const stage = window.Konva.stages[0];
118+
stage.fire("click", { clientX: x, clientY: y, evt: { offsetX: x, offsetY: y } });
119+
done();
120+
};
121+
122+
/**
123+
* Click multiple times on the Stage
124+
* @param {number[][]} points array of coords arrays ([[x1, y1], [x2, y2], ...])
125+
* @param {function} done
126+
*/
127+
const clickMultipleKonva = async (points, done) => {
128+
const stage = window.Konva.stages[0];
129+
for (let point of points) {
130+
stage.fire("click", { evt: { offsetX: point[0], offsetY: point[1] } });
131+
// await delay(10);
132+
}
133+
done();
134+
};
135+
136+
/**
137+
* Create Polygon on Stage by clicking multiple times and click on the first point at the end
138+
* @param {number[][]} points array of coords arrays ([[x1, y1], [x2, y2], ...])
139+
* @param {function} done
140+
*/
141+
const polygonKonva = async (points, done) => {
142+
const delay = () => new Promise(resolve => setTimeout(resolve, 10));
143+
const stage = window.Konva.stages[0];
144+
const firstCoords = points[0];
145+
for (let point of points) {
146+
stage.fire("click", { evt: { offsetX: point[0], offsetY: point[1] } });
147+
await delay();
148+
}
149+
150+
// for closing the Polygon we should place cursor over the first point
151+
const firstPoint = stage.getIntersection({ x: firstCoords[0], y: firstCoords[1] });
152+
firstPoint.fire("mouseover");
153+
await delay();
154+
// and only after that we can click on it
155+
firstPoint.fire("click");
156+
done();
157+
};
158+
159+
/**
160+
* Click and hold, move the cursor (with one pause in the middle) and release the mouse
161+
* @param {number} x
162+
* @param {number} y
163+
* @param {number} shiftX
164+
* @param {number} shiftY
165+
* @param {function} done
166+
*/
167+
const dragKonva = async (x, y, shiftX, shiftY, done) => {
168+
const stage = window.Konva.stages[0];
169+
stage.fire("mousedown", { evt: { offsetX: x, offsetY: y } });
170+
// await delay(10);
171+
stage.fire("mousemove", { evt: { offsetX: x + (shiftX >> 1), offsetY: y + (shiftY >> 1) } });
172+
// await delay(10);
173+
// we should move the cursor to the last point and only after that release the mouse
174+
stage.fire("mousemove", { evt: { offsetX: x + shiftX, offsetY: y + shiftY } });
175+
// await delay(10);
176+
// because some events work on mousemove and not on mouseup
177+
stage.fire("mouseup", { evt: { offsetX: x + shiftX, offsetY: y + shiftY } });
178+
done();
179+
};
180+
43181
const serialize = () => window.Htx.completionStore.selected.serializeCompletion();
44182

45-
module.exports = { initLabelStudio, emulateClick, clickRect, serialize };
183+
module.exports = {
184+
initLabelStudio,
185+
waitForImage,
186+
delay,
187+
188+
getSizeConvertor,
189+
convertToFixed,
190+
191+
emulateClick,
192+
clickRect,
193+
clickKonva,
194+
clickMultipleKonva,
195+
polygonKonva,
196+
dragKonva,
197+
198+
serialize,
199+
};

e2e/tests/image.shapes.test.js

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/* global Feature, Scenario */
2+
3+
const {
4+
initLabelStudio,
5+
waitForImage,
6+
getSizeConvertor,
7+
convertToFixed,
8+
clickKonva,
9+
polygonKonva,
10+
dragKonva,
11+
serialize,
12+
} = require("./helpers");
13+
14+
const assert = require("assert");
15+
16+
Feature("Test Image object");
17+
18+
const getConfigWithShape = (shape, props = "") => `
19+
<View>
20+
<Image name="img" value="$image" />
21+
<${shape} ${props} name="tag" toName="img" />
22+
</View>`;
23+
24+
const IMAGE =
25+
"https://htx-misc.s3.amazonaws.com/opensource/label-studio/examples/images/nick-owuor-astro-nic-visuals-wDifg5xc9Z4-unsplash.jpg";
26+
27+
// precalculated image size on the screen; may change because of different reasons
28+
const WIDTH = 706;
29+
const HEIGHT = 882;
30+
const convertToImageSize = getSizeConvertor(WIDTH, HEIGHT);
31+
32+
const completionEmpty = {
33+
id: "1000",
34+
result: [],
35+
};
36+
37+
const shapes = [
38+
{
39+
shape: "KeyPoint",
40+
props: 'strokeWidth="5"',
41+
action: clickKonva,
42+
regions: [
43+
{
44+
params: [200, 100],
45+
result: { x: 200, y: 100, width: 5 },
46+
},
47+
{
48+
params: [100, 100],
49+
result: { x: 100, y: 100, width: 5 },
50+
},
51+
],
52+
},
53+
{
54+
shape: "Polygon",
55+
action: polygonKonva,
56+
regions: [
57+
{
58+
// outer array — params, inner array — points as the first param
59+
params: [
60+
[
61+
[200, 20],
62+
[400, 100],
63+
[300, 200],
64+
],
65+
],
66+
result: {
67+
points: [
68+
[200, 20],
69+
[400, 100],
70+
[300, 200],
71+
],
72+
},
73+
},
74+
{
75+
// outer array — params, inner array — points as the first param
76+
params: [
77+
[
78+
[400, 10],
79+
[400, 90],
80+
[370, 30],
81+
[300, 10],
82+
],
83+
],
84+
result: {
85+
points: [
86+
[400, 10],
87+
[400, 90],
88+
[370, 30],
89+
[300, 10],
90+
],
91+
},
92+
},
93+
],
94+
},
95+
{
96+
shape: "Rectangle",
97+
action: dragKonva,
98+
regions: [
99+
{
100+
params: [100, 210, 80, 30],
101+
result: { width: 80, height: 30, rotation: 0, x: 100, y: 210 },
102+
},
103+
{
104+
params: [100, 350, -50, -50],
105+
result: { width: 50, height: 50, rotation: 0, x: 50, y: 300 },
106+
},
107+
],
108+
},
109+
{
110+
shape: "Ellipse",
111+
action: dragKonva,
112+
regions: [
113+
{
114+
params: [300, 300, 50, 50],
115+
result: { radiusX: 50, radiusY: 50, rotation: 0, x: 300, y: 300 },
116+
},
117+
{
118+
// @todo Ellipse behave differently depending on direction of drawing
119+
// it keeps center at the start point on right-down movement
120+
// and it moves center after the cursor on left-up movement
121+
// @todo looks like a bug
122+
params: [230, 300, -50, -30],
123+
result: { radiusX: 50, radiusY: 30, rotation: 0, x: 180, y: 270 },
124+
},
125+
],
126+
},
127+
];
128+
129+
Scenario("Simple shapes on Image", async function(I) {
130+
for (let shape of shapes) {
131+
const params = {
132+
config: getConfigWithShape(shape.shape, shape.props),
133+
data: { image: IMAGE },
134+
completions: [completionEmpty],
135+
};
136+
137+
I.amOnPage("/");
138+
await I.executeAsyncScript(initLabelStudio, params);
139+
// canvas won't be initialized fully before the image loads
140+
await I.executeAsyncScript(waitForImage);
141+
I.waitForVisible("canvas");
142+
I.see("Regions (0)");
143+
144+
for (let region of shape.regions) {
145+
// draw the shape using corresponding helper and params
146+
await I.executeAsyncScript(shape.action, ...region.params);
147+
}
148+
149+
const result = await I.executeScript(serialize);
150+
for (let i = 0; i < shape.regions.length; i++) {
151+
assert.equal(result[i].type, shape.shape.toLowerCase());
152+
assert.deepEqual(convertToFixed(result[i].value), convertToImageSize(shape.regions[i].result));
153+
}
154+
}
155+
});

0 commit comments

Comments
 (0)