@robotical/martyblocksjr 4.2.0 → 4.2.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/editions/free/src/app.bundle.js +1 -1
- package/editions/free/src/assets/ui/viewOnCompact.png +0 -0
- package/editions/free/src/assets/ui/viewOnCompactWideLabel.png +0 -0
- package/editions/free/src/assets/ui/viewOnTargetCompact.png +0 -0
- package/editions/free/src/css/editor.css +96 -0
- package/editions/free/src/css/editorleftpanel.css +21 -22
- package/editions/free/src/css/editorstage.css +62 -0
- package/editions/free/src/css/paintlook.css +23 -0
- package/editions/free/src/localizations/bg.json +1 -0
- package/editions/free/src/localizations/ca.json +1 -0
- package/editions/free/src/localizations/cs.json +1 -0
- package/editions/free/src/localizations/cy.json +1 -0
- package/editions/free/src/localizations/da.json +1 -0
- package/editions/free/src/localizations/de.json +1 -0
- package/editions/free/src/localizations/el.json +1 -0
- package/editions/free/src/localizations/en.json +1 -0
- package/editions/free/src/localizations/es.json +1 -0
- package/editions/free/src/localizations/fi.json +1 -0
- package/editions/free/src/localizations/fr.json +1 -0
- package/editions/free/src/localizations/it.json +1 -0
- package/editions/free/src/localizations/ja.json +1 -0
- package/editions/free/src/localizations/ko.json +1 -0
- package/editions/free/src/localizations/nl.json +1 -0
- package/editions/free/src/localizations/no.json +1 -0
- package/editions/free/src/localizations/pl.json +1 -0
- package/editions/free/src/localizations/pt-br.json +1 -0
- package/editions/free/src/localizations/pt.json +1 -0
- package/editions/free/src/localizations/sv.json +1 -0
- package/editions/free/src/localizations/th.json +1 -0
- package/editions/free/src/localizations/tr.json +1 -0
- package/editions/free/src/localizations/uk.json +1 -0
- package/editions/free/src/localizations/zh-cn.json +1 -0
- package/editions/free/src/localizations/zh-tw.json +1 -0
- package/editions/free/src/media.json +3 -0
- package/package.json +1 -1
- package/tests/e2e/accessibility.e2e.test.js +19 -2
- package/tests/e2e/animated-sprite-paint-disabled.e2e.test.js +358 -0
- package/tests/e2e/background-paint-active-selection.e2e.test.js +162 -0
- package/tests/e2e/blocks-jr-hidden-blocks.e2e.test.js +262 -0
- package/tests/e2e/chromium-79-smoke.test.js +19 -10
- package/tests/e2e/delete-last-sprite-mode.e2e.test.js +162 -0
- package/tests/e2e/duplicate-page.e2e.test.js +414 -0
- package/tests/e2e/marty-script-activation-paths.e2e.test.js +593 -0
- package/tests/e2e/microbit-extension-project-reload.e2e.test.js +6 -1
- package/tests/e2e/paint-editor-cancel.e2e.test.js +274 -0
- package/tests/e2e/project-load-progress-loop.e2e.test.js +6 -1
- package/tests/e2e/project-save-stall.e2e.test.js +230 -0
- package/tests/e2e/sprite-watermark-grayscale.e2e.test.js +269 -0
- package/tests/e2e/top-toolbar-tooltips.e2e.test.js +152 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import puppeteer from "puppeteer";
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
import http from "http";
|
|
5
|
+
|
|
6
|
+
const PORT = 3015;
|
|
7
|
+
const HOST = `http://localhost:${PORT}`;
|
|
8
|
+
const AFFECTED_SPRITES = [
|
|
9
|
+
{ name: "Pink Octopus", md5: "Sprites_Octopus-Pink.svg" },
|
|
10
|
+
{ name: "Raccoon", md5: "Sprites_Raccoon.svg" },
|
|
11
|
+
{ name: "Unicorn", md5: "Sprites_Unicorn.svg" },
|
|
12
|
+
];
|
|
13
|
+
const LEGACY_SPRITES = [
|
|
14
|
+
{ name: "Dog", md5: "Dog.svg", expectsGrayscale: false },
|
|
15
|
+
{ name: "Marty", md5: "Sprites_Marty.svg", expectsGrayscale: false },
|
|
16
|
+
{ name: "Bee", md5: "BeeSprite1.svg", expectsGrayscale: true },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
let server;
|
|
20
|
+
|
|
21
|
+
async function waitForServer(maxAttempts = 20) {
|
|
22
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
23
|
+
const ok = await new Promise((resolve) => {
|
|
24
|
+
const req = http.get(`${HOST}/`, (res) => {
|
|
25
|
+
res.destroy();
|
|
26
|
+
resolve(true);
|
|
27
|
+
});
|
|
28
|
+
req.on("error", () => resolve(false));
|
|
29
|
+
});
|
|
30
|
+
if (ok) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
34
|
+
}
|
|
35
|
+
throw new Error("Local server did not start in time");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
beforeAll(async () => {
|
|
39
|
+
server = spawn("python3", ["-m", "http.server", `${PORT}`, "--directory", "editions/free/src"], {
|
|
40
|
+
stdio: "ignore",
|
|
41
|
+
});
|
|
42
|
+
await waitForServer();
|
|
43
|
+
}, 30_000);
|
|
44
|
+
|
|
45
|
+
afterAll(() => {
|
|
46
|
+
if (server) {
|
|
47
|
+
server.kill();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
async function openEditor() {
|
|
52
|
+
const browser = await puppeteer.launch({
|
|
53
|
+
headless: true,
|
|
54
|
+
args: ["--no-sandbox"],
|
|
55
|
+
});
|
|
56
|
+
const page = await browser.newPage();
|
|
57
|
+
const errors = [];
|
|
58
|
+
|
|
59
|
+
page.on("pageerror", (err) => errors.push(err.message || String(err)));
|
|
60
|
+
page.on("console", (msg) => {
|
|
61
|
+
if (msg.type() === "error") {
|
|
62
|
+
errors.push(msg.text());
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await page.goto(`${HOST}/editor.html?mode=edit`, { waitUntil: "networkidle2", timeout: 30_000 });
|
|
67
|
+
await waitForEditorReady(page);
|
|
68
|
+
|
|
69
|
+
return { browser, page, errors };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function waitForEditorReady(page) {
|
|
73
|
+
await page.waitForFunction(
|
|
74
|
+
() => {
|
|
75
|
+
const backdrop = document.getElementById("backdrop");
|
|
76
|
+
return Boolean(
|
|
77
|
+
window.ScratchJr
|
|
78
|
+
&& window.ScratchJr.stage
|
|
79
|
+
&& window.ScratchJr.stage.currentPage
|
|
80
|
+
&& document.querySelector(".watermark")
|
|
81
|
+
&& backdrop
|
|
82
|
+
&& window.getComputedStyle(backdrop).display === "none"
|
|
83
|
+
&& !window.ScratchJr.onHold
|
|
84
|
+
&& window.ScratchJr.getActiveScript()
|
|
85
|
+
);
|
|
86
|
+
},
|
|
87
|
+
{ timeout: 30_000 }
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function addSpriteAndWait(page, sprite) {
|
|
92
|
+
await page.evaluate((spriteMd5, spriteName) => {
|
|
93
|
+
window.ScratchJr.stage.currentPage.addSprite(0.5, spriteMd5, spriteName);
|
|
94
|
+
}, sprite.md5, sprite.name);
|
|
95
|
+
|
|
96
|
+
await page.waitForFunction(
|
|
97
|
+
(spriteMd5) => {
|
|
98
|
+
const activeSprite = window.ScratchJr && window.ScratchJr.getSprite();
|
|
99
|
+
const watermark = document.querySelector(".watermark img");
|
|
100
|
+
return Boolean(
|
|
101
|
+
activeSprite
|
|
102
|
+
&& activeSprite.md5 === spriteMd5
|
|
103
|
+
&& watermark
|
|
104
|
+
&& watermark.complete
|
|
105
|
+
&& (watermark.naturalWidth > 0 || watermark.width > 0)
|
|
106
|
+
);
|
|
107
|
+
},
|
|
108
|
+
{ timeout: 30_000 },
|
|
109
|
+
sprite.md5
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function measureGrayscaleMatch(page, sprite) {
|
|
114
|
+
return page.evaluate(async (spriteName, spriteMd5) => {
|
|
115
|
+
const watermark = document.querySelector(".watermark img");
|
|
116
|
+
if (watermark.decode) {
|
|
117
|
+
try {
|
|
118
|
+
await watermark.decode();
|
|
119
|
+
} catch (err) {
|
|
120
|
+
// Some SVG data URLs report as already usable even when decode rejects.
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const original = new Image();
|
|
125
|
+
original.src = `/svglibrary/${spriteMd5}`;
|
|
126
|
+
await original.decode();
|
|
127
|
+
|
|
128
|
+
const width = original.naturalWidth;
|
|
129
|
+
const height = original.naturalHeight;
|
|
130
|
+
const filter = window.getComputedStyle(watermark).filter;
|
|
131
|
+
|
|
132
|
+
const renderPixels = (image, imageFilter) => {
|
|
133
|
+
const canvas = document.createElement("canvas");
|
|
134
|
+
canvas.width = width;
|
|
135
|
+
canvas.height = height;
|
|
136
|
+
const ctx = canvas.getContext("2d");
|
|
137
|
+
ctx.filter = imageFilter;
|
|
138
|
+
ctx.drawImage(image, 0, 0, width, height);
|
|
139
|
+
return ctx.getImageData(0, 0, width, height).data;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const actualPixels = renderPixels(watermark, filter);
|
|
143
|
+
const expectedPixels = renderPixels(original, "grayscale(1)");
|
|
144
|
+
let comparedPixels = 0;
|
|
145
|
+
let differentPixels = 0;
|
|
146
|
+
let totalChannelDifference = 0;
|
|
147
|
+
let maxChannelDifference = 0;
|
|
148
|
+
let opaquePixels = 0;
|
|
149
|
+
let coloredPixels = 0;
|
|
150
|
+
|
|
151
|
+
for (let i = 0; i < actualPixels.length; i += 4) {
|
|
152
|
+
if (actualPixels[i + 3] <= 20 && expectedPixels[i + 3] <= 20) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
comparedPixels += 1;
|
|
157
|
+
let pixelDifference = 0;
|
|
158
|
+
for (let channel = 0; channel < 4; channel += 1) {
|
|
159
|
+
const difference = Math.abs(actualPixels[i + channel] - expectedPixels[i + channel]);
|
|
160
|
+
totalChannelDifference += difference;
|
|
161
|
+
pixelDifference = Math.max(pixelDifference, difference);
|
|
162
|
+
maxChannelDifference = Math.max(maxChannelDifference, difference);
|
|
163
|
+
}
|
|
164
|
+
if (pixelDifference > 2) {
|
|
165
|
+
differentPixels += 1;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (actualPixels[i + 3] > 20) {
|
|
169
|
+
opaquePixels += 1;
|
|
170
|
+
const highestChannel = Math.max(actualPixels[i], actualPixels[i + 1], actualPixels[i + 2]);
|
|
171
|
+
const lowestChannel = Math.min(actualPixels[i], actualPixels[i + 1], actualPixels[i + 2]);
|
|
172
|
+
if (highestChannel - lowestChannel > 2) {
|
|
173
|
+
coloredPixels += 1;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
name: spriteName,
|
|
180
|
+
md5: spriteMd5,
|
|
181
|
+
width,
|
|
182
|
+
height,
|
|
183
|
+
filter,
|
|
184
|
+
opaquePixels,
|
|
185
|
+
meanChannelDifference: comparedPixels === 0 ? 0 : totalChannelDifference / (comparedPixels * 4),
|
|
186
|
+
differentPixelRatio: comparedPixels === 0 ? 0 : differentPixels / comparedPixels,
|
|
187
|
+
maxChannelDifference,
|
|
188
|
+
coloredPixelRatio: opaquePixels === 0 ? 0 : coloredPixels / opaquePixels,
|
|
189
|
+
};
|
|
190
|
+
}, sprite.name, sprite.md5);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function getLegacyWatermarkState(page, sprite) {
|
|
194
|
+
return page.evaluate((spriteName, spriteMd5) => {
|
|
195
|
+
const activeSprite = window.ScratchJr.getSprite();
|
|
196
|
+
const watermark = document.querySelector(".watermark img");
|
|
197
|
+
const legacyWatermark = activeSprite.getSVGimage(activeSprite.watermark);
|
|
198
|
+
const sourceWatermark = activeSprite.getSVGimage(activeSprite.svg);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
name: spriteName,
|
|
202
|
+
md5: spriteMd5,
|
|
203
|
+
filter: window.getComputedStyle(watermark).filter,
|
|
204
|
+
usesLegacySource: watermark.src === legacyWatermark.src,
|
|
205
|
+
usesOriginalSource: watermark.src === sourceWatermark.src,
|
|
206
|
+
};
|
|
207
|
+
}, sprite.name, sprite.md5);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
describe("sprite scripts watermark grayscale rendering", () => {
|
|
211
|
+
it(
|
|
212
|
+
"renders affected sprite watermarks from their source SVG in grayscale",
|
|
213
|
+
async () => {
|
|
214
|
+
const { browser, page, errors } = await openEditor();
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const metrics = [];
|
|
218
|
+
for (const sprite of AFFECTED_SPRITES) {
|
|
219
|
+
await addSpriteAndWait(page, sprite);
|
|
220
|
+
metrics.push(await measureGrayscaleMatch(page, sprite));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
for (const metric of metrics) {
|
|
224
|
+
expect(metric.filter).toContain("grayscale");
|
|
225
|
+
expect(metric.opaquePixels).toBeGreaterThan(0);
|
|
226
|
+
expect(metric.meanChannelDifference).toBeLessThan(0.5);
|
|
227
|
+
expect(metric.differentPixelRatio).toBeLessThan(0.01);
|
|
228
|
+
expect(metric.coloredPixelRatio).toBeLessThan(0.01);
|
|
229
|
+
}
|
|
230
|
+
expect(errors).toEqual([]);
|
|
231
|
+
} finally {
|
|
232
|
+
await browser.close();
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
60_000
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
it(
|
|
239
|
+
"preserves the legacy watermark path for existing sprites",
|
|
240
|
+
async () => {
|
|
241
|
+
const { browser, page, errors } = await openEditor();
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
const states = [];
|
|
245
|
+
for (const sprite of LEGACY_SPRITES) {
|
|
246
|
+
await addSpriteAndWait(page, sprite);
|
|
247
|
+
states.push({
|
|
248
|
+
...await getLegacyWatermarkState(page, sprite),
|
|
249
|
+
expectsGrayscale: sprite.expectsGrayscale,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
for (const state of states) {
|
|
254
|
+
if (state.expectsGrayscale) {
|
|
255
|
+
expect(state.filter).toContain("grayscale");
|
|
256
|
+
} else {
|
|
257
|
+
expect(state.filter).toBe("none");
|
|
258
|
+
}
|
|
259
|
+
expect(state.usesLegacySource).toBe(true);
|
|
260
|
+
expect(state.usesOriginalSource).toBe(false);
|
|
261
|
+
}
|
|
262
|
+
expect(errors).toEqual([]);
|
|
263
|
+
} finally {
|
|
264
|
+
await browser.close();
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
60_000
|
|
268
|
+
);
|
|
269
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import puppeteer from "puppeteer";
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
import http from "http";
|
|
5
|
+
|
|
6
|
+
const PORT = 3013;
|
|
7
|
+
const HOST = `http://localhost:${PORT}`;
|
|
8
|
+
|
|
9
|
+
const TOP_TOOLBAR_BUTTON_IDS = [
|
|
10
|
+
"full",
|
|
11
|
+
"grid",
|
|
12
|
+
"traceBtn",
|
|
13
|
+
"traceClear",
|
|
14
|
+
"setbkg",
|
|
15
|
+
"addtext",
|
|
16
|
+
"resetall",
|
|
17
|
+
"go",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
let server;
|
|
21
|
+
|
|
22
|
+
async function waitForServer(maxAttempts = 20) {
|
|
23
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
24
|
+
const ok = await new Promise((resolve) => {
|
|
25
|
+
const req = http.get(`${HOST}/`, (res) => {
|
|
26
|
+
res.destroy();
|
|
27
|
+
resolve(true);
|
|
28
|
+
});
|
|
29
|
+
req.on("error", () => resolve(false));
|
|
30
|
+
});
|
|
31
|
+
if (ok) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
35
|
+
}
|
|
36
|
+
throw new Error("Local server did not start in time");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
beforeAll(async () => {
|
|
40
|
+
server = spawn("python3", ["-m", "http.server", `${PORT}`, "--directory", "editions/free/src"], {
|
|
41
|
+
stdio: "ignore",
|
|
42
|
+
});
|
|
43
|
+
await waitForServer();
|
|
44
|
+
}, 30_000);
|
|
45
|
+
|
|
46
|
+
afterAll(() => {
|
|
47
|
+
if (server) {
|
|
48
|
+
server.kill();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
async function openEditor() {
|
|
53
|
+
const browser = await puppeteer.launch({
|
|
54
|
+
headless: true,
|
|
55
|
+
args: ["--no-sandbox"],
|
|
56
|
+
});
|
|
57
|
+
const page = await browser.newPage();
|
|
58
|
+
const errors = [];
|
|
59
|
+
|
|
60
|
+
page.on("pageerror", (err) => errors.push(err.message || String(err)));
|
|
61
|
+
page.on("console", (msg) => {
|
|
62
|
+
if (msg.type() === "error") {
|
|
63
|
+
errors.push(msg.text());
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await page.goto(`${HOST}/editor.html?mode=edit`, { waitUntil: "networkidle2", timeout: 30_000 });
|
|
68
|
+
|
|
69
|
+
return { browser, page, errors };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe("top toolbar tooltips", () => {
|
|
73
|
+
it(
|
|
74
|
+
"renders bounded app-owned tooltips for hover and keyboard focus",
|
|
75
|
+
async () => {
|
|
76
|
+
const { browser, page, errors } = await openEditor();
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
await page.waitForSelector("#stageframe", { timeout: 30_000 });
|
|
80
|
+
await page.waitForFunction(
|
|
81
|
+
(buttonIds) =>
|
|
82
|
+
buttonIds.every((id) => {
|
|
83
|
+
const button = document.getElementById(id);
|
|
84
|
+
return button && button.tagName === "BUTTON" && button.getAttribute("aria-label");
|
|
85
|
+
}),
|
|
86
|
+
{ timeout: 30_000 },
|
|
87
|
+
TOP_TOOLBAR_BUTTON_IDS
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const invalidTooltips = await page.evaluate((buttonIds) => {
|
|
91
|
+
return buttonIds
|
|
92
|
+
.map((id) => {
|
|
93
|
+
const button = document.getElementById(id);
|
|
94
|
+
const label = button.getAttribute("aria-label") || "";
|
|
95
|
+
return {
|
|
96
|
+
id,
|
|
97
|
+
ariaLabel: label,
|
|
98
|
+
nativeTitle: button.getAttribute("title"),
|
|
99
|
+
tooltip: button.getAttribute("data-tooltip") || "",
|
|
100
|
+
};
|
|
101
|
+
})
|
|
102
|
+
.filter(
|
|
103
|
+
(button) =>
|
|
104
|
+
button.nativeTitle !== null ||
|
|
105
|
+
button.tooltip !== button.ariaLabel
|
|
106
|
+
);
|
|
107
|
+
}, TOP_TOOLBAR_BUTTON_IDS);
|
|
108
|
+
|
|
109
|
+
expect(invalidTooltips).toEqual([]);
|
|
110
|
+
|
|
111
|
+
await page.hover("#grid");
|
|
112
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
113
|
+
|
|
114
|
+
const hoverTooltip = await page.$eval("#grid", (button) => {
|
|
115
|
+
const style = window.getComputedStyle(button, "::after");
|
|
116
|
+
return {
|
|
117
|
+
content: style.content,
|
|
118
|
+
opacity: style.opacity,
|
|
119
|
+
visibility: style.visibility,
|
|
120
|
+
maxWidth: style.maxWidth,
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(hoverTooltip.content).toContain("Grid");
|
|
125
|
+
expect(hoverTooltip.opacity).toBe("1");
|
|
126
|
+
expect(hoverTooltip.visibility).toBe("visible");
|
|
127
|
+
expect(hoverTooltip.maxWidth).toBe("240px");
|
|
128
|
+
|
|
129
|
+
await page.mouse.move(0, 0);
|
|
130
|
+
await page.focus("#traceBtn");
|
|
131
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
132
|
+
|
|
133
|
+
const focusedTooltip = await page.$eval("#traceBtn", (button) => {
|
|
134
|
+
const style = window.getComputedStyle(button, "::after");
|
|
135
|
+
return {
|
|
136
|
+
focused: document.activeElement === button,
|
|
137
|
+
opacity: style.opacity,
|
|
138
|
+
visibility: style.visibility,
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(focusedTooltip.focused).toBe(true);
|
|
143
|
+
expect(focusedTooltip.opacity).toBe("1");
|
|
144
|
+
expect(focusedTooltip.visibility).toBe("visible");
|
|
145
|
+
expect(errors).toEqual([]);
|
|
146
|
+
} finally {
|
|
147
|
+
await browser.close();
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
60_000
|
|
151
|
+
);
|
|
152
|
+
});
|