mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
fix(detect): use system Chrome on Windows to stop GPU crash-loop window (#372)
On Windows, `impeccable detect <url>` flashed a persistent black window during scans. The scan uses puppeteer's bundled Chrome, which runs from an untrusted user-cache path; Windows blocks its GPU process, so it crash-loops and flashes a compositor surface on every retry. It is not a real application window (not in Alt+Tab, not clickable, invisible to window enumeration) and not malware. Prefer the system-installed Chrome via channel:'chrome' on Windows, which runs from a trusted location with a healthy GPU: no crash loop, no window. Fall back to the bundled browser when Chrome is not installed. Scoped to Windows only, so mac and linux keep the pinned bundled build for consistent measurement. Both render on hardware GPU, so contrast measurement is unaffected. Also routes both launch sites through one helper and fixes a pre-existing bug where detectUrl hardcoded headless:true instead of honoring options.headless. Tests: new tests/detect-url-launch.test.mjs covers the launch choice per platform (Windows prefers channel:'chrome' and falls back to bundled; non-Windows never attempts it), wired into the detector suite. Verified on Windows 11 / Chrome 150: zero GPU crashes, window gone, findings unchanged. This change was prepared with AI assistance.
This commit is contained in:
@@ -7,6 +7,28 @@ import { profileFindingsAsync, profileStep, profileStepAsync } from '../../profi
|
||||
import { captureVisualContrastCandidate } from '../visual/screenshot-contrast.mjs';
|
||||
import { checkContentHiddenAtRest } from '../../rules/checks.mjs';
|
||||
|
||||
// On Windows, puppeteer's bundled Chrome lives in a user-writable cache
|
||||
// directory. Its GPU process can be denied (STATUS_ACCESS_DENIED) by security
|
||||
// software or the GPU sandbox because it launches from an untrusted path.
|
||||
// Chrome then crash-loops the GPU process, and each relaunch briefly flashes a
|
||||
// compositor surface, the black window users report during `detect <url>`
|
||||
// (issue #372). The system-installed Chrome runs from a trusted location with a
|
||||
// healthy GPU, so channel:'chrome' avoids the crash entirely; both use hardware
|
||||
// GPU, so contrast measurement is unaffected. Scope this to Windows only: other
|
||||
// platforms do not have the bug, so they keep the pinned bundled build for
|
||||
// consistent measurement across machines. Fall back to bundled when the switch
|
||||
// fails (Chrome not installed, or channel resolution fails).
|
||||
async function launchBrowser(puppeteer, { headless = true, args = [] } = {}) {
|
||||
if (process.platform === 'win32') {
|
||||
try {
|
||||
return await puppeteer.default.launch({ channel: 'chrome', headless, args });
|
||||
} catch {
|
||||
// No system Chrome available; fall through to the bundled browser.
|
||||
}
|
||||
}
|
||||
return await puppeteer.default.launch({ headless, args });
|
||||
}
|
||||
|
||||
// Reveal sweep + invisible-text measurement for the content-hidden-at-rest
|
||||
// rule. Scrolls through the document with instant jumps (bypasses CSS
|
||||
// scroll-behavior: smooth) so IntersectionObserver / scroll reveal handlers
|
||||
@@ -178,7 +200,7 @@ async function detectUrl(url, options = {}) {
|
||||
phase: 'load',
|
||||
ruleId: 'launch-browser',
|
||||
target: url,
|
||||
}, () => puppeteer.default.launch({ headless: true, args: launchArgs }));
|
||||
}, () => launchBrowser(puppeteer, { headless: options?.headless ?? true, args: launchArgs }));
|
||||
const page = await profileStepAsync(profile, {
|
||||
engine: 'browser',
|
||||
phase: 'load',
|
||||
@@ -312,7 +334,7 @@ async function createBrowserDetector(options = {}) {
|
||||
throw new Error('puppeteer is required for URL scanning. Install: npm install puppeteer');
|
||||
}
|
||||
const launchArgs = options.launchArgs || (process.env.CI ? ['--no-sandbox', '--disable-setuid-sandbox'] : []);
|
||||
const browser = options.browser || await puppeteer.default.launch({
|
||||
const browser = options.browser || await launchBrowser(puppeteer, {
|
||||
headless: options.headless ?? true,
|
||||
args: launchArgs,
|
||||
});
|
||||
@@ -337,4 +359,4 @@ async function createBrowserDetector(options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
export { runVisualContrastFallback, detectUrl, createBrowserDetector };
|
||||
export { runVisualContrastFallback, detectUrl, createBrowserDetector, launchBrowser };
|
||||
|
||||
@@ -86,13 +86,14 @@ export const SUITES = {
|
||||
/^scripts\/(benchmark-detector|build-browser-detector|build-extension)\.js$/,
|
||||
/^site\/(pages\/detector|public\/antipattern|data\/anti-patterns-catalog\.js)/,
|
||||
/^tests\/design-system\.test\.mjs$/,
|
||||
/^tests\/(detect-antipatterns|inline-ignores|extension-build|fixtures\/antipatterns)/,
|
||||
/^tests\/(detect-antipatterns|detect-url-launch|inline-ignores|extension-build|fixtures\/antipatterns)/,
|
||||
],
|
||||
commands: [
|
||||
{
|
||||
runner: 'bun',
|
||||
files: [
|
||||
'tests/detect-antipatterns.test.js',
|
||||
'tests/detect-url-launch.test.mjs',
|
||||
'tests/inline-ignores.test.mjs',
|
||||
'tests/lib/detector-bundle.test.js',
|
||||
],
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { describe, test, expect, afterEach } from 'bun:test';
|
||||
import { launchBrowser } from '../cli/engine/engines/browser/detect-url.mjs';
|
||||
|
||||
// launchBrowser prefers the system-installed Chrome on Windows to dodge the
|
||||
// bundled-Chrome GPU crash-loop (issue #372), and keeps the pinned bundled
|
||||
// build everywhere else. The function takes the puppeteer module as a
|
||||
// parameter, so a fake lets us assert the launch strategy without a real
|
||||
// browser or a real OS.
|
||||
|
||||
const realPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
|
||||
|
||||
function setPlatform(value) {
|
||||
Object.defineProperty(process, 'platform', { value, configurable: true });
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, 'platform', realPlatform);
|
||||
});
|
||||
|
||||
function makePuppeteer({ failChannel = false } = {}) {
|
||||
const calls = [];
|
||||
const fakeBrowser = { __fake: true };
|
||||
return {
|
||||
calls,
|
||||
fakeBrowser,
|
||||
mod: {
|
||||
default: {
|
||||
async launch(opts) {
|
||||
calls.push(opts);
|
||||
if (failChannel && opts.channel === 'chrome') {
|
||||
throw new Error('Could not find Chrome (channel: chrome)');
|
||||
}
|
||||
return fakeBrowser;
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('launchBrowser', () => {
|
||||
test('Windows: prefers system Chrome via channel:chrome', async () => {
|
||||
setPlatform('win32');
|
||||
const p = makePuppeteer();
|
||||
const browser = await launchBrowser(p.mod, { headless: true, args: ['--foo'] });
|
||||
|
||||
expect(browser).toBe(p.fakeBrowser);
|
||||
expect(p.calls).toHaveLength(1);
|
||||
expect(p.calls[0].channel).toBe('chrome');
|
||||
expect(p.calls[0].headless).toBe(true);
|
||||
expect(p.calls[0].args).toEqual(['--foo']);
|
||||
});
|
||||
|
||||
test('Windows: falls back to bundled when system Chrome is unavailable', async () => {
|
||||
setPlatform('win32');
|
||||
const p = makePuppeteer({ failChannel: true });
|
||||
const browser = await launchBrowser(p.mod, { headless: true, args: [] });
|
||||
|
||||
expect(browser).toBe(p.fakeBrowser);
|
||||
expect(p.calls).toHaveLength(2);
|
||||
expect(p.calls[0].channel).toBe('chrome'); // first attempt
|
||||
expect(p.calls[1].channel).toBeUndefined(); // fallback: bundled, no channel
|
||||
});
|
||||
|
||||
test('non-Windows: uses bundled Chrome directly, no channel', async () => {
|
||||
setPlatform('linux');
|
||||
const p = makePuppeteer();
|
||||
const browser = await launchBrowser(p.mod, { headless: true, args: [] });
|
||||
|
||||
expect(browser).toBe(p.fakeBrowser);
|
||||
expect(p.calls).toHaveLength(1);
|
||||
expect(p.calls[0].channel).toBeUndefined();
|
||||
});
|
||||
|
||||
test('non-Windows: never attempts channel:chrome even if it would succeed', async () => {
|
||||
setPlatform('darwin');
|
||||
const p = makePuppeteer();
|
||||
await launchBrowser(p.mod, {});
|
||||
|
||||
expect(p.calls.every(c => c.channel === undefined)).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user