Skip to content
This repository was archived by the owner on Jun 23, 2026. It is now read-only.

Commit 664ecf4

Browse files
authored
fix: CI tests on OS X (#770)
Fixes #746 This PR does a bunch of things, but most importantly it fixes repeatedly failing OS X tests that ran in CI. These fell into two categories: - A single scrolling test that would always fail due to a dropped rendering frame in headless Chrome at an inopportune point in the test. - Two `alert` tests that would reliably cause a _different_ test to timeout. These two fixes required completely different solutions and underwent different investigations. A long discussion of the investigation can be found in the conversation thread in this PR. ### Fixing `alert` dialog problems These failures were a bit odd. Two flyout tests verify callback logic which is set up in the test to open an alert dialog. These tests pass without an issue. However, when either one of these tests is run exactly before move start tests (both of them). The first move start test will pass, the second will reliably time out in CI on OS X. Sometimes WebdriverIO will include errors about focus issues, but this didn't always happen. The best guess here is that there's an actual issue in Chrome and/or WebdriverIO specifically with alert dialogs that is putting the framework or browser into a bad state. Blockly shares no state across its tests due to page reloads and no cookies or local storage being leveraged, but forcing a session reload between tests (i.e. a full browser reopen) fixes the issue which seems to confirm that it's an issue in the framework environment and not Blockly. This seems like a reasonable fix. Note that this is sort of discouraged due to session reloading increasing test runtime, but the CI completions do not seem much slower than before even with the extra reloads. Locally it takes 2-3x longer to run when using headless mode. When using non-headless mode it encounters actual failures, but fortunately this needs to be forced since the setup currently is such that session reloading will only ever happen with headless runs. It also seems likely that forcing session reloads like this will actually improve other behavioral inconsistencies or flakes in the tests, as well. Session reloading has only been enabled for CI runs of tests since it shouldn't be needed locally, at least per observation (though it can be demonstrated locally using the command: `CI=true npm run test:ci`). ### Fixing rendering synchronization issues The rendering issue came down to the fact that Blockly can get into a temporarily broken state if its rendering queue is not run. Since Blockly relies on `requestAnimationFrame` for this to occur, dropped frames (which are legitimate according to the spec and something we can observe specifically in Mac CI with headless Chrome) will lead to block positioning not updating when the test expects, leading to a failing test. This can be easily reproduced even on Linux locally by linking against a version of Blockly that forces immediate rendering (e.g. pretending JavaFX is enabled). The fix was to force the tests to try and trigger a re-render of the workspace at select moments. During investigation it was easiest just to do this everywhere there's a moment to pause execution since that's effectively a time when the test actually wants to wait for things to 'settle', so this is what's actually changed in the PR. All previous moments of pausing execution now route through a specific utility function that also will try to force rendering of Blockly, bypassing any dropped frames that the browser might introduce and ensuring tests are a lot more stable. It seems possible (though it hasn't been proven) that other tests may have also been flaky due to this behavior. It's important to note that this _might_ be a bug in core Blockly, but the reality is it seems nearly impossible to ever actually occur. Even if a browser dropped or delayed a rendering frame, eventual consistency should kick in since Blockly does deterministically render itself and thus will self-correct on the next render (which is fundamentally why the solution above works as well as it does). Note also that this fix had a compatibility issue with the dialog fix above and required a special setting to disable synchronization since it's not valid in WebdriverIO to execute browser-side code while a dialog is open (and attempting to detect this results in a significant slowdown in CI runtime and a large increase in console warnings). ### Other test infrastructure improvements As part of the investigation it was useful to try and see what the tests were doing when they failed. WebdriverIO supports this with a built-in `saveScreenshot` function. All test suites have been updated in this PR to automatically check for failures when they finish each test and, if there's a failure, the current screen state will be snapshotted and uploaded to a zip file (specific to the OS) in GitHub Actions. This actually did help with determining the root rendering issue because the screenshot showed a correct block layout even though debug logs showed incorrect positions. At first this hinted toward inconsistencies between the data model and actual rendering, but continued investigation eventually revealed the rendering frame being dropped (and the screenshot function was likely forcing rendering and 'correcting' the broken state). Failures will also be screenshotted for local tests and saved in `test/webdriverio/test/failures/` with a filename corresponding to the test's name. These are automatically ignored from being added to Git via `.gitignore`. Finally, a couple other miscellaneous changes: - One additional pause was added to the scroll test. This probably isn't needed, but more pauses generally help with stability. - Ditto for `sendKeyAndWait` though this was more of a contract correction: if `PAUSE_TIME` was zero then it wasn't actually waiting anymore which seemed incorrect. That's been fixed. - There are some new string casts that seem necessary after some NPM upgrades, and they're generally innocuous. And one final note: this PR mainly ensures all tests generally pass, not that there are no flakes. From observation tests may still flake on occasion, but they can be restarted and generally expected to pass. The OS X tests ran 14 times in a row without a failure before running into the 6 hour GitHub Actions runtime limit (it seems they slowed down each subsequent run, perhaps due to some sort of memory or resource leak). It may be possible to add a "try 3 times" approach to the WebdriverIO tests in CI in the future to significantly reduce flakes and increase the trustworthiness of failures to be actual problems, but this PR does not address that.
1 parent c773ad9 commit 664ecf4

20 files changed

Lines changed: 408 additions & 107 deletions

.github/workflows/test.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ jobs:
6565
cd main
6666
npm run test:ci
6767
68+
- name: Upload test failure screenshots
69+
if: always()
70+
uses: actions/upload-artifact@v5
71+
with:
72+
name: test-failure-screenshots-${{ matrix.os }}
73+
path: main/test/webdriverio/test/failures
74+
6875
webdriverio_tests:
6976
name: WebdriverIO tests (against pinned v12)
7077
# Don't run pinned version checks for PRs.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ build-debug.log
44
node_modules/
55
build/
66
dist/
7+
test/webdriverio/test/failures/

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222
"wdio:build": "npm run wdio:build:app && npm run wdio:build:tests",
2323
"wdio:build:app": "cd test/webdriverio && webpack",
2424
"wdio:build:tests": "tsc -p ./test/webdriverio/test/tsconfig.json",
25-
"wdio:clean": "cd test/webdriverio/test && rm -rf dist",
26-
"wdio:run": "npm run wdio:build && cd test/webdriverio/test && npx mocha dist",
27-
"wdio:run:ci": "npm run wdio:build && cd test/webdriverio/test && npx mocha --timeout 30000 dist"
25+
"wdio:clean": "cd test/webdriverio/test && rm -rf dist && rm -rf failures",
26+
"wdio:run": "npm run wdio:build && cd test/webdriverio/test && mkdir -p failures && npx mocha dist",
27+
"wdio:run:ci": "npm run wdio:build && cd test/webdriverio/test && mkdir -p failures && npx mocha --timeout 30000 dist"
2828
},
2929
"main": "./dist/index.js",
3030
"module": "./src/index.js",

