| // Copyright 2020 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 type * as ElementsModule from '../../../../../front_end/panels/elements/elements.js'; |
| import * as SDK from '../../../../../front_end/core/sdk/sdk.js'; |
| import {createFileSystemUISourceCode} from '../../helpers/UISourceCodeHelpers.js'; |
| import {describeWithRealConnection} from '../../helpers/RealConnection.js'; |
| import * as Workspace from '../../../../../front_end/models/workspace/workspace.js'; |
| import {assertNotNullOrUndefined} from '../../../../../front_end/core/platform/platform.js'; |
| import type * as Platform from '../../../../../front_end/core/platform/platform.js'; |
| import * as Protocol from '../../../../../front_end/generated/protocol.js'; |
| import * as Bindings from '../../../../../front_end/models/bindings/bindings.js'; |
| import {describeWithEnvironment} from '../../helpers/EnvironmentHelpers.js'; |
| |
| const {assert} = chai; |
| |
| describeWithRealConnection('StylesSidebarPane', async () => { |
| let Elements: typeof ElementsModule; |
| before(async () => { |
| Elements = await import('../../../../../front_end/panels/elements/elements.js'); |
| }); |
| |
| it('unescapes CSS strings', () => { |
| assert.strictEqual( |
| Elements.StylesSidebarPane.unescapeCssString( |
| String.raw`"I\F1 t\EB rn\E2 ti\F4 n\E0 liz\E6 ti\F8 n\2603 \1F308 can be \t\r\ic\k\y"`), |
| '"I\xF1t\xEBrn\xE2ti\xF4n\xE0liz\xE6ti\xF8n\u2603\u{1F308} can be tricky"'); |
| assert.strictEqual( |
| Elements.StylesSidebarPane.unescapeCssString(String.raw`"_\DBFF_\\DBFF_\\\DBFF_\\\\DBFF_\\\\\DBFF_"`), |
| '"_\uFFFD_\\DBFF_\\\\DBFF_\\\\\\DBFF_\\\\\\\\DBFF_"'); |
| assert.strictEqual( |
| Elements.StylesSidebarPane.unescapeCssString(String.raw`"\0_\DBFF_\DFFF_\110000"`), |
| '"\uFFFD_\uFFFD_\uFFFD_\uFFFD"', 'U+0000, lone surrogates, and values above U+10FFFF should become U+FFFD'); |
| assert.strictEqual( |
| Elements.StylesSidebarPane.unescapeCssString(String.raw`"_\D83C\DF08_"`), '"_\uFFFD\uFFFD_"', |
| 'surrogates should not be combined'); |
| assert.strictEqual( |
| Elements.StylesSidebarPane.unescapeCssString('"_\\41\n_\\41\t_\\41\x20_"'), '"_A_A_A_"', |
| 'certain trailing whitespace characters should be consumed as part of the escape sequence'); |
| }); |
| |
| it('escapes URL as CSS comments', () => { |
| assert.strictEqual(Elements.StylesSidebarPane.escapeUrlAsCssComment('https://abc.com/'), 'https://abc.com/'); |
| assert.strictEqual(Elements.StylesSidebarPane.escapeUrlAsCssComment('https://abc.com/*/'), 'https://abc.com/*/'); |
| assert.strictEqual( |
| Elements.StylesSidebarPane.escapeUrlAsCssComment('https://abc.com/*/?q=*'), 'https://abc.com/*/?q=*'); |
| assert.strictEqual( |
| Elements.StylesSidebarPane.escapeUrlAsCssComment('https://abc.com/*/?q=*/'), 'https://abc.com/*/?q=*%2F'); |
| assert.strictEqual( |
| Elements.StylesSidebarPane.escapeUrlAsCssComment('https://abc.com/*/?q=*/#hash'), |
| 'https://abc.com/*/?q=*%2F#hash'); |
| }); |
| |
| it('tracks property changes with formatting', async () => { |
| const workspace = Workspace.Workspace.WorkspaceImpl.instance(); |
| const URL = 'file:///tmp/example.html' as Platform.DevToolsPath.UrlString; |
| const {uiSourceCode, project} = createFileSystemUISourceCode({ |
| url: URL, |
| content: '.rule{display:none}', |
| mimeType: 'text/css', |
| }); |
| |
| uiSourceCode.setWorkingCopy('.rule{display:block}'); |
| |
| const stylesSidebarPane = Elements.StylesSidebarPane.StylesSidebarPane.instance({forceNew: true}); |
| await stylesSidebarPane.trackURLForChanges(URL); |
| const targetManager = SDK.TargetManager.TargetManager.instance(); |
| const target = targetManager.rootTarget(); |
| assertNotNullOrUndefined(target); |
| |
| const resourceURL = () => URL; |
| const cssModel = new SDK.CSSModel.CSSModel(target); |
| cssModel.styleSheetHeaderForId = () => ({ |
| lineNumberInSource: (line: number) => line, |
| columnNumberInSource: (_line: number, column: number) => column, |
| cssModel: () => cssModel, |
| resourceURL, |
| isConstructedByNew: () => false, |
| } as unknown as SDK.CSSStyleSheetHeader.CSSStyleSheetHeader); |
| |
| const cssWorkspaceBinding = Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance(); |
| cssWorkspaceBinding.modelAdded(cssModel); |
| cssWorkspaceBinding.addSourceMapping({ |
| rawLocationToUILocation: (loc: SDK.CSSModel.CSSLocation) => new Workspace.UISourceCode.UILocation( |
| uiSourceCode as Workspace.UISourceCode.UISourceCode, loc.lineNumber, loc.columnNumber), |
| uiLocationToRawLocations: (_: Workspace.UISourceCode.UILocation): SDK.CSSModel.CSSLocation[] => [], |
| }); |
| |
| const cssProperty = { |
| ownerStyle: { |
| type: SDK.CSSStyleDeclaration.Type.Regular, |
| styleSheetId: 'STYLE_SHEET_ID' as Protocol.CSS.StyleSheetId, |
| cssModel: () => cssModel, |
| parentRule: {resourceURL}, |
| }, |
| nameRange: () => ({startLine: 0, startColumn: '.rule{'.length}), |
| } as unknown as SDK.CSSProperty.CSSProperty; |
| |
| assert.isTrue(stylesSidebarPane.isPropertyChanged(cssProperty)); |
| |
| Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance().modelRemoved(cssModel); |
| workspace.removeProject(project); |
| await stylesSidebarPane.trackURLForChanges(URL); // Clean up diff subscription |
| }); |
| |
| describe('rebuildSectionsForMatchedStyleRulesForTest', () => { |
| it('should add @position-fallback section to the end', async () => { |
| const stylesSidebarPane = Elements.StylesSidebarPane.StylesSidebarPane.instance({forceNew: true}); |
| const matchedStyles = new SDK.CSSMatchedStyles.CSSMatchedStyles({ |
| cssModel: stylesSidebarPane.cssModel() as SDK.CSSModel.CSSModel, |
| node: stylesSidebarPane.node() as SDK.DOMModel.DOMNode, |
| inlinePayload: null, |
| attributesPayload: null, |
| matchedPayload: [], |
| pseudoPayload: [], |
| inheritedPayload: [], |
| inheritedPseudoPayload: [], |
| animationsPayload: [], |
| parentLayoutNodeId: undefined, |
| positionFallbackRules: [{ |
| name: {text: '--compass'}, |
| tryRules: [{ |
| origin: Protocol.CSS.StyleSheetOrigin.Regular, |
| style: { |
| cssProperties: [{name: 'bottom', value: 'anchor(--anchor-name bottom)'}], |
| shorthandEntries: [], |
| }, |
| }], |
| }], |
| }); |
| |
| const sectionBlocks = |
| await stylesSidebarPane.rebuildSectionsForMatchedStyleRulesForTest(matchedStyles, new Map(), new Map()); |
| |
| assert.strictEqual(sectionBlocks.length, 2); |
| assert.strictEqual(sectionBlocks[1].titleElement()?.textContent, '@position-fallback --compass'); |
| assert.strictEqual(sectionBlocks[1].sections.length, 1); |
| assert.instanceOf(sectionBlocks[1].sections[0], Elements.StylePropertiesSection.TryRuleSection); |
| }); |
| }); |
| }); |
| |
| describeWithEnvironment('StylesSidebarPropertyRenderer', () => { |
| let Elements: typeof ElementsModule; |
| before(async () => { |
| Elements = await import('../../../../../front_end/panels/elements/elements.js'); |
| }); |
| |
| it('parses animation-name correctly', () => { |
| const throwingHandler = () => { |
| throw new Error('Invalid handler called'); |
| }; |
| const renderer = |
| new Elements.StylesSidebarPane.StylesSidebarPropertyRenderer(null, null, 'animation-name', 'foobar'); |
| renderer.setColorHandler(throwingHandler); |
| renderer.setBezierHandler(throwingHandler); |
| renderer.setFontHandler(throwingHandler); |
| renderer.setShadowHandler(throwingHandler); |
| renderer.setGridHandler(throwingHandler); |
| renderer.setVarHandler(throwingHandler); |
| renderer.setAngleHandler(throwingHandler); |
| renderer.setLengthHandler(throwingHandler); |
| |
| const nodeContents = `NAME: ${name}`; |
| renderer.setAnimationNameHandler(() => document.createTextNode(nodeContents)); |
| |
| const node = renderer.renderValue(); |
| assert.deepEqual(node.textContent, nodeContents); |
| }); |
| |
| it('parses color-mix correctly', () => { |
| const renderer = new Elements.StylesSidebarPane.StylesSidebarPropertyRenderer( |
| null, null, 'color', 'color-mix(in srgb, red, blue)'); |
| renderer.setColorMixHandler(() => document.createTextNode(nodeContents)); |
| |
| const nodeContents = 'nodeContents'; |
| |
| const node = renderer.renderValue(); |
| assert.deepEqual(node.textContent, nodeContents); |
| }); |
| |
| it('does not call bezier handler when color() value contains srgb-linear color space in a variable definition', |
| () => { |
| const colorHandler = sinon.fake.returns(document.createTextNode('colorHandler')); |
| const bezierHandler = sinon.fake.returns(document.createTextNode('bezierHandler')); |
| const renderer = new Elements.StylesSidebarPane.StylesSidebarPropertyRenderer( |
| null, null, '--color', 'color(srgb-linear 1 0.55 0.72)'); |
| renderer.setColorHandler(colorHandler); |
| renderer.setBezierHandler(bezierHandler); |
| |
| renderer.renderValue(); |
| |
| assert.isTrue(colorHandler.called); |
| assert.isFalse(bezierHandler.called); |
| }); |
| |
| it('runs animation handler for animation property', () => { |
| const renderer = |
| new Elements.StylesSidebarPane.StylesSidebarPropertyRenderer(null, null, 'animation', 'example 5s'); |
| renderer.setAnimationHandler(() => document.createTextNode(nodeContents)); |
| |
| const nodeContents = 'nodeContents'; |
| |
| const node = renderer.renderValue(); |
| assert.deepEqual(node.textContent, nodeContents); |
| }); |
| |
| it('runs positionFallbackHandler for position-fallback property', () => { |
| const nodeContents = 'nodeContents'; |
| const renderer = |
| new Elements.StylesSidebarPane.StylesSidebarPropertyRenderer(null, null, 'position-fallback', '--compass'); |
| renderer.setPositionFallbackHandler(() => document.createTextNode(nodeContents)); |
| |
| const node = renderer.renderValue(); |
| |
| assert.deepEqual(node.textContent, nodeContents); |
| }); |
| }); |
| |
| describe('IdleCallbackManager', () => { |
| let Elements: typeof ElementsModule; |
| before(async () => { |
| Elements = await import('../../../../../front_end/panels/elements/elements.js'); |
| }); |
| |
| // IdleCallbackManager delegates work using requestIdleCallback, which does not generally execute requested callbacks |
| // in order. This test verifies that callbacks do happen in order even if timeouts are run out. |
| it('schedules callbacks in order', async () => { |
| // Override the default timeout with a very short one |
| class QuickIdleCallbackManager extends Elements.StylesSidebarPane.IdleCallbackManager { |
| protected scheduleIdleCallback(_: number): void { |
| super.scheduleIdleCallback(1); |
| } |
| } |
| |
| const timeout = (time: number) => new Promise<void>(resolve => setTimeout(resolve, time)); |
| |
| const elements: number[] = []; |
| |
| const callbacks = new QuickIdleCallbackManager(); |
| callbacks.schedule(() => elements.push(0)); |
| callbacks.schedule(() => elements.push(1)); |
| callbacks.schedule(() => elements.push(2)); |
| callbacks.schedule(() => elements.push(3)); |
| await timeout(10); |
| callbacks.schedule(() => elements.push(4)); |
| callbacks.schedule(() => elements.push(5)); |
| callbacks.schedule(() => elements.push(6)); |
| callbacks.schedule(() => elements.push(7)); |
| await timeout(10); |
| |
| await callbacks.awaitDone(); |
| |
| assert.deepEqual(elements, [0, 1, 2, 3, 4, 5, 6, 7]); |
| }); |
| }); |