mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-09-28 11:22:57 +08:00
feat: update css formatter class to add matched rules (#2713)
Updates CssFormatter class to support matched CSS rules It also adds formatting for ancestor rules, including `@media`, `@supports`, `@layer`, `@container`, `@scope`, `@starting-style`, and nested styles Co-authored-by: Samiya Caur <samiyac@chromium.org>
This commit is contained in:
co-authored by
Samiya Caur
parent
aa255629f2
commit
8c8616fcc4
@@ -7,8 +7,21 @@
|
||||
import {DevTools} from '../third_party/index.js';
|
||||
import type {MatchedStyles} from '../tools/ToolDefinition.js';
|
||||
|
||||
export type UidResolver = (backendNodeId: number) => string | undefined;
|
||||
export type ContainerQuery =
|
||||
DevTools.CSSRule.CSSStyleRule['containerQueries'][number];
|
||||
|
||||
export interface ResolvedContainerDetails {
|
||||
container?: {
|
||||
uid?: string;
|
||||
selector: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CssFormatterOptions {
|
||||
uid: string;
|
||||
resolveUid?: UidResolver;
|
||||
containerDetails?: Map<ContainerQuery, ResolvedContainerDetails>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,6 +34,41 @@ export interface CssFormatterOptions {
|
||||
export type CssPropertyStatus =
|
||||
'active' | 'overloaded' | 'invalid' | 'disabled';
|
||||
|
||||
export type AncestorCSSRule =
|
||||
| {
|
||||
type: 'layer';
|
||||
name?: string;
|
||||
}
|
||||
| {
|
||||
type: 'media' | 'supports' | 'navigation';
|
||||
query: string;
|
||||
}
|
||||
| {
|
||||
type: 'scope';
|
||||
query?: string;
|
||||
}
|
||||
| {
|
||||
type: 'container';
|
||||
query: string;
|
||||
name?: string;
|
||||
container?: {
|
||||
uid?: string;
|
||||
selector: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'starting-style';
|
||||
}
|
||||
| {
|
||||
type: 'nesting';
|
||||
selector: string;
|
||||
}
|
||||
| {
|
||||
type: 'at-rule';
|
||||
atRuleType: string;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export interface StructuredCssProperty {
|
||||
name: string;
|
||||
value: string;
|
||||
@@ -41,7 +89,17 @@ export interface AnimationRule {
|
||||
properties: StructuredCssProperty[];
|
||||
}
|
||||
|
||||
export type CascadeRule = NodeStyleRule | AnimationRule;
|
||||
export interface MatchedRule {
|
||||
type: 'matched';
|
||||
selector: string;
|
||||
matchingSelectors?: string[];
|
||||
source?: string;
|
||||
isUserAgent?: boolean;
|
||||
ancestors?: AncestorCSSRule[];
|
||||
properties: StructuredCssProperty[];
|
||||
}
|
||||
|
||||
export type CascadeRule = NodeStyleRule | AnimationRule | MatchedRule;
|
||||
|
||||
export interface StructuredCssStyles {
|
||||
element: {
|
||||
@@ -89,6 +147,224 @@ class IndentedWriter {
|
||||
}
|
||||
}
|
||||
|
||||
function getFilenameFromUrl(sheetUrl: string): string {
|
||||
const parsed = new DevTools.Common.ParsedURL.ParsedURL(sheetUrl);
|
||||
if (parsed.isDataURL()) {
|
||||
return 'data-uri';
|
||||
}
|
||||
if (parsed.isBlobURL()) {
|
||||
return 'blob';
|
||||
}
|
||||
if (parsed.lastPathComponent) {
|
||||
return parsed.lastPathComponent;
|
||||
}
|
||||
return 'index';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the human-readable source location
|
||||
*
|
||||
* Checks in precedence order:
|
||||
* 1. Special origins: 'user agent stylesheet', 'injected stylesheet', 'via inspector', 'constructed stylesheet'.
|
||||
* 2. 1-based line coordinates from rule header or style range.
|
||||
* 3. File source: `<style>` for inline sheets, `(index)` for root documents, or filename for external stylesheets.
|
||||
*/
|
||||
function getSourceLocation(rule: DevTools.CSSRule.CSSRule): string {
|
||||
if (rule.isUserAgent?.()) {
|
||||
return 'user agent stylesheet';
|
||||
}
|
||||
if (rule.isInjected?.()) {
|
||||
return 'injected stylesheet';
|
||||
}
|
||||
if (rule.isViaInspector?.()) {
|
||||
return 'via inspector';
|
||||
}
|
||||
if (rule.header?.isConstructedByNew?.() && !rule.sourceURL) {
|
||||
return 'constructed stylesheet';
|
||||
}
|
||||
|
||||
let locSuffix = '';
|
||||
if (rule instanceof DevTools.CSSRule.CSSStyleRule) {
|
||||
const lineNum = rule.lineNumberInSource(0);
|
||||
if (lineNum >= 0) {
|
||||
locSuffix = `:${lineNum + 1}`;
|
||||
}
|
||||
} else if (rule.header && rule.style?.range) {
|
||||
locSuffix = `:${rule.header.lineNumberInSource(rule.style.range.startLine) + 1}`;
|
||||
}
|
||||
|
||||
const sheetUrl = rule.sourceURL;
|
||||
if (!sheetUrl) {
|
||||
return `<style>${locSuffix}`;
|
||||
}
|
||||
|
||||
return `${getFilenameFromUrl(sheetUrl)}${locSuffix}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the subset of selectors in a comma-separated selector group that
|
||||
* actually matched the target element. Returns undefined if single selector
|
||||
* or no filter is available.
|
||||
*/
|
||||
function getMatchingSelectors(
|
||||
matchedStyles: MatchedStyles,
|
||||
rule: DevTools.CSSRule.CSSStyleRule,
|
||||
): string[] | undefined {
|
||||
if (!rule.selectors || rule.selectors.length <= 1) {
|
||||
return undefined;
|
||||
}
|
||||
const indexes = matchedStyles.getMatchingSelectors?.(rule);
|
||||
if (!indexes || indexes.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const indexSet = new Set(indexes);
|
||||
const matching: string[] = [];
|
||||
for (const [idx, sel] of rule.selectors.entries()) {
|
||||
if (indexSet.has(idx)) {
|
||||
matching.push(sel.text);
|
||||
}
|
||||
}
|
||||
return matching.length > 0 ? matching : undefined;
|
||||
}
|
||||
|
||||
function createLayerAncestor(layer?: {
|
||||
text?: string;
|
||||
}): AncestorCSSRule | undefined {
|
||||
if (!layer) {
|
||||
return undefined;
|
||||
}
|
||||
return {type: 'layer', ...(layer.text ? {name: layer.text} : {})};
|
||||
}
|
||||
|
||||
function createQueryAncestor(
|
||||
type: 'media' | 'scope' | 'supports' | 'navigation',
|
||||
query?: {text?: string},
|
||||
): AncestorCSSRule | undefined {
|
||||
if (!query) {
|
||||
return undefined;
|
||||
}
|
||||
if (type === 'scope') {
|
||||
return {type, ...(query.text ? {query: query.text} : {})};
|
||||
}
|
||||
return query.text ? {type, query: query.text} : undefined;
|
||||
}
|
||||
|
||||
function createContainerQueryAncestor(
|
||||
containerQuery?: ContainerQuery,
|
||||
containerDetails?: Map<ContainerQuery, ResolvedContainerDetails>,
|
||||
): AncestorCSSRule | undefined {
|
||||
if (!containerQuery) {
|
||||
return undefined;
|
||||
}
|
||||
const details = containerDetails?.get(containerQuery);
|
||||
return {
|
||||
type: 'container',
|
||||
query: containerQuery.text ?? '',
|
||||
...(containerQuery.name ? {name: containerQuery.name} : {}),
|
||||
...(details?.container ? {container: details.container} : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects enclosing ancestor rules (@media, @container, @supports, @layer,
|
||||
* @scope, @starting-style, @navigation, and CSS nesting) for a style rule.
|
||||
*/
|
||||
function collectAncestorRules(
|
||||
rule: DevTools.CSSRule.CSSStyleRule,
|
||||
containerDetails?: Map<ContainerQuery, ResolvedContainerDetails>,
|
||||
): AncestorCSSRule[] | undefined {
|
||||
if (!rule.ruleTypes || rule.ruleTypes.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let mediaIndex = 0;
|
||||
let containerIndex = 0;
|
||||
let scopeIndex = 0;
|
||||
let supportsIndex = 0;
|
||||
let nestingIndex = 0;
|
||||
let layerIndex = 0;
|
||||
let navigationsIndex = 0;
|
||||
|
||||
const ancestors: AncestorCSSRule[] = [];
|
||||
|
||||
for (const ruleType of rule.ruleTypes) {
|
||||
let item: AncestorCSSRule | undefined;
|
||||
switch (ruleType) {
|
||||
case DevTools.Protocol.CSS.CSSRuleType.MediaRule:
|
||||
item = createQueryAncestor('media', rule.media?.[mediaIndex++]);
|
||||
break;
|
||||
case DevTools.Protocol.CSS.CSSRuleType.ContainerRule:
|
||||
item = createContainerQueryAncestor(
|
||||
rule.containerQueries?.[containerIndex++],
|
||||
containerDetails,
|
||||
);
|
||||
break;
|
||||
case DevTools.Protocol.CSS.CSSRuleType.LayerRule:
|
||||
item = createLayerAncestor(rule.layers?.[layerIndex++]);
|
||||
break;
|
||||
case DevTools.Protocol.CSS.CSSRuleType.ScopeRule:
|
||||
item = createQueryAncestor('scope', rule.scopes?.[scopeIndex++]);
|
||||
break;
|
||||
case DevTools.Protocol.CSS.CSSRuleType.SupportsRule:
|
||||
item = createQueryAncestor(
|
||||
'supports',
|
||||
rule.supports?.[supportsIndex++],
|
||||
);
|
||||
break;
|
||||
case DevTools.Protocol.CSS.CSSRuleType.StartingStyleRule:
|
||||
item = {type: 'starting-style'};
|
||||
break;
|
||||
case DevTools.Protocol.CSS.CSSRuleType.StyleRule: {
|
||||
const selector = rule.nestingSelectors?.[nestingIndex++];
|
||||
if (selector) {
|
||||
item = {type: 'nesting', selector};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case DevTools.Protocol.CSS.CSSRuleType.NavigationRule:
|
||||
item = createQueryAncestor(
|
||||
'navigation',
|
||||
rule.navigations?.[navigationsIndex++],
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (item) {
|
||||
ancestors.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
ancestors.reverse();
|
||||
return ancestors.length > 0 ? ancestors : undefined;
|
||||
}
|
||||
|
||||
interface RuleMetadata {
|
||||
ancestors?: AncestorCSSRule[];
|
||||
matchingSelectors?: string[];
|
||||
source?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts common metadata (`ancestors`, `matchingSelectors`, `source`)
|
||||
* uniformly across matched rules, inherited rules, and pseudo-element rules.
|
||||
*/
|
||||
function getRuleMetadata(
|
||||
rule: DevTools.CSSRule.CSSStyleRule | undefined,
|
||||
matchedStyles: MatchedStyles,
|
||||
containerDetails?: Map<ContainerQuery, ResolvedContainerDetails>,
|
||||
): RuleMetadata {
|
||||
if (!rule) {
|
||||
return {};
|
||||
}
|
||||
const matchingSelectors = getMatchingSelectors(matchedStyles, rule);
|
||||
const ancestors = collectAncestorRules(rule, containerDetails);
|
||||
const source = getSourceLocation(rule);
|
||||
return {
|
||||
...(ancestors ? {ancestors} : {}),
|
||||
...(matchingSelectors ? {matchingSelectors} : {}),
|
||||
...(source ? {source} : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a CSS property into standard CSS syntax with optional status tags.
|
||||
*
|
||||
@@ -112,6 +388,7 @@ function getCascadeRuleHeader(rule: CascadeRule): string {
|
||||
case 'transition':
|
||||
case 'animation':
|
||||
case 'attributes':
|
||||
case 'matched':
|
||||
selector = rule.selector;
|
||||
break;
|
||||
}
|
||||
@@ -119,7 +396,70 @@ function getCascadeRuleHeader(rule: CascadeRule): string {
|
||||
return source ? `${selector} (${source})` : selector;
|
||||
}
|
||||
|
||||
function appendRule(writer: IndentedWriter, rule: CascadeRule): void {
|
||||
function formatAncestorRuleHeader(ancestor: AncestorCSSRule): {
|
||||
comment?: string;
|
||||
header: string;
|
||||
} {
|
||||
switch (ancestor.type) {
|
||||
case 'layer':
|
||||
return {header: ancestor.name ? `@layer ${ancestor.name}` : '@layer'};
|
||||
case 'media':
|
||||
return {header: `@media ${ancestor.query}`};
|
||||
case 'container': {
|
||||
let comment: string | undefined;
|
||||
if (ancestor.container?.selector) {
|
||||
comment = ancestor.container.selector;
|
||||
}
|
||||
if (ancestor.container?.uid) {
|
||||
const uidStr = `(uid: "${ancestor.container.uid}")`;
|
||||
comment = comment ? `${comment} ${uidStr}` : uidStr;
|
||||
}
|
||||
if (comment) {
|
||||
comment = `container: ${comment}`;
|
||||
}
|
||||
const shouldPrependName =
|
||||
ancestor.name && !ancestor.query.startsWith(ancestor.name);
|
||||
const nameStr = shouldPrependName ? `${ancestor.name} ` : '';
|
||||
return {comment, header: `@container ${nameStr}${ancestor.query}`};
|
||||
}
|
||||
case 'scope': {
|
||||
const queryStr = ancestor.query?.trim();
|
||||
return {header: queryStr ? `@scope ${queryStr}` : '@scope'};
|
||||
}
|
||||
case 'supports':
|
||||
return {header: `@supports ${ancestor.query}`};
|
||||
case 'starting-style':
|
||||
return {header: '@starting-style'};
|
||||
case 'nesting':
|
||||
return {header: ancestor.selector};
|
||||
case 'navigation':
|
||||
return {header: `@navigation ${ancestor.query}`};
|
||||
case 'at-rule': {
|
||||
const nameStr = ancestor.name ? ` ${ancestor.name}` : '';
|
||||
return {header: `@${ancestor.atRuleType}${nameStr}`};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function appendRuleWithAncestors(
|
||||
writer: IndentedWriter,
|
||||
rule: CascadeRule,
|
||||
): void {
|
||||
const ancestors = 'ancestors' in rule ? rule.ancestors : undefined;
|
||||
let ancestorCount = 0;
|
||||
|
||||
if (ancestors && ancestors.length > 0) {
|
||||
for (const ancestor of ancestors) {
|
||||
const {comment, header} = formatAncestorRuleHeader(ancestor);
|
||||
if (comment) {
|
||||
writer.writeComment(comment);
|
||||
}
|
||||
writer.writeLine(`${header} {`);
|
||||
writer.indent();
|
||||
ancestorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const header = getCascadeRuleHeader(rule);
|
||||
writer.writeLine(`${header} {`);
|
||||
writer.indent();
|
||||
@@ -128,6 +468,11 @@ function appendRule(writer: IndentedWriter, rule: CascadeRule): void {
|
||||
}
|
||||
writer.dedent();
|
||||
writer.writeLine('}');
|
||||
|
||||
for (let i = 0; i < ancestorCount; i++) {
|
||||
writer.dedent();
|
||||
writer.writeLine('}');
|
||||
}
|
||||
}
|
||||
|
||||
function appendCssSectionsToString(
|
||||
@@ -136,7 +481,7 @@ function appendCssSectionsToString(
|
||||
): void {
|
||||
for (const rule of styles.rules) {
|
||||
writer.writeEmptyLine();
|
||||
appendRule(writer, rule);
|
||||
appendRuleWithAncestors(writer, rule);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,15 +495,19 @@ export class CssFormatter {
|
||||
/**
|
||||
* Aggregates all cascading rules impacting the target node.
|
||||
*/
|
||||
static collectRules(matchedStyles: MatchedStyles): CascadeRule[] {
|
||||
static collectRules(
|
||||
matchedStyles: MatchedStyles,
|
||||
options: CssFormatterOptions,
|
||||
): CascadeRule[] {
|
||||
const rules: CascadeRule[] = [];
|
||||
CssFormatter.#collectNodeStyles(rules, matchedStyles);
|
||||
CssFormatter.#collectNodeStyles(rules, matchedStyles, options);
|
||||
return rules;
|
||||
}
|
||||
|
||||
static #collectNodeStyles(
|
||||
rules: CascadeRule[],
|
||||
matchedStyles: MatchedStyles,
|
||||
options: CssFormatterOptions,
|
||||
): void {
|
||||
for (const style of matchedStyles.nodeStyles?.() ?? []) {
|
||||
const properties = CssFormatter.#getStyleProperties(style);
|
||||
@@ -194,10 +543,35 @@ export class CssFormatter {
|
||||
selector: 'element.style',
|
||||
properties: CssFormatter.#formatProperties(properties, matchedStyles),
|
||||
});
|
||||
} else if (style.parentRule instanceof DevTools.CSSRule.CSSStyleRule) {
|
||||
rules.push(
|
||||
CssFormatter.#createMatchedRule(
|
||||
style.parentRule,
|
||||
properties,
|
||||
matchedStyles,
|
||||
options,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static #createMatchedRule(
|
||||
rule: DevTools.CSSRule.CSSStyleRule,
|
||||
properties: DevTools.CSSProperty.CSSProperty[],
|
||||
matchedStyles: MatchedStyles,
|
||||
options: CssFormatterOptions,
|
||||
): MatchedRule {
|
||||
const meta = getRuleMetadata(rule, matchedStyles, options.containerDetails);
|
||||
return {
|
||||
type: 'matched',
|
||||
selector: rule.selectorText(),
|
||||
...meta,
|
||||
...(rule.isUserAgent?.() ? {isUserAgent: true} : {}),
|
||||
properties: CssFormatter.#formatProperties(properties, matchedStyles),
|
||||
};
|
||||
}
|
||||
|
||||
static #formatProperties(
|
||||
props: DevTools.CSSProperty.CSSProperty[],
|
||||
matchedStyles: MatchedStyles,
|
||||
@@ -249,7 +623,7 @@ export class CssFormatter {
|
||||
this.#matchedStyles = matchedStyles;
|
||||
this.#options = options;
|
||||
this.#cascadeRules =
|
||||
cascadeRules ?? CssFormatter.collectRules(matchedStyles);
|
||||
cascadeRules ?? CssFormatter.collectRules(matchedStyles, options);
|
||||
}
|
||||
|
||||
get rules(): readonly CascadeRule[] {
|
||||
|
||||
@@ -1,3 +1,231 @@
|
||||
exports[`CssFormatter > formats @layer, @media, @supports, and @starting-style ancestor rules toJSON 1`] = `
|
||||
{
|
||||
"element": {
|
||||
"uid": "btn-1",
|
||||
"selector": "button"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"type": "matched",
|
||||
"selector": ".test-btn",
|
||||
"ancestors": [
|
||||
{
|
||||
"type": "layer",
|
||||
"name": "base"
|
||||
},
|
||||
{
|
||||
"type": "media",
|
||||
"query": "(min-width: 500px)"
|
||||
},
|
||||
{
|
||||
"type": "supports",
|
||||
"query": "(display: flex)"
|
||||
},
|
||||
{
|
||||
"type": "starting-style"
|
||||
}
|
||||
],
|
||||
"source": "<style>",
|
||||
"properties": [
|
||||
{
|
||||
"name": "display",
|
||||
"value": "flex",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats @layer, @media, @supports, and @starting-style ancestor rules toString 1`] = `
|
||||
Styles for button (uid: "btn-1"):
|
||||
|
||||
@layer base {
|
||||
@media (min-width: 500px) {
|
||||
@supports (display: flex) {
|
||||
@starting-style {
|
||||
.test-btn (<style>) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats @navigation ancestor rule toJSON 1`] = `
|
||||
{
|
||||
"element": {
|
||||
"uid": "elem-nav",
|
||||
"selector": "button"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"type": "matched",
|
||||
"selector": ".nav-link",
|
||||
"ancestors": [
|
||||
{
|
||||
"type": "navigation",
|
||||
"query": "same-document"
|
||||
}
|
||||
],
|
||||
"source": "<style>",
|
||||
"properties": [
|
||||
{
|
||||
"name": "color",
|
||||
"value": "navy",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats @navigation ancestor rule toString 1`] = `
|
||||
Styles for button (uid: "elem-nav"):
|
||||
|
||||
@navigation same-document {
|
||||
.nav-link (<style>) {
|
||||
color: navy;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats @scope ancestor rule toJSON 1`] = `
|
||||
{
|
||||
"element": {
|
||||
"uid": "item-1",
|
||||
"selector": "button"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"type": "matched",
|
||||
"selector": ".scoped-item",
|
||||
"ancestors": [
|
||||
{
|
||||
"type": "scope",
|
||||
"query": "(:root)"
|
||||
}
|
||||
],
|
||||
"source": "<style>",
|
||||
"properties": [
|
||||
{
|
||||
"name": "color",
|
||||
"value": "blue",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats @scope ancestor rule toString 1`] = `
|
||||
Styles for button (uid: "item-1"):
|
||||
|
||||
@scope (:root) {
|
||||
.scoped-item (<style>) {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats constructed stylesheets with and without sourceURL pragma toJSON 1`] = `
|
||||
{
|
||||
"element": {
|
||||
"uid": "elem-constructed",
|
||||
"selector": "button"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"type": "matched",
|
||||
"selector": ".constructed-btn",
|
||||
"source": "constructed stylesheet",
|
||||
"properties": [
|
||||
{
|
||||
"name": "color",
|
||||
"value": "purple",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "matched",
|
||||
"selector": ".themed-btn",
|
||||
"source": "theme.css:11",
|
||||
"properties": [
|
||||
{
|
||||
"name": "color",
|
||||
"value": "orange",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats constructed stylesheets with and without sourceURL pragma toString 1`] = `
|
||||
Styles for button (uid: "elem-constructed"):
|
||||
|
||||
.constructed-btn (constructed stylesheet) {
|
||||
color: purple;
|
||||
}
|
||||
|
||||
.themed-btn (theme.css:11) {
|
||||
color: orange;
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats data: and blob: stylesheet URLs correctly toJSON 1`] = `
|
||||
{
|
||||
"element": {
|
||||
"uid": "data-elem",
|
||||
"selector": "button"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"type": "matched",
|
||||
"selector": ".data-rule",
|
||||
"source": "data-uri",
|
||||
"properties": [
|
||||
{
|
||||
"name": "color",
|
||||
"value": "blue",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "matched",
|
||||
"selector": ".blob-rule",
|
||||
"source": "blob",
|
||||
"properties": [
|
||||
{
|
||||
"name": "color",
|
||||
"value": "green",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats data: and blob: stylesheet URLs correctly toString 1`] = `
|
||||
Styles for button (uid: "data-elem"):
|
||||
|
||||
.data-rule (data-uri) {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.blob-rule (blob) {
|
||||
color: green;
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats element label with id, class, and uid and no styles toJSON 1`] = `
|
||||
{
|
||||
"element": {
|
||||
@@ -14,6 +242,37 @@ Styles for div#main (uid: "1_1"):
|
||||
(no styles)
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats injected stylesheet rules toJSON 1`] = `
|
||||
{
|
||||
"element": {
|
||||
"uid": "elem-injected",
|
||||
"selector": "button"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"type": "matched",
|
||||
"selector": ".extension-override",
|
||||
"source": "injected stylesheet",
|
||||
"properties": [
|
||||
{
|
||||
"name": "display",
|
||||
"value": "none",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats injected stylesheet rules toString 1`] = `
|
||||
Styles for button (uid: "elem-injected"):
|
||||
|
||||
.extension-override (injected stylesheet) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats inline styles with active and overloaded properties toJSON 1`] = `
|
||||
{
|
||||
"element": {
|
||||
@@ -51,6 +310,122 @@ Styles for button (uid: "1_2"):
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats inspector stylesheet rules toJSON 1`] = `
|
||||
{
|
||||
"element": {
|
||||
"uid": "elem-inspector",
|
||||
"selector": "button"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"type": "matched",
|
||||
"selector": "#interactive-test",
|
||||
"source": "via inspector",
|
||||
"properties": [
|
||||
{
|
||||
"name": "outline",
|
||||
"value": "2px solid red",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats inspector stylesheet rules toString 1`] = `
|
||||
Styles for button (uid: "elem-inspector"):
|
||||
|
||||
#interactive-test (via inspector) {
|
||||
outline: 2px solid red;
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats mixed inline and matched rules toJSON 1`] = `
|
||||
{
|
||||
"element": {
|
||||
"uid": "1_1",
|
||||
"selector": "button#btn-id"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"type": "inline",
|
||||
"selector": "element.style",
|
||||
"properties": [
|
||||
{
|
||||
"name": "color",
|
||||
"value": "red",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "matched",
|
||||
"selector": ".btn",
|
||||
"source": "style.css:6",
|
||||
"properties": [
|
||||
{
|
||||
"name": "font-size",
|
||||
"value": "16px",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats mixed inline and matched rules toString 1`] = `
|
||||
Styles for button#btn-id (uid: "1_1"):
|
||||
|
||||
element.style {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.btn (style.css:6) {
|
||||
font-size: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats nested CSS rules with nesting ancestors toJSON 1`] = `
|
||||
{
|
||||
"element": {
|
||||
"uid": "elem-child",
|
||||
"selector": "button"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"type": "matched",
|
||||
"selector": "& .child",
|
||||
"ancestors": [
|
||||
{
|
||||
"type": "nesting",
|
||||
"selector": ".card"
|
||||
}
|
||||
],
|
||||
"source": "styles.css:16",
|
||||
"properties": [
|
||||
{
|
||||
"name": "color",
|
||||
"value": "blue",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats nested CSS rules with nesting ancestors toString 1`] = `
|
||||
Styles for button (uid: "elem-child"):
|
||||
|
||||
.card {
|
||||
& .child (styles.css:16) {
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > formats transition, animation, and attributes styles toJSON 1`] = `
|
||||
{
|
||||
"element": {
|
||||
@@ -111,3 +486,184 @@ Styles for table#data (uid: "table-1"):
|
||||
border: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > maps invalid and disabled property statuses toJSON 1`] = `
|
||||
{
|
||||
"element": {
|
||||
"uid": "elem-diag",
|
||||
"selector": "button"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"type": "inline",
|
||||
"selector": "element.style",
|
||||
"properties": [
|
||||
{
|
||||
"name": "color",
|
||||
"value": "red",
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"name": "background",
|
||||
"value": "invalid-val",
|
||||
"status": "invalid"
|
||||
},
|
||||
{
|
||||
"name": "opacity",
|
||||
"value": "0.5",
|
||||
"status": "disabled"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > maps invalid and disabled property statuses toString 1`] = `
|
||||
Styles for button (uid: "elem-diag"):
|
||||
|
||||
element.style {
|
||||
color: red;
|
||||
[invalid] background: invalid-val;
|
||||
[disabled] opacity: 0.5;
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > resolves container queries with node uid toJSON 1`] = `
|
||||
{
|
||||
"element": {
|
||||
"uid": "elem-widget",
|
||||
"selector": "button"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"type": "matched",
|
||||
"selector": ".widget",
|
||||
"ancestors": [
|
||||
{
|
||||
"type": "container",
|
||||
"query": "(min-width: 300px)",
|
||||
"name": "sidebar-cq",
|
||||
"container": {
|
||||
"uid": "uid-42",
|
||||
"selector": "aside#sidebar"
|
||||
}
|
||||
}
|
||||
],
|
||||
"source": "<style>",
|
||||
"properties": [
|
||||
{
|
||||
"name": "padding",
|
||||
"value": "10px",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > resolves container queries with node uid toString 1`] = `
|
||||
Styles for button (uid: "elem-widget"):
|
||||
|
||||
/* container: aside#sidebar (uid: "uid-42") */
|
||||
@container sidebar-cq (min-width: 300px) {
|
||||
.widget (<style>) {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > rule subsets > matched rules all 3 rules toJSON 1`] = `
|
||||
{
|
||||
"element": {
|
||||
"uid": "btn-1",
|
||||
"selector": "button"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"type": "matched",
|
||||
"selector": ".rule-1",
|
||||
"source": "app.css:11",
|
||||
"properties": [
|
||||
{
|
||||
"name": "color",
|
||||
"value": "red",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "matched",
|
||||
"selector": ".rule-2",
|
||||
"source": "app.css:21",
|
||||
"properties": [
|
||||
{
|
||||
"name": "color",
|
||||
"value": "blue",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "matched",
|
||||
"selector": ".rule-3",
|
||||
"source": "app.css:31",
|
||||
"properties": [
|
||||
{
|
||||
"name": "color",
|
||||
"value": "green",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > rule subsets > matched rules all 3 rules toString 1`] = `
|
||||
Styles for button (uid: "btn-1"):
|
||||
|
||||
.rule-1 (app.css:11) {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.rule-2 (app.css:21) {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.rule-3 (app.css:31) {
|
||||
color: green;
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > rule subsets > matched rules subset - last rule toJSON 1`] = `
|
||||
{
|
||||
"element": {
|
||||
"uid": "btn-1",
|
||||
"selector": "button"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"type": "matched",
|
||||
"selector": ".rule-3",
|
||||
"source": "app.css:31",
|
||||
"properties": [
|
||||
{
|
||||
"name": "color",
|
||||
"value": "green",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CssFormatter > rule subsets > matched rules subset - last rule toString 1`] = `
|
||||
Styles for button (uid: "btn-1"):
|
||||
|
||||
.rule-3 (app.css:31) {
|
||||
color: green;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
import {afterEach, describe, it} from 'node:test';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import {CssFormatter} from '../../src/formatters/CssFormatter.js';
|
||||
import {
|
||||
type ContainerQuery,
|
||||
CssFormatter,
|
||||
type ResolvedContainerDetails,
|
||||
} from '../../src/formatters/CssFormatter.js';
|
||||
import {DevTools} from '../../src/third_party/index.js';
|
||||
import {
|
||||
createMockCSSInlineStyle,
|
||||
@@ -15,6 +19,7 @@ import {
|
||||
createMockCSSProperty,
|
||||
createMockCSSStyleDeclaration,
|
||||
createMockDOMNode,
|
||||
createMockCSSStyleRule,
|
||||
} from '../mocks.js';
|
||||
|
||||
describe('CssFormatter', () => {
|
||||
@@ -87,4 +92,287 @@ describe('CssFormatter', () => {
|
||||
|
||||
return new CssFormatter(matchedStyles, {uid: 'table-1'});
|
||||
});
|
||||
|
||||
describe('rule subsets', () => {
|
||||
function createMatchedStylesForRuleSubsets() {
|
||||
return createMockCSSMatchedStyles({
|
||||
nodeStyles: [
|
||||
createMockCSSStyleDeclaration(
|
||||
[createMockCSSProperty('color', 'red')],
|
||||
{
|
||||
rule: createMockCSSStyleRule('.rule-1', {
|
||||
sourceURL: 'app.css',
|
||||
lineNumber: 10,
|
||||
}),
|
||||
},
|
||||
),
|
||||
createMockCSSStyleDeclaration(
|
||||
[createMockCSSProperty('color', 'blue')],
|
||||
{
|
||||
rule: createMockCSSStyleRule('.rule-2', {
|
||||
sourceURL: 'app.css',
|
||||
lineNumber: 20,
|
||||
}),
|
||||
},
|
||||
),
|
||||
createMockCSSStyleDeclaration(
|
||||
[createMockCSSProperty('color', 'green')],
|
||||
{
|
||||
rule: createMockCSSStyleRule('.rule-3', {
|
||||
sourceURL: 'app.css',
|
||||
lineNumber: 30,
|
||||
}),
|
||||
},
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
formatterTest('matched rules all 3 rules', () => {
|
||||
return new CssFormatter(createMatchedStylesForRuleSubsets(), {
|
||||
uid: 'btn-1',
|
||||
});
|
||||
});
|
||||
|
||||
formatterTest('matched rules subset - last rule', () => {
|
||||
const matchedStyles = createMatchedStylesForRuleSubsets();
|
||||
const fullFormatter = new CssFormatter(matchedStyles, {uid: 'btn-1'});
|
||||
return new CssFormatter(
|
||||
matchedStyles,
|
||||
{uid: 'btn-1'},
|
||||
fullFormatter.rules.slice(2, 3),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
formatterTest('formats data: and blob: stylesheet URLs correctly', () => {
|
||||
const matchedStyles = createMockCSSMatchedStyles({
|
||||
nodeStyles: [
|
||||
createMockCSSStyleDeclaration(
|
||||
[createMockCSSProperty('color', 'blue')],
|
||||
{
|
||||
rule: createMockCSSStyleRule('.data-rule', {
|
||||
sourceURL: 'data:text/css;base64,LmRhdGEte30=',
|
||||
}),
|
||||
},
|
||||
),
|
||||
createMockCSSStyleDeclaration(
|
||||
[createMockCSSProperty('color', 'green')],
|
||||
{
|
||||
rule: createMockCSSStyleRule('.blob-rule', {
|
||||
sourceURL: 'blob:http://example.com/1234-5678-90ab',
|
||||
}),
|
||||
},
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
return new CssFormatter(matchedStyles, {uid: 'data-elem'});
|
||||
});
|
||||
|
||||
formatterTest('formats mixed inline and matched rules', () => {
|
||||
const matchedStyles = createMockCSSMatchedStyles({
|
||||
node: 'button#btn-id',
|
||||
nodeStyles: [
|
||||
createMockCSSInlineStyle([createMockCSSProperty('color', 'red')]),
|
||||
createMockCSSStyleDeclaration(
|
||||
[createMockCSSProperty('font-size', '16px')],
|
||||
{
|
||||
rule: createMockCSSStyleRule('.btn', {
|
||||
sourceURL: 'style.css',
|
||||
lineNumber: 5,
|
||||
}),
|
||||
},
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
return new CssFormatter(matchedStyles, {uid: '1_1'});
|
||||
});
|
||||
|
||||
formatterTest('formats nested CSS rules with nesting ancestors', () => {
|
||||
const matchedStyles = createMockCSSMatchedStyles({
|
||||
nodeStyles: [
|
||||
createMockCSSStyleDeclaration(
|
||||
[createMockCSSProperty('color', 'blue')],
|
||||
{
|
||||
rule: createMockCSSStyleRule('& .child', {
|
||||
sourceURL: 'styles.css',
|
||||
lineNumber: 15,
|
||||
columnNumber: 2,
|
||||
nestingSelectors: ['.card'],
|
||||
}),
|
||||
},
|
||||
),
|
||||
],
|
||||
});
|
||||
return new CssFormatter(matchedStyles, {uid: 'elem-child'});
|
||||
});
|
||||
|
||||
formatterTest(
|
||||
'formats constructed stylesheets with and without sourceURL pragma',
|
||||
() => {
|
||||
const matchedStyles = createMockCSSMatchedStyles({
|
||||
nodeStyles: [
|
||||
createMockCSSStyleDeclaration(
|
||||
[createMockCSSProperty('color', 'purple')],
|
||||
{
|
||||
rule: createMockCSSStyleRule('.constructed-btn', {
|
||||
isConstructed: true,
|
||||
}),
|
||||
},
|
||||
),
|
||||
createMockCSSStyleDeclaration(
|
||||
[createMockCSSProperty('color', 'orange')],
|
||||
{
|
||||
rule: createMockCSSStyleRule('.themed-btn', {
|
||||
sourceURL: 'theme.css',
|
||||
lineNumber: 10,
|
||||
columnNumber: 5,
|
||||
isConstructed: true,
|
||||
}),
|
||||
},
|
||||
),
|
||||
],
|
||||
});
|
||||
return new CssFormatter(matchedStyles, {uid: 'elem-constructed'});
|
||||
},
|
||||
);
|
||||
|
||||
formatterTest('formats injected stylesheet rules', () => {
|
||||
const matchedStyles = createMockCSSMatchedStyles({
|
||||
nodeStyles: [
|
||||
createMockCSSStyleDeclaration(
|
||||
[createMockCSSProperty('display', 'none')],
|
||||
{
|
||||
rule: createMockCSSStyleRule('.extension-override', {
|
||||
origin: 'injected',
|
||||
}),
|
||||
},
|
||||
),
|
||||
],
|
||||
});
|
||||
return new CssFormatter(matchedStyles, {uid: 'elem-injected'});
|
||||
});
|
||||
|
||||
formatterTest('formats inspector stylesheet rules', () => {
|
||||
const matchedStyles = createMockCSSMatchedStyles({
|
||||
nodeStyles: [
|
||||
createMockCSSStyleDeclaration(
|
||||
[createMockCSSProperty('outline', '2px solid red')],
|
||||
{
|
||||
rule: createMockCSSStyleRule('#interactive-test', {
|
||||
sourceURL: 'inspector-stylesheet',
|
||||
origin: 'inspector',
|
||||
}),
|
||||
},
|
||||
),
|
||||
],
|
||||
});
|
||||
return new CssFormatter(matchedStyles, {uid: 'elem-inspector'});
|
||||
});
|
||||
|
||||
formatterTest('formats @navigation ancestor rule', () => {
|
||||
const rule = createMockCSSStyleRule('.nav-link', {
|
||||
navigations: [{text: 'same-document'}],
|
||||
});
|
||||
const style = createMockCSSStyleDeclaration(
|
||||
[createMockCSSProperty('color', 'navy')],
|
||||
{rule},
|
||||
);
|
||||
const matchedStyles = createMockCSSMatchedStyles({nodeStyles: [style]});
|
||||
|
||||
return new CssFormatter(matchedStyles, {uid: 'elem-nav'});
|
||||
});
|
||||
|
||||
formatterTest('resolves container queries with node uid', async () => {
|
||||
const containerNode = createMockDOMNode({
|
||||
selector: 'aside#sidebar',
|
||||
backendNodeId: 42,
|
||||
});
|
||||
const query = {
|
||||
text: '(min-width: 300px)',
|
||||
name: 'sidebar-cq',
|
||||
getContainerForNode: async () => ({
|
||||
containerNode,
|
||||
getContainerSizeDetails: async () => ({
|
||||
queryAxis: 'inline-size',
|
||||
width: '350px',
|
||||
}),
|
||||
}),
|
||||
};
|
||||
const rule = createMockCSSStyleRule('.widget', {
|
||||
containerQueries: [query],
|
||||
});
|
||||
const style = createMockCSSStyleDeclaration(
|
||||
[createMockCSSProperty('padding', '10px')],
|
||||
{rule},
|
||||
);
|
||||
const matchedStyles = createMockCSSMatchedStyles({nodeStyles: [style]});
|
||||
|
||||
const containerDetails = new Map<
|
||||
ContainerQuery,
|
||||
ResolvedContainerDetails
|
||||
>();
|
||||
containerDetails.set(query as unknown as ContainerQuery, {
|
||||
container: {
|
||||
uid: `uid-42`,
|
||||
selector: 'aside#sidebar',
|
||||
},
|
||||
});
|
||||
|
||||
return new CssFormatter(matchedStyles, {
|
||||
uid: 'elem-widget',
|
||||
containerDetails,
|
||||
});
|
||||
});
|
||||
|
||||
formatterTest('maps invalid and disabled property statuses', () => {
|
||||
const validProp = createMockCSSProperty('color', 'red');
|
||||
const invalidProp = createMockCSSProperty('background', 'invalid-val', {
|
||||
parsedOk: false,
|
||||
});
|
||||
const disabledProp = createMockCSSProperty('opacity', '0.5', {
|
||||
disabled: true,
|
||||
});
|
||||
|
||||
const style = createMockCSSInlineStyle([
|
||||
validProp,
|
||||
invalidProp,
|
||||
disabledProp,
|
||||
]);
|
||||
const matchedStyles = createMockCSSMatchedStyles({nodeStyles: [style]});
|
||||
|
||||
return new CssFormatter(matchedStyles, {uid: 'elem-diag'});
|
||||
});
|
||||
|
||||
formatterTest(
|
||||
'formats @layer, @media, @supports, and @starting-style ancestor rules',
|
||||
() => {
|
||||
const rule = createMockCSSStyleRule('.test-btn', {
|
||||
layers: [{text: 'base'}],
|
||||
media: [{text: '(min-width: 500px)'}],
|
||||
supports: [{text: '(display: flex)'}],
|
||||
startingStyles: [{}],
|
||||
});
|
||||
const style = createMockCSSStyleDeclaration(
|
||||
[createMockCSSProperty('display', 'flex')],
|
||||
{rule},
|
||||
);
|
||||
const matchedStyles = createMockCSSMatchedStyles({nodeStyles: [style]});
|
||||
return new CssFormatter(matchedStyles, {uid: 'btn-1'});
|
||||
},
|
||||
);
|
||||
|
||||
formatterTest('formats @scope ancestor rule', () => {
|
||||
const rule = createMockCSSStyleRule('.scoped-item', {
|
||||
scopes: [{text: '(:root)'}],
|
||||
});
|
||||
const style = createMockCSSStyleDeclaration(
|
||||
[createMockCSSProperty('color', 'blue')],
|
||||
{rule},
|
||||
);
|
||||
const matchedStyles = createMockCSSMatchedStyles({nodeStyles: [style]});
|
||||
return new CssFormatter(matchedStyles, {uid: 'item-1'});
|
||||
});
|
||||
});
|
||||
|
||||
+124
@@ -43,6 +43,8 @@ export type MockCSSStyleDeclaration =
|
||||
sinon.SinonStubbedInstance<DevTools.CSSStyleDeclaration.CSSStyleDeclaration>;
|
||||
export type MockCSSMatchedStyles =
|
||||
sinon.SinonStubbedInstance<DevTools.CSSMatchedStyles.CSSMatchedStyles>;
|
||||
export type MockCSSStyleRule =
|
||||
sinon.SinonStubbedInstance<DevTools.CSSRule.CSSStyleRule>;
|
||||
|
||||
/**
|
||||
* A minimal event emitter used to back mocked `on`/`off`/`emit` methods on
|
||||
@@ -168,6 +170,8 @@ export function createHandlerMocks(): {
|
||||
return {page, context, response};
|
||||
}
|
||||
|
||||
type RuleOrigin = 'regular' | 'user-agent' | 'injected' | 'inspector';
|
||||
|
||||
function isBackendNodeId(
|
||||
id: unknown,
|
||||
): id is DevTools.Protocol.DOM.BackendNodeId {
|
||||
@@ -251,6 +255,126 @@ export function createMockCSSInlineStyle(
|
||||
});
|
||||
}
|
||||
|
||||
export interface MockRuleOptions {
|
||||
sourceURL?: string;
|
||||
lineNumber?: number;
|
||||
columnNumber?: number;
|
||||
origin?: RuleOrigin;
|
||||
isConstructed?: boolean;
|
||||
nestingSelectors?: string[];
|
||||
selectors?: Array<{text: string}>;
|
||||
layers?: Array<{text?: string}>;
|
||||
media?: Array<{text: string}>;
|
||||
containerQueries?: Array<{
|
||||
text?: string;
|
||||
name?: string;
|
||||
getContainerForNode?: (nodeId: number) => Promise<unknown>;
|
||||
}>;
|
||||
scopes?: Array<{text: string}>;
|
||||
supports?: Array<{text: string}>;
|
||||
startingStyles?: unknown[];
|
||||
navigations?: Array<{text?: string}>;
|
||||
ruleTypes?: DevTools.Protocol.CSS.CSSRuleType[];
|
||||
}
|
||||
|
||||
export function synthesizeRuleTypes(
|
||||
options: MockRuleOptions,
|
||||
): DevTools.Protocol.CSS.CSSRuleType[] | undefined {
|
||||
if (options.ruleTypes !== undefined) {
|
||||
return options.ruleTypes;
|
||||
}
|
||||
const ruleTypes: DevTools.Protocol.CSS.CSSRuleType[] = [];
|
||||
const mappings: Array<
|
||||
[unknown[] | undefined, DevTools.Protocol.CSS.CSSRuleType]
|
||||
> = [
|
||||
[options.navigations, DevTools.Protocol.CSS.CSSRuleType.NavigationRule],
|
||||
[options.nestingSelectors, DevTools.Protocol.CSS.CSSRuleType.StyleRule],
|
||||
[
|
||||
options.startingStyles,
|
||||
DevTools.Protocol.CSS.CSSRuleType.StartingStyleRule,
|
||||
],
|
||||
[options.scopes, DevTools.Protocol.CSS.CSSRuleType.ScopeRule],
|
||||
[options.supports, DevTools.Protocol.CSS.CSSRuleType.SupportsRule],
|
||||
[options.containerQueries, DevTools.Protocol.CSS.CSSRuleType.ContainerRule],
|
||||
[options.media, DevTools.Protocol.CSS.CSSRuleType.MediaRule],
|
||||
[options.layers, DevTools.Protocol.CSS.CSSRuleType.LayerRule],
|
||||
];
|
||||
for (const [items, ruleType] of mappings) {
|
||||
if (items) {
|
||||
for (const _ of items) {
|
||||
ruleTypes.push(ruleType);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ruleTypes.length > 0 ? ruleTypes : undefined;
|
||||
}
|
||||
|
||||
export function attachRuleMeta(
|
||||
rule: sinon.SinonStubbedInstance<DevTools.CSSRule.CSSRule>,
|
||||
sourceURL?: string,
|
||||
origin: RuleOrigin = 'regular',
|
||||
isConstructed = false,
|
||||
): void {
|
||||
rule.isUserAgent.returns(origin === 'user-agent');
|
||||
rule.isInjected.returns(origin === 'injected');
|
||||
rule.isViaInspector.returns(origin === 'inspector');
|
||||
if (sourceURL || isConstructed) {
|
||||
const header = {
|
||||
sourceURL: sourceURL ?? '',
|
||||
lineNumberInSource: (line: number) => line,
|
||||
columnNumberInSource: (_line: number, col: number) => col,
|
||||
isConstructedByNew: () => isConstructed,
|
||||
};
|
||||
Object.assign(rule, {header});
|
||||
}
|
||||
Object.defineProperty(rule, 'sourceURL', {
|
||||
value: sourceURL,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
function createCSSValue(text: string) {
|
||||
return {
|
||||
text,
|
||||
rebase() {
|
||||
// no-op
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createMockCSSStyleRule(
|
||||
selector: string,
|
||||
options: MockRuleOptions = {},
|
||||
): MockCSSStyleRule {
|
||||
const rule = sinon.createStubInstance(DevTools.CSSRule.CSSStyleRule);
|
||||
attachRuleMeta(
|
||||
rule,
|
||||
options.sourceURL,
|
||||
options.origin,
|
||||
options.isConstructed ?? false,
|
||||
);
|
||||
rule.selectorText.returns(selector);
|
||||
rule.lineNumberInSource.returns(options.lineNumber ?? -1);
|
||||
rule.columnNumberInSource.returns(options.columnNumber);
|
||||
const selectors = options.selectors
|
||||
? options.selectors.map(s => ({...createCSSValue(s.text), ...s}))
|
||||
: [createCSSValue(selector)];
|
||||
Object.assign(rule, {
|
||||
selectors,
|
||||
nestingSelectors: options.nestingSelectors,
|
||||
layers: options.layers,
|
||||
media: options.media,
|
||||
containerQueries: options.containerQueries,
|
||||
scopes: options.scopes,
|
||||
supports: options.supports,
|
||||
startingStyles: options.startingStyles,
|
||||
navigations: options.navigations,
|
||||
ruleTypes: synthesizeRuleTypes(options),
|
||||
});
|
||||
return rule;
|
||||
}
|
||||
|
||||
export interface MockCSSMatchedStylesParams {
|
||||
node?: string | DevTools.DOMModel.DOMNode;
|
||||
nodeStyles?: DevTools.CSSStyleDeclaration.CSSStyleDeclaration[];
|
||||
|
||||
Reference in New Issue
Block a user