Skip to content

Commit

Permalink
feat(utils): convert budgets to assertions
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickhulce committed Oct 7, 2019
1 parent f61a4ef commit 00cf1a4
Show file tree
Hide file tree
Showing 4 changed files with 339 additions and 42 deletions.
55 changes: 55 additions & 0 deletions packages/utils/src/budgets-converter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* @license Copyright 2019 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
'use strict';

/**
* @param {string|undefined} path
* @return {RegExp}
*/
function convertPathExpressionToRegExp(path) {
if (!path || path === '/') return /.*/;
const escapedPath = path
.split('*')
.map(part => part.replace(/([-[\]{}()*+?.,\\^|#\s])/g, '\\$1'))
.join('.*');
return new RegExp(`https?://[^/]+${escapedPath}`);
}

/**
* @param {Array<LHCI.AssertCommand.Budget>} budgets
* @return {LHCI.AssertCommand.Options}
*/
function convertBudgetsToAssertions(budgets) {
// @ts-ignore - .d.ts files no yet shipped with lighthouse
const Budget = require('lighthouse/lighthouse-core/config/budget.js');
// Normalize the definition using built-in Lighthouse validation.
budgets = Budget.initializeBudget(budgets);

/** @type {Array<LHCI.AssertCommand.BaseOptions>} */
const assertMatrix = [];

for (const budget of budgets) {
/** @type {LHCI.AssertCommand.Assertions} */
const assertions = {};

for (const {resourceType, budget: maxNumericValue} of budget.resourceCounts || []) {
assertions[`resource-summary.${resourceType}.count`] = ['error', {maxNumericValue}];
}

for (const {resourceType, budget: maxNumericValue} of budget.resourceSizes || []) {
assertions[`resource-summary.${resourceType}.size`] = ['error', {maxNumericValue}];
}

assertMatrix.push({
matchingUrlPattern: convertPathExpressionToRegExp(budget.path).source,
assertions,
});
}

return {assertMatrix};
}

module.exports = {convertPathExpressionToRegExp, convertBudgetsToAssertions};
140 changes: 98 additions & 42 deletions packages/utils/test/assertions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
/* eslint-env jest */

const lighthouseAllPreset = require('@lhci/utils/src/presets/all.js');
const {convertBudgetsToAssertions} = require('@lhci/utils/src/budgets-converter.js');
const {getAllAssertionResults} = require('@lhci/utils/src/assertions.js');

describe('getAllAssertionResults', () => {
Expand Down Expand Up @@ -322,12 +323,11 @@ describe('getAllAssertionResults', () => {
});

describe('budgets', () => {
it('should return assertion results for budgets UI', () => {
const assertions = {
'performance-budget': 'error',
};
let lhrWithBudget;
let lhrWithResourceSummary;

const lhr = {
beforeEach(() => {
lhrWithBudget = {
finalUrl: 'http://page-1.com',
audits: {
'performance-budget': {
Expand Down Expand Up @@ -358,8 +358,50 @@ describe('getAllAssertionResults', () => {
},
};

lhrWithResourceSummary = {
finalUrl: 'http://example.com',
audits: {
'resource-summary': {
details: {
items: [
{
resourceType: 'document',
label: 'Document',
requestCount: 1,
size: 1143,
},
{
resourceType: 'font',
label: 'Font',
requestCount: 2,
size: 86751,
},
{
resourceType: 'stylesheet',
label: 'Stylesheet',
requestCount: 3,
size: 9842,
},
{
resourceType: 'third-party',
label: 'Third-party',
requestCount: 7,
size: 94907,
},
],
},
},
},
};
});

it('should return assertion results for budgets UI', () => {
const assertions = {
'performance-budget': 'error',
};

// Include the LHR twice to exercise our de-duping logic.
const lhrs = [lhr, lhr];
const lhrs = [lhrWithBudget, lhrWithBudget];
const results = getAllAssertionResults({assertions}, lhrs);
expect(results).toEqual([
{
Expand Down Expand Up @@ -398,50 +440,64 @@ describe('getAllAssertionResults', () => {
]);
});

it('should assert budgets after the fact', () => {
const lhr = {
finalUrl: 'http://example.com',
audits: {
'resource-summary': {
details: {
items: [
{
resourceType: 'document',
label: 'Document',
requestCount: 1,
size: 1143,
},
{
resourceType: 'font',
label: 'Font',
requestCount: 2,
size: 86751,
},
{
resourceType: 'stylesheet',
label: 'Stylesheet',
requestCount: 3,
size: 9842,
},
{
resourceType: 'third-party',
label: 'Third-party',
requestCount: 7,
size: 94907,
},
],
},
},
it('should assert budgets natively', () => {
const budgets = [
{
resourceCounts: [
{resourceType: 'font', budget: 1},
{resourceType: 'third-party', budget: 5},
],
resourceSizes: [{resourceType: 'document', budget: 400}],
},
};
];

const lhrs = [lhrWithResourceSummary, lhrWithResourceSummary];
const results = getAllAssertionResults(convertBudgetsToAssertions(budgets), lhrs);
expect(results).toEqual([
{
url: 'http://example.com',
actual: 2,
auditId: 'resource-summary',
auditProperty: 'font.count',
expected: 1,
level: 'error',
name: 'maxNumericValue',
operator: '<=',
values: [2, 2],
},
{
url: 'http://example.com',
actual: 7,
auditId: 'resource-summary',
auditProperty: 'third-party.count',
expected: 5,
level: 'error',
name: 'maxNumericValue',
operator: '<=',
values: [7, 7],
},
{
url: 'http://example.com',
actual: 1143,
auditId: 'resource-summary',
auditProperty: 'document.size',
expected: 400,
level: 'error',
name: 'maxNumericValue',
operator: '<=',
values: [1143, 1143],
},
]);
});

it('should assert budgets after the fact', () => {
const assertions = {
'resource-summary.document.size': ['error', {maxNumericValue: 400}],
'resource-summary.font.count': ['warn', {maxNumericValue: 1}],
'resource-summary.third-party.count': ['warn', {maxNumericValue: 5}],
};

const lhrs = [lhr, lhr];
const lhrs = [lhrWithResourceSummary, lhrWithResourceSummary];
const results = getAllAssertionResults({assertions}, lhrs);
expect(results).toEqual([
{
Expand Down
129 changes: 129 additions & 0 deletions packages/utils/test/budgets-converter.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/**
* @license Copyright 2019 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
'use strict';

/* eslint-env jest */

const {
convertPathExpressionToRegExp,
convertBudgetsToAssertions,
} = require('@lhci/utils/src/budgets-converter.js');

describe('convertPathExpressionToRegExp', () => {
const pathMatch = (path, pattern) => {
const origin = 'https://example.com';
return convertPathExpressionToRegExp(pattern).test(origin + path);
};

it('matches root', () => {
expect(convertPathExpressionToRegExp('/').test('https://google.com')).toBe(true);
});

it('ignores origin', () => {
expect(convertPathExpressionToRegExp('/go').test('https://go.com/dogs')).toBe(false);
expect(convertPathExpressionToRegExp('/videos').test('https://yt.com/videos?id=')).toBe(true);
});

it('is case-sensitive', () => {
expect(convertPathExpressionToRegExp('/aaa').test('https://abc.com/aaa')).toBe(true);
expect(convertPathExpressionToRegExp('/aaa').test('https://abc.com/AAA')).toBe(false);
expect(convertPathExpressionToRegExp('/AAA').test('https://abc.com/aaa')).toBe(false);
});

it('matches all pages if path is not defined', () => {
expect(convertPathExpressionToRegExp(undefined).test('https://example.com')).toBe(true);
expect(convertPathExpressionToRegExp(undefined).test('https://example.com/dogs')).toBe(true);
});

it('handles patterns that do not contain * or $', () => {
expect(pathMatch('/anything', '/')).toBe(true);
expect(pathMatch('/anything', '/any')).toBe(true);
expect(pathMatch('/anything', '/anything')).toBe(true);
expect(pathMatch('/anything', '/anything1')).toBe(false);
});

it('handles patterns that do not contain * but contain $', () => {
expect(pathMatch('/fish.php', '/fish.php$')).toBe(true);
expect(pathMatch('/Fish.PHP', '/fish.php$')).toBe(false);
});

it('handles patterns that contain * but do not contain $', () => {
expect(pathMatch('/anything', '/*')).toBe(true);
expect(pathMatch('/fish', '/fish*')).toBe(true);
expect(pathMatch('/fishfood', '/*food')).toBe(true);
expect(pathMatch('/fish/food/and/other/things', '/*food')).toBe(true);
expect(pathMatch('/fis/', '/fish*')).toBe(false);
expect(pathMatch('/fish', '/fish*fish')).toBe(false);
});

it('handles patterns that contain * and $', () => {
expect(pathMatch('/fish.php', '/*.php$')).toBe(true);
expect(pathMatch('/folder/filename.php', '/folder*.php$')).toBe(true);
expect(pathMatch('/folder/filename.php', '/folder/filename*.php$')).toBe(true);
expect(pathMatch('/fish.php?species=', '/*.php$')).toBe(false);
expect(pathMatch('/filename.php/', '/folder*.php$')).toBe(false);
expect(pathMatch('/folder', '/folder*folder$')).toBe(false);
});
});

describe('convertBudgetsToAssertions', () => {
it('should convert budgets to assertions format', () => {
const budgets = [
{
resourceSizes: [
{
resourceType: 'script',
budget: 123,
},
{
resourceType: 'image',
budget: 456,
},
],
resourceCounts: [
{
resourceType: 'total',
budget: 100,
},
{
resourceType: 'third-party',
budget: 10,
},
],
},
{
path: '/second-path',
resourceSizes: [
{
resourceType: 'script',
budget: 1000,
},
],
},
];

const results = convertBudgetsToAssertions(budgets);
expect(results).toEqual({
assertMatrix: [
{
matchingUrlPattern: '.*',
assertions: {
'resource-summary.script.size': ['error', {maxNumericValue: 123}],
'resource-summary.image.size': ['error', {maxNumericValue: 456}],
'resource-summary.third-party.count': ['error', {maxNumericValue: 10}],
'resource-summary.total.count': ['error', {maxNumericValue: 100}],
},
},
{
matchingUrlPattern: 'https?:\\/\\/[^\\/]+\\/second\\-path',
assertions: {
'resource-summary.script.size': ['error', {maxNumericValue: 1000}],
},
},
],
});
});
});
Loading

0 comments on commit 00cf1a4

Please sign in to comment.