test: fix retry-reliant spellchecker and OOM crash-key specs (#55226)

The macOS spellchecker specs relied on retries because the system spelling
server starts on first use, which took 30 to 60 seconds on fresh CI
machines. Until then AppKit times every request out and Blink never asks
again about the text it already checked, so the first context menu spec
never saw its misspelling. Wait once, in a beforeAll, until the spell
checker answers with suggestions for a misspelled word, and have the
context menu waits require the suggestions as well as the misspelled word.

The renderer OOM crash-key specs filled the heap with arrays of pointers,
so each of the ~20 full GCs V8 runs near the heap limit traced the whole
heap. On the Intel macOS runners that took long enough for the crash
fixture's 30 second exit timer to end the app before the renderer crashed.
Fill the heap with arrays of doubles instead, which the GC does not trace.


Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
claude[bot]andClaude authored and GitHub committed 2026-10-26 08:01:54 +00:00
1 parent 74141f73b9
commit 4ba52efe38
2 files changed
+46 -11

No files matched your search

+7 -2
View File
@@ -73,10 +73,14 @@ app.whenReady().then(() => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.webContents.on('render-process-gone', () => process.exit(0));
w.webContents.on('did-finish-load', () => {
// Fill the heap with arrays of doubles, which hold no pointers. V8 runs
// about twenty full GCs near the heap limit before it gives up; arrays of
// pointers make each of them trace the whole heap, which on slow machines
// took long enough for the exit timer below to end the app first.
w.webContents.executeJavaScript(`
function oomTrigger() {
const arr = [];
while (true) arr.push(new Array(10000).fill('x'.repeat(100)));
while (true) arr.push(new Array(10000).fill(0.5));
}
oomTrigger();
`);
@@ -103,11 +107,12 @@ app.whenReady().then(() => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.webContents.on('render-process-gone', () => process.exit(0));
w.webContents.on('did-finish-load', () => {
// Arrays of doubles, as in 'renderer-oom', so the GCs near the limit are quick.
w.webContents.executeJavaScript(`
const blob = new Blob([\`
function workerLeakMemory() {
const arr = [];
while (true) { arr.push(new Array(1000).fill("x".repeat(1000))); }
while (true) { arr.push(new Array(1000).fill(0.5)); }
}
function triggerWorkerOom() { workerLeakMemory(); }
triggerWorkerOom();
+39 -9
View File
@@ -8,7 +8,7 @@ import * as http from 'node:http';
import * as path from 'node:path';
import { setTimeout } from 'node:timers/promises';
import { ifit, ifdescribe, listen } from './lib/spec-helpers.ts';
import { ifit, ifdescribe, listen, waitUntil } from './lib/spec-helpers.ts';
import { closeWindow } from './lib/window-helpers.ts';
const features = process._linkedBinding('electron_common_features');
@@ -45,6 +45,13 @@ ifdescribe(features.isBuiltinSpellCheckerEnabled())('spellchecker', { timeout },
return contextMenuParams;
}
// The misspelled word comes from the asynchronous check that underlines it,
// but its suggestions are looked up separately when the menu opens, and on
// macOS that lookup can time out on its own ("NSSpellServer
// suggestGuessesForWordRange timed out"). Wait for the menu to have both.
const isMisspelledWithSuggestions = (params: Electron.ContextMenuParams) =>
params.misspelledWord.length > 0 && params.dictionarySuggestions.length > 0;
// Setup a server to download hunspell dictionary.
const server = http.createServer(async (req, res) => {
// The provided is minimal dict for testing only, full list of words can
@@ -70,6 +77,33 @@ ifdescribe(features.isBuiltinSpellCheckerEnabled())('spellchecker', { timeout },
const fixtures = path.resolve(import.meta.dirname, 'fixtures');
const preload = path.join(fixtures, 'module', 'preload-electron.js');
// On macOS the specs below go through the system spell checker, whose
// spelling server starts on first use. On a fresh CI machine that took 30 to
// 60 seconds, and until then AppKit times every request out ("NSSpellServer
// dataFromCheckingString timed out") and nothing is reported as misspelled.
// Blink does not ask again about text it has already checked, so the first
// spec's page never had its misspelling marked. Wait once, here, until the
// spell checker answers with suggestions for a misspelled word.
const spellServerStartTimeout = 120 * 1000;
beforeAll(async () => {
if (process.platform !== 'darwin') return;
const warmUpWindow = new BrowserWindow({
show: false,
webPreferences: { partition: 'spellchecker-warm-up', contextIsolation: false, preload }
});
try {
await warmUpWindow.loadFile(path.resolve(import.meta.dirname, './fixtures/chromium/spellchecker.html'));
await waitUntil(
async () =>
(await warmUpWindow.webContents.executeJavaScript('electron.webFrame.getWordSuggestions("typograpy")'))
.length > 0,
{ rate: 100, timeout: spellServerStartTimeout }
);
} finally {
await closeWindow(warmUpWindow);
}
}, spellServerStartTimeout + timeout);
const generateSpecs = (description: string, sandbox: boolean) => {
describe(description, () => {
beforeEach(async () => {
@@ -107,9 +141,7 @@ ifdescribe(features.isBuiltinSpellCheckerEnabled())('spellchecker', { timeout },
ifit(shouldRun)('should detect incorrectly spelled words as incorrect', async () => {
await w.webContents.executeJavaScript('document.body.querySelector("textarea").value = "typograpy"');
await w.webContents.executeJavaScript('document.body.querySelector("textarea").focus()');
const contextMenuParams = await rightClickUntil(
(contextMenuParams) => contextMenuParams.misspelledWord.length > 0
);
const contextMenuParams = await rightClickUntil(isMisspelledWithSuggestions);
expect(contextMenuParams.misspelledWord).toBe('typograpy');
expect(contextMenuParams.dictionarySuggestions.length).toBeGreaterThanOrEqual(1);
});
@@ -122,9 +154,7 @@ ifdescribe(features.isBuiltinSpellCheckerEnabled())('spellchecker', { timeout },
w.webContents.session.setSpellCheckerLanguages(['en-US']);
await w.webContents.executeJavaScript('document.body.querySelector("textarea").value = "typograpy"');
await w.webContents.executeJavaScript('document.body.querySelector("textarea").focus()');
const contextMenuParams = await rightClickUntil(
(contextMenuParams) => contextMenuParams.misspelledWord.length > 0
);
const contextMenuParams = await rightClickUntil(isMisspelledWithSuggestions);
expect(contextMenuParams.misspelledWord).toBe('typograpy');
expect(contextMenuParams.dictionarySuggestions.length).toBeGreaterThanOrEqual(1);
}
@@ -133,7 +163,7 @@ ifdescribe(features.isBuiltinSpellCheckerEnabled())('spellchecker', { timeout },
ifit(shouldRun)('should expose webFrame spellchecker correctly', async () => {
await w.webContents.executeJavaScript('document.body.querySelector("textarea").value = "typograpy"');
await w.webContents.executeJavaScript('document.body.querySelector("textarea").focus()');
await rightClickUntil((contextMenuParams) => contextMenuParams.misspelledWord.length > 0);
await rightClickUntil(isMisspelledWithSuggestions);
const callWebFrameFn = (expr: string) => w.webContents.executeJavaScript(`electron.webFrame.${expr}`);
@@ -151,7 +181,7 @@ ifdescribe(features.isBuiltinSpellCheckerEnabled())('spellchecker', { timeout },
ifit(shouldRun)('can be dynamically changed', async () => {
await w.webContents.executeJavaScript('document.body.querySelector("textarea").value = "typograpy"');
await w.webContents.executeJavaScript('document.body.querySelector("textarea").focus()');
await rightClickUntil((contextMenuParams) => contextMenuParams.misspelledWord.length > 0);
await rightClickUntil(isMisspelledWithSuggestions);
const callWebFrameFn = (expr: string) => w.webContents.executeJavaScript(`electron.webFrame.${expr}`);