blob: 851f6f125bcadd208501c1406d4f36506e8b553c [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// 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 type * as Protocol from '../../generated/protocol.js';
import {createTarget, stubNoopSettings} from '../../testing/EnvironmentHelpers.js';
import {describeWithMockConnection} from '../../testing/MockConnection.js';
import {getMatchedStyles} from '../../testing/StyleHelpers.js';
import * as ComputedStyle from './computed_style.js';
function createNode(target: SDK.Target.Target, {nodeId}: {nodeId: Protocol.DOM.NodeId}): SDK.DOMModel.DOMNode {
const domModel = target.model(SDK.DOMModel.DOMModel);
assert.exists(domModel);
const node = SDK.DOMModel.DOMNode.create(domModel, null, false, {
nodeId,
backendNodeId: 2 as Protocol.DOM.BackendNodeId,
nodeType: Node.ELEMENT_NODE,
nodeName: 'div',
localName: 'div',
nodeValue: '',
});
return node;
}
describeWithMockConnection('ComputedStyleModel', () => {
let target: SDK.Target.Target;
let computedStyleModel: ComputedStyle.ComputedStyleModel.ComputedStyleModel;
let domNode1: SDK.DOMModel.DOMNode;
beforeEach(() => {
stubNoopSettings();
target = createTarget();
domNode1 = createNode(target, {nodeId: 1 as Protocol.DOM.NodeId});
const cssModel = target.model(SDK.CSSModel.CSSModel);
sinon.stub(ComputedStyle.ComputedStyleModel.ComputedStyleModel.prototype, 'cssModel').returns(cssModel);
computedStyleModel = new ComputedStyle.ComputedStyleModel.ComputedStyleModel();
});
afterEach(() => {});
it('listens to events on the CSS Model when there is a node given', async () => {
const cssModel = domNode1.domModel().cssModel();
assert.isOk(cssModel);
const listenerSpy = sinon.spy(cssModel, 'addEventListener');
computedStyleModel.node = domNode1;
// Feels silly to assert each individual call; but assert 1 to verify that
// code path was executed as expected.
sinon.assert.calledWith(listenerSpy, SDK.CSSModel.Events.StyleSheetAdded);
});
it('does not listen to events when there is no node given', async () => {
const cssModel = domNode1.domModel().cssModel();
assert.isOk(cssModel);
const listenerSpy = sinon.spy(SDK.CSSModel.CSSModel.prototype, 'addEventListener');
computedStyleModel.node = null;
sinon.assert.callCount(listenerSpy, 0);
});
it('emits the CSS_MODEL_CHANGED event when there is a change', async () => {
const cssModel = domNode1.domModel().cssModel();
assert.isOk(cssModel);
computedStyleModel.node = domNode1;
const modelChangedListener = sinon.spy();
computedStyleModel.addEventListener(
ComputedStyle.ComputedStyleModel.Events.CSS_MODEL_CHANGED, event => modelChangedListener(event.data));
const FAKE_CSS_STYLESHEET_HEADER = {} as SDK.CSSStyleSheetHeader.CSSStyleSheetHeader;
cssModel.dispatchEventToListeners(SDK.CSSModel.Events.StyleSheetAdded, FAKE_CSS_STYLESHEET_HEADER);
sinon.assert.calledOnceWithExactly(modelChangedListener, FAKE_CSS_STYLESHEET_HEADER);
});
it('emits the COMPUTED_STYLE_CHANGED event when the node ID matches', async () => {
const cssModel = domNode1.domModel().cssModel();
assert.isOk(cssModel);
computedStyleModel.node = domNode1;
const computedStyleListener = sinon.spy();
computedStyleModel.addEventListener(
ComputedStyle.ComputedStyleModel.Events.COMPUTED_STYLE_CHANGED, computedStyleListener);
cssModel.dispatchEventToListeners(SDK.CSSModel.Events.ComputedStyleUpdated, {nodeId: domNode1.id});
sinon.assert.callCount(computedStyleListener, 1);
});
it('does not emit the COMPUTED_STYLE_CHANGED event if the Node ID is different', async () => {
const cssModel = domNode1.domModel().cssModel();
assert.isOk(cssModel);
computedStyleModel.node = domNode1;
const computedStyleListener = sinon.spy();
computedStyleModel.addEventListener(
ComputedStyle.ComputedStyleModel.Events.COMPUTED_STYLE_CHANGED, computedStyleListener);
cssModel.dispatchEventToListeners(
SDK.CSSModel.Events.ComputedStyleUpdated, {nodeId: (domNode1.id + 1) as Protocol.DOM.NodeId});
sinon.assert.callCount(computedStyleListener, 0);
});
it('fetchMatchedCascade returns null for matchedStyles if the node does not match', async () => {
const cssModel = domNode1.domModel().cssModel();
assert.isOk(cssModel);
computedStyleModel.node = domNode1;
const domNode2 = createNode(target, {nodeId: 2 as Protocol.DOM.NodeId});
const mockMatchedStylesForNode2 = await getMatchedStyles({
node: domNode2,
});
const cachedMatchedCascadeForNodeStub =
sinon.stub(cssModel, 'cachedMatchedCascadeForNode').resolves(mockMatchedStylesForNode2);
const matchedStyles = await computedStyleModel.fetchMatchedCascade();
sinon.assert.calledOnce(cachedMatchedCascadeForNodeStub);
assert.isNull(matchedStyles);
});
it('fetchComputedStyle returns null if the node has become outdated', async () => {
const cssModel = domNode1.domModel().cssModel();
assert.isOk(cssModel);
computedStyleModel.node = domNode1;
const domNode2 = createNode(target, {nodeId: 2 as Protocol.DOM.NodeId});
// We need to control when this promise resolves, hence using callsFake and
// providing the promise manually.
const computedStylePromise = Promise.withResolvers<Map<string, string>>();
const getComputedStyleStub = sinon.stub(cssModel, 'getComputedStyle').callsFake(() => {
return computedStylePromise.promise;
});
// To emulate this scenario we need to:
// 1. Set the node to ID=1, and make the fetchComputedStyle() call.
const stylesPromise = computedStyleModel.fetchComputedStyle();
// 2. Before that resolves, set the node to ID = 2
computedStyleModel.node = domNode2;
// 3. Resolve the getComputedStyle promise, at which point the node check
// will see that the nodes are different.
const mockComputedStyle = new Map([['color', 'red']]);
computedStylePromise.resolve(mockComputedStyle);
const styles = await stylesPromise;
sinon.assert.calledOnce(getComputedStyleStub);
assert.isNull(styles);
});
});