test/webdriverio/test/actions_test.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import {
2020
sendKeyAndWait,
2121
keyRight,
2222
contextMenuItems,
23+
checkForFailures,
24+
pause,
2325
} from './test_setup.js';
2426

2527
const isDarwin = process.platform === 'darwin';
@@ -96,13 +98,21 @@ suite('Menus test', function () {
9698
testFileLocations.MORE_BLOCKS,
9799
this.timeout(),
98100
);
99-
await this.browser.pause(PAUSE_TIME);
101+
await pause(this.browser);
102+
});
103+
104+
teardown(async function () {
105+
await checkForFailures(
106+
this.browser,
107+
this.currentTest?.title,
108+
this.currentTest?.state,
109+
);
100110
});
101111

102112
test('Menu action via keyboard on block opens menu', async function () {
103113
// Navigate to draw_circle_1.
104114
await focusOnBlock(this.browser, 'draw_circle_1');
105-
await this.browser.pause(PAUSE_TIME);
115+
await pause(this.browser);
106116
await sendKeyAndWait(this.browser, [Key.Ctrl, Key.Return]);
107117

108118
chai.assert.deepEqual(
@@ -126,7 +136,7 @@ suite('Menus test', function () {
126136
await focusOnBlock(this.browser, 'text_print_1');
127137
await this.browser.keys(Key.ArrowRight);
128138
await this.browser.keys([Key.Ctrl, Key.Return]);
129-
await this.browser.pause(PAUSE_TIME);
139+
await pause(this.browser);
130140

131141
chai.assert.deepEqual(
132142
await contextMenuItems(this.browser),
@@ -154,9 +164,9 @@ suite('Menus test', function () {
154164
await moveToToolboxCategory(this.browser, 'Math');
155165
// Move to flyout.
156166
await keyRight(this.browser);
157-
await this.browser.pause(PAUSE_TIME);
167+
await pause(this.browser);
158168
await rightClickOnFlyoutBlockType(this.browser, 'math_number');
159-
await this.browser.pause(PAUSE_TIME);
169+
await pause(this.browser);
160170

161171
chai.assert.deepEqual(
162172
await contextMenuItems(this.browser),
@@ -192,11 +202,11 @@ suite('Menus test', function () {
192202
test('Escape key dismisses menu', async function () {
193203
await tabNavigateToWorkspace(this.browser);
194204
await focusOnBlock(this.browser, 'draw_circle_1');
195-
await this.browser.pause(PAUSE_TIME);
205+
await pause(this.browser);
196206
await this.browser.keys([Key.Ctrl, Key.Return]);
197-
await this.browser.pause(PAUSE_TIME);
207+
await pause(this.browser);
198208
await this.browser.keys(Key.Escape);
199-
await this.browser.pause(PAUSE_TIME);
209+
await pause(this.browser);
200210

201211
chai.assert.isTrue(
202212
await contextMenuExists(this.browser, 'Duplicate', /* reverse= */ true),
@@ -207,9 +217,9 @@ suite('Menus test', function () {
207217
test('Clicking workspace dismisses menu', async function () {
208218
await tabNavigateToWorkspace(this.browser);
209219
await clickBlock(this.browser, 'draw_circle_1', {button: 'right'});
210-
await this.browser.pause(PAUSE_TIME);
220+
await pause(this.browser);
211221
await focusWorkspace(this.browser);
212-
await this.browser.pause(PAUSE_TIME);
222+
await pause(this.browser);
213223

214224
chai.assert.isTrue(
215225
await contextMenuExists(this.browser, 'Duplicate', /* reverse= */ true),

test/webdriverio/test/basic_test.ts

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import {
2222
keyRight,
2323
keyUp,
2424
keyDown,
25+
checkForFailures,
26+
pause,
2527
} from './test_setup.js';
2628
import {Key} from 'webdriverio';
2729

@@ -37,6 +39,14 @@ suite('Keyboard navigation on Blocks', function () {
3739
);
3840
});
3941

42+
teardown(async function () {
43+
await checkForFailures(
44+
this.browser,
45+
this.currentTest?.title,
46+
this.currentTest?.state,
47+
);
48+
});
49+
4050
test('Default workspace', async function () {
4151
const blockCount = await this.browser.execute(() => {
4252
return Blockly.getMainWorkspace().getAllBlocks(false).length;
@@ -47,7 +57,7 @@ suite('Keyboard navigation on Blocks', function () {
4757

4858
test('Selected block', async function () {
4959
await tabNavigateToWorkspace(this.browser);
50-
await this.browser.pause(PAUSE_TIME);
60+
await pause(this.browser);
5161

5262
await keyDown(this.browser, 14);
5363

@@ -58,7 +68,7 @@ suite('Keyboard navigation on Blocks', function () {
5868

5969
test('Down from statement block selects next block across stacks', async function () {
6070
await focusOnBlock(this.browser, 'p5_canvas_1');
61-
await this.browser.pause(PAUSE_TIME);
71+
await pause(this.browser);
6272
await keyDown(this.browser);
6373

6474
chai
@@ -68,7 +78,7 @@ suite('Keyboard navigation on Blocks', function () {
6878

6979
test('Up from statement block selects previous block', async function () {
7080
await focusOnBlock(this.browser, 'simple_circle_1');
71-
await this.browser.pause(PAUSE_TIME);
81+
await pause(this.browser);
7282
await keyUp(this.browser);
7383

7484
chai
@@ -78,7 +88,7 @@ suite('Keyboard navigation on Blocks', function () {
7888

7989
test('Down from parent block selects first child block', async function () {
8090
await focusOnBlock(this.browser, 'p5_setup_1');
81-
await this.browser.pause(PAUSE_TIME);
91+
await pause(this.browser);
8292
await keyDown(this.browser);
8393
chai
8494
.expect(await getCurrentFocusedBlockId(this.browser))
@@ -87,7 +97,7 @@ suite('Keyboard navigation on Blocks', function () {
8797

8898
test('Up from child block selects parent block', async function () {
8999
await focusOnBlock(this.browser, 'p5_canvas_1');
90-
await this.browser.pause(PAUSE_TIME);
100+
await pause(this.browser);
91101
await keyUp(this.browser);
92102
chai
93103
.expect(await getCurrentFocusedBlockId(this.browser))
@@ -96,7 +106,7 @@ suite('Keyboard navigation on Blocks', function () {
96106

97107
test('Right from block selects first field', async function () {
98108
await focusOnBlock(this.browser, 'p5_canvas_1');
99-
await this.browser.pause(PAUSE_TIME);
109+
await pause(this.browser);
100110
await keyRight(this.browser);
101111

102112
chai
@@ -108,7 +118,7 @@ suite('Keyboard navigation on Blocks', function () {
108118

109119
test('Right from block selects first inline input', async function () {
110120
await focusOnBlock(this.browser, 'simple_circle_1');
111-
await this.browser.pause(PAUSE_TIME);
121+
await pause(this.browser);
112122
await keyRight(this.browser);
113123

114124
chai.assert.equal(
@@ -119,7 +129,7 @@ suite('Keyboard navigation on Blocks', function () {
119129

120130
test('Up from inline input selects statement block', async function () {
121131
await focusOnBlock(this.browser, 'math_number_2');
122-
await this.browser.pause(PAUSE_TIME);
132+
await pause(this.browser);
123133
await keyUp(this.browser);
124134

125135
chai.assert.equal(
@@ -130,7 +140,7 @@ suite('Keyboard navigation on Blocks', function () {
130140

131141
test('Left from first inline input selects block', async function () {
132142
await focusOnBlock(this.browser, 'math_number_2');
133-
await this.browser.pause(PAUSE_TIME);
143+
await pause(this.browser);
134144
await keyLeft(this.browser);
135145

136146
chai.assert.equal(
@@ -141,7 +151,7 @@ suite('Keyboard navigation on Blocks', function () {
141151

142152
test('Right from first inline input selects second inline input', async function () {
143153
await focusOnBlock(this.browser, 'math_number_2');
144-
await this.browser.pause(PAUSE_TIME);
154+
await pause(this.browser);
145155
await keyRight(this.browser);
146156

147157
chai.assert.equal(
@@ -152,7 +162,7 @@ suite('Keyboard navigation on Blocks', function () {
152162

153163
test('Left from second inline input selects first inline input', async function () {
154164
await focusOnBlock(this.browser, 'math_number_3');
155-
await this.browser.pause(PAUSE_TIME);
165+
await pause(this.browser);
156166
await keyLeft(this.browser);
157167

158168
chai.assert.equal(
@@ -163,7 +173,7 @@ suite('Keyboard navigation on Blocks', function () {
163173

164174
test('Right from last inline input selects next block', async function () {
165175
await focusOnBlock(this.browser, 'colour_picker_1');
166-
await this.browser.pause(PAUSE_TIME);
176+
await pause(this.browser);
167177
await keyRight(this.browser);
168178

169179
chai
@@ -173,7 +183,7 @@ suite('Keyboard navigation on Blocks', function () {
173183

174184
test('Down from inline input selects next block', async function () {
175185
await focusOnBlock(this.browser, 'colour_picker_1');
176-
await this.browser.pause(PAUSE_TIME);
186+
await pause(this.browser);
177187
await keyDown(this.browser);
178188

179189
chai
@@ -183,7 +193,7 @@ suite('Keyboard navigation on Blocks', function () {
183193

184194
test("Down from inline input selects block's child block", async function () {
185195
await focusOnBlock(this.browser, 'logic_boolean_1');
186-
await this.browser.pause(PAUSE_TIME);
196+
await pause(this.browser);
187197
await keyDown(this.browser);
188198

189199
chai
@@ -193,7 +203,7 @@ suite('Keyboard navigation on Blocks', function () {
193203

194204
test('Right from text block selects shadow block then field', async function () {
195205
await focusOnBlock(this.browser, 'text_print_1');
196-
await this.browser.pause(PAUSE_TIME);
206+
await pause(this.browser);
197207
await keyRight(this.browser);
198208

199209
chai.assert.equal(await getCurrentFocusedBlockId(this.browser), 'text_1');
@@ -235,9 +245,17 @@ suite('Keyboard navigation on Fields', function () {
235245
);
236246
});
237247

248+
teardown(async function () {
249+
await checkForFailures(
250+
this.browser,
251+
this.currentTest?.title,
252+
this.currentTest?.state,
253+
);
254+
});
255+
238256
test('Up from first field selects block', async function () {
239257
await focusOnBlockField(this.browser, 'p5_canvas_1', 'WIDTH');
240-
await this.browser.pause(PAUSE_TIME);
258+
await pause(this.browser);
241259
await keyUp(this.browser);
242260

243261
chai.assert.equal(
@@ -248,7 +266,7 @@ suite('Keyboard navigation on Fields', function () {
248266

249267
test('Left from first field selects block', async function () {
250268
await focusOnBlockField(this.browser, 'p5_canvas_1', 'WIDTH');
251-
await this.browser.pause(PAUSE_TIME);
269+
await pause(this.browser);
252270
await keyLeft(this.browser);
253271

254272
chai.assert.equal(
@@ -259,7 +277,7 @@ suite('Keyboard navigation on Fields', function () {
259277

260278
test('Right from first field selects second field', async function () {
261279
await focusOnBlockField(this.browser, 'p5_canvas_1', 'WIDTH');
262-
await this.browser.pause(PAUSE_TIME);
280+
await pause(this.browser);
263281
await keyRight(this.browser);
264282

265283
chai
@@ -271,7 +289,7 @@ suite('Keyboard navigation on Fields', function () {
271289

272290
test('Left from second field selects first field', async function () {
273291
await focusOnBlockField(this.browser, 'p5_canvas_1', 'HEIGHT');
274-
await this.browser.pause(PAUSE_TIME);
292+
await pause(this.browser);
275293
await keyLeft(this.browser);
276294

277295
chai
@@ -283,7 +301,7 @@ suite('Keyboard navigation on Fields', function () {
283301

284302
test('Right from second field selects next block', async function () {
285303
await focusOnBlockField(this.browser, 'p5_canvas_1', 'HEIGHT');
286-
await this.browser.pause(PAUSE_TIME);
304+
await pause(this.browser);
287305
await keyRight(this.browser);
288306

289307
chai
@@ -293,7 +311,7 @@ suite('Keyboard navigation on Fields', function () {
293311

294312
test('Down from field selects next block', async function () {
295313
await focusOnBlockField(this.browser, 'p5_canvas_1', 'WIDTH');
296-
await this.browser.pause(PAUSE_TIME);
314+
await pause(this.browser);
297315
await keyDown(this.browser);
298316

299317
chai
@@ -303,7 +321,7 @@ suite('Keyboard navigation on Fields', function () {
303321

304322
test("Down from field selects block's child block", async function () {
305323
await focusOnBlockField(this.browser, 'controls_repeat_1', 'TIMES');
306-
await this.browser.pause(PAUSE_TIME);
324+
await pause(this.browser);
307325
await keyDown(this.browser);
308326

309327
chai
@@ -314,7 +332,7 @@ suite('Keyboard navigation on Fields', function () {
314332
test('Do not navigate while field editor is open', async function () {
315333
// Open a field editor dropdown
316334
await focusOnBlockField(this.browser, 'logic_boolean_1', 'BOOL');
317-
await this.browser.pause(PAUSE_TIME);
335+
await pause(this.browser);
318336
await sendKeyAndWait(this.browser, Key.Enter);
319337

320338
// Try to navigate to a different block
@@ -327,7 +345,7 @@ suite('Keyboard navigation on Fields', function () {
327345
test('Do not reopen field editor when handling enter to make a choice inside the editor', async function () {
328346
// Open colour picker
329347
await focusOnBlockField(this.browser, 'colour_picker_1', 'COLOUR');
330-
await this.browser.pause(PAUSE_TIME);
348+
await pause(this.browser);
331349
await sendKeyAndWait(this.browser, Key.Enter);
332350

333351
// Move right to pick a new colour.

test/webdriverio/test/block_comment_test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
testFileLocations,
1515
keyRight,
1616
PAUSE_TIME,
17+
checkForFailures,
1718
} from './test_setup.js';
1819
import {Key} from 'webdriverio';
1920

@@ -34,6 +35,14 @@ suite('Block comment navigation', function () {
3435
});
3536
});
3637

38+
teardown(async function () {
39+
await checkForFailures(
40+
this.browser,
41+
this.currentTest?.title,
42+
this.currentTest?.state,
43+
);
44+
});
45+
3746
test('Activating a block comment icon focuses the comment', async function () {
3847
await focusOnBlock(this.browser, 'p5_canvas_1');
3948
await keyRight(this.browser);

0 commit comments

Comments
 (0)