chrome-devtools-frontend
Version:
Chrome DevTools UI
360 lines (294 loc) • 11.7 kB
text/typescript
// Copyright 2024 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as SDK from '../../../core/sdk/sdk.js';
import * as CrUXManager from '../../../models/crux-manager/crux-manager.js';
import {renderElementIntoDOM} from '../../../testing/DOMHelpers.js';
import {describeWithMockConnection} from '../../../testing/MockConnection.js';
import * as RenderCoordinator from '../../../ui/components/render_coordinator/render_coordinator.js';
import * as UI from '../../../ui/legacy/legacy.js';
import * as Components from './components.js';
function getOriginMappings(view: Element): Array<[string, string, string?]> {
const rows = view.querySelector('.vbox')!.shadowRoot!.querySelectorAll('.origin-mapping-row:not(.header)');
return Array.from(rows).map(row => {
const warning = row.querySelector<HTMLElement>('.origin-warning-icon');
return [
row.querySelector('.development-origin .origin')!.textContent || '',
row.querySelector('.production-origin .origin')!.textContent || '',
warning?.title,
];
});
}
function getDevInput(view: Element): HTMLInputElement|null {
return view.querySelector('.vbox')!.shadowRoot!.querySelector('.development-origin-input input');
}
function getProdInput(view: Element): HTMLInputElement|null {
return view.querySelector('.vbox')!.shadowRoot!.querySelector('.production-origin-input input');
}
function getConfirmButton(view: Element): HTMLElementTagNameMap['devtools-button']|null {
const buttons = view.querySelector('.vbox')!.shadowRoot!.querySelectorAll('devtools-button');
return Array.from(buttons).find(b => b.textContent === 'Add') || null;
}
function getCancelButton(view: Element): HTMLElementTagNameMap['devtools-button']|null {
const buttons = view.querySelector('.vbox')!.shadowRoot!.querySelectorAll('devtools-button');
return Array.from(buttons).find(b => b.textContent === 'Cancel') || null;
}
function getValidationErrors(view: Element): string {
const errors =
view.querySelector('.vbox')!.shadowRoot!.querySelector<HTMLElement>('.list-widget-input-validation-error');
return errors?.innerText || '';
}
function mockResponse(): CrUXManager.CrUXResponse {
return {
record: {
key: {},
metrics: {
largest_contentful_paint: {
histogram: [
{start: 0, end: 2500, density: 0.5},
{start: 2500, end: 4000, density: 0.3},
{start: 4000, density: 0.2},
],
percentiles: {p75: 1000},
},
cumulative_layout_shift: {
histogram: [
{start: 0, end: 0.1, density: 0.1},
{start: 0.1, end: 0.25, density: 0.1},
{start: 0.25, density: 0.8},
],
percentiles: {p75: 0.25},
},
},
collectionPeriod: {
firstDate: {year: 2024, month: 1, day: 1},
lastDate: {year: 2024, month: 1, day: 29},
},
},
};
}
function createOriginMap(): Components.OriginMap.OriginMap {
const root = document.createElement('div');
renderElementIntoDOM(root);
const widget = new UI.Widget.Widget();
widget.markAsRoot();
widget.show(root);
const view = new Components.OriginMap.OriginMap();
widget.contentElement.append(view);
return view;
}
describeWithMockConnection('OriginMap', () => {
let cruxManager: CrUXManager.CrUXManager;
let targetManager: SDK.TargetManager.TargetManager;
let mockFieldData: CrUXManager.PageResult;
let getFieldDataStub: sinon.SinonStub;
let mockInspectedURL: sinon.SinonStub;
beforeEach(async () => {
cruxManager = CrUXManager.CrUXManager.instance({forceNew: true});
targetManager = SDK.TargetManager.TargetManager.instance({forceNew: true});
getFieldDataStub = sinon.stub(cruxManager, 'getFieldDataForPage').callsFake(async () => mockFieldData);
mockInspectedURL = sinon.stub(targetManager, 'inspectedURL').returns('http://localhost:8080');
mockFieldData = {
'origin-ALL': mockResponse(),
'origin-DESKTOP': null,
'origin-PHONE': null,
'origin-TABLET': null,
'url-ALL': null,
'url-DESKTOP': null,
'url-PHONE': null,
'url-TABLET': null,
warnings: [],
};
cruxManager.getConfigSetting().set({enabled: true, override: ''});
});
afterEach(async () => {
getFieldDataStub.restore();
mockInspectedURL.restore();
});
it('should show mappings from setting', async () => {
cruxManager.getConfigSetting().set({
enabled: true,
override: '',
originMappings: [
{developmentOrigin: 'http://localhost:8080', productionOrigin: 'https://example.com'},
],
});
const view = createOriginMap();
await RenderCoordinator.done();
const mappings = getOriginMappings(view);
assert.deepEqual(mappings, [
['http://localhost:8080', 'https://example.com', undefined],
]);
});
it('should show warning if there is no field data', async () => {
mockFieldData['origin-ALL'] = null;
cruxManager.getConfigSetting().set({
enabled: true,
override: '',
originMappings: [
{developmentOrigin: 'http://localhost:8080', productionOrigin: 'https://no-data.com'},
],
});
const view = createOriginMap();
await RenderCoordinator.done();
const mappings = getOriginMappings(view);
assert.deepEqual(mappings, [
[
'http://localhost:8080',
'https://no-data.com',
'The Chrome UX Report does not have sufficient real user data for this page.',
],
]);
});
it('should not show warning if field is disabled', async () => {
mockFieldData['origin-ALL'] = null;
cruxManager.getConfigSetting().set({
enabled: false,
override: '',
originMappings: [
{developmentOrigin: 'http://localhost:8080', productionOrigin: 'https://no-data.com'},
],
});
const view = createOriginMap();
await RenderCoordinator.done();
const mappings = getOriginMappings(view);
assert.deepEqual(mappings, [
['http://localhost:8080', 'https://no-data.com', undefined],
]);
});
it('should react to setting changes', async () => {
cruxManager.getConfigSetting().set({
enabled: true,
override: '',
originMappings: [
{developmentOrigin: 'http://localhost:8080', productionOrigin: 'https://example.com'},
],
});
const view = createOriginMap();
await RenderCoordinator.done();
{
const mappings = getOriginMappings(view);
assert.deepEqual(mappings, [
['http://localhost:8080', 'https://example.com', undefined],
]);
}
cruxManager.getConfigSetting().set({
enabled: true,
override: '',
originMappings: [
{developmentOrigin: 'http://localhost:8080', productionOrigin: 'https://example.com'},
{developmentOrigin: 'http://localhost:8081', productionOrigin: 'https://example2.com'},
],
});
await RenderCoordinator.done();
{
const mappings = getOriginMappings(view);
assert.deepEqual(mappings, [
['http://localhost:8080', 'https://example.com', undefined],
['http://localhost:8081', 'https://example2.com', undefined],
]);
}
});
it('should pre-fill new mapping fields', async () => {
const originMap = createOriginMap();
originMap.startCreation();
await RenderCoordinator.done();
const devInput = getDevInput(originMap)!;
assert.strictEqual(devInput.value, 'http://localhost:8080');
const prodInput = getProdInput(originMap)!;
assert.strictEqual(prodInput.value, '');
});
it('should accept new entries', async () => {
const originMap = createOriginMap();
originMap.startCreation();
await RenderCoordinator.done();
const devInput = getDevInput(originMap)!;
devInput.value = 'http://localhost:8080';
devInput.dispatchEvent(new Event('input'));
const prodInput = getProdInput(originMap)!;
prodInput.value = 'https://example.com';
prodInput.dispatchEvent(new Event('input'));
await RenderCoordinator.done();
getConfirmButton(originMap)!.click();
await RenderCoordinator.done();
const mappings = getOriginMappings(originMap);
assert.deepEqual(mappings, [
['http://localhost:8080', 'https://example.com', undefined],
]);
});
it('should ignore cancelled entries', async () => {
const originMap = createOriginMap();
originMap.startCreation();
await RenderCoordinator.done();
const devInput = getDevInput(originMap)!;
devInput.value = 'http://localhost:8080';
devInput.dispatchEvent(new Event('input'));
const prodInput = getProdInput(originMap)!;
prodInput.value = 'https://example.com';
prodInput.dispatchEvent(new Event('input'));
await RenderCoordinator.done();
getCancelButton(originMap)!.click();
await RenderCoordinator.done();
const mappings = getOriginMappings(originMap);
assert.deepEqual(mappings, []);
assert.isNull(getDevInput(originMap));
assert.isNull(getProdInput(originMap));
});
it('should coerce inputs to origin values', async () => {
const originMap = createOriginMap();
originMap.startCreation();
await RenderCoordinator.done();
const devInput = getDevInput(originMap)!;
devInput.value = 'http://localhost:8080/path/to/something';
devInput.dispatchEvent(new Event('input'));
const prodInput = getProdInput(originMap)!;
prodInput.value = 'https://example.com?hello';
prodInput.dispatchEvent(new Event('input'));
await RenderCoordinator.done();
getConfirmButton(originMap)!.click();
await RenderCoordinator.done();
const mappings = getOriginMappings(originMap);
assert.deepEqual(mappings, [
['http://localhost:8080', 'https://example.com', undefined],
]);
});
it('should show errors from invalid origins', async () => {
const originMap = createOriginMap();
originMap.startCreation();
await RenderCoordinator.done();
const devInput = getDevInput(originMap)!;
devInput.value = 'bad-origin';
devInput.dispatchEvent(new Event('input'));
const prodInput = getProdInput(originMap)!;
prodInput.value = 'jj**Sdafsdf';
prodInput.dispatchEvent(new Event('input'));
await RenderCoordinator.done();
const errors = getValidationErrors(originMap);
assert.deepEqual(errors, '"bad-origin" is not a valid origin or URL.\n"jj**Sdafsdf" is not a valid origin or URL.');
const confirmButton = getConfirmButton(originMap);
assert.isTrue(confirmButton!.shadowRoot?.querySelector('button')!.disabled);
});
it('should show warning for duplicate dev origin', async () => {
cruxManager.getConfigSetting().set({
enabled: true,
override: '',
originMappings: [
{developmentOrigin: 'http://localhost:8080', productionOrigin: 'https://example.com'},
],
});
const originMap = createOriginMap();
originMap.startCreation();
await RenderCoordinator.done();
const devInput = getDevInput(originMap)!;
devInput.value = 'http://localhost:8080';
devInput.dispatchEvent(new Event('input'));
const prodInput = getProdInput(originMap)!;
prodInput.value = 'https://example2.com';
prodInput.dispatchEvent(new Event('input'));
await RenderCoordinator.done();
const errors = getValidationErrors(originMap);
assert.deepEqual(errors, '"http://localhost:8080" is already mapped to a production origin.');
const confirmButton = getConfirmButton(originMap);
assert.isFalse(confirmButton!.shadowRoot?.querySelector('button')!.disabled);
});
});