mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
Merge pull request #425 from vinaypokharkar/fix/detect-system-chrome-gpu-window
fix(detect): use system Chrome on Windows to stop GPU crash-loop window (#372)
This commit is contained in:
@@ -7,6 +7,38 @@ 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). If the bundled
|
||||
// launch then also fails, surface the original system-Chrome error as the
|
||||
// cause so the real failure is not lost.
|
||||
async function launchBrowser(puppeteer, { headless = true, args = [] } = {}) {
|
||||
let channelError;
|
||||
if (process.platform === 'win32') {
|
||||
try {
|
||||
return await puppeteer.default.launch({ channel: 'chrome', headless, args });
|
||||
} catch (err) {
|
||||
// System Chrome unavailable or unlaunchable; fall through to the bundled
|
||||
// browser, but keep the error in case the fallback fails too.
|
||||
channelError = err;
|
||||
}
|
||||
}
|
||||
try {
|
||||
return await puppeteer.default.launch({ headless, args });
|
||||
} catch (err) {
|
||||
if (channelError && err && err.cause === undefined) err.cause = channelError;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 +210,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 +344,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 +369,4 @@ async function createBrowserDetector(options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
export { runVisualContrastFallback, detectUrl, createBrowserDetector };
|
||||
export { runVisualContrastFallback, detectUrl, createBrowserDetector, launchBrowser };
|
||||
|
||||
@@ -87,13 +87,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|detect-cli-design-contamination|inline-ignores|extension-build|fixtures\/antipatterns)/,
|
||||
/^tests\/(detect-antipatterns|detect-cli-design-contamination|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