Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core(css-usage): prevent late stylesheet additions #15865

Merged
merged 3 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 23 additions & 25 deletions core/gather/gatherers/css-usage.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
/**
* @param {LH.Crdp.CSS.StyleSheetAddedEvent} event
*/
async _onStylesheetAdded(event) {
_onStylesheetAdded(event) {
if (!this._session) throw new Error('Session not initialized');
const styleSheetId = event.header.styleSheetId;
const sheetPromise = this._session.sendCommand('CSS.getStyleSheetText', {styleSheetId})
Expand Down Expand Up @@ -66,59 +66,60 @@
/**
* @param {LH.Gatherer.Context} context
*/
async startCSSUsageTracking(context) {
async startInstrumentation(context) {
const session = context.driver.defaultSession;
this._session = session;
session.on('CSS.styleSheetAdded', this._onStylesheetAdded);

// Calling `CSS.enable` will emit events for stylesheets currently on the page.
// We want to ignore these events in navigation mode because they are not relevant to the
// navigation that is about to happen. Adding the event listener *after* calling `CSS.enable`
// ensures that the events for pre-existing stylesheets are ignored.
const isNavigation = context.gatherMode === 'navigation';
if (!isNavigation) {
session.on('CSS.styleSheetAdded', this._onStylesheetAdded);
}

Check warning on line 80 in core/gather/gatherers/css-usage.js

View check run for this annotation

Codecov / codecov/patch

core/gather/gatherers/css-usage.js#L79-L80

Added lines #L79 - L80 were not covered by tests

await session.sendCommand('DOM.enable');
await session.sendCommand('CSS.enable');
await session.sendCommand('CSS.startRuleUsageTracking');

if (isNavigation) {
session.on('CSS.styleSheetAdded', this._onStylesheetAdded);
}
}


/**
* @param {LH.Gatherer.Context} context
*/
async stopCSSUsageTracking(context) {
async stopInstrumentation(context) {
const session = context.driver.defaultSession;
const coverageResponse = await session.sendCommand('CSS.stopRuleUsageTracking');
this._ruleUsage = coverageResponse.ruleUsage;
session.off('CSS.styleSheetAdded', this._onStylesheetAdded);
}

/**
* @param {LH.Gatherer.Context} context
*/
async startInstrumentation(context) {
if (context.gatherMode !== 'timespan') return;
await this.startCSSUsageTracking(context);
}
// Ensure we finish fetching all stylesheet contents before disabling the CSS domain
await Promise.all(this._sheetPromises.values());

/**
* @param {LH.Gatherer.Context} context
*/
async stopInstrumentation(context) {
if (context.gatherMode !== 'timespan') return;
await this.stopCSSUsageTracking(context);
await session.sendCommand('CSS.disable');
await session.sendCommand('DOM.disable');
}

/**
* @param {LH.Gatherer.Context} context
* @return {Promise<LH.Artifacts['CSSUsage']>}
*/
async getArtifact(context) {
const session = context.driver.defaultSession;
const executionContext = context.driver.executionContext;

if (context.gatherMode !== 'timespan') {
await this.startCSSUsageTracking(context);
if (context.gatherMode === 'snapshot') {
await this.startInstrumentation(context);

Check warning on line 116 in core/gather/gatherers/css-usage.js

View check run for this annotation

Codecov / codecov/patch

core/gather/gatherers/css-usage.js#L116

Added line #L116 was not covered by tests

// Force style to recompute.
// Doesn't appear to be necessary in newer versions of Chrome.
await executionContext.evaluateAsync('getComputedStyle(document.body)');

await this.stopCSSUsageTracking(context);
await this.stopInstrumentation(context);

Check warning on line 122 in core/gather/gatherers/css-usage.js

View check run for this annotation

Codecov / codecov/patch

core/gather/gatherers/css-usage.js#L122

Added line #L122 was not covered by tests
}

/** @type {Map<string, LH.Artifacts.CSSStyleSheetInfo>} */
Expand All @@ -140,9 +141,6 @@
dedupedStylesheets.set(sheet.content, sheet);
}

await session.sendCommand('CSS.disable');
await session.sendCommand('DOM.disable');

if (!this._ruleUsage) throw new Error('Issue collecting rule usages');

return {
Expand Down
4 changes: 1 addition & 3 deletions core/gather/gatherers/root-causes.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,7 @@ class RootCauses extends BaseGatherer {
rootCauses.layoutShifts[layoutShiftEvents.indexOf(event)] = r;
}

// Yeah this gatherer enabled it, and so you'd think it should disable it too...
// ...but we don't give gatherers their own session so this stomps on others.
// await driver.defaultSession.sendCommand('DOM.disable');
await driver.defaultSession.sendCommand('DOM.disable');
await driver.defaultSession.sendCommand('CSS.disable');

return rootCauses;
Expand Down
9 changes: 9 additions & 0 deletions core/test/fixtures/user-flows/css-change/end.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,14 @@
<body>
<style>h1 {color: red}</style>
<h1>Should have different styles than the start page</h1>
<button>Add more styles</button>
<script>
const button = document.querySelector('button');
button.onclick = () => {
const style = document.createElement('style');
style.innerHTML = 'h1 {font-size: 30px}';
document.body.append(style);
}
</script>
</body>
</html>
77 changes: 77 additions & 0 deletions core/test/scenarios/stylesheet-tracking.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import * as api from '../../index.js';
import {createTestState} from './pptr-test-utils.js';
import {LH_ROOT} from '../../../shared/root.js';

/* eslint-env browser */

describe('User flow stylesheet tracking', function() {
// eslint-disable-next-line no-invalid-this
this.timeout(120_000);

const state = createTestState();

state.installSetupAndTeardownHooks();

before(() => {
state.server.baseDir = `${LH_ROOT}/core/test/fixtures/user-flows/css-change`;
});

it('should correctly scope stylesheets based on mode', async () => {
const pageUrl = `${state.serverBaseUrl}/start.html`;
await state.page.goto(pageUrl, {waitUntil: ['networkidle0']});

const flow = await api.startFlow(state.page);

await flow.navigate(`${state.serverBaseUrl}/start.html`);

await flow.navigate(async () => {
await state.page.click('a');
});

await flow.startTimespan();
await state.page.click('button');
await flow.endTimespan();

const flowArtifacts = flow.createArtifactsJson();

const artifacts0 = flowArtifacts.gatherSteps[0].artifacts;
const artifacts1 = flowArtifacts.gatherSteps[1].artifacts;
const artifacts2 = flowArtifacts.gatherSteps[2].artifacts;

state.saveTrace(artifacts1.Trace);

const stylesheets0 = artifacts0.CSSUsage.stylesheets
.map(s => s.content.trim())
.sort((a, b) => a.localeCompare(b));
const stylesheets1 = artifacts1.CSSUsage.stylesheets
.map(s => s.content.trim())
.sort((a, b) => a.localeCompare(b));
const stylesheets2 = artifacts2.CSSUsage.stylesheets
.map(s => s.content.trim())
.sort((a, b) => a.localeCompare(b));

expect(stylesheets0).toEqual([
'body {\n border: 5px solid black;\n}',
'h1 {color: gray}',
]);

// Some stylesheets are pre-existing but they are out of scope for navigation mode
expect(stylesheets1).toEqual([
'body {\n border: 5px solid red;\n}',
'h1 {color: red}',
]);

// Some stylesheets are pre-existing and they are in in scope for timespan mode
expect(stylesheets2).toEqual([
'body {\n border: 5px solid red;\n}',
'h1 {color: red}',
'h1 {font-size: 30px}',
]);
});
});
Loading