chrome-devtools-frontend
Version:
Chrome DevTools UI
325 lines (303 loc) • 15.1 kB
text/typescript
// Copyright 2023 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 Common from '../../core/common/common.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Protocol from '../../generated/protocol.js';
import type * as TextUtils from '../../models/text_utils/text_utils.js';
import {createTarget} from '../../testing/EnvironmentHelpers.js';
import {describeWithMockConnection} from '../../testing/MockConnection.js';
import {getMatchedStylesWithBlankRule, getMatchedStylesWithStylesheet} from '../../testing/StyleHelpers.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as Elements from './elements.js';
describeWithMockConnection('StylesPropertySection', () => {
let computedStyleModel: Elements.ComputedStyleModel.ComputedStyleModel;
beforeEach(() => {
computedStyleModel = new Elements.ComputedStyleModel.ComputedStyleModel();
});
it('contains specificity information', async () => {
const specificity = {a: 0, b: 1, c: 0};
const matchedStyles = await getMatchedStylesWithBlankRule(new SDK.CSSModel.CSSModel(createTarget()));
const section = new Elements.StylePropertiesSection.StylePropertiesSection(
new Elements.StylesSidebarPane.StylesSidebarPane(computedStyleModel), matchedStyles,
matchedStyles.nodeStyles()[0], 0, new Map(), new Map());
section.renderSelectors([{text: '.child', specificity}], [true], new WeakMap());
const selectorElement = section.element.querySelector('.selector');
assert.strictEqual(selectorElement?.textContent, '.child');
assert.deepEqual(section.element?.querySelector('devtools-tooltip')?.textContent?.trim(), 'Specificity: (0,1,0)');
});
it('renders selectors correctly', async () => {
const matchedStyles = await getMatchedStylesWithBlankRule(new SDK.CSSModel.CSSModel(createTarget()));
const section = new Elements.StylePropertiesSection.StylePropertiesSection(
new Elements.StylesSidebarPane.StylesSidebarPane(computedStyleModel), matchedStyles,
matchedStyles.nodeStyles()[0], 0, new Map(), new Map());
section.renderSelectors(
[{text: '.child', specificity: {a: 0, b: 2, c: 0}}, {text: '.item', specificity: {a: 0, b: 2, c: 0}}], [true],
new WeakMap());
const selectorElement = section.element.querySelector('.selector');
assert.deepEqual(selectorElement?.textContent, '.child, .item');
section.renderSelectors(
[{text: '.child', specificity: {a: 0, b: 2, c: 0}}, {text: '& .item', specificity: {a: 0, b: 2, c: 0}}], [true],
new WeakMap());
assert.deepEqual(selectorElement?.textContent, '.child, & .item');
section.renderSelectors(
[{text: '&.child', specificity: {a: 0, b: 2, c: 0}}, {text: '& .item', specificity: {a: 0, b: 2, c: 0}}],
[true], new WeakMap());
assert.deepEqual(selectorElement?.textContent, '&.child, & .item');
});
it('displays the proper sourceURL origin for constructed stylesheets', async () => {
const cssModel = createTarget().model(SDK.CSSModel.CSSModel);
assert.exists(cssModel);
const origin = Protocol.CSS.StyleSheetOrigin.Regular;
const styleSheetId = '0' as Protocol.CSS.StyleSheetId;
const range = {startLine: 0, endLine: 1, startColumn: 0, endColumn: 0};
const header =
{sourceURL: 'constructed.css', isMutable: true, isConstructed: true, hasSourceURL: true, length: 1, ...range};
const matchedPayload: Protocol.CSS.RuleMatch[] = [{
rule: {
selectorList: {selectors: [{text: 'div'}], text: 'div'},
origin,
styleSheetId,
style: {cssProperties: [{name: 'color', value: 'red'}], shorthandEntries: [], range},
},
matchingSelectors: [0],
}];
const matchedStyles =
await getMatchedStylesWithStylesheet(cssModel, origin, styleSheetId, header, {matchedPayload});
const rule = matchedStyles.nodeStyles()[0].parentRule;
const linkifier = sinon.createStubInstance(Components.Linkifier.Linkifier);
const originNode =
Elements.StylePropertiesSection.StylePropertiesSection.createRuleOriginNode(matchedStyles, linkifier, rule);
assert.strictEqual(originNode.textContent, '<style>');
sinon.assert.calledOnce(linkifier.linkifyCSSLocation);
assert.strictEqual(linkifier.linkifyCSSLocation.args[0][0].styleSheetId, styleSheetId);
assert.strictEqual(linkifier.linkifyCSSLocation.args[0][0].url, 'constructed.css');
});
it('displays the proper sourceMappingURL origin for constructed stylesheets', async () => {
const cssModel = createTarget().model(SDK.CSSModel.CSSModel);
assert.exists(cssModel);
const origin = Protocol.CSS.StyleSheetOrigin.Regular;
const styleSheetId = '0' as Protocol.CSS.StyleSheetId;
const range = {startLine: 0, endLine: 1, startColumn: 0, endColumn: 0};
const header: Partial<Protocol.CSS.CSSStyleSheetHeader> = {
sourceMapURL: 'http://example.com/constructed.css.map',
isMutable: true,
isConstructed: true,
length: 1,
...range,
};
const matchedPayload: Protocol.CSS.RuleMatch[] = [{
rule: {
selectorList: {selectors: [{text: 'div'}], text: 'div'},
origin,
styleSheetId,
style: {cssProperties: [{name: 'color', value: 'red'}], shorthandEntries: [], range},
},
matchingSelectors: [0],
}];
sinon.stub(SDK.PageResourceLoader.PageResourceLoader.instance(), 'loadResource').callsFake(url => Promise.resolve({
content: url === header.sourceMapURL ? '{"sources": []}' : '',
}));
const matchedStyles =
await getMatchedStylesWithStylesheet(cssModel, origin, styleSheetId, header, {matchedPayload});
const styleSheetHeader = cssModel.styleSheetHeaderForId(styleSheetId);
assert.exists(styleSheetHeader);
const sourceMap = await cssModel.sourceMapManager().sourceMapForClientPromise(styleSheetHeader);
assert.exists(sourceMap);
const rule = matchedStyles.nodeStyles()[0].parentRule;
const linkifier = sinon.createStubInstance(Components.Linkifier.Linkifier);
const originNode =
Elements.StylePropertiesSection.StylePropertiesSection.createRuleOriginNode(matchedStyles, linkifier, rule);
assert.strictEqual(originNode.textContent, 'constructed stylesheet');
sinon.assert.calledOnce(linkifier.linkifyCSSLocation);
// Since we already asserted that a sourcemap exists for our header, it's sufficient to check that
// linkifyCSSLocation has been called. Verifying that linkifyCSSLocation applies source mapping is out of scope
// for this unit under test.
assert.strictEqual(linkifier.linkifyCSSLocation.args[0][0].styleSheetId, styleSheetId);
assert.strictEqual(linkifier.linkifyCSSLocation.args[0][0].url, '');
});
it('properly renders ancestor rules', async () => {
Common.Settings.Settings.instance().moduleSetting('text-editor-indent').set(' ');
const cssModel = createTarget().model(SDK.CSSModel.CSSModel);
assert.exists(cssModel);
const stylesSidebarPane = new Elements.StylesSidebarPane.StylesSidebarPane(computedStyleModel);
const origin = Protocol.CSS.StyleSheetOrigin.Regular;
const styleSheetId = '0' as Protocol.CSS.StyleSheetId;
const range = {startLine: 0, startColumn: 0, endLine: 0, endColumn: 6};
{
const matchedPayload: Protocol.CSS.RuleMatch[] = [{
rule: {
nestingSelectors: ['body', '& ul', 'div'],
ruleTypes: [
Protocol.CSS.CSSRuleType.StyleRule,
Protocol.CSS.CSSRuleType.StyleRule,
Protocol.CSS.CSSRuleType.StyleRule,
],
selectorList: {selectors: [{text: 'div'}], text: 'div'},
origin,
style: {cssProperties: [{name: 'color', value: 'red'}], shorthandEntries: []},
},
matchingSelectors: [0],
}];
const matchedStyles =
await getMatchedStylesWithStylesheet(cssModel, origin, styleSheetId, {...range}, {matchedPayload});
const declaration = matchedStyles.nodeStyles()[0];
assert.exists(declaration);
const section = new Elements.StylePropertiesSection.StylePropertiesSection(
stylesSidebarPane, matchedStyles, declaration, 0, null, null);
assert.strictEqual(section.element.textContent, 'div { & ul { body { div { } } }}');
}
{
const matchedPayload: Protocol.CSS.RuleMatch[] = [{
rule: {
nestingSelectors: ['body', 'div'],
ruleTypes: [
Protocol.CSS.CSSRuleType.StyleRule,
Protocol.CSS.CSSRuleType.StyleRule,
],
selectorList: {selectors: [], text: ''},
origin,
style: {cssProperties: [{name: 'color', value: 'red'}], shorthandEntries: []},
},
matchingSelectors: [0],
}];
const matchedStyles =
await getMatchedStylesWithStylesheet(cssModel, origin, styleSheetId, {...range}, {matchedPayload});
const declaration = matchedStyles.nodeStyles()[0];
assert.exists(declaration);
const section = new Elements.StylePropertiesSection.StylePropertiesSection(
stylesSidebarPane, matchedStyles, declaration, 0, null, null);
assert.strictEqual(section.element.textContent, 'div { body { }}');
}
});
it('updates property rule property names', async () => {
const cssModel = createTarget().model(SDK.CSSModel.CSSModel);
assert.exists(cssModel);
const stylesSidebarPane = new Elements.StylesSidebarPane.StylesSidebarPane(computedStyleModel);
const origin = Protocol.CSS.StyleSheetOrigin.Regular;
const styleSheetId = '0' as Protocol.CSS.StyleSheetId;
const range = {startLine: 0, startColumn: 0, endLine: 0, endColumn: 6};
const propertyName: Protocol.CSS.Value = {text: '--prop', range};
const propertyRuleStyle: Protocol.CSS.CSSStyle = {
cssProperties: [
{name: 'inherits', value: 'false'},
{name: 'initial-value', value: 'red'},
{name: 'syntax', value: '"<color>"'},
],
shorthandEntries: [],
};
const propertyRules: Protocol.CSS.CSSPropertyRule[] = [{
propertyName,
origin,
style: propertyRuleStyle,
styleSheetId,
}];
const matchedPayload: Protocol.CSS.RuleMatch[] = [{
rule: {
selectorList: {selectors: [{text: 'div'}], text: 'div'},
origin,
style: {cssProperties: [{name: propertyName.text, value: 'red'}], shorthandEntries: []},
},
matchingSelectors: [0],
}];
const matchedStyles = await getMatchedStylesWithStylesheet(
cssModel, origin, styleSheetId, {...range}, {propertyRules, matchedPayload});
function assertIsPropertyRule(rule: SDK.CSSRule.CSSRule|null): asserts rule is SDK.CSSRule.CSSPropertyRule {
assert.instanceOf(rule, SDK.CSSRule.CSSPropertyRule);
}
const declaration = matchedStyles.getRegisteredProperty(propertyName.text)?.style();
assert.exists(declaration);
const rule = declaration.parentRule;
assertIsPropertyRule(rule);
const section = new Elements.StylePropertiesSection.RegisteredPropertiesSection(
stylesSidebarPane, matchedStyles, declaration, 0, propertyName.text, /* expandedByDefault=*/ true);
const forceUpdateSpy = sinon.spy(stylesSidebarPane, 'forceUpdate');
const setNameSpy = sinon.stub(cssModel, 'setPropertyRulePropertyName');
setNameSpy.returns(Promise.resolve(true));
await section.setHeaderText(rule, propertyName.text);
assert.isTrue(forceUpdateSpy.calledAfter(setNameSpy));
sinon.assert.calledOnceWithExactly(
setNameSpy, styleSheetId,
sinon.match(
(r: TextUtils.TextRange.TextRange) => r.startLine === range.startLine &&
r.startColumn === range.startColumn && r.endLine === range.endLine && r.endColumn === range.endColumn),
propertyName.text);
});
it('renders braces correctly with a non-style-rule section', async () => {
Common.Settings.Settings.instance().moduleSetting('text-editor-indent').set(' ');
const cssModel = createTarget().model(SDK.CSSModel.CSSModel);
assert.exists(cssModel);
const stylesSidebarPane = new Elements.StylesSidebarPane.StylesSidebarPane(computedStyleModel);
const origin = Protocol.CSS.StyleSheetOrigin.Regular;
const styleSheetId = '0' as Protocol.CSS.StyleSheetId;
const range = {startLine: 0, startColumn: 0, endLine: 0, endColumn: 6};
const fontPaletteValuesRule = {
styleSheetId,
origin,
style: {
range,
cssProperties: [],
shorthandEntries: [],
},
fontPaletteName: {
range,
text: '--palette-name',
},
};
const matchedStyles =
await getMatchedStylesWithStylesheet(cssModel, origin, styleSheetId, {...range}, {fontPaletteValuesRule});
const declaration = matchedStyles.fontPaletteValuesRule()?.style;
assert.exists(declaration);
const section = new Elements.StylePropertiesSection.FontPaletteValuesRuleSection(
stylesSidebarPane, matchedStyles, declaration, 0);
assert.strictEqual(section.element.textContent, '{}');
});
it('renders active and inactive position-try rule sections correctly', async () => {
const cssModel = createTarget().model(SDK.CSSModel.CSSModel);
assert.exists(cssModel);
const stylesSidebarPane = new Elements.StylesSidebarPane.StylesSidebarPane(computedStyleModel);
const origin = Protocol.CSS.StyleSheetOrigin.Regular;
const styleSheetId = '0' as Protocol.CSS.StyleSheetId;
const range = {startLine: 0, startColumn: 0, endLine: 0, endColumn: 6};
const positionTryRules = [
{
styleSheetId,
origin,
name: {
text: '--try-1',
},
style: {
range,
cssProperties: [],
shorthandEntries: [],
},
active: true,
},
{
styleSheetId,
origin,
name: {
text: '--try-2',
},
style: {
range,
cssProperties: [],
shorthandEntries: [],
},
active: false,
},
];
const matchedStyles =
await getMatchedStylesWithStylesheet(cssModel, origin, styleSheetId, {...range}, {positionTryRules});
const declaration1 = matchedStyles.positionTryRules()[0].style;
const declaration2 = matchedStyles.positionTryRules()[1].style;
assert.exists(declaration1);
assert.exists(declaration2);
const section1 = new Elements.StylePropertiesSection.PositionTryRuleSection(
stylesSidebarPane, matchedStyles, declaration1, 0, positionTryRules[0].active);
const section2 = new Elements.StylePropertiesSection.PositionTryRuleSection(
stylesSidebarPane, matchedStyles, declaration1, 1, positionTryRules[1].active);
assert.isFalse(section1.propertiesTreeOutline.element.classList.contains('no-affect'));
assert.isTrue(section2.propertiesTreeOutline.element.classList.contains('no-affect'));
});
});