├── .eslintignore ├── .eslintrc.js ├── .flowconfig ├── .gitignore ├── README.md ├── flow-typed └── pixelmatch.js ├── icons ├── icon-active-16.png ├── icon-active.png ├── icon-default-16.png ├── icon-inactive-16.png ├── icon-inactive.png └── icon.png ├── manifest.json ├── package.json ├── src ├── _background.js ├── _config.js ├── background.js ├── metadata │ ├── cssProperties.js │ └── index.js ├── normalize.js ├── pdiff.js ├── socket │ ├── index.js │ └── messageTypes.js ├── styleEdit.js ├── styles.js ├── tabs.js ├── types.js └── utils.js ├── test ├── helpers │ ├── helpers.js │ ├── setup.js │ └── styleHelpers.js ├── images │ ├── 10x2.png │ ├── base.png │ ├── changed.png │ ├── unchanged.png │ ├── with.png │ └── without.png ├── pdiff.test.js ├── styleEdit.test.js └── styles.test.js ├── webpack.config.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | build/* 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'babel-eslint', 3 | parserOptions: { 4 | ecmaVersion: 6, 5 | sourceType: 'module', 6 | }, 7 | extends: ['eslint:recommended', 'plugin:flowtype/recommended'], 8 | plugins: ['flowtype'], 9 | rules: { 10 | 'no-console': 'off', 11 | 'no-unused-vars': ['error', { 12 | 'varsIgnorePattern': '^_', 13 | 'argsIgnorePattern': '^_', 14 | } ] 15 | }, 16 | env: { 17 | node: true, 18 | browser: true, 19 | es6: true, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [libs] 2 | flow-typed/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build output 2 | build 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chrome-remote-css 2 | 3 | A Chrome extension which exposes DOM and CSS inspection and manipulation, including [visual regression pruning](http://users.eecs.northwestern.edu/~scl025/files/ply.pdf), over the local network. 4 | 5 | Powers [Ply](https://github.com/sarahlim/ply), a beginner-friendly HTML and CSS inspector. 6 | 7 | Developed through [Design, Technology, and Research](http://dtr.northwestern.edu) at Northwestern University. 8 | 9 | ## Installing the extension 10 | 11 | First install dependencies and build the extension: 12 | 13 | ```sh 14 | # Clone repository 15 | git clone https://github.com/sarahlim/chrome-remote-css/ 16 | cd chrome-remote-css 17 | # Install dependencies 18 | yarn install 19 | # Build extension 20 | yarn build 21 | ``` 22 | 23 | From the [Chrome extension developer tutorial](https://developer.chrome.com/extensions/getstarted): 24 | 25 | > 1. Visit `chrome://extensions` in your browser (or open up the Chrome menu by clicking the icon to the far right of the address bar, and select **Extensions** under the **Tools** menu to get to the same place). 26 | > 2. Ensure that the **Developer mode** checkbox in the top right-hand corner is checked. 27 | > 3. Click **Load unpacked extension…** to pop up a file-selection dialog. 28 | > 4. Navigate to the directory in which your extension files live, and select it. Alternatively, you can drag and drop the directory where your extension files live onto `chrome://extensions` in your browser to load it. 29 | > 30 | > If the extension is valid, it'll be loaded up and active right away! If it's invalid, an error message will be displayed at the top of the page. Correct the error, and try again. 31 | 32 | ## Running Extension 33 | 34 | Install [Ply](https://github.com/sarahlim/ply/) and launch the server with 35 | 36 | ```sh 37 | yarn run server # in the Ply directory, not the chrome-remote-css directory 38 | ``` 39 | 40 | (You can optionally also run Ply itself with `yarn start`, but only the server is needed for the extension to work, and even that could be easily refactored out.) 41 | 42 | Next, open the debug console. 43 | 44 | - Go to `chrome://extensions`, find the entry for `chrome-remote-css`, and click **Inspect views:** `background.html` to launch the console for the background page. 45 | 46 | Finally, in Chrome: 47 | 48 | 1. Navigate to any site 49 | 2. Click on the icon next to the address bar: ![grey inactive Delta icon](https://github.com/sarahlim/chrome-remote-css/raw/master/icons/icon-inactive-16.png) 50 | 3. After a short delay (nearly-instant for small websites, up to ~5-10s for large production sites with lots of styles like Facebook), an element selection cursor should appear on the page. 51 | - If not, check the background page console for errors. 52 | 4. Click an element to select it. 53 | 54 | You can now interact with the inspection target through the background page console: 55 | 56 | ```js 57 | endpoint._sendDebugCommand({ 58 | method: 'CSS.getMatchedStylesForNode', 59 | params: { 60 | nodeId: endpoint.document.nodeId, 61 | }, 62 | }).then(console.log); // async methods return Promises 63 | ``` 64 | 65 | To get a better idea of what you can do, see the following resources: 66 | 67 | - [Full protocol documentation](https://chromedevtools.github.io/devtools-protocol/tot/) 68 | + Note that the extension is officially pegged to the 1.1 version of the protocol. Weirdly enough, some of the tip-of-tree methods work, and some of them work under different names (e.g. most `Overlay` methods on tip-of-tree are accessible via the `DOM` domain). 69 | - [chrome-remote-interface](https://github.com/cyrus-and/chrome-remote-interface)'s wiki and Issues section contain a wealth of examples and recipes. 70 | 71 | ## API 72 | 73 | TODO: Document API methods. 74 | -------------------------------------------------------------------------------- /flow-typed/pixelmatch.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare module 'pixelmatch' { 4 | declare export type PixelmatchOptions = { 5 | threshold?: number, 6 | maxDiff?: number, 7 | }; 8 | 9 | declare type PixelmatchInput = { 10 | img1: Uint8ClampedArray, 11 | img2: Uint8ClampedArray, 12 | diff: ?Uint8ClampedArray, 13 | height: number, 14 | width: number, 15 | options?: PixelmatchOptions, 16 | }; 17 | 18 | declare export default function pixelmatch(input: PixelmatchInput): number; 19 | } 20 | -------------------------------------------------------------------------------- /icons/icon-active-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sliminality/chrome-remote-css/e7a24c0fcb91842b82229b01b48de6e88fc175ff/icons/icon-active-16.png -------------------------------------------------------------------------------- /icons/icon-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sliminality/chrome-remote-css/e7a24c0fcb91842b82229b01b48de6e88fc175ff/icons/icon-active.png -------------------------------------------------------------------------------- /icons/icon-default-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sliminality/chrome-remote-css/e7a24c0fcb91842b82229b01b48de6e88fc175ff/icons/icon-default-16.png -------------------------------------------------------------------------------- /icons/icon-inactive-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sliminality/chrome-remote-css/e7a24c0fcb91842b82229b01b48de6e88fc175ff/icons/icon-inactive-16.png -------------------------------------------------------------------------------- /icons/icon-inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sliminality/chrome-remote-css/e7a24c0fcb91842b82229b01b48de6e88fc175ff/icons/icon-inactive.png -------------------------------------------------------------------------------- /icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sliminality/chrome-remote-css/e7a24c0fcb91842b82229b01b48de6e88fc175ff/icons/icon.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "chrome-remote-css", 4 | "description": "expose DOM and CSSOM on a webpage over a socket.io connection", 5 | "version": "0.1", 6 | "browser_action": { 7 | "default_icon": "./icons/icon-inactive-16.png" 8 | }, 9 | "background": { 10 | "page": "./build/background.html" 11 | }, 12 | "content_security_policy": "script-src 'self' https://unpkg.com; object-src 'self'", 13 | "permissions": [ 14 | "tabs", 15 | "activeTab", 16 | "debugger", 17 | "contextMenus" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrome-remote-css", 3 | "version": "1.0.0", 4 | "description": "browser extension to expose debugging capabilities over localhost", 5 | "main": "background.js", 6 | "repository": "https://github.com/sarahlim/chrome-remote-css.git", 7 | "author": "Sarah Lim ", 8 | "license": "MIT", 9 | "devDependencies": { 10 | "ava": "^0.22.0", 11 | "babel-core": "^6.26.0", 12 | "babel-eslint": "^8.0.1", 13 | "babel-loader": "^7.1.2", 14 | "babel-plugin-transform-class-properties": "^6.24.1", 15 | "babel-plugin-transform-runtime": "^6.23.0", 16 | "babel-preset-env": "^1.6.0", 17 | "babel-preset-flow": "^6.23.0", 18 | "browser-env": "^3.2.0", 19 | "canvas-prebuilt": "^1.6.5-prerelease.1", 20 | "devtools-typed": "https://github.com/sarahlim/devtools-typed", 21 | "eslint": "^4.8.0", 22 | "eslint-plugin-flowtype": "^2.37.0", 23 | "flow-bin": "^0.47.0", 24 | "html-webpack-plugin": "^2.29.0", 25 | "webpack": "^3.0.0" 26 | }, 27 | "ava": { 28 | "require": [ 29 | "babel-register", 30 | "./test/helpers/setup.js" 31 | ], 32 | "babel": "inherit" 33 | }, 34 | "babel": { 35 | "presets": [ 36 | "env", 37 | "flow" 38 | ], 39 | "plugins": [ 40 | "transform-runtime", 41 | "transform-class-properties", 42 | [ 43 | "babel-plugin-transform-builtin-extend", 44 | { 45 | "globals": [ 46 | "Error", 47 | "Array" 48 | ] 49 | } 50 | ] 51 | ] 52 | }, 53 | "scripts": { 54 | "build": "webpack --env", 55 | "build:watch": "webpack --env.dev", 56 | "lint": "eslint .", 57 | "test": "ava" 58 | }, 59 | "dependencies": { 60 | "babel-plugin-transform-builtin-extend": "^1.1.2", 61 | "babel-runtime": "^6.26.0", 62 | "chrome-promise": "^2.0.3", 63 | "cssbeautify": "^0.3.1", 64 | "lodash": "^4.17.5", 65 | "normalizr": "^3.2.4", 66 | "pixelmatch": "https://github.com/sarahlim/pixelmatch.git#be54caf11a5", 67 | "socket.io-client": "^2.0.2", 68 | "webpack-chrome-extension-reloader": "^0.4.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/_background.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import ChromePromise from 'chrome-promise'; 3 | import io from 'socket.io-client'; 4 | import config from './_config'; 5 | 6 | const cp = new ChromePromise(); 7 | 8 | type TargetInfo = { 9 | type: 'page' | 'background_page' | 'worker' | 'other', 10 | id: string, 11 | tabId?: number, // Defined if type === 'page'. 12 | attached: boolean, 13 | title: string, 14 | url: string, 15 | }; 16 | 17 | async function getActiveTab(): Promise { 18 | const tabs = await cp.tabs.query({ 19 | active: true, 20 | currentWindow: true, 21 | }); 22 | if (tabs.length === 0) { 23 | return null; 24 | } 25 | return tabs[0]; 26 | } 27 | 28 | // debugger stuff 29 | -------------------------------------------------------------------------------- /src/_config.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export type Config = { 4 | serverPort: number, 5 | protocolVersion: string, 6 | }; 7 | 8 | const config = { 9 | serverPort: 1111, 10 | protocolVersion: '1.2', 11 | }; 12 | 13 | export default config; 14 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | // @flow @format 2 | import ChromePromise from 'chrome-promise'; 3 | import cssbeautify from 'cssbeautify'; 4 | import io from 'socket.io-client'; 5 | import pdiff, { DimensionMismatchError, prefixURI } from './pdiff'; 6 | import { createStyleMask, diffStyleMasks } from './styles'; 7 | import { replacePropertyInStyleText } from './styleEdit'; 8 | import { assert } from './utils'; 9 | import { 10 | serverToClient as outgoing, 11 | clientToServer as incoming, 12 | } from './socket/messageTypes'; 13 | import normalizeNodes from './normalize'; 14 | 15 | import type { CRDP$HighlightConfig } from 'devtools-typed/domain/overlay'; 16 | import type { 17 | CRDP$BackendNodeId, 18 | CRDP$NodeId, 19 | CRDP$Node, 20 | } from 'devtools-typed/domain/dom'; 21 | import type { 22 | CRDP$CSSStyle, 23 | CRDP$CSSRule, 24 | CRDP$CSSProperty, 25 | CRDP$RuleMatch, 26 | } from 'devtools-typed/domain/css'; 27 | import type { 28 | NodeStyleMask, 29 | CSSPropertyPath, 30 | CSSPropertyIndices, 31 | NodeMap, 32 | DebugStatus, 33 | Target, 34 | } from './types'; 35 | 36 | type Socket = Object; 37 | 38 | declare var chrome: Object; 39 | 40 | const cp = new ChromePromise(); 41 | const PROTOCOL = '1.2'; 42 | const SOCKET_PORT = 1111; 43 | 44 | // HACK 45 | window.DEBUG = { 46 | pdiff: { 47 | writeDiff: true, 48 | threshold: 0, 49 | maxDiff: 0, 50 | }, 51 | annotations: true, 52 | shadowBlacklist: ['margin', 'box-sizing'], 53 | }; 54 | 55 | // Highlighting for DOM overlays. 56 | const NODE_HIGHLIGHT: CRDP$HighlightConfig = { 57 | contentColor: { 58 | r: 255, 59 | g: 0, 60 | b: 0, 61 | a: 0.3, 62 | }, 63 | paddingColor: { 64 | r: 0, 65 | g: 255, 66 | b: 0, 67 | a: 0.3, 68 | }, 69 | marginColor: { 70 | r: 0, 71 | g: 0, 72 | b: 255, 73 | a: 0.3, 74 | }, 75 | }; 76 | 77 | class BrowserEndpoint { 78 | socket: ?Socket; 79 | target: ?Target; 80 | document: ?CRDP$Node; 81 | nodes: NodeMap; 82 | styles: { [CRDP$NodeId]: MatchedStyles }; 83 | pruned: { [CRDP$NodeId]: EffectiveStyles }; 84 | inspectedNode: ?CRDP$Node; 85 | entities: { nodes: NormalizedNodeMap }; 86 | ruleAnnotations: { [CRDP$NodeId]: Array }; 87 | 88 | _debugEventDispatch: (Target, string, Object) => Promise<*>; 89 | 90 | constructor(port) { 91 | this.socket = io(`http://localhost:${port}/browsers`, { 92 | autoConnect: false, 93 | reconnectionAttempts: 5, 94 | }); 95 | this.target = null; 96 | this.document = null; 97 | this.inspectedNode = null; 98 | this.styles = {}; 99 | this.pruned = {}; 100 | this.nodes = {}; 101 | this.ruleAnnotations = {}; 102 | 103 | // Bind `this` in the constructor, so we can 104 | // detach event handler by reference during cleanup. 105 | this._debugEventDispatch = this._debugEventDispatch.bind(this); 106 | } 107 | 108 | /** 109 | * Get the currently active tab in the focused Chrome 110 | * instance. 111 | */ 112 | static async getActiveTab() { 113 | const tabs = await cp.tabs.query({ 114 | active: true, 115 | currentWindow: true, 116 | }); 117 | 118 | if (tabs.length === 0) { 119 | throw new Error('getActiveTab: no active tab found'); 120 | } else { 121 | return tabs[0]; 122 | } 123 | } 124 | 125 | /** 126 | * Prepare to receive requests for debugger data. 127 | */ 128 | async initConnections(tabId) { 129 | // Mount debugger. 130 | await cp.debugger.attach({ tabId }, PROTOCOL); 131 | 132 | this.target = { tabId }; 133 | chrome.debugger.onDetach.addListener(this._onDebuggerDetach.bind(this)); 134 | chrome.debugger.onEvent.addListener(this._debugEventDispatch); 135 | await Promise.all([ 136 | this._sendDebugCommand({ 137 | method: 'Page.enable', 138 | }), 139 | this._sendDebugCommand({ 140 | method: 'DOM.enable', 141 | }), 142 | ]); 143 | // These domains require DOM to be enabled first. 144 | await Promise.all([ 145 | this._sendDebugCommand({ 146 | method: 'CSS.enable', 147 | }), 148 | this._sendDebugCommand({ 149 | method: 'Overlay.enable', 150 | }), 151 | ]); 152 | console.log('Attached debugger to target', this.target); 153 | 154 | // Once we have the DOM and are ready to handle 155 | // incoming requests, open the socket. 156 | if (this.socket && !this.socket.connected) { 157 | // Need to store the value of this.socket to 158 | // prevent Flow from invalidating the refinement. 159 | const { socket } = this; 160 | await new Promise(resolve => { 161 | socket.open(); 162 | socket.on('disconnect', this._onSocketDisconnect.bind(this)); 163 | socket.on('connect', () => { 164 | if (this.entities) { 165 | this._socketEmit(outgoing.SET_DOCUMENT, { 166 | entities: this.entities, 167 | }); 168 | } 169 | if (this.styles) { 170 | this._socketEmit(outgoing.SET_STYLES, { 171 | styles: this.styles, 172 | }); 173 | } 174 | if (this.inspectedNode) { 175 | this._socketEmit(outgoing.SET_INSPECTION_ROOT, { 176 | nodeId: this.inspectedNode.nodeId, 177 | }); 178 | } 179 | }); 180 | socket.on('connect', resolve); 181 | 182 | socket.on(incoming.PRUNE_NODE, this.pruneNode.bind(this)); 183 | socket.on(incoming.CLEAR_HIGHLIGHT, this.clearHighlight.bind(this)); 184 | socket.on(incoming.HIGHLIGHT_NODE, this.highlightNode.bind(this)); 185 | socket.on( 186 | incoming.REQUEST_STYLE_FOR_NODE, 187 | this.requestStyleForNode.bind(this), 188 | ); 189 | socket.on( 190 | incoming.TOGGLE_CSS_PROPERTY, 191 | this.toggleCSSProperty.bind(this), 192 | ); 193 | socket.on( 194 | incoming.COMPUTE_DEPENDENCIES, 195 | this.computeDependencies.bind(this), 196 | ); 197 | }); 198 | console.log('Opened socket', this.socket); 199 | } else { 200 | console.error('No socket found, could not setup connections'); 201 | } 202 | 203 | this.updateIcon('ACTIVE'); 204 | 205 | // Once debugger is mounted and sockets are open, 206 | // get the DOM and push it to the server. 207 | await this.getDocumentRoot(); 208 | console.log('Retrieved document root', this.document); 209 | } 210 | 211 | /** 212 | * Updates the browser icon badge to indicate the status 213 | * of the debugging process. 214 | */ 215 | updateIcon(status: DebugStatus) { 216 | const path = { 217 | ACTIVE: '../icons/icon-active-16.png', 218 | INACTIVE: '../icons/icon-inactive-16.png', 219 | }[status]; 220 | 221 | // When status is active, this.target will be 222 | // an object containing the tabId of the debugging 223 | // target. 224 | // We only want to change the icon for the active tab. 225 | const options = Object.assign( 226 | {}, 227 | { path }, 228 | this.target && { tabId: this.target.tabId }, 229 | ); 230 | 231 | chrome.browserAction.setIcon(options); 232 | } 233 | 234 | /** 235 | * Set the root of the document. 236 | */ 237 | async getDocumentRoot() { 238 | // Calling DOM.getDocument will invalidate all nodes. 239 | this.inspectedNode = null; 240 | this.styles = {}; 241 | this.nodes = {}; 242 | this.pruned = {}; 243 | 244 | const { root } = await this._sendDebugCommand({ 245 | method: 'DOM.getDocument', 246 | params: { depth: -1 }, 247 | }); 248 | 249 | // Set parentId value on every node. 250 | const withParents = this._addParentIds(-1)(root); 251 | this.document = withParents; 252 | 253 | const { entities } = normalizeNodes(withParents); 254 | this.entities = entities; 255 | this._socketEmit(outgoing.SET_DOCUMENT, { 256 | entities, 257 | }); 258 | 259 | return withParents; 260 | } 261 | 262 | /** 263 | * Allow the user to select a node for focus. 264 | */ 265 | async selectNode() { 266 | // Launch inspect mode. 267 | this._sendDebugCommand({ 268 | method: 'Overlay.setInspectMode', 269 | params: { 270 | mode: 'searchForNode', 271 | highlightConfig: NODE_HIGHLIGHT, 272 | }, 273 | }); 274 | } 275 | 276 | /** 277 | * Highlight a node on the inspected page. 278 | * If argument is null, disable highlight. 279 | */ 280 | async highlightNode( 281 | { 282 | nodeId, 283 | selectorList, 284 | }: { 285 | nodeId: CRDP$NodeId, 286 | selectorList?: string, 287 | }, 288 | ) { 289 | let highlightConfig = NODE_HIGHLIGHT; 290 | 291 | if (selectorList) { 292 | highlightConfig = Object.assign({}, highlightConfig, { selectorList }); 293 | } 294 | 295 | this._sendDebugCommand({ 296 | method: 'DOM.highlightNode', 297 | params: { 298 | highlightConfig, 299 | nodeId, 300 | }, 301 | }); 302 | } 303 | 304 | async clearHighlight() { 305 | await this._sendDebugCommand({ 306 | method: 'DOM.hideHighlight', 307 | }); 308 | } 309 | 310 | /** 311 | * Set parentId on every node in a given tree. 312 | */ 313 | _addParentIds(parentId) { 314 | return node => { 315 | const { children, nodeId } = node; 316 | let edit = { parentId }; 317 | 318 | if (children && children.length) { 319 | edit = { 320 | parentId, 321 | children: children.map(this._addParentIds(nodeId)), 322 | }; 323 | } 324 | 325 | // Cache the node in an object for faster indexing later. 326 | const nodeWithParent = Object.assign({}, node, edit); 327 | this.nodes[nodeId] = nodeWithParent; 328 | return nodeWithParent; 329 | }; 330 | } 331 | 332 | /** 333 | * Get the nodeId of the first match for the specified selector. 334 | */ 335 | async getNodeId(selector: string): Promise { 336 | if (!this.document) { 337 | this.document = await this.getDocumentRoot(); 338 | } 339 | 340 | let nodeId: CRDP$NodeId; 341 | try { 342 | const response = await this._sendDebugCommand({ 343 | method: 'DOM.querySelector', 344 | params: { 345 | selector, 346 | nodeId: this.document.nodeId, 347 | }, 348 | }); 349 | nodeId = response.nodeId; 350 | } catch (err) { 351 | throw err; 352 | } 353 | 354 | // Chrome Debugging Protocol returns nodeId of 0 355 | // if the node was not found. 356 | if (!nodeId) { 357 | throw new Error(`Couldn't retrieve nodeId for ${selector}`); 358 | } 359 | 360 | return nodeId; 361 | } 362 | 363 | async getNodeById( 364 | nodeId: CRDP$NodeId, 365 | offsetParent = false, 366 | ): Promise { 367 | let result: CRDP$Node; 368 | 369 | if (this.nodes[nodeId]) { 370 | result = this.nodes[nodeId]; 371 | } else { 372 | try { 373 | const searchResult = await this.searchDocument([nodeId]); 374 | result = searchResult[nodeId]; 375 | } catch (err) { 376 | throw err; 377 | } 378 | } 379 | 380 | // Optionally also search for the node's offsetParent. 381 | if (offsetParent) { 382 | try { 383 | let offsetParentId: CRDP$NodeId = await this.getOffsetParentId(nodeId); 384 | let offsetParent: CRDP$Node = await this.getNodeById(offsetParentId); 385 | result.offsetParent = offsetParent; 386 | } catch (err) { 387 | console.error('Error retrieving offsetParent for node', nodeId); 388 | } 389 | } 390 | 391 | return result; 392 | } 393 | 394 | /** 395 | * Search breadth-first for a given set of nodes. 396 | */ 397 | async searchDocument(wanted: CRDP$NodeId[]): Promise { 398 | if (!this.document) { 399 | this.document = await this.getDocumentRoot(); 400 | } 401 | 402 | const queue: CRDP$Node[] = [this.document]; 403 | // NodeIds we are looking for, but haven't found. 404 | const missing: Set = new Set(wanted); 405 | // Nodes we've found, associated with their nodeId. 406 | const found: NodeMap = {}; 407 | 408 | while (queue.length > 0) { 409 | const node: CRDP$Node = queue.shift(); 410 | const { nodeId } = node; 411 | 412 | if (missing.has(nodeId)) { 413 | found[nodeId] = node; 414 | missing.delete(nodeId); 415 | } 416 | 417 | // If `missing` is empty, we've found everything. 418 | if (missing.size === 0) { 419 | return found; 420 | } 421 | 422 | if (node.children) { 423 | queue.push(...node.children); 424 | } 425 | } 426 | 427 | /** 428 | * If search fails, return an Error object, which will be 429 | * checked by the caller and emitted back to the server. 430 | */ 431 | const missingFormat: string = JSON.stringify(missing); 432 | throw new Error(`couldn't find nodes for ${missingFormat}`); 433 | } 434 | 435 | async inspectNodeBySelector(selector: string) { 436 | const nodeId = await this.getNodeId(selector); 437 | const { backendNodeId } = this.nodes[nodeId]; 438 | this.nodeInspected({ backendNodeId }); 439 | } 440 | 441 | async requestStyleForNode({ nodeId }: { nodeId: CRDP$NodeId }) { 442 | const style = await this.getStyles(nodeId); 443 | this._socketEmit(outgoing.SET_STYLES, { 444 | styles: { 445 | [nodeId]: style, 446 | }, 447 | }); 448 | } 449 | 450 | /** 451 | * Get computed and matched styles for the given node. 452 | */ 453 | async getStyles(nodeId: CRDP$NodeId): Promise { 454 | let node: CRDP$Node; 455 | try { 456 | node = await this.getNodeById(nodeId); 457 | } catch (err) { 458 | throw err; 459 | } 460 | 461 | const { parentId } = node; 462 | 463 | const commands = [ 464 | { 465 | method: 'CSS.getMatchedStylesForNode', 466 | params: { nodeId }, 467 | }, 468 | { 469 | method: 'CSS.getComputedStyleForNode', 470 | params: { nodeId }, 471 | }, 472 | { 473 | method: 'CSS.getComputedStyleForNode', 474 | params: { nodeId: parentId }, 475 | }, 476 | ]; 477 | 478 | const commandPromises = commands.map(this._sendDebugCommand.bind(this)); 479 | const [matchedStyles, ...computedStyles] = await Promise.all( 480 | commandPromises, 481 | ); 482 | 483 | // Turn computed style arrays into ComputedStyleObjects. 484 | const [computedStyle, parentComputedStyle] = computedStyles 485 | // Extract the computed styles array from the response object. 486 | .map(({ computedStyle: cs }) => 487 | cs.reduce( 488 | (memo, current) => 489 | Object.assign(memo, { [current.name]: current.value }), 490 | {}, 491 | )); 492 | 493 | // Reverse the order of the matched styles, so that the 494 | // highest-specificity styles come first. 495 | matchedStyles.matchedCSSRules.reverse(); 496 | 497 | // Get any rule annotations and pruned filters. 498 | const ruleAnnotations = this.ruleAnnotations[nodeId]; 499 | const pruned = this.pruned[nodeId]; 500 | 501 | const styles = Object.assign( 502 | {}, 503 | matchedStyles, 504 | { computedStyle, parentComputedStyle }, 505 | ruleAnnotations ? { ruleAnnotations } : null, 506 | pruned ? { pruned } : null, 507 | ); 508 | 509 | this.styles[nodeId] = styles; 510 | 511 | return styles; 512 | } 513 | 514 | captureScreenshot = async (nodeId?: CRDP$NodeId) => { 515 | const params = {}; 516 | 517 | if (nodeId && typeof nodeId === 'number') { 518 | const [{ visualViewport }, { model }] = await Promise.all([ 519 | this._sendDebugCommand({ 520 | method: 'Page.getLayoutMetrics', 521 | }), 522 | this._sendDebugCommand({ 523 | method: 'DOM.getBoxModel', 524 | params: { nodeId }, 525 | }), 526 | ]); 527 | 528 | const { pageY } = visualViewport; 529 | const { border: [elX, elY], width, height } = model; 530 | const elViewport = { 531 | x: elX, 532 | y: pageY + elY, 533 | width, 534 | height, 535 | scale: 1, 536 | }; 537 | 538 | params.clip = elViewport; 539 | } 540 | 541 | const { data } = await this._sendDebugCommand({ 542 | method: 'Page.captureScreenshot', 543 | params, 544 | }); 545 | 546 | // const url = 'data:image/png;base64,'.concat(data); 547 | // chrome.tabs.create({url}); 548 | 549 | return data; 550 | }; 551 | 552 | nodeInspected = async ( 553 | { 554 | backendNodeId, 555 | }: { 556 | backendNodeId: CRDP$BackendNodeId, 557 | }, 558 | ) => { 559 | this.disableInspectMode(); 560 | 561 | // Get the nodeId corresponding to the backendId. 562 | const [inspectedNodeId] = await this.pushNodesByBackendIdsToFrontend({ 563 | backendNodeIds: [backendNodeId], 564 | }); 565 | const node = await this.getNodeById(inspectedNodeId); 566 | const style = await this.getStyles(inspectedNodeId); 567 | this.inspectedNode = node; 568 | 569 | // Send resulting node to server. 570 | this._socketEmit(outgoing.SET_INSPECTION_ROOT, { 571 | nodeId: inspectedNodeId, 572 | }); 573 | this._socketEmit(outgoing.SET_STYLES, { 574 | styles: { [inspectedNodeId]: style }, 575 | }); 576 | console.log(`Inspecting node ${inspectedNodeId}`, this.inspectedNode); 577 | }; 578 | 579 | async pushNodesByBackendIdsToFrontend( 580 | { 581 | backendNodeIds, 582 | }: { 583 | backendNodeIds: Array, 584 | }, 585 | ): Promise> { 586 | const { nodeIds } = await this._sendDebugCommand({ 587 | method: 'DOM.pushNodesByBackendIdsToFrontend', 588 | params: { 589 | backendNodeIds, 590 | }, 591 | }); 592 | return nodeIds; 593 | } 594 | 595 | async disableInspectMode() { 596 | this._sendDebugCommand({ 597 | method: 'Overlay.setInspectMode', 598 | params: { mode: 'none' }, 599 | }); 600 | } 601 | 602 | /** 603 | * Refresh stored styles, e.g. after a style edit has been made. 604 | */ 605 | async refreshStyles(nodeId?: CRDP$NodeId): Promise { 606 | console.groupCollapsed('refreshStyles'); 607 | const timerName = `Refreshing ${Object.keys(this.styles).length} styles:`; 608 | console.time(timerName); 609 | 610 | const nodesToUpdate: Array = typeof nodeId === 'number' 611 | ? [nodeId] 612 | : Object.keys(this.styles).map(str => parseInt(str, 10)); 613 | 614 | // const storedNodeIds: NodeId[] = Object.keys(this.styles).map(nodeId => 615 | // parseInt(nodeId), 616 | // ); 617 | 618 | if (nodesToUpdate.length) { 619 | console.time('awaiting updates'); 620 | const updatedStyles = await Promise.all( 621 | nodesToUpdate.map(this.getStyles.bind(this)), 622 | ); 623 | console.timeEnd('awaiting updates'); 624 | console.log('finished updating styles:', Date.now()); 625 | 626 | console.time('reducing style arrays'); 627 | // Reduce the pair of arrays back into an object. 628 | for (let i = 0; i < updatedStyles.length; i += 1) { 629 | this.styles[nodesToUpdate[i]] = updatedStyles[i]; 630 | } 631 | 632 | // this.styles = updatedStyles.reduce( 633 | // (acc, currentStyle, i) => 634 | // Object.assign(acc, { 635 | // [nodesToUpdate[i]]: currentStyle, 636 | // }), 637 | // {}, 638 | // ); 639 | console.timeEnd('reducing style arrays'); 640 | } else { 641 | console.log('No styles currently stored'); 642 | } 643 | console.timeEnd(timerName); 644 | console.groupEnd(); 645 | return this.styles; 646 | } 647 | 648 | /** 649 | * Get the nodeId of the given node's offsetParent. 650 | */ 651 | async getOffsetParentId(nodeId: CRDP$NodeId): Promise { 652 | // Set the node in question to the "currently" 653 | // "inspected" node, so we can use $0 in our 654 | // evaluation. 655 | await this._sendDebugCommand({ 656 | method: 'Overlay.setInspectedNode', 657 | params: { nodeId }, 658 | }); 659 | 660 | const { result } = await this._sendDebugCommand({ 661 | method: 'Runtime.evaluate', 662 | params: { 663 | expression: '$0.parentNode', 664 | includeCommandLineAPI: true, 665 | }, 666 | }); 667 | 668 | const { objectId } = result; 669 | const offsetParentNode = await this._sendDebugCommand({ 670 | method: 'DOM.requestNode', 671 | params: { objectId }, 672 | }); 673 | const offsetParentNodeId = offsetParentNode.nodeId; 674 | return offsetParentNodeId; 675 | } 676 | 677 | /** 678 | * Exposed handler, which toggles the style, updates the styles cache, 679 | * and responds with the updated styles. 680 | */ 681 | async toggleStyleAndRefresh( 682 | { 683 | nodeId, 684 | ruleIndex, 685 | propertyIndex, 686 | }: CSSPropertyPath, 687 | ): Promise<{ 688 | [CRDP$NodeId]: MatchedStyles, 689 | }> { 690 | await this._toggleStyle(nodeId, ruleIndex, propertyIndex); 691 | return await this.refreshStyles(); 692 | } 693 | 694 | async pushError({ error }: { error: string }) { 695 | this._socketEmit(outgoing.ERROR, { error }); 696 | } 697 | 698 | async toggleCSSProperty( 699 | { 700 | nodeId, 701 | ruleIndex, 702 | propertyIndex, 703 | }: { 704 | nodeId: CRDP$NodeId, 705 | ruleIndex: number, 706 | propertyIndex: number, 707 | }, 708 | ) { 709 | await this._toggleStyle(nodeId, ruleIndex, propertyIndex); 710 | const styles = await this.refreshStyles(nodeId); 711 | console.log('emitting styles:', Date.now()); 712 | this._socketEmit(outgoing.SET_STYLES, { 713 | styles, 714 | }); 715 | } 716 | 717 | async _toggleStyle( 718 | nodeId: CRDP$NodeId, 719 | ruleIndex: number, 720 | propertyIndex: number, 721 | ): Promise<> { 722 | const style: CRDP$CSSStyle = this.styles[nodeId].matchedCSSRules[ 723 | ruleIndex 724 | ].rule.style; 725 | const { range, styleSheetId, cssText: styleText } = style; 726 | const errorMsgRange = `node ${nodeId}, rule ${ruleIndex}, property ${propertyIndex}`; 727 | const property: CRDP$CSSProperty = style.cssProperties[propertyIndex]; 728 | if (!property) { 729 | throw new Error(`Couldn't get property for ${errorMsgRange}`); 730 | } 731 | const currentPropertyText = property.text; 732 | if (!currentPropertyText) { 733 | throw new Error(`Couldn't get text for property ${errorMsgRange}`); 734 | } 735 | let nextPropertyText; 736 | const hasDisabledProperty = Object.prototype.hasOwnProperty.call( 737 | property, 738 | 'disabled', 739 | ); 740 | if (!hasDisabledProperty) { 741 | throw new Error( 742 | `Property ${errorMsgRange} appears to not be a source-based property`, 743 | ); 744 | } 745 | const isDisabled = property.disabled; 746 | if (isDisabled) { 747 | // Need to re-enable it. 748 | // /* foo: bar; */ => foo: bar; 749 | // TODO: Fix bug with capturing regex. 750 | const disabledRegex = /\/\*\s+(.+)\s+\*\//; 751 | const matches = currentPropertyText.match(disabledRegex); 752 | 753 | if (!matches || !matches[1]) { 754 | throw new Error( 755 | `Property ${errorMsgRange} is marked as disabled, but disabled pattern was not found`, 756 | ); 757 | } 758 | nextPropertyText = matches[1]; 759 | if (!nextPropertyText) { 760 | throw new Error( 761 | `Couldn't find the original text in property ${currentPropertyText}`, 762 | ); 763 | } 764 | } else { 765 | // Property is enabled, need to disable it. 766 | if (currentPropertyText.lastIndexOf('\n') === -1) { 767 | nextPropertyText = `/* ${currentPropertyText} */`; 768 | } else { 769 | // If a property is last in its rule, it may have a newline 770 | // at the end. Appending */ to the end would invalidate the 771 | // SourceRange for the rule. 772 | const noNewLineRegex = /.+(?=\n)/m; 773 | // $` gives the part before the matched substring 774 | // $& gives the match 775 | // $' gives the suffix 776 | const replacementString = "$`/* $& */$'"; 777 | nextPropertyText = currentPropertyText.replace( 778 | noNewLineRegex, 779 | replacementString, 780 | ); 781 | } 782 | } 783 | 784 | // Need to replace the current *style text* by searching for 785 | // the current *property text* within it, and replacing with 786 | // the updated *property text*. 787 | const currentStyleText = styleText; 788 | if (!currentStyleText) { 789 | throw new Error( 790 | `Couldn't get style text for node ${nodeId}, rule ${ruleIndex}`, 791 | ); 792 | } 793 | const nextStyleText = replacePropertyInStyleText( 794 | style, 795 | property, 796 | nextPropertyText, 797 | ); 798 | const edit = { 799 | styleSheetId, 800 | range, 801 | text: nextStyleText, 802 | }; 803 | await this._sendDebugCommand({ 804 | method: 'CSS.setStyleTexts', 805 | params: { 806 | edits: [edit], 807 | }, 808 | }); 809 | 810 | /** 811 | * Patch the locally-stored style. 812 | * Note that MULTIPLE style objects could be potentially stale, 813 | * and the caller needs to take care of refreshing the stored 814 | * styles and pushing an update to the server. 815 | * However, this monkeypatch will allow us to test an individual 816 | * toggling change more easily. 817 | */ 818 | style.cssText = nextStyleText; 819 | property.text = nextPropertyText; 820 | property.disabled = !isDisabled; 821 | } 822 | 823 | isDisabled(path: CSSPropertyPath): boolean { 824 | let prop: ?CRDP$CSSProperty; 825 | try { 826 | prop = this.resolveProp(path); 827 | } catch (propNotFoundErr) { 828 | return false; 829 | } 830 | return !!prop.disabled; 831 | } 832 | 833 | isParsedOk(path: CSSPropertyPath): boolean { 834 | let prop: ?CRDP$CSSProperty; 835 | try { 836 | prop = this.resolveProp(path); 837 | } catch (propNotFoundErr) { 838 | return false; 839 | } 840 | return typeof prop.parsedOk === 'boolean' ? !!prop.parsedOk : true; 841 | } 842 | 843 | /** 844 | * Longhand properties that are expansions of shorthand properties 845 | * will not have their own SourceRanges or property text. 846 | */ 847 | isDeclaredProperty(path: CSSPropertyPath): boolean { 848 | let prop: ?CRDP$CSSProperty; 849 | try { 850 | prop = this.resolveProp(path); 851 | } catch (propNotFoundErr) { 852 | return false; 853 | } 854 | const hasText = !!prop.text; 855 | const hasRange = !!prop.range; 856 | return hasText && hasRange; 857 | } 858 | 859 | resolveProp(path: CSSPropertyPath): CRDP$CSSProperty { 860 | if (!this.propExists(path)) { 861 | throw new Error( 862 | `resolveProp: property ${path.nodeId}:${path.ruleIndex}:${path.propertyIndex} does not exist`, 863 | ); 864 | } 865 | const { nodeId, ruleIndex, propertyIndex } = path; 866 | return this.styles[nodeId].matchedCSSRules[ 867 | ruleIndex 868 | ].rule.style.cssProperties[propertyIndex]; 869 | } 870 | 871 | propExists({ nodeId, ruleIndex, propertyIndex }: CSSPropertyPath): boolean { 872 | const nodeStyles: MatchedStyles = this.styles[nodeId]; 873 | if (!nodeStyles) { 874 | return false; 875 | } 876 | const ruleMatch: CRDP$RuleMatch = nodeStyles.matchedCSSRules[ruleIndex]; 877 | if (!ruleMatch) { 878 | return false; 879 | } 880 | const prop: CRDP$CSSProperty = ruleMatch.rule.style.cssProperties[ 881 | propertyIndex 882 | ]; 883 | if (!prop) { 884 | return false; 885 | } 886 | return true; 887 | } 888 | 889 | async pruneNode({ nodeId }: { nodeId: CRDP$NodeId }): Promise { 890 | const _pruneNodeHelper = async (nodeId: CRDP$NodeId) => { 891 | await this.prune(nodeId); 892 | const currentNode = this.nodes[nodeId]; 893 | if (currentNode.children) { 894 | const { children } = currentNode; 895 | for (const child of children) { 896 | await _pruneNodeHelper(child.nodeId); 897 | } 898 | } 899 | }; 900 | 901 | let error; 902 | let allPropertiesEnabled = false; 903 | 904 | try { 905 | // await _pruneNodeHelper(nodeId); 906 | allPropertiesEnabled = await this.prune(nodeId); 907 | } catch (pruneError) { 908 | error = { message: pruneError.message, stack: pruneError.stack }; 909 | 910 | // Catch the group started at the beginning of the function. 911 | console.groupEnd(); 912 | } 913 | 914 | const styles = await this.refreshStyles(); 915 | const mask = createStyleMask(this.styles[nodeId].matchedCSSRules); 916 | 917 | // If this is the first time we are pruning, AND all properties were 918 | // originally enabled, set the pruned style mask. 919 | // TODO: Figure out how to handle invalidation. 920 | let diff; 921 | 922 | if (this.pruned[nodeId]) { 923 | try { 924 | diff = diffStyleMasks(nodeId, this.pruned[nodeId])(mask); 925 | } catch (diffStyleMasksErr) { 926 | // TODO: If we have two masks with different numbers of rules, 927 | // we should probably just set the new mask as the new `pruned` state. 928 | error = diffStyleMasksErr.message; 929 | } 930 | } else if (allPropertiesEnabled) { 931 | this.pruned[nodeId] = mask; 932 | } else { 933 | // HACK: Allows saving the pruned mask even if some number of properties 934 | // are already disabled. Useful for refreshes. 935 | // TODO: REMOVE THIS AFTER TESTING 936 | this.pruned[nodeId] = mask; 937 | } 938 | 939 | this._socketEmit( 940 | outgoing.PRUNE_NODE_RESULT, 941 | Object.assign({}, { error, nodeId }, diff ? { diff } : { mask }), 942 | ); 943 | 944 | this._socketEmit(outgoing.SET_STYLES, { 945 | styles, 946 | }); 947 | 948 | return mask; 949 | } 950 | 951 | async applyStyleMask(nodeId: CRDP$NodeId, mask: NodeStyleMask) { 952 | const nodeStyles = this.styles[nodeId]; 953 | const { matchedCSSRules } = nodeStyles; 954 | const currentMask = createStyleMask(matchedCSSRules); 955 | const maskDiff = diffStyleMasks(nodeId, mask)(currentMask); 956 | 957 | let worklist = []; 958 | const { enabled, disabled } = maskDiff; 959 | if (enabled) { 960 | worklist = worklist.concat(enabled); 961 | } 962 | if (disabled) { 963 | worklist = worklist.concat(disabled); 964 | } 965 | 966 | for (const [ruleIndex, propertyIndex] of worklist) { 967 | await this._toggleStyle(nodeId, ruleIndex, propertyIndex); 968 | } 969 | 970 | const styles = await this.refreshStyles(nodeId); 971 | 972 | this._socketEmit(outgoing.SET_STYLES, { 973 | styles, 974 | }); 975 | } 976 | 977 | async computeDependencies(path: CSSPropertyPath) { 978 | const { nodeId, ruleIndex, propertyIndex } = path; 979 | const { dependants, error } = await this._computeDependants(path); 980 | 981 | // HACK: Replace the index with something less prone to breakage. 982 | const indices = `${nodeId},${ruleIndex},${propertyIndex}`; 983 | 984 | this._socketEmit(outgoing.SET_DEPENDENCIES, { 985 | dependencies: { 986 | [indices]: dependants, 987 | }, 988 | error, 989 | }); 990 | 991 | this._socketEmit(outgoing.SET_STYLES, { 992 | styles: await this.refreshStyles(nodeId), 993 | }); 994 | } 995 | 996 | async _computeDependants( 997 | path: CSSPropertyPath, 998 | ): Promise<{ dependants: ?Array, error?: string }> { 999 | const { nodeId, ruleIndex, propertyIndex } = path; 1000 | const given = [nodeId, ruleIndex, propertyIndex]; 1001 | assert(this.pruned[nodeId], 'node must be pruned to compute dependencies'); 1002 | assert(!this.isDisabled(path), 'property must be enabled to start'); 1003 | 1004 | // Sometimes pruning a second time can yield new changes, so we should 1005 | // prune again first. 1006 | let beforeMask = this.pruned[nodeId]; 1007 | try { 1008 | await this.prune(nodeId); 1009 | await this.refreshStyles(); 1010 | beforeMask = createStyleMask(this.styles[nodeId].matchedCSSRules); 1011 | } catch (err) { 1012 | console.error('Error while pre-pruning', err.message); 1013 | } 1014 | 1015 | // Toggle the property, and then prune. 1016 | await this._toggleStyle(...given); 1017 | try { 1018 | await this.prune(nodeId); 1019 | } catch (pruneError) { 1020 | console.error( 1021 | 'pruning after disabling keystone failed', 1022 | pruneError.message, 1023 | ); 1024 | } 1025 | 1026 | // TODO: this is repetitive with the prune function; make it a helper. 1027 | await this.refreshStyles(); 1028 | const mask = createStyleMask(this.styles[nodeId].matchedCSSRules); 1029 | const maskDiff = diffStyleMasks(nodeId, beforeMask)(mask); 1030 | 1031 | // Dependants are properties that were enabled before, but disabled now. 1032 | const dependants = maskDiff.disabled && 1033 | maskDiff.disabled.filter( 1034 | ([depNode, depRule, depProperty]) => 1035 | depNode !== nodeId || 1036 | depRule !== ruleIndex || 1037 | depProperty !== propertyIndex, 1038 | ); 1039 | const errorStyles = []; 1040 | 1041 | // Reinstate all dependants, including keystone. 1042 | if (maskDiff.disabled) { 1043 | for (const indices of maskDiff.disabled) { 1044 | // TODO: Can we do this in parallel??? 1045 | try { 1046 | await this._toggleStyle(...indices); 1047 | } catch (toggleStyleError) { 1048 | errorStyles.push(indices); 1049 | } 1050 | } 1051 | try { 1052 | await this._toggleStyle(...given); 1053 | } catch (toggleStyleError) { 1054 | errorStyles.push(given); 1055 | } 1056 | } 1057 | 1058 | const result: { dependants: ?Array, error?: string } = { 1059 | dependants, 1060 | }; 1061 | 1062 | if (errorStyles.length > 0) { 1063 | result.error = `Could not toggle properties ${JSON.stringify(errorStyles)}`; 1064 | } 1065 | 1066 | return result; 1067 | } 1068 | 1069 | /** 1070 | * Prune properties for some node. 1071 | * Returns a boolean, indicating whether or not all properties were enabled before pruning. 1072 | */ 1073 | async prune( 1074 | nodeId: CRDP$NodeId, 1075 | condition?: CSSPropertyPath, 1076 | ): Promise { 1077 | console.groupCollapsed('Pruning node', nodeId); 1078 | 1079 | // Get current styles and screenshots for node. 1080 | console.log('Getting initial styles...'); 1081 | const nodeStyles: MatchedStyles = await this.getStyles(nodeId); 1082 | const { matchedCSSRules } = nodeStyles; 1083 | const allPruned = []; 1084 | 1085 | let basePage; 1086 | let baseElement; 1087 | 1088 | // If conditioning on a property, disable it first. 1089 | if (condition) { 1090 | ({ 1091 | page: basePage, 1092 | el: baseElement, 1093 | } = await this.getScreenshotForProperty(condition)); 1094 | await this.scrollIntoViewIfNeeded(nodeId); 1095 | } else { 1096 | basePage = await this.captureScreenshot(); 1097 | baseElement = await this.captureScreenshot(nodeId); 1098 | } 1099 | 1100 | // Attach screenshots to the window for debugging. 1101 | window.screenshots = []; 1102 | window.screenshots.push({ 1103 | type: 'base', 1104 | page: basePage, 1105 | element: baseElement, 1106 | }); 1107 | 1108 | const pdiffOptions = Object.assign( 1109 | {}, 1110 | { threshold: 0, maxDiff: 0, writeDiff: false }, 1111 | window.DEBUG.pdiff, 1112 | ); 1113 | const [pdiffAll, pdiffElement] = await Promise.all([ 1114 | pdiff(basePage, pdiffOptions), 1115 | pdiff(baseElement, pdiffOptions), 1116 | ]); 1117 | 1118 | const ruleAnnotations = []; 1119 | const rules = matchedCSSRules.entries(); 1120 | 1121 | // If all properties are enabled, we'll save the pruning result as the 1122 | // "pruned styles" for the node. 1123 | let allPropertiesEnabled = true; 1124 | 1125 | for (const [ruleIndex, ruleMatch] of rules) { 1126 | const { cssProperties } = ruleMatch.rule.style; 1127 | let pruneResults = []; 1128 | let ruleAnnotation = null; 1129 | 1130 | for (const [propertyIndex, prop] of cssProperties.entries()) { 1131 | const propPath: CSSPropertyPath = { 1132 | nodeId, 1133 | ruleIndex, 1134 | propertyIndex, 1135 | }; 1136 | 1137 | // Don't try to toggle if the property is a longhand expansion, 1138 | // or if it's already disabled. 1139 | if (!this.isDeclaredProperty(propPath) || !this.isParsedOk(propPath)) { 1140 | continue; 1141 | } 1142 | if (this.isDisabled(propPath)) { 1143 | allPropertiesEnabled = false; 1144 | continue; 1145 | } 1146 | // Static VRP can't detect animation and interactive properties, 1147 | // so don't prune these. 1148 | if (prop.name === 'transition' || prop.name === 'cursor') { 1149 | continue; 1150 | } 1151 | 1152 | console.log(prop.name); 1153 | 1154 | await this.toggleStyleAndRefresh({ nodeId, ruleIndex, propertyIndex }); 1155 | 1156 | const hasDiff = { element: false, page: false }; 1157 | const screenshotElement = await this.captureScreenshot(nodeId); 1158 | 1159 | // First compute pdiff wrt the element itself. If removing the property 1160 | // causes regression wrt the element, it trivially causes regression wrt 1161 | // the page. 1162 | try { 1163 | const { numPixelsDifferent, diffImage } = await pdiffElement( 1164 | screenshotElement, 1165 | ); 1166 | if (numPixelsDifferent > 0) { 1167 | hasDiff.element = true; 1168 | hasDiff.page = true; 1169 | window.screenshots.push({ 1170 | prop: prop.name, 1171 | element: diffImage || screenshotElement, 1172 | }); 1173 | } 1174 | } catch (pdiffErr) { 1175 | if (pdiffErr instanceof DimensionMismatchError) { 1176 | // If disabling the property causes the element to change shape, 1177 | // there is a clear visual regression. 1178 | hasDiff.element = true; 1179 | hasDiff.page = true; 1180 | } else { 1181 | // Log this error? 1182 | console.error(pdiffErr); 1183 | } 1184 | } 1185 | 1186 | // Only need to compute the page-level pdiff if the element-level result 1187 | // is zero. 1188 | if (!hasDiff.element) { 1189 | const screenshotPage = await this.captureScreenshot(); 1190 | window.screenshots[ 1191 | window.screenshots.length - 1 1192 | ].page = screenshotPage; 1193 | try { 1194 | const { numPixelsDifferent, diffImage } = await pdiffAll( 1195 | screenshotPage, 1196 | ); 1197 | if (numPixelsDifferent > 0) { 1198 | hasDiff.page = true; 1199 | window.screenshots.push({ 1200 | prop: prop.name, 1201 | page: diffImage || screenshotPage, 1202 | }); 1203 | } 1204 | } catch (pdiffErr) { 1205 | if (pdiffErr instanceof DimensionMismatchError) { 1206 | hasDiff.page = true; 1207 | } else { 1208 | console.error(pdiffErr); 1209 | } 1210 | } 1211 | } 1212 | 1213 | // If there is a nonzero difference at either the element or page level, 1214 | // the property is potentially relevant, so we need to reinstate it. 1215 | const hasAnyDiff = hasDiff.element || hasDiff.page; 1216 | if (hasAnyDiff) { 1217 | try { 1218 | await this._toggleStyle(nodeId, ruleIndex, propertyIndex); 1219 | } catch (toggleStyleError) { 1220 | console.error(toggleStyleError); 1221 | } 1222 | } 1223 | 1224 | // If the current property has effects outside the current element, 1225 | // mark the current rule as having a base style, and note 1226 | // the property index. 1227 | // TODO: Make this a separate function, annotateRule. 1228 | const isBlacklisted = window.DEBUG.shadowBlacklist.some( 1229 | blacklistedProperty => prop.name.indexOf(blacklistedProperty) !== -1, 1230 | ); 1231 | if (!hasDiff.element && hasDiff.page && !isBlacklisted) { 1232 | if (!ruleAnnotation) { 1233 | ruleAnnotation = { 1234 | type: 'BASE_STYLE', 1235 | shadowedProperties: [propertyIndex], 1236 | }; 1237 | } else { 1238 | ruleAnnotation.shadowedProperties.push(propertyIndex); 1239 | } 1240 | } 1241 | 1242 | // Log the differences recorded. 1243 | pruneResults.push( 1244 | Object.assign({}, hasDiff, { 1245 | property: prop.name, 1246 | }), 1247 | ); 1248 | } 1249 | 1250 | // At the end of the rule, add the list of prune results to an object. 1251 | allPruned.push([ruleMatch.rule.selectorList.text, pruneResults]); 1252 | 1253 | // TODO: This assumes the number and order of rules never 1254 | // changes between style refreshes. Need to change this to 1255 | // something less brittle if we ever add the ability to 1256 | // insert/delete/move rules around within the cascade. 1257 | ruleAnnotations.push(ruleAnnotation); 1258 | } 1259 | 1260 | // Done pruning all rules in style. Mark the rules pruned in source. 1261 | // TODO: Figure out some way to invalidate this later? 1262 | // for (const rule of matchedCSSRules) { 1263 | // await this.markRulePruned(rule); 1264 | // } 1265 | 1266 | // Update rule annotations for node. 1267 | if (window.DEBUG.annotations) { 1268 | this.ruleAnnotations[nodeId] = ruleAnnotations; 1269 | } 1270 | 1271 | console.log('allPruned', allPruned); 1272 | console.log( 1273 | 'page not self', 1274 | allPruned 1275 | .map(([selector, props]) => [ 1276 | selector, 1277 | props.filter(({ element, page }) => !element && page), 1278 | ]) 1279 | .filter(([, specialProps]) => specialProps.length > 0), 1280 | ); 1281 | console.groupEnd(); 1282 | 1283 | return allPropertiesEnabled; 1284 | } 1285 | 1286 | async markRulePruned(rule: CRDP$CSSRule) { 1287 | const { style, styleSheetId, origin } = rule; 1288 | const { cssText, range } = style; 1289 | 1290 | if (origin !== 'regular' || !cssText) { 1291 | throw new Error(`Cannot edit range for rule with origin ${origin}`); 1292 | } 1293 | 1294 | if (cssText && !cssText.match(/^\/\*\* PRUNED \*\//)) { 1295 | // No need to double-mark. 1296 | return; 1297 | } 1298 | 1299 | const PRUNED_PREFIX = '/** PRUNED */'; 1300 | const nextStyleText = `${PRUNED_PREFIX}${cssText}`; 1301 | const edit = { 1302 | styleSheetId, 1303 | range, 1304 | text: nextStyleText, 1305 | }; 1306 | 1307 | await this._sendDebugCommand({ 1308 | method: 'CSS.setStyleTexts', 1309 | params: { 1310 | edits: [edit], 1311 | }, 1312 | }); 1313 | 1314 | // TODO: Need to call this? 1315 | // this.refreshStyles(nodeId); 1316 | } 1317 | 1318 | async scrollIntoViewIfNeeded(nodeId: CRDP$NodeId): Promise<> { 1319 | const { model } = await this._sendDebugCommand({ 1320 | method: 'DOM.getBoxModel', 1321 | params: { nodeId }, 1322 | }); 1323 | const { border, content, height, width } = model; 1324 | const [x, y] = border || content; 1325 | const { visualViewport } = await this._sendDebugCommand({ 1326 | method: 'Page.getLayoutMetrics', 1327 | }); 1328 | const { clientHeight, clientWidth, pageX, pageY } = visualViewport; 1329 | 1330 | const page = { 1331 | x: { start: pageX, end: pageX + clientWidth }, 1332 | y: { start: pageY, end: pageY + clientHeight }, 1333 | }; 1334 | const el = { 1335 | x: { start: x, end: x + width }, 1336 | y: { start: y, end: y + height }, 1337 | }; 1338 | 1339 | // Check whether element is small enough to view entirely. 1340 | const elementTooWide = width > clientWidth; 1341 | const elementTooTall = height > clientHeight; 1342 | const elementTooLarge = elementTooWide || elementTooTall; 1343 | const isVisibleX = page.x.start <= el.x.start && el.x.start <= page.x.end; 1344 | const isVisibleY = page.y.start <= el.y.start && el.y.start <= page.y.end; 1345 | 1346 | // TODO: Don't scroll page if element is too large, and at all in view. 1347 | const isVisible = elementTooLarge || (isVisibleX && isVisibleY); 1348 | 1349 | if (!isVisible) { 1350 | // Scroll entire element into view. 1351 | return await this._sendDebugCommand({ 1352 | method: 'Runtime.evaluate', 1353 | params: { 1354 | expression: `window.scrollTo(document.documentElement.scrollLeft + ${x}, document.documentElement.scrollTop + ${y})`, 1355 | }, 1356 | }); 1357 | } 1358 | } 1359 | 1360 | async getScreenshotForProperty( 1361 | { 1362 | nodeId, 1363 | ruleIndex, 1364 | propertyIndex, 1365 | }: CSSPropertyPath, 1366 | ): Promise<{ page: string, el: string }> { 1367 | // TODO: Deprecate this function. 1368 | await this.toggleStyleAndRefresh({ nodeId, ruleIndex, propertyIndex }); 1369 | 1370 | // Can't do these in parallel because of viewport resize. 1371 | const page = await this.captureScreenshot(); 1372 | const el = await this.captureScreenshot(nodeId); 1373 | 1374 | const result = { 1375 | page, 1376 | el, 1377 | }; 1378 | return result; 1379 | } 1380 | 1381 | /** 1382 | * Handle certain events from the debugger. 1383 | */ 1384 | _debugEventDispatch(target: Target, method: string, params: Object) { 1385 | const dispatch = { 1386 | /** 1387 | * When new stylesheets are added, reformat the text so that 1388 | * each property is on its own line. 1389 | * 1390 | * This will make it easier to disable/re-enable without 1391 | * messing up the SourceRanges of all other properties. 1392 | */ 1393 | 'CSS.styleSheetAdded': async ({ header }) => { 1394 | const { styleSheetId } = header; 1395 | const { text } = await window.endpoint._sendDebugCommand({ 1396 | method: 'CSS.getStyleSheetText', 1397 | params: { 1398 | styleSheetId, 1399 | }, 1400 | }); 1401 | const formattedText = cssbeautify(text); 1402 | this._sendDebugCommand({ 1403 | method: 'CSS.setStyleSheetText', 1404 | params: { 1405 | styleSheetId, 1406 | text: formattedText, 1407 | }, 1408 | }); 1409 | }, 1410 | /** 1411 | * Fired when the document is updated and NodeIds 1412 | * are no longer valid. 1413 | */ 1414 | 'DOM.documentUpdated': async () => { 1415 | console.log('%cdocument updated', 'color: green; font-weight: bold;'); 1416 | this.getDocumentRoot(); 1417 | this.refreshStyles(); 1418 | }, 1419 | /** 1420 | * Fired when a node is inspected after calling DOM.setInspectMode. 1421 | * Sets this.inspectedNode to the NodeId of the clicked element. 1422 | */ 1423 | 'Overlay.inspectNodeRequested': this.nodeInspected, 1424 | 1425 | /** 1426 | * Clean up when we refresh page. 1427 | */ 1428 | 'Page.loadEventFired': this.cleanup, 1429 | }; 1430 | 1431 | const action = dispatch[method]; 1432 | 1433 | if (action) { 1434 | action(params); 1435 | } 1436 | } 1437 | 1438 | /** 1439 | * Emit data over the socket. 1440 | */ 1441 | _socketEmit(message: IncomingMessage, data?: Object) { 1442 | if (this.socket && this.socket.connected) { 1443 | this.socket.emit(message, data); 1444 | console.log(`Emitting ${message} to server`, data); 1445 | } else { 1446 | console.error(`No socket connection, couldn't emit message`, data); 1447 | } 1448 | } 1449 | 1450 | /** 1451 | * Dispatch a command to the chrome.debugger API. 1452 | */ 1453 | async _sendDebugCommand( 1454 | { 1455 | method, 1456 | params, 1457 | }: { 1458 | method: string, 1459 | params?: Object, 1460 | }, 1461 | ) { 1462 | // Highlighting will get called frequently and clog the console. 1463 | if (method !== 'DOM.highlightNode' && method !== 'DOM.hideHighlight') { 1464 | console.log( 1465 | `%c${method}`, 1466 | 'color: grey; font-weight: light;', 1467 | params, 1468 | this.target, 1469 | ); 1470 | } 1471 | return await cp.debugger.sendCommand(this.target, method, params); 1472 | } 1473 | 1474 | /** 1475 | * Clean up socket connection and properties. 1476 | */ 1477 | async cleanup() { 1478 | if (this.socket && this.socket.connected) { 1479 | this.socket.close(); 1480 | } 1481 | if (this.target) { 1482 | chrome.debugger.onEvent.removeListener(this._debugEventDispatch); 1483 | await cp.debugger.detach(this.target); 1484 | } 1485 | if (window.endpoint) { 1486 | delete window.endpoint; 1487 | } 1488 | } 1489 | 1490 | _onSocketDisconnect() { 1491 | if (this.socket) { 1492 | // this.socket.off(); 1493 | // this.socket = null; 1494 | } 1495 | console.log('Disconnected from socket'); 1496 | // this.cleanup(); 1497 | } 1498 | 1499 | _onDebuggerDetach() { 1500 | if (this.target) { 1501 | this.updateIcon('INACTIVE'); 1502 | this.target = null; 1503 | this.document = null; 1504 | console.log('Detached from debugging target'); 1505 | } 1506 | this.cleanup(); 1507 | } 1508 | } 1509 | 1510 | async function main() { 1511 | if (!window.endpoint) { 1512 | const endpoint = new BrowserEndpoint(SOCKET_PORT); 1513 | window.endpoint = endpoint; 1514 | } 1515 | 1516 | const { id: tabId } = await BrowserEndpoint.getActiveTab(); 1517 | const hasConnection = window.endpoint.target && 1518 | window.endpoint.target.tabId === tabId; 1519 | 1520 | if (!hasConnection) { 1521 | // Need to init connections. 1522 | await window.endpoint.initConnections(tabId); 1523 | } 1524 | 1525 | /** 1526 | * Invoke node selection, now that we are guaranteed 1527 | * to have an active endpoint attached to current tab. 1528 | */ 1529 | window.endpoint.selectNode(); 1530 | } 1531 | 1532 | // // HACK: Bind the test menu item. 1533 | // chrome.contextMenus.create({ 1534 | // title: 'Test pruning', 1535 | // contexts: ['browser_action'], 1536 | // onclick: testPruning, 1537 | // }); 1538 | 1539 | window.open = uri => chrome.tabs.create({ url: prefixURI(uri) }); 1540 | 1541 | chrome.browserAction.onClicked.addListener(main); 1542 | -------------------------------------------------------------------------------- /src/metadata/cssProperties.js: -------------------------------------------------------------------------------- 1 | // @format @flow 2 | 3 | export default [ 4 | { 5 | name: '-webkit-app-region', 6 | }, 7 | { 8 | name: '-webkit-appearance', 9 | }, 10 | { 11 | longhands: [ 12 | '-webkit-border-after-width', 13 | '-webkit-border-after-style', 14 | '-webkit-border-after-color', 15 | ], 16 | name: '-webkit-border-after', 17 | }, 18 | { 19 | name: '-webkit-border-after-color', 20 | }, 21 | { 22 | name: '-webkit-border-after-style', 23 | }, 24 | { 25 | name: '-webkit-border-after-width', 26 | }, 27 | { 28 | longhands: [ 29 | '-webkit-border-before-width', 30 | '-webkit-border-before-style', 31 | '-webkit-border-before-color', 32 | ], 33 | name: '-webkit-border-before', 34 | }, 35 | { 36 | name: '-webkit-border-before-color', 37 | }, 38 | { 39 | name: '-webkit-border-before-style', 40 | }, 41 | { 42 | name: '-webkit-border-before-width', 43 | }, 44 | { 45 | longhands: [ 46 | '-webkit-border-end-width', 47 | '-webkit-border-end-style', 48 | '-webkit-border-end-color', 49 | ], 50 | name: '-webkit-border-end', 51 | }, 52 | { 53 | name: '-webkit-border-end-color', 54 | }, 55 | { 56 | name: '-webkit-border-end-style', 57 | }, 58 | { 59 | name: '-webkit-border-end-width', 60 | }, 61 | { 62 | name: '-webkit-border-horizontal-spacing', 63 | inherited: true, 64 | }, 65 | { 66 | name: '-webkit-border-image', 67 | }, 68 | { 69 | longhands: [ 70 | '-webkit-border-start-width', 71 | '-webkit-border-start-style', 72 | '-webkit-border-start-color', 73 | ], 74 | name: '-webkit-border-start', 75 | }, 76 | { 77 | name: '-webkit-border-start-color', 78 | }, 79 | { 80 | name: '-webkit-border-start-style', 81 | }, 82 | { 83 | name: '-webkit-border-start-width', 84 | }, 85 | { 86 | name: '-webkit-border-vertical-spacing', 87 | inherited: true, 88 | }, 89 | { 90 | name: '-webkit-box-align', 91 | }, 92 | { 93 | name: '-webkit-box-decoration-break', 94 | }, 95 | { 96 | name: '-webkit-box-direction', 97 | inherited: true, 98 | }, 99 | { 100 | name: '-webkit-box-flex', 101 | }, 102 | { 103 | name: '-webkit-box-flex-group', 104 | }, 105 | { 106 | name: '-webkit-box-lines', 107 | }, 108 | { 109 | name: '-webkit-box-ordinal-group', 110 | }, 111 | { 112 | name: '-webkit-box-orient', 113 | }, 114 | { 115 | name: '-webkit-box-pack', 116 | }, 117 | { 118 | name: '-webkit-box-reflect', 119 | }, 120 | { 121 | longhands: ['break-after'], 122 | name: '-webkit-column-break-after', 123 | }, 124 | { 125 | longhands: ['break-before'], 126 | name: '-webkit-column-break-before', 127 | }, 128 | { 129 | longhands: ['break-inside'], 130 | name: '-webkit-column-break-inside', 131 | }, 132 | { 133 | name: '-webkit-font-size-delta', 134 | }, 135 | { 136 | name: '-webkit-font-smoothing', 137 | inherited: true, 138 | }, 139 | { 140 | name: '-webkit-highlight', 141 | inherited: true, 142 | }, 143 | { 144 | name: '-webkit-hyphenate-character', 145 | inherited: true, 146 | }, 147 | { 148 | name: '-webkit-line-break', 149 | inherited: true, 150 | }, 151 | { 152 | name: '-webkit-line-clamp', 153 | }, 154 | { 155 | name: '-webkit-locale', 156 | inherited: true, 157 | }, 158 | { 159 | name: '-webkit-logical-height', 160 | }, 161 | { 162 | name: '-webkit-logical-width', 163 | }, 164 | { 165 | name: '-webkit-margin-after', 166 | }, 167 | { 168 | name: '-webkit-margin-after-collapse', 169 | }, 170 | { 171 | name: '-webkit-margin-before', 172 | }, 173 | { 174 | name: '-webkit-margin-before-collapse', 175 | }, 176 | { 177 | name: '-webkit-margin-bottom-collapse', 178 | }, 179 | { 180 | longhands: [ 181 | '-webkit-margin-before-collapse', 182 | '-webkit-margin-after-collapse', 183 | ], 184 | name: '-webkit-margin-collapse', 185 | }, 186 | { 187 | name: '-webkit-margin-end', 188 | }, 189 | { 190 | name: '-webkit-margin-start', 191 | }, 192 | { 193 | name: '-webkit-margin-top-collapse', 194 | }, 195 | { 196 | longhands: [ 197 | '-webkit-mask-image', 198 | '-webkit-mask-position-x', 199 | '-webkit-mask-position-y', 200 | '-webkit-mask-size', 201 | '-webkit-mask-repeat-x', 202 | '-webkit-mask-repeat-y', 203 | '-webkit-mask-origin', 204 | '-webkit-mask-clip', 205 | ], 206 | name: '-webkit-mask', 207 | }, 208 | { 209 | longhands: [ 210 | '-webkit-mask-box-image-source', 211 | '-webkit-mask-box-image-slice', 212 | '-webkit-mask-box-image-width', 213 | '-webkit-mask-box-image-outset', 214 | '-webkit-mask-box-image-repeat', 215 | ], 216 | name: '-webkit-mask-box-image', 217 | }, 218 | { 219 | name: '-webkit-mask-box-image-outset', 220 | }, 221 | { 222 | name: '-webkit-mask-box-image-repeat', 223 | }, 224 | { 225 | name: '-webkit-mask-box-image-slice', 226 | }, 227 | { 228 | name: '-webkit-mask-box-image-source', 229 | }, 230 | { 231 | name: '-webkit-mask-box-image-width', 232 | }, 233 | { 234 | name: '-webkit-mask-clip', 235 | }, 236 | { 237 | name: '-webkit-mask-composite', 238 | }, 239 | { 240 | name: '-webkit-mask-image', 241 | }, 242 | { 243 | name: '-webkit-mask-origin', 244 | }, 245 | { 246 | longhands: ['-webkit-mask-position-x', '-webkit-mask-position-y'], 247 | name: '-webkit-mask-position', 248 | }, 249 | { 250 | name: '-webkit-mask-position-x', 251 | }, 252 | { 253 | name: '-webkit-mask-position-y', 254 | }, 255 | { 256 | longhands: ['-webkit-mask-repeat-x', '-webkit-mask-repeat-y'], 257 | name: '-webkit-mask-repeat', 258 | }, 259 | { 260 | name: '-webkit-mask-repeat-x', 261 | }, 262 | { 263 | name: '-webkit-mask-repeat-y', 264 | }, 265 | { 266 | name: '-webkit-mask-size', 267 | }, 268 | { 269 | name: '-webkit-max-logical-height', 270 | }, 271 | { 272 | name: '-webkit-max-logical-width', 273 | }, 274 | { 275 | name: '-webkit-min-logical-height', 276 | }, 277 | { 278 | name: '-webkit-min-logical-width', 279 | }, 280 | { 281 | name: '-webkit-padding-after', 282 | }, 283 | { 284 | name: '-webkit-padding-before', 285 | }, 286 | { 287 | name: '-webkit-padding-end', 288 | }, 289 | { 290 | name: '-webkit-padding-start', 291 | }, 292 | { 293 | name: '-webkit-perspective-origin-x', 294 | }, 295 | { 296 | name: '-webkit-perspective-origin-y', 297 | }, 298 | { 299 | name: '-webkit-print-color-adjust', 300 | inherited: true, 301 | }, 302 | { 303 | name: '-webkit-rtl-ordering', 304 | inherited: true, 305 | }, 306 | { 307 | name: '-webkit-ruby-position', 308 | inherited: true, 309 | }, 310 | { 311 | name: '-webkit-tap-highlight-color', 312 | inherited: true, 313 | }, 314 | { 315 | name: '-webkit-text-combine', 316 | inherited: true, 317 | }, 318 | { 319 | name: '-webkit-text-decorations-in-effect', 320 | inherited: true, 321 | }, 322 | { 323 | longhands: ['-webkit-text-emphasis-style', '-webkit-text-emphasis-color'], 324 | name: '-webkit-text-emphasis', 325 | }, 326 | { 327 | name: '-webkit-text-emphasis-color', 328 | inherited: true, 329 | }, 330 | { 331 | name: '-webkit-text-emphasis-position', 332 | inherited: true, 333 | }, 334 | { 335 | name: '-webkit-text-emphasis-style', 336 | inherited: true, 337 | }, 338 | { 339 | name: '-webkit-text-fill-color', 340 | inherited: true, 341 | }, 342 | { 343 | name: '-webkit-text-orientation', 344 | inherited: true, 345 | }, 346 | { 347 | name: '-webkit-text-security', 348 | inherited: true, 349 | }, 350 | { 351 | longhands: ['-webkit-text-stroke-width', '-webkit-text-stroke-color'], 352 | name: '-webkit-text-stroke', 353 | }, 354 | { 355 | name: '-webkit-text-stroke-color', 356 | inherited: true, 357 | }, 358 | { 359 | name: '-webkit-text-stroke-width', 360 | inherited: true, 361 | }, 362 | { 363 | name: '-webkit-transform-origin-x', 364 | }, 365 | { 366 | name: '-webkit-transform-origin-y', 367 | }, 368 | { 369 | name: '-webkit-transform-origin-z', 370 | }, 371 | { 372 | name: '-webkit-user-drag', 373 | }, 374 | { 375 | name: '-webkit-user-modify', 376 | inherited: true, 377 | }, 378 | { 379 | name: '-webkit-writing-mode', 380 | inherited: true, 381 | }, 382 | { 383 | name: 'align-content', 384 | }, 385 | { 386 | name: 'align-items', 387 | }, 388 | { 389 | name: 'align-self', 390 | }, 391 | { 392 | svg: true, 393 | name: 'alignment-baseline', 394 | }, 395 | { 396 | name: 'all', 397 | }, 398 | { 399 | longhands: [ 400 | 'animation-name', 401 | 'animation-duration', 402 | 'animation-timing-function', 403 | 'animation-delay', 404 | 'animation-iteration-count', 405 | 'animation-direction', 406 | 'animation-fill-mode', 407 | 'animation-play-state', 408 | ], 409 | name: 'animation', 410 | }, 411 | { 412 | name: 'animation-delay', 413 | }, 414 | { 415 | name: 'animation-direction', 416 | }, 417 | { 418 | name: 'animation-duration', 419 | }, 420 | { 421 | name: 'animation-fill-mode', 422 | }, 423 | { 424 | name: 'animation-iteration-count', 425 | }, 426 | { 427 | name: 'animation-name', 428 | }, 429 | { 430 | name: 'animation-play-state', 431 | }, 432 | { 433 | name: 'animation-timing-function', 434 | }, 435 | { 436 | name: 'backdrop-filter', 437 | }, 438 | { 439 | name: 'backface-visibility', 440 | }, 441 | { 442 | longhands: [ 443 | 'background-image', 444 | 'background-position-x', 445 | 'background-position-y', 446 | 'background-size', 447 | 'background-repeat-x', 448 | 'background-repeat-y', 449 | 'background-attachment', 450 | 'background-origin', 451 | 'background-clip', 452 | 'background-color', 453 | ], 454 | name: 'background', 455 | }, 456 | { 457 | name: 'background-attachment', 458 | }, 459 | { 460 | name: 'background-blend-mode', 461 | }, 462 | { 463 | name: 'background-clip', 464 | }, 465 | { 466 | name: 'background-color', 467 | }, 468 | { 469 | name: 'background-image', 470 | }, 471 | { 472 | name: 'background-origin', 473 | }, 474 | { 475 | longhands: ['background-position-x', 'background-position-y'], 476 | name: 'background-position', 477 | }, 478 | { 479 | name: 'background-position-x', 480 | }, 481 | { 482 | name: 'background-position-y', 483 | }, 484 | { 485 | longhands: ['background-repeat-x', 'background-repeat-y'], 486 | name: 'background-repeat', 487 | }, 488 | { 489 | name: 'background-repeat-x', 490 | }, 491 | { 492 | name: 'background-repeat-y', 493 | }, 494 | { 495 | name: 'background-size', 496 | }, 497 | { 498 | name: 'baseline-shift', 499 | svg: true, 500 | }, 501 | { 502 | name: 'block-size', 503 | }, 504 | { 505 | longhands: [ 506 | 'border-top-color', 507 | 'border-top-style', 508 | 'border-top-width', 509 | 'border-right-color', 510 | 'border-right-style', 511 | 'border-right-width', 512 | 'border-bottom-color', 513 | 'border-bottom-style', 514 | 'border-bottom-width', 515 | 'border-left-color', 516 | 'border-left-style', 517 | 'border-left-width', 518 | 'border-image-source', 519 | 'border-image-slice', 520 | 'border-image-width', 521 | 'border-image-outset', 522 | 'border-image-repeat', 523 | ], 524 | name: 'border', 525 | }, 526 | { 527 | longhands: [ 528 | 'border-bottom-width', 529 | 'border-bottom-style', 530 | 'border-bottom-color', 531 | ], 532 | name: 'border-bottom', 533 | }, 534 | { 535 | name: 'border-bottom-color', 536 | }, 537 | { 538 | name: 'border-bottom-left-radius', 539 | }, 540 | { 541 | name: 'border-bottom-right-radius', 542 | }, 543 | { 544 | name: 'border-bottom-style', 545 | }, 546 | { 547 | name: 'border-bottom-width', 548 | }, 549 | { 550 | name: 'border-collapse', 551 | inherited: true, 552 | }, 553 | { 554 | longhands: [ 555 | 'border-top-color', 556 | 'border-right-color', 557 | 'border-bottom-color', 558 | 'border-left-color', 559 | ], 560 | name: 'border-color', 561 | }, 562 | { 563 | longhands: [ 564 | 'border-image-source', 565 | 'border-image-slice', 566 | 'border-image-width', 567 | 'border-image-outset', 568 | 'border-image-repeat', 569 | ], 570 | name: 'border-image', 571 | }, 572 | { 573 | name: 'border-image-outset', 574 | }, 575 | { 576 | name: 'border-image-repeat', 577 | }, 578 | { 579 | name: 'border-image-slice', 580 | }, 581 | { 582 | name: 'border-image-source', 583 | }, 584 | { 585 | name: 'border-image-width', 586 | }, 587 | { 588 | longhands: ['border-left-width', 'border-left-style', 'border-left-color'], 589 | name: 'border-left', 590 | }, 591 | { 592 | name: 'border-left-color', 593 | }, 594 | { 595 | name: 'border-left-style', 596 | }, 597 | { 598 | name: 'border-left-width', 599 | }, 600 | { 601 | longhands: [ 602 | 'border-top-left-radius', 603 | 'border-top-right-radius', 604 | 'border-bottom-right-radius', 605 | 'border-bottom-left-radius', 606 | ], 607 | name: 'border-radius', 608 | }, 609 | { 610 | longhands: [ 611 | 'border-right-width', 612 | 'border-right-style', 613 | 'border-right-color', 614 | ], 615 | name: 'border-right', 616 | }, 617 | { 618 | name: 'border-right-color', 619 | }, 620 | { 621 | name: 'border-right-style', 622 | }, 623 | { 624 | name: 'border-right-width', 625 | }, 626 | { 627 | longhands: [ 628 | '-webkit-border-horizontal-spacing', 629 | '-webkit-border-vertical-spacing', 630 | ], 631 | name: 'border-spacing', 632 | }, 633 | { 634 | name: 'border-style', 635 | longhands: [ 636 | 'border-top-style', 637 | 'border-right-style', 638 | 'border-bottom-style', 639 | 'border-left-style', 640 | ], 641 | }, 642 | { 643 | longhands: ['border-top-width', 'border-top-style', 'border-top-color'], 644 | name: 'border-top', 645 | }, 646 | { 647 | name: 'border-top-color', 648 | }, 649 | { 650 | name: 'border-top-left-radius', 651 | }, 652 | { 653 | name: 'border-top-right-radius', 654 | }, 655 | { 656 | name: 'border-top-style', 657 | }, 658 | { 659 | name: 'border-top-width', 660 | }, 661 | { 662 | longhands: [ 663 | 'border-top-width', 664 | 'border-right-width', 665 | 'border-bottom-width', 666 | 'border-left-width', 667 | ], 668 | name: 'border-width', 669 | }, 670 | { 671 | name: 'bottom', 672 | }, 673 | { 674 | name: 'box-shadow', 675 | }, 676 | { 677 | name: 'box-sizing', 678 | }, 679 | { 680 | name: 'break-after', 681 | }, 682 | { 683 | name: 'break-before', 684 | }, 685 | { 686 | name: 'break-inside', 687 | }, 688 | { 689 | svg: true, 690 | name: 'buffered-rendering', 691 | }, 692 | { 693 | name: 'caption-side', 694 | inherited: true, 695 | }, 696 | { 697 | name: 'caret-color', 698 | inherited: true, 699 | }, 700 | { 701 | name: 'clear', 702 | }, 703 | { 704 | name: 'clip', 705 | }, 706 | { 707 | name: 'clip-path', 708 | }, 709 | { 710 | svg: true, 711 | name: 'clip-rule', 712 | inherited: true, 713 | }, 714 | { 715 | name: 'color', 716 | inherited: true, 717 | }, 718 | { 719 | svg: true, 720 | name: 'color-interpolation', 721 | inherited: true, 722 | }, 723 | { 724 | svg: true, 725 | name: 'color-interpolation-filters', 726 | inherited: true, 727 | }, 728 | { 729 | svg: true, 730 | name: 'color-rendering', 731 | inherited: true, 732 | }, 733 | { 734 | name: 'column-count', 735 | }, 736 | { 737 | name: 'column-fill', 738 | }, 739 | { 740 | name: 'column-gap', 741 | }, 742 | { 743 | longhands: ['column-rule-width', 'column-rule-style', 'column-rule-color'], 744 | name: 'column-rule', 745 | }, 746 | { 747 | name: 'column-rule-color', 748 | }, 749 | { 750 | name: 'column-rule-style', 751 | }, 752 | { 753 | name: 'column-rule-width', 754 | }, 755 | { 756 | name: 'column-span', 757 | }, 758 | { 759 | name: 'column-width', 760 | }, 761 | { 762 | longhands: ['column-width', 'column-count'], 763 | name: 'columns', 764 | }, 765 | { 766 | name: 'contain', 767 | }, 768 | { 769 | name: 'content', 770 | }, 771 | { 772 | name: 'counter-increment', 773 | }, 774 | { 775 | name: 'counter-reset', 776 | }, 777 | { 778 | name: 'cursor', 779 | inherited: true, 780 | }, 781 | { 782 | name: 'cx', 783 | svg: true, 784 | }, 785 | { 786 | name: 'cy', 787 | svg: true, 788 | }, 789 | { 790 | name: 'd', 791 | svg: true, 792 | }, 793 | { 794 | name: 'direction', 795 | inherited: true, 796 | }, 797 | { 798 | name: 'display', 799 | }, 800 | { 801 | svg: true, 802 | name: 'dominant-baseline', 803 | inherited: true, 804 | }, 805 | { 806 | name: 'empty-cells', 807 | inherited: true, 808 | }, 809 | { 810 | name: 'fill', 811 | svg: true, 812 | inherited: true, 813 | }, 814 | { 815 | name: 'fill-opacity', 816 | svg: true, 817 | inherited: true, 818 | }, 819 | { 820 | svg: true, 821 | name: 'fill-rule', 822 | inherited: true, 823 | }, 824 | { 825 | name: 'filter', 826 | }, 827 | { 828 | longhands: ['flex-grow', 'flex-shrink', 'flex-basis'], 829 | name: 'flex', 830 | }, 831 | { 832 | name: 'flex-basis', 833 | }, 834 | { 835 | name: 'flex-direction', 836 | }, 837 | { 838 | longhands: ['flex-direction', 'flex-wrap'], 839 | name: 'flex-flow', 840 | }, 841 | { 842 | name: 'flex-grow', 843 | }, 844 | { 845 | name: 'flex-shrink', 846 | }, 847 | { 848 | name: 'flex-wrap', 849 | }, 850 | { 851 | name: 'float', 852 | }, 853 | { 854 | name: 'flood-color', 855 | svg: true, 856 | }, 857 | { 858 | name: 'flood-opacity', 859 | svg: true, 860 | }, 861 | { 862 | longhands: [ 863 | 'font-style', 864 | 'font-variant-ligatures', 865 | 'font-variant-caps', 866 | 'font-variant-numeric', 867 | 'font-variant-east-asian', 868 | 'font-weight', 869 | 'font-stretch', 870 | 'font-size', 871 | 'line-height', 872 | 'font-family', 873 | ], 874 | name: 'font', 875 | }, 876 | { 877 | name: 'font-display', 878 | }, 879 | { 880 | name: 'font-family', 881 | inherited: true, 882 | }, 883 | { 884 | name: 'font-feature-settings', 885 | inherited: true, 886 | }, 887 | { 888 | name: 'font-kerning', 889 | inherited: true, 890 | }, 891 | { 892 | name: 'font-size', 893 | inherited: true, 894 | }, 895 | { 896 | name: 'font-size-adjust', 897 | inherited: true, 898 | }, 899 | { 900 | name: 'font-stretch', 901 | inherited: true, 902 | }, 903 | { 904 | name: 'font-style', 905 | inherited: true, 906 | }, 907 | { 908 | longhands: [ 909 | 'font-variant-ligatures', 910 | 'font-variant-caps', 911 | 'font-variant-numeric', 912 | 'font-variant-east-asian', 913 | ], 914 | name: 'font-variant', 915 | }, 916 | { 917 | name: 'font-variant-caps', 918 | inherited: true, 919 | }, 920 | { 921 | name: 'font-variant-east-asian', 922 | inherited: true, 923 | }, 924 | { 925 | name: 'font-variant-ligatures', 926 | inherited: true, 927 | }, 928 | { 929 | name: 'font-variant-numeric', 930 | inherited: true, 931 | }, 932 | { 933 | name: 'font-variation-settings', 934 | inherited: true, 935 | }, 936 | { 937 | name: 'font-weight', 938 | inherited: true, 939 | }, 940 | { 941 | longhands: [ 942 | 'grid-template-rows', 943 | 'grid-template-columns', 944 | 'grid-template-areas', 945 | 'grid-auto-flow', 946 | 'grid-auto-rows', 947 | 'grid-auto-columns', 948 | ], 949 | name: 'grid', 950 | }, 951 | { 952 | longhands: [ 953 | 'grid-row-start', 954 | 'grid-column-start', 955 | 'grid-row-end', 956 | 'grid-column-end', 957 | ], 958 | name: 'grid-area', 959 | }, 960 | { 961 | name: 'grid-auto-columns', 962 | }, 963 | { 964 | name: 'grid-auto-flow', 965 | }, 966 | { 967 | name: 'grid-auto-rows', 968 | }, 969 | { 970 | longhands: ['grid-column-start', 'grid-column-end'], 971 | name: 'grid-column', 972 | }, 973 | { 974 | name: 'grid-column-end', 975 | }, 976 | { 977 | name: 'grid-column-gap', 978 | }, 979 | { 980 | name: 'grid-column-start', 981 | }, 982 | { 983 | longhands: ['grid-row-gap', 'grid-column-gap'], 984 | name: 'grid-gap', 985 | }, 986 | { 987 | longhands: ['grid-row-start', 'grid-row-end'], 988 | name: 'grid-row', 989 | }, 990 | { 991 | name: 'grid-row-end', 992 | }, 993 | { 994 | name: 'grid-row-gap', 995 | }, 996 | { 997 | name: 'grid-row-start', 998 | }, 999 | { 1000 | longhands: [ 1001 | 'grid-template-rows', 1002 | 'grid-template-columns', 1003 | 'grid-template-areas', 1004 | ], 1005 | name: 'grid-template', 1006 | }, 1007 | { 1008 | name: 'grid-template-areas', 1009 | }, 1010 | { 1011 | name: 'grid-template-columns', 1012 | }, 1013 | { 1014 | name: 'grid-template-rows', 1015 | }, 1016 | { 1017 | name: 'height', 1018 | }, 1019 | { 1020 | name: 'hyphens', 1021 | inherited: true, 1022 | }, 1023 | { 1024 | name: 'image-orientation', 1025 | inherited: true, 1026 | }, 1027 | { 1028 | name: 'image-rendering', 1029 | inherited: true, 1030 | }, 1031 | { 1032 | name: 'inline-size', 1033 | }, 1034 | { 1035 | name: 'isolation', 1036 | }, 1037 | { 1038 | name: 'justify-content', 1039 | }, 1040 | { 1041 | name: 'justify-items', 1042 | }, 1043 | { 1044 | name: 'justify-self', 1045 | }, 1046 | { 1047 | name: 'left', 1048 | }, 1049 | { 1050 | inherited: true, 1051 | name: 'letter-spacing', 1052 | }, 1053 | { 1054 | name: 'lighting-color', 1055 | svg: true, 1056 | }, 1057 | { 1058 | name: 'line-break', 1059 | inherited: true, 1060 | }, 1061 | { 1062 | name: 'line-height', 1063 | inherited: true, 1064 | }, 1065 | { 1066 | name: 'line-height-step', 1067 | inherited: true, 1068 | }, 1069 | { 1070 | longhands: ['list-style-type', 'list-style-position', 'list-style-image'], 1071 | name: 'list-style', 1072 | }, 1073 | { 1074 | name: 'list-style-image', 1075 | inherited: true, 1076 | }, 1077 | { 1078 | name: 'list-style-position', 1079 | inherited: true, 1080 | }, 1081 | { 1082 | name: 'list-style-type', 1083 | inherited: true, 1084 | }, 1085 | { 1086 | longhands: ['margin-top', 'margin-right', 'margin-bottom', 'margin-left'], 1087 | name: 'margin', 1088 | }, 1089 | { 1090 | name: 'margin-bottom', 1091 | }, 1092 | { 1093 | name: 'margin-left', 1094 | }, 1095 | { 1096 | name: 'margin-right', 1097 | }, 1098 | { 1099 | name: 'margin-top', 1100 | }, 1101 | { 1102 | longhands: ['marker-start', 'marker-mid', 'marker-end'], 1103 | name: 'marker', 1104 | }, 1105 | { 1106 | name: 'marker-end', 1107 | svg: true, 1108 | inherited: true, 1109 | }, 1110 | { 1111 | name: 'marker-mid', 1112 | svg: true, 1113 | inherited: true, 1114 | }, 1115 | { 1116 | name: 'marker-start', 1117 | svg: true, 1118 | inherited: true, 1119 | }, 1120 | { 1121 | name: 'mask', 1122 | svg: true, 1123 | }, 1124 | { 1125 | name: 'mask-source-type', 1126 | }, 1127 | { 1128 | svg: true, 1129 | name: 'mask-type', 1130 | }, 1131 | { 1132 | name: 'max-block-size', 1133 | }, 1134 | { 1135 | name: 'max-height', 1136 | }, 1137 | { 1138 | name: 'max-inline-size', 1139 | }, 1140 | { 1141 | name: 'max-width', 1142 | }, 1143 | { 1144 | name: 'max-zoom', 1145 | }, 1146 | { 1147 | name: 'min-block-size', 1148 | }, 1149 | { 1150 | name: 'min-height', 1151 | }, 1152 | { 1153 | name: 'min-inline-size', 1154 | }, 1155 | { 1156 | name: 'min-width', 1157 | }, 1158 | { 1159 | name: 'min-zoom', 1160 | }, 1161 | { 1162 | name: 'mix-blend-mode', 1163 | }, 1164 | { 1165 | name: 'object-fit', 1166 | }, 1167 | { 1168 | name: 'object-position', 1169 | }, 1170 | { 1171 | longhands: [ 1172 | 'offset-position', 1173 | 'offset-path', 1174 | 'offset-distance', 1175 | 'offset-rotate', 1176 | 'offset-anchor', 1177 | ], 1178 | name: 'offset', 1179 | }, 1180 | { 1181 | name: 'offset-anchor', 1182 | }, 1183 | { 1184 | name: 'offset-distance', 1185 | }, 1186 | { 1187 | name: 'offset-path', 1188 | }, 1189 | { 1190 | name: 'offset-position', 1191 | }, 1192 | { 1193 | name: 'offset-rotate', 1194 | }, 1195 | { 1196 | name: 'opacity', 1197 | }, 1198 | { 1199 | name: 'order', 1200 | }, 1201 | { 1202 | name: 'orientation', 1203 | }, 1204 | { 1205 | name: 'orphans', 1206 | inherited: true, 1207 | }, 1208 | { 1209 | longhands: ['outline-color', 'outline-style', 'outline-width'], 1210 | name: 'outline', 1211 | }, 1212 | { 1213 | name: 'outline-color', 1214 | }, 1215 | { 1216 | name: 'outline-offset', 1217 | }, 1218 | { 1219 | name: 'outline-style', 1220 | }, 1221 | { 1222 | name: 'outline-width', 1223 | }, 1224 | { 1225 | longhands: ['overflow-x', 'overflow-y'], 1226 | name: 'overflow', 1227 | }, 1228 | { 1229 | name: 'overflow-anchor', 1230 | inherited: false, 1231 | }, 1232 | { 1233 | name: 'overflow-wrap', 1234 | inherited: true, 1235 | }, 1236 | { 1237 | name: 'overflow-x', 1238 | }, 1239 | { 1240 | name: 'overflow-y', 1241 | }, 1242 | { 1243 | longhands: ['overscroll-behavior-x', 'overscroll-behavior-y'], 1244 | name: 'overscroll-behavior', 1245 | }, 1246 | { 1247 | name: 'overscroll-behavior-x', 1248 | }, 1249 | { 1250 | name: 'overscroll-behavior-y', 1251 | }, 1252 | { 1253 | longhands: [ 1254 | 'padding-top', 1255 | 'padding-right', 1256 | 'padding-bottom', 1257 | 'padding-left', 1258 | ], 1259 | name: 'padding', 1260 | }, 1261 | { 1262 | name: 'padding-bottom', 1263 | }, 1264 | { 1265 | name: 'padding-left', 1266 | }, 1267 | { 1268 | name: 'padding-right', 1269 | }, 1270 | { 1271 | name: 'padding-top', 1272 | }, 1273 | { 1274 | name: 'page', 1275 | }, 1276 | { 1277 | longhands: ['break-after'], 1278 | name: 'page-break-after', 1279 | }, 1280 | { 1281 | longhands: ['break-before'], 1282 | name: 'page-break-before', 1283 | }, 1284 | { 1285 | longhands: ['break-inside'], 1286 | name: 'page-break-inside', 1287 | }, 1288 | { 1289 | svg: true, 1290 | inherited: true, 1291 | name: 'paint-order', 1292 | }, 1293 | { 1294 | name: 'perspective', 1295 | }, 1296 | { 1297 | name: 'perspective-origin', 1298 | }, 1299 | { 1300 | longhands: ['align-content', 'justify-content'], 1301 | name: 'place-content', 1302 | }, 1303 | { 1304 | longhands: ['align-items', 'justify-items'], 1305 | name: 'place-items', 1306 | }, 1307 | { 1308 | longhands: ['align-self', 'justify-self'], 1309 | name: 'place-self', 1310 | }, 1311 | { 1312 | name: 'pointer-events', 1313 | inherited: true, 1314 | }, 1315 | { 1316 | name: 'position', 1317 | }, 1318 | { 1319 | name: 'quotes', 1320 | inherited: true, 1321 | }, 1322 | { 1323 | name: 'r', 1324 | svg: true, 1325 | }, 1326 | { 1327 | name: 'resize', 1328 | }, 1329 | { 1330 | name: 'right', 1331 | }, 1332 | { 1333 | name: 'rotate', 1334 | }, 1335 | { 1336 | name: 'rx', 1337 | svg: true, 1338 | }, 1339 | { 1340 | name: 'ry', 1341 | svg: true, 1342 | }, 1343 | { 1344 | name: 'scale', 1345 | }, 1346 | { 1347 | name: 'scroll-behavior', 1348 | }, 1349 | { 1350 | longhands: [ 1351 | 'scroll-padding-top', 1352 | 'scroll-padding-right', 1353 | 'scroll-padding-bottom', 1354 | 'scroll-padding-left', 1355 | ], 1356 | name: 'scroll-padding', 1357 | }, 1358 | { 1359 | longhands: ['scroll-padding-block-start', 'scroll-padding-block-end'], 1360 | name: 'scroll-padding-block', 1361 | }, 1362 | { 1363 | name: 'scroll-padding-block-end', 1364 | }, 1365 | { 1366 | name: 'scroll-padding-block-start', 1367 | }, 1368 | { 1369 | name: 'scroll-padding-bottom', 1370 | }, 1371 | { 1372 | longhands: ['scroll-padding-inline-start', 'scroll-padding-inline-end'], 1373 | name: 'scroll-padding-inline', 1374 | }, 1375 | { 1376 | name: 'scroll-padding-inline-end', 1377 | }, 1378 | { 1379 | name: 'scroll-padding-inline-start', 1380 | }, 1381 | { 1382 | name: 'scroll-padding-left', 1383 | }, 1384 | { 1385 | name: 'scroll-padding-right', 1386 | }, 1387 | { 1388 | name: 'scroll-padding-top', 1389 | }, 1390 | { 1391 | name: 'scroll-snap-align', 1392 | }, 1393 | { 1394 | longhands: [ 1395 | 'scroll-snap-margin-top', 1396 | 'scroll-snap-margin-right', 1397 | 'scroll-snap-margin-bottom', 1398 | 'scroll-snap-margin-left', 1399 | ], 1400 | name: 'scroll-snap-margin', 1401 | }, 1402 | { 1403 | longhands: [ 1404 | 'scroll-snap-margin-block-start', 1405 | 'scroll-snap-margin-block-end', 1406 | ], 1407 | name: 'scroll-snap-margin-block', 1408 | }, 1409 | { 1410 | name: 'scroll-snap-margin-block-end', 1411 | }, 1412 | { 1413 | name: 'scroll-snap-margin-block-start', 1414 | }, 1415 | { 1416 | name: 'scroll-snap-margin-bottom', 1417 | }, 1418 | { 1419 | longhands: [ 1420 | 'scroll-snap-margin-inline-start', 1421 | 'scroll-snap-margin-inline-end', 1422 | ], 1423 | name: 'scroll-snap-margin-inline', 1424 | }, 1425 | { 1426 | name: 'scroll-snap-margin-inline-end', 1427 | }, 1428 | { 1429 | name: 'scroll-snap-margin-inline-start', 1430 | }, 1431 | { 1432 | name: 'scroll-snap-margin-left', 1433 | }, 1434 | { 1435 | name: 'scroll-snap-margin-right', 1436 | }, 1437 | { 1438 | name: 'scroll-snap-margin-top', 1439 | }, 1440 | { 1441 | name: 'scroll-snap-stop', 1442 | }, 1443 | { 1444 | name: 'scroll-snap-type', 1445 | }, 1446 | { 1447 | name: 'shape-image-threshold', 1448 | }, 1449 | { 1450 | name: 'shape-margin', 1451 | }, 1452 | { 1453 | name: 'shape-outside', 1454 | }, 1455 | { 1456 | svg: true, 1457 | name: 'shape-rendering', 1458 | inherited: true, 1459 | }, 1460 | { 1461 | name: 'size', 1462 | }, 1463 | { 1464 | name: 'speak', 1465 | inherited: true, 1466 | }, 1467 | { 1468 | name: 'src', 1469 | }, 1470 | { 1471 | name: 'stop-color', 1472 | svg: true, 1473 | }, 1474 | { 1475 | name: 'stop-opacity', 1476 | svg: true, 1477 | }, 1478 | { 1479 | name: 'stroke', 1480 | svg: true, 1481 | inherited: true, 1482 | }, 1483 | { 1484 | name: 'stroke-dasharray', 1485 | svg: true, 1486 | inherited: true, 1487 | }, 1488 | { 1489 | name: 'stroke-dashoffset', 1490 | svg: true, 1491 | inherited: true, 1492 | }, 1493 | { 1494 | name: 'stroke-linecap', 1495 | svg: true, 1496 | inherited: true, 1497 | }, 1498 | { 1499 | name: 'stroke-linejoin', 1500 | svg: true, 1501 | inherited: true, 1502 | }, 1503 | { 1504 | name: 'stroke-miterlimit', 1505 | svg: true, 1506 | inherited: true, 1507 | }, 1508 | { 1509 | name: 'stroke-opacity', 1510 | svg: true, 1511 | inherited: true, 1512 | }, 1513 | { 1514 | name: 'stroke-width', 1515 | svg: true, 1516 | inherited: true, 1517 | }, 1518 | { 1519 | name: 'tab-size', 1520 | inherited: true, 1521 | }, 1522 | { 1523 | name: 'table-layout', 1524 | }, 1525 | { 1526 | name: 'text-align', 1527 | inherited: true, 1528 | }, 1529 | { 1530 | name: 'text-align-last', 1531 | inherited: true, 1532 | }, 1533 | { 1534 | svg: true, 1535 | name: 'text-anchor', 1536 | inherited: true, 1537 | }, 1538 | { 1539 | name: 'text-combine-upright', 1540 | inherited: true, 1541 | }, 1542 | { 1543 | longhands: [ 1544 | 'text-decoration-line', 1545 | 'text-decoration-style', 1546 | 'text-decoration-color', 1547 | ], 1548 | name: 'text-decoration', 1549 | }, 1550 | { 1551 | name: 'text-decoration-color', 1552 | }, 1553 | { 1554 | name: 'text-decoration-line', 1555 | }, 1556 | { 1557 | name: 'text-decoration-skip-ink', 1558 | inherited: true, 1559 | }, 1560 | { 1561 | name: 'text-decoration-style', 1562 | }, 1563 | { 1564 | name: 'text-indent', 1565 | inherited: true, 1566 | }, 1567 | { 1568 | name: 'text-justify', 1569 | inherited: true, 1570 | }, 1571 | { 1572 | name: 'text-orientation', 1573 | inherited: true, 1574 | }, 1575 | { 1576 | name: 'text-overflow', 1577 | }, 1578 | { 1579 | name: 'text-rendering', 1580 | inherited: true, 1581 | }, 1582 | { 1583 | name: 'text-shadow', 1584 | inherited: true, 1585 | }, 1586 | { 1587 | name: 'text-size-adjust', 1588 | inherited: true, 1589 | }, 1590 | { 1591 | name: 'text-transform', 1592 | inherited: true, 1593 | }, 1594 | { 1595 | name: 'text-underline-position', 1596 | inherited: true, 1597 | }, 1598 | { 1599 | name: 'top', 1600 | }, 1601 | { 1602 | name: 'touch-action', 1603 | }, 1604 | { 1605 | name: 'transform', 1606 | }, 1607 | { 1608 | name: 'transform-box', 1609 | }, 1610 | { 1611 | name: 'transform-origin', 1612 | }, 1613 | { 1614 | name: 'transform-style', 1615 | }, 1616 | { 1617 | longhands: [ 1618 | 'transition-property', 1619 | 'transition-duration', 1620 | 'transition-timing-function', 1621 | 'transition-delay', 1622 | ], 1623 | name: 'transition', 1624 | }, 1625 | { 1626 | name: 'transition-delay', 1627 | }, 1628 | { 1629 | name: 'transition-duration', 1630 | }, 1631 | { 1632 | name: 'transition-property', 1633 | }, 1634 | { 1635 | name: 'transition-timing-function', 1636 | }, 1637 | { 1638 | name: 'translate', 1639 | }, 1640 | { 1641 | name: 'unicode-bidi', 1642 | }, 1643 | { 1644 | name: 'unicode-range', 1645 | }, 1646 | { 1647 | name: 'user-select', 1648 | inherited: true, 1649 | }, 1650 | { 1651 | name: 'user-zoom', 1652 | }, 1653 | { 1654 | svg: true, 1655 | name: 'vector-effect', 1656 | }, 1657 | { 1658 | name: 'vertical-align', 1659 | }, 1660 | { 1661 | name: 'visibility', 1662 | inherited: true, 1663 | }, 1664 | { 1665 | name: 'white-space', 1666 | inherited: true, 1667 | }, 1668 | { 1669 | name: 'widows', 1670 | inherited: true, 1671 | }, 1672 | { 1673 | name: 'width', 1674 | }, 1675 | { 1676 | name: 'will-change', 1677 | }, 1678 | { 1679 | name: 'word-break', 1680 | inherited: true, 1681 | }, 1682 | { 1683 | inherited: true, 1684 | name: 'word-spacing', 1685 | }, 1686 | { 1687 | name: 'word-wrap', 1688 | inherited: true, 1689 | }, 1690 | { 1691 | name: 'writing-mode', 1692 | inherited: true, 1693 | }, 1694 | { 1695 | name: 'x', 1696 | svg: true, 1697 | }, 1698 | { 1699 | name: 'y', 1700 | svg: true, 1701 | }, 1702 | { 1703 | name: 'z-index', 1704 | }, 1705 | { 1706 | name: 'zoom', 1707 | }, 1708 | ]; 1709 | -------------------------------------------------------------------------------- /src/metadata/index.js: -------------------------------------------------------------------------------- 1 | // @flow @format 2 | import CSS_PROPERTIES from './cssProperties'; 3 | 4 | type CSSPropertyData = Array<{ 5 | name: string, 6 | longhands?: Array, 7 | svg?: boolean, 8 | inherited?: boolean, 9 | }>; 10 | 11 | class CSSMetadata { 12 | _allProperties: Set; 13 | _inherited: Set; 14 | _longhands: Map>; 15 | 16 | constructor(data: CSSPropertyData) { 17 | this._allProperties = new Set(); 18 | this._inherited = new Set(); 19 | this._longhands = new Map(); 20 | 21 | for (const property of data) { 22 | const { name } = property; 23 | this._allProperties.add(name); 24 | if (property.longhands) { 25 | this._longhands.set(name, property.longhands); 26 | } 27 | if (property.inherited) { 28 | this._inherited.add(name); 29 | } 30 | } 31 | } 32 | 33 | isValidProperty(property: string): boolean { 34 | return this._allProperties.has(property); 35 | } 36 | 37 | isInherited(property: string): boolean { 38 | return this._inherited.has(property); 39 | } 40 | 41 | longhandProperties(property: string): ?Array { 42 | return this._longhands.get(property); 43 | } 44 | 45 | canonicalPropertyName(name: string): string { 46 | if (name.charAt(0) !== '-') { 47 | return name; 48 | } 49 | const matches = name.match(/(?:-webkit-)(.+)/); 50 | if (matches && matches.length > 1) { 51 | // eslint-disable-next-line no-unused-vars 52 | const [_, canonical] = matches; 53 | return canonical; 54 | } else { 55 | // NOTE: CDT also computes a set of property names for each style, and 56 | // checks whether `canonical` is in that set. We might want to do that. 57 | return name; 58 | } 59 | } 60 | } 61 | 62 | const cssMetadata = new CSSMetadata(CSS_PROPERTIES); 63 | 64 | export default cssMetadata; 65 | -------------------------------------------------------------------------------- /src/normalize.js: -------------------------------------------------------------------------------- 1 | // @flow @format 2 | import { normalize, schema } from 'normalizr'; 3 | 4 | import type { CRDP$NodeId, CRDP$Node } from 'devtools-typed/domain/DOM'; 5 | 6 | export type NormalizeResult = { 7 | entities: { 8 | nodes: { [CRDP$NodeId]: NormalizedNode }, 9 | }, 10 | result: CRDP$NodeId, 11 | }; 12 | 13 | const nodeSchema = new schema.Entity( 14 | 'nodes', 15 | {}, // Defined below. 16 | { 17 | idAttribute: 'nodeId', 18 | }, 19 | ); 20 | 21 | const nodeArraySchema = new schema.Array(nodeSchema); 22 | 23 | // Recursive template for CRDP$Node types. 24 | nodeSchema.define({ 25 | children: nodeArraySchema, 26 | pseudoElements: nodeArraySchema, 27 | }); 28 | 29 | function normalizeNodes(root: CRDP$Node): NormalizeResult { 30 | return normalize(root, nodeSchema); 31 | } 32 | 33 | export default normalizeNodes; 34 | 35 | -------------------------------------------------------------------------------- /src/pdiff.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import pixelmatch from 'pixelmatch'; 3 | import { assert } from './utils'; 4 | 5 | import type { PixelmatchOptions } from 'pixelmatch'; 6 | 7 | /** 8 | * Computes whether two images are different. 9 | */ 10 | 11 | export type Base64String = string; 12 | 13 | type DiffOptions = PixelmatchOptions & { 14 | writeDiff?: boolean, 15 | }; 16 | 17 | type DiffResult = { 18 | // Number of pixels calculated as different. 19 | numPixelsDifferent: number, 20 | // If `options.maxDiff` was specified, it will be included here. 21 | // If defined, then `numPixelsDifferent` will be upper-bounded 22 | // by this value, even though the true difference between the 23 | // two images may be greater. 24 | maxDiff?: number, 25 | // If `options.writeDiff` is specified, the diff image data (with diff 26 | // pixels drawn) will be returned. 27 | // TODO(slim): Check if this works with early-return behavior? 28 | diffImage?: Base64String, 29 | }; 30 | 31 | type Dimensions = { 32 | width: number, 33 | height: number, 34 | }; 35 | 36 | class DimensionMismatchError extends Error { 37 | constructor(message?: string) { 38 | super(message); 39 | this.name = 'DimensionMismatchError'; 40 | } 41 | } 42 | 43 | function _formatDimensions({ width, height }: Dimensions) { 44 | return `${width}x${height}`; 45 | } 46 | 47 | async function pdiff(before64: Base64String, options: DiffOptions = {}) { 48 | const before = await getImageData(before64); 49 | const resolvedOptions = resolveDiffOptions(options); 50 | 51 | return async function(after64: Base64String): Promise { 52 | const after = await getImageData(after64); 53 | const dimensionsMatch = before.height === after.height && 54 | before.width === after.width; 55 | if (!dimensionsMatch) { 56 | const beforeDims = _formatDimensions(before); 57 | const afterDims = _formatDimensions(after); 58 | throw new DimensionMismatchError( 59 | `Images do not match: ${beforeDims} vs ${afterDims}`, 60 | ); 61 | } 62 | const { width, height } = before; 63 | 64 | const writeDiff = !!resolvedOptions.writeDiff; 65 | let diffImage: ?ImageData; 66 | let diffCtx: ?CanvasRenderingContext2D; 67 | if (writeDiff) { 68 | // Create an empty context, just so we can create an appropriately- 69 | // sized ImageData to pass to Pixelmatch. 70 | diffCtx = createContext({ width, height }); 71 | diffImage = diffCtx.createImageData(width, height); 72 | } 73 | 74 | const numPixelsDifferent = pixelmatch({ 75 | width, 76 | height, 77 | img1: before.data, 78 | img2: after.data, 79 | diff: diffImage ? diffImage.data : null, 80 | options: resolvedOptions, 81 | }); 82 | 83 | const result: DiffResult = { numPixelsDifferent }; 84 | const { maxDiff } = options; 85 | const hasMaxDiff = typeof maxDiff === 'number'; 86 | if (hasMaxDiff) { 87 | result.maxDiff = maxDiff; 88 | } 89 | if (diffImage && diffCtx) { 90 | diffCtx.putImageData(diffImage, 0, 0); 91 | const diffURI = diffCtx.canvas.toDataURL('image/png'); 92 | result.diffImage = diffURI; 93 | } 94 | return result; 95 | }; 96 | } 97 | 98 | function resolveDiffOptions( 99 | given?: DiffOptions = {}, 100 | ): PixelmatchOptions & { 101 | writeDiff: boolean, 102 | } { 103 | // By default, we don't specify either a threshold or maxDiff, 104 | // just whether to write the diff. 105 | const DEFAULTS = { 106 | writeDiff: false, 107 | }; 108 | return Object.assign({}, DEFAULTS, given); 109 | } 110 | 111 | async function getImageData(uri: Base64String): Promise { 112 | const img = new Image(); 113 | try { 114 | await new Promise((resolve, reject) => { 115 | img.onload = resolve; 116 | img.onerror = reject; 117 | img.src = prefixURI(uri); 118 | }); 119 | } catch (imageLoadError) { 120 | throw new Error(`getImageData: image failed to load`); 121 | } 122 | const { width, height } = img; 123 | const ctx = createContext({ width, height }); 124 | ctx.drawImage(img, 0, 0); 125 | const imageData: ImageData = ctx.getImageData(0, 0, width, height); 126 | return imageData; 127 | } 128 | 129 | function createContext( 130 | { 131 | width, 132 | height, 133 | }: { 134 | width: number, 135 | height: number, 136 | }, 137 | ): CanvasRenderingContext2D { 138 | const canvas = document.createElement('canvas'); 139 | canvas.width = width; 140 | canvas.height = height; 141 | const ctx: ?CanvasRenderingContext2D = canvas.getContext('2d'); 142 | if (!ctx) { 143 | throw new Error(`createContext: couldn't get context from canvas`); 144 | } 145 | return ctx; 146 | } 147 | 148 | function prefixURI(uri: Base64String): Base64String { 149 | const PREFIX = 'data:image/png;base64,'; 150 | const index = uri.indexOf(PREFIX); 151 | assert( 152 | index === -1 || index === 0, 153 | 'image URI must begin with data prefix, or omit entirely', 154 | ); 155 | return index === -1 ? `${PREFIX}${uri}` : uri; 156 | } 157 | 158 | export default pdiff; 159 | export { prefixURI, getImageData, DimensionMismatchError }; 160 | -------------------------------------------------------------------------------- /src/socket/index.js: -------------------------------------------------------------------------------- 1 | // @flow @format 2 | import io from 'socket.io-client'; 3 | import messageTypes from './messageTypes'; 4 | 5 | const socket = io(`http://localhost:${1111}/browsers`, { 6 | reconnectionAttempts: 5, 7 | }); 8 | -------------------------------------------------------------------------------- /src/socket/messageTypes.js: -------------------------------------------------------------------------------- 1 | // @flow @format 2 | 3 | // TODO: ply/shared 4 | 5 | const socketio = { 6 | connect: 'connect', 7 | disconnect: 'disconnect', 8 | reconnect: 'reconnect', 9 | reconnect_attempt: 'reconnect_attempt', 10 | reconnect_failed: 'reconnect_failed', 11 | }; 12 | const serverToClient = { 13 | // Server -> Client 14 | TARGET_CONNECTED: 'TARGET_CONNECTED', 15 | TARGET_DISCONNECTED: 'TARGET_DISCONNECTED', 16 | ERROR: 'ERROR', 17 | PRUNE_NODE_RESULT: 'PRUNE_NODE_RESULT', 18 | SET_DEPENDENCIES: 'SET_DEPENDENCIES', 19 | SET_DOCUMENT: 'SET_DOCUMENT', 20 | SET_STYLES: 'SET_STYLES', 21 | SET_INSPECTION_ROOT: 'SET_INSPECTION_ROOT', 22 | }; 23 | const clientToServer = { 24 | // Dispatched to store AND pushed to server 25 | PRUNE_NODE: 'PRUNE_NODE', 26 | COMPUTE_DEPENDENCIES: 'COMPUTE_DEPENDENCIES', 27 | 28 | // Pushed to server only 29 | CLEAR_HIGHLIGHT: 'CLEAR_HIGHLIGHT', 30 | HIGHLIGHT_NODE: 'HIGHLIGHT_NODE', 31 | REQUEST_STYLE_FOR_NODE: 'REQUEST_STYLE_FOR_NODE', 32 | TOGGLE_CSS_PROPERTY: 'TOGGLE_CSS_PROPERTY', 33 | }; 34 | 35 | export { socketio, serverToClient, clientToServer }; 36 | -------------------------------------------------------------------------------- /src/styleEdit.js: -------------------------------------------------------------------------------- 1 | // @flow @format 2 | 3 | /** 4 | * Utilities for editing style texts. 5 | */ 6 | 7 | import { assert } from './utils'; 8 | 9 | import type { 10 | CRDP$CSSStyle, 11 | CRDP$CSSProperty, 12 | CRDP$SourceRange, 13 | } from 'devtools-typed/domain/CSS'; 14 | 15 | type IndexRange = [number, number]; 16 | 17 | export function replacePropertyInStyleText( 18 | style: CRDP$CSSStyle, 19 | property: CRDP$CSSProperty, 20 | replacement: string, 21 | ): string { 22 | if (!style.range) { 23 | throw new Error('Style not editable'); 24 | } 25 | if (!property.range) { 26 | throw new Error('Property not editable'); 27 | } 28 | if (!style.cssText) { 29 | throw new Error('No style text'); 30 | } 31 | const { cssText } = style; 32 | const range = relativeRange(style.range, property.range); 33 | const newStyleText = replaceRange(cssText, range, replacement); 34 | return newStyleText; 35 | } 36 | 37 | function relativeRange( 38 | styleRange: CRDP$SourceRange, 39 | propertyRange: CRDP$SourceRange, 40 | ): CRDP$SourceRange { 41 | assert( 42 | styleRange.startLine <= propertyRange.startLine && 43 | propertyRange.endLine <= styleRange.endLine, 44 | 'property not part of style', 45 | ); 46 | 47 | const startLine = propertyRange.startLine - styleRange.startLine; 48 | const endLine = propertyRange.endLine - styleRange.startLine; 49 | const startColumn = propertyRange.startColumn; 50 | const endColumn = propertyRange.endColumn; 51 | 52 | assert( 53 | startLine <= endLine, 54 | `invalid range lines ${startLine} and ${endLine}`, 55 | ); 56 | 57 | return { startLine, startColumn, endLine, endColumn }; 58 | } 59 | 60 | export function replaceRange( 61 | str: string, 62 | range: CRDP$SourceRange, 63 | replacement: string, 64 | ): string { 65 | const [startIndex, endIndex] = sourceRangeToIndices(str, range); 66 | const prefix = str.substring(0, startIndex); 67 | const suffix = str.substring(endIndex); 68 | const result = `${prefix}${replacement}${suffix}`; 69 | return result; 70 | } 71 | 72 | export function sourceRangeToIndices( 73 | str: string, 74 | range: CRDP$SourceRange, 75 | ): IndexRange { 76 | const { startLine, endLine, startColumn, endColumn } = range; 77 | assert( 78 | startLine <= endLine, 79 | `invalid range lines ${startLine} and ${endLine}`, 80 | ); 81 | 82 | const lines = str.split('\n'); 83 | const numLines = lines.length; 84 | assert( 85 | endLine - startLine <= numLines, 86 | `range ${endLine - startLine} exceeds number of lines ${numLines}`, 87 | ); 88 | 89 | // Add 1 to the length of every line but the last, for the newline character. 90 | const lineLengths = lines.map( 91 | (s, idx) => (idx < numLines - 1 ? s.length + 1 : s.length), 92 | ); 93 | 94 | assert( 95 | startColumn < lines[startLine].length, 96 | `startColumn ${startColumn} exceeds startLine length ${lines[startLine] 97 | .length}`, 98 | ); 99 | assert( 100 | endColumn <= lines[endLine].length, 101 | `endColumn ${endColumn} exceeds endLine length ${lines[endLine].length}`, 102 | ); 103 | 104 | const sum = arr => arr.reduce((a, b) => a + b, 0); 105 | const startIndex = sum(lineLengths.slice(0, startLine)) + startColumn; 106 | const endIndex = sum(lineLengths.slice(0, endLine)) + endColumn; 107 | 108 | assert(startIndex <= endIndex, 'endIndex greater than startIndex'); 109 | 110 | return [startIndex, endIndex]; 111 | } 112 | -------------------------------------------------------------------------------- /src/styles.js: -------------------------------------------------------------------------------- 1 | // @flow @format 2 | import zip from 'lodash/zip'; 3 | import { assert, toInt } from './utils'; 4 | import cssMetadata from './metadata'; 5 | 6 | import type { CRDP$NodeId } from 'devtools-typed/domain/DOM'; 7 | import type { 8 | CRDP$RuleMatch, 9 | CRDP$CSSProperty, 10 | } from 'devtools-typed/domain/css'; 11 | import type { 12 | NodeStyleMask, 13 | CSSPropertyIndices, 14 | NodeStyleMaskDiff, 15 | } from './types'; 16 | 17 | // PRECONDITION: Node is pruned. 18 | export const getEffectiveValueForProperty = (rm: Array) => ( 19 | propertyName: string, 20 | ) => { 21 | const effectiveProperties: { [property: string]: CRDP$CSSProperty } = {}; 22 | 23 | for (const { rule } of rm) { 24 | const { style } = rule; 25 | const { cssProperties } = style; 26 | 27 | if (rule.origin !== 'regular') { 28 | continue; 29 | } 30 | 31 | for (const property of cssProperties) { 32 | // Disabled properties can't be effective. 33 | // TODO: also check for properties without a SourceRange (logic is already in 34 | // background.js somewhere) 35 | if (property.disabled) { 36 | continue; 37 | } 38 | 39 | const { name } = property; 40 | const canonicalName = cssMetadata.canonicalPropertyName(name); 41 | const longhands = cssMetadata.longhandProperties(canonicalName); 42 | 43 | if (longhands && longhands.length > 0) { 44 | // Check each longhand, e.g. `margin-left`, `margin-right`, etc. 45 | // To see if it has an active property. 46 | longhands.forEach(lh => { 47 | if (!effectiveProperties[lh]) { 48 | effectiveProperties[lh] = property; 49 | } 50 | }); 51 | 52 | // Map the shorthand to each of the longhands. 53 | // TODO: Check implemented?? 54 | // TODO: This is definitely going to fail if we set a shorthand first and 55 | // then subsequently overwrite with `margin-left: 10px` or something 56 | effectiveProperties[canonicalName] = longhands.map( 57 | lh => effectiveProperties[lh], 58 | ); 59 | } else { 60 | // Property does not have longhands. 61 | // If the property has not yet been found, record it. 62 | if (!effectiveProperties[canonicalName]) { 63 | effectiveProperties[canonicalName] = property; 64 | } 65 | } 66 | } 67 | } 68 | 69 | const canonicalName = cssMetadata.canonicalPropertyName(propertyName); 70 | const longhands = cssMetadata.longhandProperties(canonicalName); 71 | 72 | const query = longhands || [canonicalName]; 73 | const result = query.map(prop => effectiveProperties[prop]).filter(Boolean); 74 | 75 | return result; 76 | }; 77 | 78 | // TODO: Figure out a better way to handle longhand properties. 79 | export const createStyleMask = (rules: Array): NodeStyleMask => 80 | rules.map(ruleMatch => 81 | ruleMatch.rule.style.cssProperties.map(property => !property.disabled), 82 | ); 83 | 84 | // TODO: Move this to the frontend repo. 85 | export const isPropertyActive = (mask: NodeStyleMask) => ( 86 | path: [number, number], 87 | ): boolean => { 88 | const [ruleIndex, propertyIndex] = path; 89 | const rule = mask[ruleIndex]; 90 | assert(Array.isArray(rule), 'rule not found in mask'); 91 | const value = rule[propertyIndex]; 92 | assert(typeof value === 'boolean', 'property not found in rule in mask'); 93 | return value; 94 | }; 95 | 96 | export const diffStyleMasks = (nodeId: CRDP$NodeId, before: NodeStyleMask) => ( 97 | after: NodeStyleMask, 98 | ): NodeStyleMaskDiff => { 99 | assert( 100 | before.length === after.length, 101 | 'masks must have the same number of rules', 102 | ); 103 | 104 | const result: NodeStyleMaskDiff = {}; 105 | const enabled: Array = []; 106 | const disabled: Array = []; 107 | 108 | // If any properties were disabled before but are now enabled. 109 | zip(before, after).forEach(([beforeRule, afterRule], ruleIndex) => 110 | zip( 111 | beforeRule, 112 | afterRule, 113 | ).forEach(([beforeProperty, afterProperty], propertyIndex) => { 114 | // TODO: throw error? 115 | // if (beforeProperty == null) { ... } 116 | 117 | if (beforeProperty && !afterProperty) { 118 | // Was enabled before, now disabled. 119 | disabled.push([toInt(nodeId), ruleIndex, propertyIndex]); 120 | } else if (!beforeProperty && afterProperty) { 121 | // Was disabled before, now enabled. 122 | enabled.push([toInt(nodeId), ruleIndex, propertyIndex]); 123 | } 124 | }), 125 | ); 126 | 127 | if (enabled.length > 0) { 128 | result.enabled = enabled; 129 | } 130 | 131 | if (disabled.length > 0) { 132 | result.disabled = disabled; 133 | } 134 | 135 | return result; 136 | }; 137 | -------------------------------------------------------------------------------- /src/tabs.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /** 4 | * Functions for interacting with the Chrome Tabs API. 5 | */ 6 | 7 | declare var chrome: Object; 8 | 9 | type Tab = { 10 | id?: number, 11 | index: number, 12 | windowId: number, 13 | openerTabId?: number, 14 | highlighted: boolean, 15 | active: boolean, 16 | pinned: boolean, 17 | audible: boolean, 18 | discarded: boolean, 19 | url?: string, 20 | title?: string, 21 | status?: 'loading' | 'complete', 22 | incognito: boolean, 23 | width?: number, 24 | height?: number, 25 | sessionId?: string, 26 | }; 27 | 28 | function createTab(url?: string, cb?: Tab => void) { 29 | chrome.tabs.create({ url }, cb); 30 | } 31 | 32 | export { createTab }; 33 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | // @flow @format 2 | import type { CRDP$NodeId, CRDP$Node } from 'devtools-typed/domain/dom'; 3 | 4 | export type CSSPropertyPath = { 5 | nodeId: CRDP$NodeId, 6 | ruleIndex: number, 7 | propertyIndex: number, 8 | }; 9 | 10 | export type Target = { 11 | tabId: number, 12 | }; 13 | 14 | export type NodeMap = { [CRDP$NodeId]: CRDP$Node }; 15 | 16 | export type DebugStatus = 'ACTIVE' | 'INACTIVE'; 17 | 18 | export type NodeStyleMask = Array>; 19 | 20 | export type CSSPropertyIndices = [number, number, number]; 21 | 22 | export type NodeStyleMaskDiff = { 23 | enabled?: Array, 24 | disabled?: Array, 25 | }; 26 | 27 | // HACK: CSSPropertyIndices is actually a misnomer. The keys are stringified paths 28 | // `${nodeId},${ruleIndex},${propertyIndex}`. 29 | export type NodeStyleDependencies = { 30 | dependants: { [keystone: CSSPropertyIndices]: Array }, 31 | keystones: { [dependant: CSSPropertyIndices]: Array }, 32 | }; 33 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { createTab } from './tabs'; 3 | import { prefixURI } from './pdiff'; 4 | 5 | import type { Base64String } from './pdiff'; 6 | 7 | /** 8 | * Various utils for development and debugging. 9 | */ 10 | 11 | function showImage(b64: Base64String) { 12 | const uri = prefixURI(b64); 13 | createTab(uri); 14 | } 15 | 16 | function assert(condition: boolean, message: string) { 17 | // TODO(slim): Eventually check for dev vs. prod environment. 18 | if (!condition) { 19 | throw new Error(`Assertion failed: ${message}`); 20 | } 21 | } 22 | 23 | function toInt(str: string | number): number { 24 | return parseInt(str, 10); 25 | } 26 | 27 | export { showImage, assert, toInt }; 28 | -------------------------------------------------------------------------------- /test/helpers/helpers.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import fs from 'fs'; 3 | 4 | function base64Encode(path: string): Promise { 5 | return new Promise(resolve => { 6 | fs.readFile(path, (err, data) => { 7 | if (err) { 8 | throw new Error(err); 9 | } 10 | resolve(data.toString('base64')); 11 | }); 12 | }); 13 | } 14 | 15 | export { base64Encode }; 16 | -------------------------------------------------------------------------------- /test/helpers/setup.js: -------------------------------------------------------------------------------- 1 | import browserEnv from 'browser-env'; 2 | 3 | browserEnv(['window', 'Image', 'document'], { 4 | resources: 'usable', 5 | }); 6 | 7 | -------------------------------------------------------------------------------- /test/helpers/styleHelpers.js: -------------------------------------------------------------------------------- 1 | // @flow @format 2 | 3 | /** 4 | * Testing utilities for CSS domain functions. 5 | */ 6 | 7 | import type { CRDP$CSSProperty } from 'devtools-typed/CSS'; 8 | 9 | export type MockCSSRuleMatch = { 10 | rule: { 11 | style: { 12 | cssProperties: Array, 13 | }, 14 | }, 15 | }; 16 | 17 | export type CSSInput = { 18 | [selector: string]: { 19 | [name: string]: string, 20 | }, 21 | }; 22 | 23 | export function buildCSS(input: CSSInput): Array { 24 | const result = []; 25 | 26 | for (const selector in input) { 27 | const style = input[selector]; 28 | const cssProperties = []; 29 | 30 | for (const name in style) { 31 | const value = style[name]; 32 | const property = { 33 | name, 34 | value, 35 | }; 36 | 37 | if (value.lastIndexOf('!important') !== -1) { 38 | // $FlowFixMe - just for testing 39 | property.important = true; 40 | } 41 | 42 | // A leading `//` signifies the property is disabled. 43 | if (value.substring(0, 2) === '//') { 44 | // $FlowFixMe - also just for testing 45 | property.disabled = true; 46 | } 47 | 48 | cssProperties.push(property); 49 | } 50 | 51 | const ruleMatch = { rule: { style: { cssProperties } } }; 52 | result.push(ruleMatch); 53 | } 54 | 55 | return result; 56 | } 57 | -------------------------------------------------------------------------------- /test/images/10x2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sliminality/chrome-remote-css/e7a24c0fcb91842b82229b01b48de6e88fc175ff/test/images/10x2.png -------------------------------------------------------------------------------- /test/images/base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sliminality/chrome-remote-css/e7a24c0fcb91842b82229b01b48de6e88fc175ff/test/images/base.png -------------------------------------------------------------------------------- /test/images/changed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sliminality/chrome-remote-css/e7a24c0fcb91842b82229b01b48de6e88fc175ff/test/images/changed.png -------------------------------------------------------------------------------- /test/images/unchanged.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sliminality/chrome-remote-css/e7a24c0fcb91842b82229b01b48de6e88fc175ff/test/images/unchanged.png -------------------------------------------------------------------------------- /test/images/with.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sliminality/chrome-remote-css/e7a24c0fcb91842b82229b01b48de6e88fc175ff/test/images/with.png -------------------------------------------------------------------------------- /test/images/without.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sliminality/chrome-remote-css/e7a24c0fcb91842b82229b01b48de6e88fc175ff/test/images/without.png -------------------------------------------------------------------------------- /test/pdiff.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import path from 'path'; 3 | import test from 'ava'; 4 | import pdiff, { getImageData } from '../src/pdiff'; 5 | import { base64Encode } from './helpers/helpers'; 6 | 7 | const images = { 8 | tiny: path.resolve(__dirname, './images/10x2.png'), 9 | base: path.resolve(__dirname, './images/base.png'), 10 | unchanged: path.resolve(__dirname, './images/unchanged.png'), 11 | changed: path.resolve(__dirname, './images/changed.png'), 12 | }; 13 | 14 | test('getImageData works', async t => { 15 | const one = await base64Encode(images.tiny); 16 | const data = await getImageData(one); 17 | const { width, height } = data; 18 | t.is(width, 10); 19 | t.is(height, 2); 20 | }); 21 | 22 | test('pdiff for two identical images is 0', async t => { 23 | const [one, two] = await Promise.all([ 24 | base64Encode(images.base), 25 | base64Encode(images.unchanged), 26 | ]); 27 | const pdiffer = await pdiff(one); 28 | const {numPixelsDifferent} = await pdiffer(two); 29 | t.is(numPixelsDifferent, 0); 30 | }); 31 | 32 | test('pdiff for two different images is > 0', async t => { 33 | const [one, two] = await Promise.all([ 34 | base64Encode(images.base), 35 | base64Encode(images.changed), 36 | ]); 37 | const pdiffer = await pdiff(one); 38 | const {numPixelsDifferent} = await pdiffer(two); 39 | t.is(numPixelsDifferent, 564855); 40 | }); 41 | 42 | // $FlowFixMe - this isn't actually an error? 43 | test.todo('pdiff for two different sizes throws an error'); 44 | -------------------------------------------------------------------------------- /test/styleEdit.test.js: -------------------------------------------------------------------------------- 1 | // @flow @format 2 | import test from 'ava'; 3 | import { 4 | sourceRangeToIndices, 5 | replaceRange, 6 | replacePropertyInStyleText, 7 | } from '../src/styleEdit'; 8 | 9 | test('converting one-line CSSSourceRange to indices', t => { 10 | const str = `line zero 11 | line one 12 | indented line two 13 | `; 14 | const range = { 15 | startLine: 0, 16 | startColumn: 0, 17 | endLine: 0, 18 | endColumn: 7, 19 | }; 20 | const indices = sourceRangeToIndices(str, range); 21 | const substring = str.substring(...indices); 22 | t.is(substring, 'line ze'); 23 | }); 24 | 25 | test('converting multi-line CSSSourceRange to indices', t => { 26 | const str = `line zero 27 | line one 28 | indented line two 29 | `; 30 | const range = { 31 | startLine: 1, 32 | startColumn: 3, 33 | endLine: 2, 34 | endColumn: 17, 35 | }; 36 | const indices = sourceRangeToIndices(str, range); 37 | const substring = str.substring(...indices); 38 | t.is( 39 | substring, 40 | `e one 41 | indented line t`, 42 | ); 43 | }); 44 | 45 | test('replace multi-line text ranges', t => { 46 | const str = `line zero 47 | line one 48 | indented line two 49 | `; 50 | const range = { 51 | startLine: 1, 52 | startColumn: 3, 53 | endLine: 2, 54 | endColumn: 17, 55 | }; 56 | const result = replaceRange(str, range, 'floobar'); 57 | t.is( 58 | result, 59 | `line zero 60 | linfloobarwo 61 | `, 62 | ); 63 | }); 64 | 65 | test('replace property in style', t => { 66 | const style = { 67 | cssProperties: [ 68 | { 69 | disabled: false, 70 | implicit: false, 71 | name: 'border-color', 72 | range: { 73 | endColumn: 23, 74 | endLine: 17471, 75 | startColumn: 4, 76 | startLine: 17471, 77 | }, 78 | text: 'border-color: #000;', 79 | value: '#000', 80 | }, 81 | { 82 | disabled: false, 83 | implicit: false, 84 | name: 'color', 85 | range: { 86 | endColumn: 16, 87 | endLine: 17472, 88 | startColumn: 4, 89 | startLine: 17472, 90 | }, 91 | text: 'color: #000;', 92 | value: '#000', 93 | }, 94 | { 95 | disabled: false, 96 | implicit: false, 97 | name: 'margin-bottom', 98 | range: { 99 | endColumn: 0, 100 | endLine: 17474, 101 | startColumn: 4, 102 | startLine: 17473, 103 | }, 104 | text: 'margin-bottom: 35px\n', 105 | value: '35px', 106 | }, 107 | { 108 | name: 'border-top-color', 109 | value: 'rgb(0, 0, 0)', 110 | }, 111 | { 112 | name: 'border-right-color', 113 | value: 'rgb(0, 0, 0)', 114 | }, 115 | { 116 | name: 'border-bottom-color', 117 | value: 'rgb(0, 0, 0)', 118 | }, 119 | { 120 | name: 'border-left-color', 121 | value: 'rgb(0, 0, 0)', 122 | }, 123 | ], 124 | cssText: 125 | '\n border-color: #000;\n color: #000;\n margin-bottom: 35px\n', 126 | range: { 127 | endColumn: 0, 128 | endLine: 17474, 129 | startColumn: 27, 130 | startLine: 17470, 131 | }, 132 | shorthandEntries: [ 133 | { 134 | name: 'border-color', 135 | value: 'rgb(0, 0, 0)', 136 | }, 137 | ], 138 | styleSheetId: '28618.56', 139 | }; 140 | 141 | const property = { 142 | disabled: false, 143 | implicit: false, 144 | name: 'border-color', 145 | range: { 146 | endColumn: 23, 147 | endLine: 17471, 148 | startColumn: 4, 149 | startLine: 17471, 150 | }, 151 | text: 'border-color: #000;', 152 | value: '#000', 153 | }; 154 | 155 | const result = replacePropertyInStyleText( 156 | style, 157 | property, 158 | '/* border-color: #000; */', 159 | ); 160 | 161 | t.is( 162 | result, 163 | '\n /* border-color: #000; */\n color: #000;\n margin-bottom: 35px\n', 164 | ); 165 | }); 166 | 167 | test('replace property in style with multiple substring occurrences', t => { 168 | const style = { 169 | cssProperties: [ 170 | { 171 | disabled: true, 172 | name: 'border-color', 173 | range: { 174 | endColumn: 29, 175 | endLine: 17471, 176 | startColumn: 4, 177 | startLine: 17471, 178 | }, 179 | text: '/* border-color: #000; */', 180 | value: '#000', 181 | }, 182 | { 183 | disabled: false, 184 | implicit: false, 185 | name: 'color', 186 | range: { 187 | endColumn: 16, 188 | endLine: 17472, 189 | startColumn: 4, 190 | startLine: 17472, 191 | }, 192 | text: 'color: #000;', 193 | value: '#000', 194 | }, 195 | { 196 | disabled: false, 197 | implicit: false, 198 | name: 'margin-bottom', 199 | range: { 200 | endColumn: 0, 201 | endLine: 17477, 202 | startColumn: 4, 203 | startLine: 17473, 204 | }, 205 | text: 'margin-bottom: 35px\n\n\n\n', 206 | value: '35px', 207 | }, 208 | ], 209 | cssText: 210 | '\n /* border-color: #000; */\n color: #000;\n margin-bottom: 35px\n\n\n\n', 211 | range: { 212 | endColumn: 0, 213 | endLine: 17477, 214 | startColumn: 27, 215 | startLine: 17470, 216 | }, 217 | shorthandEntries: [], 218 | styleSheetId: '28618.145', 219 | }; 220 | const property = { 221 | disabled: false, 222 | implicit: false, 223 | name: 'color', 224 | range: { 225 | endColumn: 16, 226 | endLine: 17472, 227 | startColumn: 4, 228 | startLine: 17472, 229 | }, 230 | text: 'color: #000;', 231 | value: '#000', 232 | }; 233 | 234 | const result = replacePropertyInStyleText( 235 | style, 236 | property, 237 | '/* color: #000; */', 238 | ); 239 | 240 | t.is( 241 | result, 242 | '\n /* border-color: #000; */\n /* color: #000; */\n margin-bottom: 35px\n\n\n\n', 243 | ); 244 | }); 245 | -------------------------------------------------------------------------------- /test/styles.test.js: -------------------------------------------------------------------------------- 1 | // @flow @format 2 | import test from 'ava'; 3 | import { buildCSS } from './helpers/styleHelpers'; 4 | import { 5 | getEffectiveValueForProperty, 6 | createStyleMask, 7 | diffStyleMasks, 8 | isPropertyActive, 9 | } from '../src/styles'; 10 | 11 | test('create style mask', t => { 12 | const input = [ 13 | { 14 | matchingSelectors: [0], 15 | rule: { 16 | media: [ 17 | { 18 | mediaList: [ 19 | { 20 | active: true, 21 | expressions: [ 22 | { 23 | computedLength: 734, 24 | feature: 'min-width', 25 | unit: 'px', 26 | value: 734, 27 | }, 28 | ], 29 | }, 30 | ], 31 | range: { 32 | endColumn: 35, 33 | endLine: 2967, 34 | startColumn: 7, 35 | startLine: 2967, 36 | }, 37 | source: 'mediaRule', 38 | sourceURL: 'http://localhost:7999/oscar/index_files/bundle.css', 39 | styleSheetId: '75164.19', 40 | text: 'screen and (min-width: 734px)', 41 | }, 42 | ], 43 | origin: 'regular', 44 | selectorList: { 45 | selectors: [ 46 | { 47 | range: { 48 | endColumn: 49, 49 | endLine: 2968, 50 | startColumn: 4, 51 | startLine: 2968, 52 | }, 53 | text: '.navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 54 | }, 55 | ], 56 | text: '.navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 57 | }, 58 | style: { 59 | cssProperties: [ 60 | { 61 | disabled: false, 62 | implicit: false, 63 | name: 'height', 64 | range: { 65 | endColumn: 4, 66 | endLine: 2970, 67 | startColumn: 8, 68 | startLine: 2969, 69 | }, 70 | text: 'height: 10rem\n ', 71 | value: '10rem', 72 | }, 73 | ], 74 | cssText: '\n height: 10rem\n ', 75 | range: { 76 | endColumn: 4, 77 | endLine: 2970, 78 | startColumn: 51, 79 | startLine: 2968, 80 | }, 81 | shorthandEntries: [], 82 | styleSheetId: '75164.19', 83 | }, 84 | styleSheetId: '75164.19', 85 | }, 86 | }, 87 | { 88 | matchingSelectors: [0], 89 | rule: { 90 | media: [], 91 | origin: 'regular', 92 | selectorList: { 93 | selectors: [ 94 | { 95 | range: { 96 | endColumn: 45, 97 | endLine: 2962, 98 | startColumn: 0, 99 | startLine: 2962, 100 | }, 101 | text: '.navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 102 | }, 103 | ], 104 | text: '.navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 105 | }, 106 | style: { 107 | cssProperties: [ 108 | { 109 | disabled: true, 110 | name: 'color', 111 | range: { 112 | endColumn: 22, 113 | endLine: 2963, 114 | startColumn: 4, 115 | startLine: 2963, 116 | }, 117 | text: '/* color: #fff; */', 118 | value: '#fff', 119 | }, 120 | { 121 | disabled: false, 122 | implicit: false, 123 | name: 'height', 124 | range: { 125 | endColumn: 0, 126 | endLine: 2965, 127 | startColumn: 4, 128 | startLine: 2964, 129 | }, 130 | text: 'height: 7rem\n', 131 | value: '7rem', 132 | }, 133 | ], 134 | cssText: '\n /* color: #fff; */\n height: 7rem\n', 135 | range: { 136 | endColumn: 0, 137 | endLine: 2965, 138 | startColumn: 47, 139 | startLine: 2962, 140 | }, 141 | shorthandEntries: [], 142 | styleSheetId: '75164.19', 143 | }, 144 | styleSheetId: '75164.19', 145 | }, 146 | }, 147 | { 148 | matchingSelectors: [4], 149 | rule: { 150 | media: [ 151 | { 152 | mediaList: [ 153 | { 154 | active: true, 155 | expressions: [ 156 | { 157 | computedLength: 734, 158 | feature: 'min-width', 159 | unit: 'px', 160 | value: 734, 161 | }, 162 | ], 163 | }, 164 | ], 165 | range: { 166 | endColumn: 35, 167 | endLine: 2940, 168 | startColumn: 7, 169 | startLine: 2940, 170 | }, 171 | source: 'mediaRule', 172 | sourceURL: 'http://localhost:7999/oscar/index_files/bundle.css', 173 | styleSheetId: '75164.19', 174 | text: 'screen and (min-width: 734px)', 175 | }, 176 | ], 177 | origin: 'regular', 178 | selectorList: { 179 | selectors: [ 180 | { 181 | range: { 182 | endColumn: 46, 183 | endLine: 2941, 184 | startColumn: 4, 185 | startLine: 2941, 186 | }, 187 | text: '.navHeaderHasBanner-3rQVtTrEkCWdKFqj2Lu8lS', 188 | }, 189 | { 190 | range: { 191 | endColumn: 88, 192 | endLine: 2941, 193 | startColumn: 47, 194 | startLine: 2941, 195 | }, 196 | text: '.navHeaderNoCenter-1-PoTwgY-CGMSNMmCL5uRP', 197 | }, 198 | { 199 | range: { 200 | endColumn: 126, 201 | endLine: 2941, 202 | startColumn: 89, 203 | startLine: 2941, 204 | }, 205 | text: '.navHeaderShort-eXOoccIdsiS38H7RjPxao', 206 | }, 207 | { 208 | range: { 209 | endColumn: 167, 210 | endLine: 2941, 211 | startColumn: 127, 212 | startLine: 2941, 213 | }, 214 | text: '.navHeaderWrapper-3GbasrtkfUf0mLlgFmMTv5', 215 | }, 216 | { 217 | range: { 218 | endColumn: 213, 219 | endLine: 2941, 220 | startColumn: 168, 221 | startLine: 2941, 222 | }, 223 | text: '.navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 224 | }, 225 | ], 226 | text: 227 | '.navHeaderHasBanner-3rQVtTrEkCWdKFqj2Lu8lS, .navHeaderNoCenter-1-PoTwgY-CGMSNMmCL5uRP, .navHeaderShort-eXOoccIdsiS38H7RjPxao, .navHeaderWrapper-3GbasrtkfUf0mLlgFmMTv5, .navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 228 | }, 229 | style: { 230 | cssProperties: [ 231 | { 232 | disabled: true, 233 | name: 'box-shadow', 234 | range: { 235 | endColumn: 31, 236 | endLine: 2942, 237 | startColumn: 8, 238 | startLine: 2942, 239 | }, 240 | text: '/* box-shadow: none; */', 241 | value: 'none', 242 | }, 243 | { 244 | disabled: true, 245 | name: 'height', 246 | range: { 247 | endColumn: 28, 248 | endLine: 2943, 249 | startColumn: 8, 250 | startLine: 2943, 251 | }, 252 | text: '/* height: 10rem; */', 253 | value: '10rem', 254 | }, 255 | { 256 | disabled: true, 257 | name: 'overflow', 258 | range: { 259 | endColumn: 31, 260 | endLine: 2944, 261 | startColumn: 8, 262 | startLine: 2944, 263 | }, 264 | text: '/* overflow: visible */', 265 | value: 'visible', 266 | }, 267 | ], 268 | cssText: 269 | '\n /* box-shadow: none; */\n /* height: 10rem; */\n /* overflow: visible */\n ', 270 | range: { 271 | endColumn: 4, 272 | endLine: 2945, 273 | startColumn: 215, 274 | startLine: 2941, 275 | }, 276 | shorthandEntries: [], 277 | styleSheetId: '75164.19', 278 | }, 279 | styleSheetId: '75164.19', 280 | }, 281 | }, 282 | { 283 | matchingSelectors: [4], 284 | rule: { 285 | media: [], 286 | origin: 'regular', 287 | selectorList: { 288 | selectors: [ 289 | { 290 | range: { 291 | endColumn: 42, 292 | endLine: 2929, 293 | startColumn: 0, 294 | startLine: 2929, 295 | }, 296 | text: '.navHeaderHasBanner-3rQVtTrEkCWdKFqj2Lu8lS', 297 | }, 298 | { 299 | range: { 300 | endColumn: 84, 301 | endLine: 2929, 302 | startColumn: 43, 303 | startLine: 2929, 304 | }, 305 | text: '.navHeaderNoCenter-1-PoTwgY-CGMSNMmCL5uRP', 306 | }, 307 | { 308 | range: { 309 | endColumn: 122, 310 | endLine: 2929, 311 | startColumn: 85, 312 | startLine: 2929, 313 | }, 314 | text: '.navHeaderShort-eXOoccIdsiS38H7RjPxao', 315 | }, 316 | { 317 | range: { 318 | endColumn: 163, 319 | endLine: 2929, 320 | startColumn: 123, 321 | startLine: 2929, 322 | }, 323 | text: '.navHeaderWrapper-3GbasrtkfUf0mLlgFmMTv5', 324 | }, 325 | { 326 | range: { 327 | endColumn: 209, 328 | endLine: 2929, 329 | startColumn: 164, 330 | startLine: 2929, 331 | }, 332 | text: '.navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 333 | }, 334 | ], 335 | text: 336 | '.navHeaderHasBanner-3rQVtTrEkCWdKFqj2Lu8lS, .navHeaderNoCenter-1-PoTwgY-CGMSNMmCL5uRP, .navHeaderShort-eXOoccIdsiS38H7RjPxao, .navHeaderWrapper-3GbasrtkfUf0mLlgFmMTv5, .navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 337 | }, 338 | style: { 339 | cssProperties: [ 340 | { 341 | disabled: true, 342 | name: 'color', 343 | range: { 344 | endColumn: 25, 345 | endLine: 2930, 346 | startColumn: 4, 347 | startLine: 2930, 348 | }, 349 | text: '/* color: #001837; */', 350 | value: '#001837', 351 | }, 352 | { 353 | disabled: true, 354 | name: 'height', 355 | range: { 356 | endColumn: 23, 357 | endLine: 2931, 358 | startColumn: 4, 359 | startLine: 2931, 360 | }, 361 | text: '/* height: 7rem; */', 362 | value: '7rem', 363 | }, 364 | { 365 | disabled: true, 366 | name: 'left', 367 | range: { 368 | endColumn: 18, 369 | endLine: 2932, 370 | startColumn: 4, 371 | startLine: 2932, 372 | }, 373 | text: '/* left: 0; */', 374 | value: '0', 375 | }, 376 | { 377 | disabled: false, 378 | implicit: false, 379 | name: 'position', 380 | range: { 381 | endColumn: 20, 382 | endLine: 2933, 383 | startColumn: 4, 384 | startLine: 2933, 385 | }, 386 | text: 'position: fixed;', 387 | value: 'fixed', 388 | }, 389 | { 390 | disabled: false, 391 | implicit: false, 392 | name: 'top', 393 | range: { 394 | endColumn: 11, 395 | endLine: 2934, 396 | startColumn: 4, 397 | startLine: 2934, 398 | }, 399 | text: 'top: 0;', 400 | value: '0', 401 | }, 402 | { 403 | disabled: false, 404 | implicit: false, 405 | name: 'transition', 406 | range: { 407 | endColumn: 93, 408 | endLine: 2935, 409 | startColumn: 4, 410 | startLine: 2935, 411 | }, 412 | text: 413 | 'transition: background-color .25s cubic-bezier(1,0,0,1),color .25s cubic-bezier(1,0,0,1);', 414 | value: 415 | 'background-color .25s cubic-bezier(1,0,0,1),color .25s cubic-bezier(1,0,0,1)', 416 | }, 417 | { 418 | disabled: false, 419 | implicit: false, 420 | name: 'width', 421 | range: { 422 | endColumn: 16, 423 | endLine: 2936, 424 | startColumn: 4, 425 | startLine: 2936, 426 | }, 427 | text: 'width: 100%;', 428 | value: '100%', 429 | }, 430 | { 431 | disabled: false, 432 | implicit: false, 433 | name: 'z-index', 434 | range: { 435 | endColumn: 0, 436 | endLine: 2938, 437 | startColumn: 4, 438 | startLine: 2937, 439 | }, 440 | text: 'z-index: 300\n', 441 | value: '300', 442 | }, 443 | { 444 | name: 'transition-duration', 445 | value: '0.25s, 0.25s', 446 | }, 447 | { 448 | name: 'transition-timing-function', 449 | value: 'cubic-bezier(1, 0, 0, 1), cubic-bezier(1, 0, 0, 1)', 450 | }, 451 | { 452 | name: 'transition-delay', 453 | value: 'initial, initial', 454 | }, 455 | { 456 | name: 'transition-property', 457 | value: 'background-color, color', 458 | }, 459 | ], 460 | cssText: 461 | '\n /* color: #001837; */\n /* height: 7rem; */\n /* left: 0; */\n position: fixed;\n top: 0;\n transition: background-color .25s cubic-bezier(1,0,0,1),color .25s cubic-bezier(1,0,0,1);\n width: 100%;\n z-index: 300\n', 462 | range: { 463 | endColumn: 0, 464 | endLine: 2938, 465 | startColumn: 211, 466 | startLine: 2929, 467 | }, 468 | shorthandEntries: [ 469 | { 470 | name: 'transition', 471 | value: 472 | 'background-color 0.25s cubic-bezier(1, 0, 0, 1), color 0.25s cubic-bezier(1, 0, 0, 1)', 473 | }, 474 | ], 475 | styleSheetId: '75164.19', 476 | }, 477 | styleSheetId: '75164.19', 478 | }, 479 | }, 480 | { 481 | matchingSelectors: [0], 482 | rule: { 483 | media: [], 484 | origin: 'regular', 485 | selectorList: { 486 | selectors: [ 487 | { 488 | range: { 489 | endColumn: 13, 490 | endLine: 219, 491 | startColumn: 0, 492 | startLine: 219, 493 | }, 494 | text: '.u-colorWhite', 495 | }, 496 | ], 497 | text: '.u-colorWhite', 498 | }, 499 | style: { 500 | cssProperties: [ 501 | { 502 | disabled: true, 503 | name: 'color', 504 | range: { 505 | endColumn: 31, 506 | endLine: 220, 507 | startColumn: 4, 508 | startLine: 220, 509 | }, 510 | text: '/* color: #fff!important */', 511 | value: '#fff!important', 512 | }, 513 | ], 514 | cssText: '\n /* color: #fff!important */\n', 515 | range: { 516 | endColumn: 0, 517 | endLine: 221, 518 | startColumn: 15, 519 | startLine: 219, 520 | }, 521 | shorthandEntries: [], 522 | styleSheetId: '75164.19', 523 | }, 524 | styleSheetId: '75164.19', 525 | }, 526 | }, 527 | { 528 | matchingSelectors: [0], 529 | rule: { 530 | media: [], 531 | origin: 'regular', 532 | selectorList: { 533 | selectors: [ 534 | { 535 | range: { 536 | endColumn: 19, 537 | endLine: 133, 538 | startColumn: 0, 539 | startLine: 133, 540 | }, 541 | text: '.u-bgOscarLightBlue', 542 | }, 543 | ], 544 | text: '.u-bgOscarLightBlue', 545 | }, 546 | style: { 547 | cssProperties: [ 548 | { 549 | disabled: false, 550 | implicit: false, 551 | important: true, 552 | name: 'background-color', 553 | range: { 554 | endColumn: 0, 555 | endLine: 135, 556 | startColumn: 4, 557 | startLine: 134, 558 | }, 559 | text: 'background-color: #0031e2!important\n', 560 | value: '#0031e2!important', 561 | }, 562 | ], 563 | cssText: '\n background-color: #0031e2!important\n', 564 | range: { 565 | endColumn: 0, 566 | endLine: 135, 567 | startColumn: 21, 568 | startLine: 133, 569 | }, 570 | shorthandEntries: [], 571 | styleSheetId: '75164.19', 572 | }, 573 | styleSheetId: '75164.19', 574 | }, 575 | }, 576 | { 577 | matchingSelectors: [0], 578 | rule: { 579 | media: [], 580 | origin: 'regular', 581 | selectorList: { 582 | selectors: [ 583 | { 584 | range: { 585 | endColumn: 1, 586 | endLine: 1151, 587 | startColumn: 0, 588 | startLine: 1151, 589 | }, 590 | text: '*', 591 | }, 592 | ], 593 | text: '*', 594 | }, 595 | style: { 596 | cssProperties: [ 597 | { 598 | disabled: false, 599 | implicit: false, 600 | name: 'box-sizing', 601 | range: { 602 | endColumn: 24, 603 | endLine: 1152, 604 | startColumn: 4, 605 | startLine: 1152, 606 | }, 607 | text: 'box-sizing: inherit;', 608 | value: 'inherit', 609 | }, 610 | { 611 | disabled: true, 612 | name: 'pointer-events', 613 | range: { 614 | endColumn: 34, 615 | endLine: 1153, 616 | startColumn: 4, 617 | startLine: 1153, 618 | }, 619 | text: '/* pointer-events: inherit; */', 620 | value: 'inherit', 621 | }, 622 | { 623 | disabled: true, 624 | name: 'outline', 625 | range: { 626 | endColumn: 23, 627 | endLine: 1154, 628 | startColumn: 4, 629 | startLine: 1154, 630 | }, 631 | text: '/* outline: none */', 632 | value: 'none', 633 | }, 634 | ], 635 | cssText: 636 | '\n box-sizing: inherit;\n /* pointer-events: inherit; */\n /* outline: none */\n', 637 | range: { 638 | endColumn: 0, 639 | endLine: 1155, 640 | startColumn: 3, 641 | startLine: 1151, 642 | }, 643 | shorthandEntries: [], 644 | styleSheetId: '75164.19', 645 | }, 646 | styleSheetId: '75164.19', 647 | }, 648 | }, 649 | { 650 | matchingSelectors: [0], 651 | rule: { 652 | media: [], 653 | origin: 'user-agent', 654 | selectorList: { 655 | selectors: [ 656 | { 657 | text: 'div', 658 | }, 659 | ], 660 | text: 'div', 661 | }, 662 | style: { 663 | cssProperties: [ 664 | { 665 | name: 'display', 666 | value: 'block', 667 | }, 668 | ], 669 | shorthandEntries: [], 670 | }, 671 | }, 672 | }, 673 | ]; 674 | 675 | const expected = [ 676 | [true], 677 | [false, true], 678 | [false, false, false], 679 | [false, false, false, true, true, true, true, true, true, true, true, true], 680 | [false], 681 | [true], 682 | [true, false, false], 683 | [true], 684 | ]; 685 | 686 | const mask = createStyleMask(input); 687 | 688 | for (let i = 0; i < mask.length; i += 1) { 689 | t.deepEqual(mask[i], expected[i]); 690 | } 691 | }); 692 | 693 | test('diff style masks', t => { 694 | const before = [ 695 | { 696 | matchingSelectors: [0], 697 | rule: { 698 | media: [ 699 | { 700 | mediaList: [ 701 | { 702 | active: true, 703 | expressions: [ 704 | { 705 | computedLength: 734, 706 | feature: 'min-width', 707 | unit: 'px', 708 | value: 734, 709 | }, 710 | ], 711 | }, 712 | ], 713 | range: { 714 | endColumn: 35, 715 | endLine: 2967, 716 | startColumn: 7, 717 | startLine: 2967, 718 | }, 719 | source: 'mediaRule', 720 | sourceURL: 'http://localhost:7999/oscar/index_files/bundle.css', 721 | styleSheetId: '75164.19', 722 | text: 'screen and (min-width: 734px)', 723 | }, 724 | ], 725 | origin: 'regular', 726 | selectorList: { 727 | selectors: [ 728 | { 729 | range: { 730 | endColumn: 49, 731 | endLine: 2968, 732 | startColumn: 4, 733 | startLine: 2968, 734 | }, 735 | text: '.navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 736 | }, 737 | ], 738 | text: '.navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 739 | }, 740 | style: { 741 | cssProperties: [ 742 | { 743 | disabled: false, 744 | implicit: false, 745 | name: 'height', 746 | range: { 747 | endColumn: 4, 748 | endLine: 2970, 749 | startColumn: 8, 750 | startLine: 2969, 751 | }, 752 | text: 'height: 10rem\n ', 753 | value: '10rem', 754 | }, 755 | ], 756 | cssText: '\n height: 10rem\n ', 757 | range: { 758 | endColumn: 4, 759 | endLine: 2970, 760 | startColumn: 51, 761 | startLine: 2968, 762 | }, 763 | shorthandEntries: [], 764 | styleSheetId: '75164.19', 765 | }, 766 | styleSheetId: '75164.19', 767 | }, 768 | }, 769 | { 770 | matchingSelectors: [0], 771 | rule: { 772 | media: [], 773 | origin: 'regular', 774 | selectorList: { 775 | selectors: [ 776 | { 777 | range: { 778 | endColumn: 45, 779 | endLine: 2962, 780 | startColumn: 0, 781 | startLine: 2962, 782 | }, 783 | text: '.navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 784 | }, 785 | ], 786 | text: '.navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 787 | }, 788 | style: { 789 | cssProperties: [ 790 | { 791 | disabled: true, 792 | name: 'color', 793 | range: { 794 | endColumn: 22, 795 | endLine: 2963, 796 | startColumn: 4, 797 | startLine: 2963, 798 | }, 799 | text: '/* color: #fff; */', 800 | value: '#fff', 801 | }, 802 | { 803 | disabled: false, 804 | implicit: false, 805 | name: 'height', 806 | range: { 807 | endColumn: 0, 808 | endLine: 2965, 809 | startColumn: 4, 810 | startLine: 2964, 811 | }, 812 | text: 'height: 7rem\n', 813 | value: '7rem', 814 | }, 815 | ], 816 | cssText: '\n /* color: #fff; */\n height: 7rem\n', 817 | range: { 818 | endColumn: 0, 819 | endLine: 2965, 820 | startColumn: 47, 821 | startLine: 2962, 822 | }, 823 | shorthandEntries: [], 824 | styleSheetId: '75164.19', 825 | }, 826 | styleSheetId: '75164.19', 827 | }, 828 | }, 829 | { 830 | matchingSelectors: [4], 831 | rule: { 832 | media: [ 833 | { 834 | mediaList: [ 835 | { 836 | active: true, 837 | expressions: [ 838 | { 839 | computedLength: 734, 840 | feature: 'min-width', 841 | unit: 'px', 842 | value: 734, 843 | }, 844 | ], 845 | }, 846 | ], 847 | range: { 848 | endColumn: 35, 849 | endLine: 2940, 850 | startColumn: 7, 851 | startLine: 2940, 852 | }, 853 | source: 'mediaRule', 854 | sourceURL: 'http://localhost:7999/oscar/index_files/bundle.css', 855 | styleSheetId: '75164.19', 856 | text: 'screen and (min-width: 734px)', 857 | }, 858 | ], 859 | origin: 'regular', 860 | selectorList: { 861 | selectors: [ 862 | { 863 | range: { 864 | endColumn: 46, 865 | endLine: 2941, 866 | startColumn: 4, 867 | startLine: 2941, 868 | }, 869 | text: '.navHeaderHasBanner-3rQVtTrEkCWdKFqj2Lu8lS', 870 | }, 871 | { 872 | range: { 873 | endColumn: 88, 874 | endLine: 2941, 875 | startColumn: 47, 876 | startLine: 2941, 877 | }, 878 | text: '.navHeaderNoCenter-1-PoTwgY-CGMSNMmCL5uRP', 879 | }, 880 | { 881 | range: { 882 | endColumn: 126, 883 | endLine: 2941, 884 | startColumn: 89, 885 | startLine: 2941, 886 | }, 887 | text: '.navHeaderShort-eXOoccIdsiS38H7RjPxao', 888 | }, 889 | { 890 | range: { 891 | endColumn: 167, 892 | endLine: 2941, 893 | startColumn: 127, 894 | startLine: 2941, 895 | }, 896 | text: '.navHeaderWrapper-3GbasrtkfUf0mLlgFmMTv5', 897 | }, 898 | { 899 | range: { 900 | endColumn: 213, 901 | endLine: 2941, 902 | startColumn: 168, 903 | startLine: 2941, 904 | }, 905 | text: '.navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 906 | }, 907 | ], 908 | text: 909 | '.navHeaderHasBanner-3rQVtTrEkCWdKFqj2Lu8lS, .navHeaderNoCenter-1-PoTwgY-CGMSNMmCL5uRP, .navHeaderShort-eXOoccIdsiS38H7RjPxao, .navHeaderWrapper-3GbasrtkfUf0mLlgFmMTv5, .navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 910 | }, 911 | style: { 912 | cssProperties: [ 913 | { 914 | disabled: true, 915 | name: 'box-shadow', 916 | range: { 917 | endColumn: 31, 918 | endLine: 2942, 919 | startColumn: 8, 920 | startLine: 2942, 921 | }, 922 | text: '/* box-shadow: none; */', 923 | value: 'none', 924 | }, 925 | { 926 | disabled: true, 927 | name: 'height', 928 | range: { 929 | endColumn: 28, 930 | endLine: 2943, 931 | startColumn: 8, 932 | startLine: 2943, 933 | }, 934 | text: '/* height: 10rem; */', 935 | value: '10rem', 936 | }, 937 | { 938 | disabled: true, 939 | name: 'overflow', 940 | range: { 941 | endColumn: 31, 942 | endLine: 2944, 943 | startColumn: 8, 944 | startLine: 2944, 945 | }, 946 | text: '/* overflow: visible */', 947 | value: 'visible', 948 | }, 949 | ], 950 | cssText: 951 | '\n /* box-shadow: none; */\n /* height: 10rem; */\n /* overflow: visible */\n ', 952 | range: { 953 | endColumn: 4, 954 | endLine: 2945, 955 | startColumn: 215, 956 | startLine: 2941, 957 | }, 958 | shorthandEntries: [], 959 | styleSheetId: '75164.19', 960 | }, 961 | styleSheetId: '75164.19', 962 | }, 963 | }, 964 | { 965 | matchingSelectors: [4], 966 | rule: { 967 | media: [], 968 | origin: 'regular', 969 | selectorList: { 970 | selectors: [ 971 | { 972 | range: { 973 | endColumn: 42, 974 | endLine: 2929, 975 | startColumn: 0, 976 | startLine: 2929, 977 | }, 978 | text: '.navHeaderHasBanner-3rQVtTrEkCWdKFqj2Lu8lS', 979 | }, 980 | { 981 | range: { 982 | endColumn: 84, 983 | endLine: 2929, 984 | startColumn: 43, 985 | startLine: 2929, 986 | }, 987 | text: '.navHeaderNoCenter-1-PoTwgY-CGMSNMmCL5uRP', 988 | }, 989 | { 990 | range: { 991 | endColumn: 122, 992 | endLine: 2929, 993 | startColumn: 85, 994 | startLine: 2929, 995 | }, 996 | text: '.navHeaderShort-eXOoccIdsiS38H7RjPxao', 997 | }, 998 | { 999 | range: { 1000 | endColumn: 163, 1001 | endLine: 2929, 1002 | startColumn: 123, 1003 | startLine: 2929, 1004 | }, 1005 | text: '.navHeaderWrapper-3GbasrtkfUf0mLlgFmMTv5', 1006 | }, 1007 | { 1008 | range: { 1009 | endColumn: 209, 1010 | endLine: 2929, 1011 | startColumn: 164, 1012 | startLine: 2929, 1013 | }, 1014 | text: '.navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 1015 | }, 1016 | ], 1017 | text: 1018 | '.navHeaderHasBanner-3rQVtTrEkCWdKFqj2Lu8lS, .navHeaderNoCenter-1-PoTwgY-CGMSNMmCL5uRP, .navHeaderShort-eXOoccIdsiS38H7RjPxao, .navHeaderWrapper-3GbasrtkfUf0mLlgFmMTv5, .navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 1019 | }, 1020 | style: { 1021 | cssProperties: [ 1022 | { 1023 | disabled: true, 1024 | name: 'color', 1025 | range: { 1026 | endColumn: 25, 1027 | endLine: 2930, 1028 | startColumn: 4, 1029 | startLine: 2930, 1030 | }, 1031 | text: '/* color: #001837; */', 1032 | value: '#001837', 1033 | }, 1034 | { 1035 | disabled: true, 1036 | name: 'height', 1037 | range: { 1038 | endColumn: 23, 1039 | endLine: 2931, 1040 | startColumn: 4, 1041 | startLine: 2931, 1042 | }, 1043 | text: '/* height: 7rem; */', 1044 | value: '7rem', 1045 | }, 1046 | { 1047 | disabled: true, 1048 | name: 'left', 1049 | range: { 1050 | endColumn: 18, 1051 | endLine: 2932, 1052 | startColumn: 4, 1053 | startLine: 2932, 1054 | }, 1055 | text: '/* left: 0; */', 1056 | value: '0', 1057 | }, 1058 | { 1059 | disabled: false, 1060 | implicit: false, 1061 | name: 'position', 1062 | range: { 1063 | endColumn: 20, 1064 | endLine: 2933, 1065 | startColumn: 4, 1066 | startLine: 2933, 1067 | }, 1068 | text: 'position: fixed;', 1069 | value: 'fixed', 1070 | }, 1071 | { 1072 | disabled: false, 1073 | implicit: false, 1074 | name: 'top', 1075 | range: { 1076 | endColumn: 11, 1077 | endLine: 2934, 1078 | startColumn: 4, 1079 | startLine: 2934, 1080 | }, 1081 | text: 'top: 0;', 1082 | value: '0', 1083 | }, 1084 | { 1085 | disabled: false, 1086 | implicit: false, 1087 | name: 'transition', 1088 | range: { 1089 | endColumn: 93, 1090 | endLine: 2935, 1091 | startColumn: 4, 1092 | startLine: 2935, 1093 | }, 1094 | text: 1095 | 'transition: background-color .25s cubic-bezier(1,0,0,1),color .25s cubic-bezier(1,0,0,1);', 1096 | value: 1097 | 'background-color .25s cubic-bezier(1,0,0,1),color .25s cubic-bezier(1,0,0,1)', 1098 | }, 1099 | { 1100 | disabled: false, 1101 | implicit: false, 1102 | name: 'width', 1103 | range: { 1104 | endColumn: 16, 1105 | endLine: 2936, 1106 | startColumn: 4, 1107 | startLine: 2936, 1108 | }, 1109 | text: 'width: 100%;', 1110 | value: '100%', 1111 | }, 1112 | { 1113 | disabled: false, 1114 | implicit: false, 1115 | name: 'z-index', 1116 | range: { 1117 | endColumn: 0, 1118 | endLine: 2938, 1119 | startColumn: 4, 1120 | startLine: 2937, 1121 | }, 1122 | text: 'z-index: 300\n', 1123 | value: '300', 1124 | }, 1125 | { 1126 | name: 'transition-duration', 1127 | value: '0.25s, 0.25s', 1128 | }, 1129 | { 1130 | name: 'transition-timing-function', 1131 | value: 'cubic-bezier(1, 0, 0, 1), cubic-bezier(1, 0, 0, 1)', 1132 | }, 1133 | { 1134 | name: 'transition-delay', 1135 | value: 'initial, initial', 1136 | }, 1137 | { 1138 | name: 'transition-property', 1139 | value: 'background-color, color', 1140 | }, 1141 | ], 1142 | cssText: 1143 | '\n /* color: #001837; */\n /* height: 7rem; */\n /* left: 0; */\n position: fixed;\n top: 0;\n transition: background-color .25s cubic-bezier(1,0,0,1),color .25s cubic-bezier(1,0,0,1);\n width: 100%;\n z-index: 300\n', 1144 | range: { 1145 | endColumn: 0, 1146 | endLine: 2938, 1147 | startColumn: 211, 1148 | startLine: 2929, 1149 | }, 1150 | shorthandEntries: [ 1151 | { 1152 | name: 'transition', 1153 | value: 1154 | 'background-color 0.25s cubic-bezier(1, 0, 0, 1), color 0.25s cubic-bezier(1, 0, 0, 1)', 1155 | }, 1156 | ], 1157 | styleSheetId: '75164.19', 1158 | }, 1159 | styleSheetId: '75164.19', 1160 | }, 1161 | }, 1162 | { 1163 | matchingSelectors: [0], 1164 | rule: { 1165 | media: [], 1166 | origin: 'regular', 1167 | selectorList: { 1168 | selectors: [ 1169 | { 1170 | range: { 1171 | endColumn: 13, 1172 | endLine: 219, 1173 | startColumn: 0, 1174 | startLine: 219, 1175 | }, 1176 | text: '.u-colorWhite', 1177 | }, 1178 | ], 1179 | text: '.u-colorWhite', 1180 | }, 1181 | style: { 1182 | cssProperties: [ 1183 | { 1184 | disabled: true, 1185 | name: 'color', 1186 | range: { 1187 | endColumn: 31, 1188 | endLine: 220, 1189 | startColumn: 4, 1190 | startLine: 220, 1191 | }, 1192 | text: '/* color: #fff!important */', 1193 | value: '#fff!important', 1194 | }, 1195 | ], 1196 | cssText: '\n /* color: #fff!important */\n', 1197 | range: { 1198 | endColumn: 0, 1199 | endLine: 221, 1200 | startColumn: 15, 1201 | startLine: 219, 1202 | }, 1203 | shorthandEntries: [], 1204 | styleSheetId: '75164.19', 1205 | }, 1206 | styleSheetId: '75164.19', 1207 | }, 1208 | }, 1209 | { 1210 | matchingSelectors: [0], 1211 | rule: { 1212 | media: [], 1213 | origin: 'regular', 1214 | selectorList: { 1215 | selectors: [ 1216 | { 1217 | range: { 1218 | endColumn: 19, 1219 | endLine: 133, 1220 | startColumn: 0, 1221 | startLine: 133, 1222 | }, 1223 | text: '.u-bgOscarLightBlue', 1224 | }, 1225 | ], 1226 | text: '.u-bgOscarLightBlue', 1227 | }, 1228 | style: { 1229 | cssProperties: [ 1230 | { 1231 | disabled: false, 1232 | implicit: false, 1233 | important: true, 1234 | name: 'background-color', 1235 | range: { 1236 | endColumn: 0, 1237 | endLine: 135, 1238 | startColumn: 4, 1239 | startLine: 134, 1240 | }, 1241 | text: 'background-color: #0031e2!important\n', 1242 | value: '#0031e2!important', 1243 | }, 1244 | ], 1245 | cssText: '\n background-color: #0031e2!important\n', 1246 | range: { 1247 | endColumn: 0, 1248 | endLine: 135, 1249 | startColumn: 21, 1250 | startLine: 133, 1251 | }, 1252 | shorthandEntries: [], 1253 | styleSheetId: '75164.19', 1254 | }, 1255 | styleSheetId: '75164.19', 1256 | }, 1257 | }, 1258 | { 1259 | matchingSelectors: [0], 1260 | rule: { 1261 | media: [], 1262 | origin: 'regular', 1263 | selectorList: { 1264 | selectors: [ 1265 | { 1266 | range: { 1267 | endColumn: 1, 1268 | endLine: 1151, 1269 | startColumn: 0, 1270 | startLine: 1151, 1271 | }, 1272 | text: '*', 1273 | }, 1274 | ], 1275 | text: '*', 1276 | }, 1277 | style: { 1278 | cssProperties: [ 1279 | { 1280 | disabled: false, 1281 | implicit: false, 1282 | name: 'box-sizing', 1283 | range: { 1284 | endColumn: 24, 1285 | endLine: 1152, 1286 | startColumn: 4, 1287 | startLine: 1152, 1288 | }, 1289 | text: 'box-sizing: inherit;', 1290 | value: 'inherit', 1291 | }, 1292 | { 1293 | disabled: true, 1294 | name: 'pointer-events', 1295 | range: { 1296 | endColumn: 34, 1297 | endLine: 1153, 1298 | startColumn: 4, 1299 | startLine: 1153, 1300 | }, 1301 | text: '/* pointer-events: inherit; */', 1302 | value: 'inherit', 1303 | }, 1304 | { 1305 | disabled: true, 1306 | name: 'outline', 1307 | range: { 1308 | endColumn: 23, 1309 | endLine: 1154, 1310 | startColumn: 4, 1311 | startLine: 1154, 1312 | }, 1313 | text: '/* outline: none */', 1314 | value: 'none', 1315 | }, 1316 | ], 1317 | cssText: 1318 | '\n box-sizing: inherit;\n /* pointer-events: inherit; */\n /* outline: none */\n', 1319 | range: { 1320 | endColumn: 0, 1321 | endLine: 1155, 1322 | startColumn: 3, 1323 | startLine: 1151, 1324 | }, 1325 | shorthandEntries: [], 1326 | styleSheetId: '75164.19', 1327 | }, 1328 | styleSheetId: '75164.19', 1329 | }, 1330 | }, 1331 | { 1332 | matchingSelectors: [0], 1333 | rule: { 1334 | media: [], 1335 | origin: 'user-agent', 1336 | selectorList: { 1337 | selectors: [ 1338 | { 1339 | text: 'div', 1340 | }, 1341 | ], 1342 | text: 'div', 1343 | }, 1344 | style: { 1345 | cssProperties: [ 1346 | { 1347 | name: 'display', 1348 | value: 'block', 1349 | }, 1350 | ], 1351 | shorthandEntries: [], 1352 | }, 1353 | }, 1354 | }, 1355 | ]; 1356 | 1357 | const after = [ 1358 | { 1359 | matchingSelectors: [0], 1360 | rule: { 1361 | media: [ 1362 | { 1363 | mediaList: [ 1364 | { 1365 | active: true, 1366 | expressions: [ 1367 | { 1368 | computedLength: 734, 1369 | feature: 'min-width', 1370 | unit: 'px', 1371 | value: 734, 1372 | }, 1373 | ], 1374 | }, 1375 | ], 1376 | range: { 1377 | endColumn: 35, 1378 | endLine: 2967, 1379 | startColumn: 7, 1380 | startLine: 2967, 1381 | }, 1382 | source: 'mediaRule', 1383 | sourceURL: 'http://localhost:7999/oscar/index_files/bundle.css', 1384 | styleSheetId: '75164.32', 1385 | text: 'screen and (min-width: 734px)', 1386 | }, 1387 | ], 1388 | origin: 'regular', 1389 | selectorList: { 1390 | selectors: [ 1391 | { 1392 | range: { 1393 | endColumn: 49, 1394 | endLine: 2968, 1395 | startColumn: 4, 1396 | startLine: 2968, 1397 | }, 1398 | text: '.navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 1399 | }, 1400 | ], 1401 | text: '.navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 1402 | }, 1403 | style: { 1404 | cssProperties: [ 1405 | { 1406 | disabled: false, 1407 | implicit: false, 1408 | name: 'height', 1409 | range: { 1410 | endColumn: 4, 1411 | endLine: 2970, 1412 | startColumn: 8, 1413 | startLine: 2969, 1414 | }, 1415 | text: 'height: 10rem\n ', 1416 | value: '10rem', 1417 | }, 1418 | ], 1419 | cssText: '\n height: 10rem\n ', 1420 | range: { 1421 | endColumn: 4, 1422 | endLine: 2970, 1423 | startColumn: 51, 1424 | startLine: 2968, 1425 | }, 1426 | shorthandEntries: [], 1427 | styleSheetId: '75164.32', 1428 | }, 1429 | styleSheetId: '75164.32', 1430 | }, 1431 | }, 1432 | { 1433 | matchingSelectors: [0], 1434 | rule: { 1435 | media: [], 1436 | origin: 'regular', 1437 | selectorList: { 1438 | selectors: [ 1439 | { 1440 | range: { 1441 | endColumn: 45, 1442 | endLine: 2962, 1443 | startColumn: 0, 1444 | startLine: 2962, 1445 | }, 1446 | text: '.navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 1447 | }, 1448 | ], 1449 | text: '.navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 1450 | }, 1451 | style: { 1452 | cssProperties: [ 1453 | { 1454 | disabled: false, 1455 | name: 'color', 1456 | range: { 1457 | endColumn: 22, 1458 | endLine: 2963, 1459 | startColumn: 4, 1460 | startLine: 2963, 1461 | }, 1462 | text: '/* color: #fff; */', 1463 | value: '#fff', 1464 | }, 1465 | { 1466 | disabled: false, 1467 | implicit: false, 1468 | name: 'height', 1469 | range: { 1470 | endColumn: 0, 1471 | endLine: 2965, 1472 | startColumn: 4, 1473 | startLine: 2964, 1474 | }, 1475 | text: 'height: 7rem\n', 1476 | value: '7rem', 1477 | }, 1478 | ], 1479 | cssText: '\n /* color: #fff; */\n height: 7rem\n', 1480 | range: { 1481 | endColumn: 0, 1482 | endLine: 2965, 1483 | startColumn: 47, 1484 | startLine: 2962, 1485 | }, 1486 | shorthandEntries: [], 1487 | styleSheetId: '75164.32', 1488 | }, 1489 | styleSheetId: '75164.32', 1490 | }, 1491 | }, 1492 | { 1493 | matchingSelectors: [4], 1494 | rule: { 1495 | media: [ 1496 | { 1497 | mediaList: [ 1498 | { 1499 | active: true, 1500 | expressions: [ 1501 | { 1502 | computedLength: 734, 1503 | feature: 'min-width', 1504 | unit: 'px', 1505 | value: 734, 1506 | }, 1507 | ], 1508 | }, 1509 | ], 1510 | range: { 1511 | endColumn: 35, 1512 | endLine: 2940, 1513 | startColumn: 7, 1514 | startLine: 2940, 1515 | }, 1516 | source: 'mediaRule', 1517 | sourceURL: 'http://localhost:7999/oscar/index_files/bundle.css', 1518 | styleSheetId: '75164.32', 1519 | text: 'screen and (min-width: 734px)', 1520 | }, 1521 | ], 1522 | origin: 'regular', 1523 | selectorList: { 1524 | selectors: [ 1525 | { 1526 | range: { 1527 | endColumn: 46, 1528 | endLine: 2941, 1529 | startColumn: 4, 1530 | startLine: 2941, 1531 | }, 1532 | text: '.navHeaderHasBanner-3rQVtTrEkCWdKFqj2Lu8lS', 1533 | }, 1534 | { 1535 | range: { 1536 | endColumn: 88, 1537 | endLine: 2941, 1538 | startColumn: 47, 1539 | startLine: 2941, 1540 | }, 1541 | text: '.navHeaderNoCenter-1-PoTwgY-CGMSNMmCL5uRP', 1542 | }, 1543 | { 1544 | range: { 1545 | endColumn: 126, 1546 | endLine: 2941, 1547 | startColumn: 89, 1548 | startLine: 2941, 1549 | }, 1550 | text: '.navHeaderShort-eXOoccIdsiS38H7RjPxao', 1551 | }, 1552 | { 1553 | range: { 1554 | endColumn: 167, 1555 | endLine: 2941, 1556 | startColumn: 127, 1557 | startLine: 2941, 1558 | }, 1559 | text: '.navHeaderWrapper-3GbasrtkfUf0mLlgFmMTv5', 1560 | }, 1561 | { 1562 | range: { 1563 | endColumn: 213, 1564 | endLine: 2941, 1565 | startColumn: 168, 1566 | startLine: 2941, 1567 | }, 1568 | text: '.navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 1569 | }, 1570 | ], 1571 | text: 1572 | '.navHeaderHasBanner-3rQVtTrEkCWdKFqj2Lu8lS, .navHeaderNoCenter-1-PoTwgY-CGMSNMmCL5uRP, .navHeaderShort-eXOoccIdsiS38H7RjPxao, .navHeaderWrapper-3GbasrtkfUf0mLlgFmMTv5, .navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 1573 | }, 1574 | style: { 1575 | cssProperties: [ 1576 | { 1577 | disabled: true, 1578 | name: 'box-shadow', 1579 | range: { 1580 | endColumn: 31, 1581 | endLine: 2942, 1582 | startColumn: 8, 1583 | startLine: 2942, 1584 | }, 1585 | text: '/* box-shadow: none; */', 1586 | value: 'none', 1587 | }, 1588 | { 1589 | disabled: true, 1590 | name: 'height', 1591 | range: { 1592 | endColumn: 28, 1593 | endLine: 2943, 1594 | startColumn: 8, 1595 | startLine: 2943, 1596 | }, 1597 | text: '/* height: 10rem; */', 1598 | value: '10rem', 1599 | }, 1600 | { 1601 | disabled: true, 1602 | name: 'overflow', 1603 | range: { 1604 | endColumn: 31, 1605 | endLine: 2944, 1606 | startColumn: 8, 1607 | startLine: 2944, 1608 | }, 1609 | text: '/* overflow: visible */', 1610 | value: 'visible', 1611 | }, 1612 | ], 1613 | cssText: 1614 | '\n /* box-shadow: none; */\n /* height: 10rem; */\n /* overflow: visible */\n ', 1615 | range: { 1616 | endColumn: 4, 1617 | endLine: 2945, 1618 | startColumn: 215, 1619 | startLine: 2941, 1620 | }, 1621 | shorthandEntries: [], 1622 | styleSheetId: '75164.32', 1623 | }, 1624 | styleSheetId: '75164.32', 1625 | }, 1626 | }, 1627 | { 1628 | matchingSelectors: [4], 1629 | rule: { 1630 | media: [], 1631 | origin: 'regular', 1632 | selectorList: { 1633 | selectors: [ 1634 | { 1635 | range: { 1636 | endColumn: 42, 1637 | endLine: 2929, 1638 | startColumn: 0, 1639 | startLine: 2929, 1640 | }, 1641 | text: '.navHeaderHasBanner-3rQVtTrEkCWdKFqj2Lu8lS', 1642 | }, 1643 | { 1644 | range: { 1645 | endColumn: 84, 1646 | endLine: 2929, 1647 | startColumn: 43, 1648 | startLine: 2929, 1649 | }, 1650 | text: '.navHeaderNoCenter-1-PoTwgY-CGMSNMmCL5uRP', 1651 | }, 1652 | { 1653 | range: { 1654 | endColumn: 122, 1655 | endLine: 2929, 1656 | startColumn: 85, 1657 | startLine: 2929, 1658 | }, 1659 | text: '.navHeaderShort-eXOoccIdsiS38H7RjPxao', 1660 | }, 1661 | { 1662 | range: { 1663 | endColumn: 163, 1664 | endLine: 2929, 1665 | startColumn: 123, 1666 | startLine: 2929, 1667 | }, 1668 | text: '.navHeaderWrapper-3GbasrtkfUf0mLlgFmMTv5', 1669 | }, 1670 | { 1671 | range: { 1672 | endColumn: 209, 1673 | endLine: 2929, 1674 | startColumn: 164, 1675 | startLine: 2929, 1676 | }, 1677 | text: '.navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 1678 | }, 1679 | ], 1680 | text: 1681 | '.navHeaderHasBanner-3rQVtTrEkCWdKFqj2Lu8lS, .navHeaderNoCenter-1-PoTwgY-CGMSNMmCL5uRP, .navHeaderShort-eXOoccIdsiS38H7RjPxao, .navHeaderWrapper-3GbasrtkfUf0mLlgFmMTv5, .navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 1682 | }, 1683 | style: { 1684 | cssProperties: [ 1685 | { 1686 | disabled: true, 1687 | name: 'color', 1688 | range: { 1689 | endColumn: 25, 1690 | endLine: 2930, 1691 | startColumn: 4, 1692 | startLine: 2930, 1693 | }, 1694 | text: '/* color: #001837; */', 1695 | value: '#001837', 1696 | }, 1697 | { 1698 | disabled: true, 1699 | name: 'height', 1700 | range: { 1701 | endColumn: 23, 1702 | endLine: 2931, 1703 | startColumn: 4, 1704 | startLine: 2931, 1705 | }, 1706 | text: '/* height: 7rem; */', 1707 | value: '7rem', 1708 | }, 1709 | { 1710 | disabled: true, 1711 | name: 'left', 1712 | range: { 1713 | endColumn: 18, 1714 | endLine: 2932, 1715 | startColumn: 4, 1716 | startLine: 2932, 1717 | }, 1718 | text: '/* left: 0; */', 1719 | value: '0', 1720 | }, 1721 | { 1722 | disabled: false, 1723 | implicit: false, 1724 | name: 'position', 1725 | range: { 1726 | endColumn: 20, 1727 | endLine: 2933, 1728 | startColumn: 4, 1729 | startLine: 2933, 1730 | }, 1731 | text: 'position: fixed;', 1732 | value: 'fixed', 1733 | }, 1734 | { 1735 | disabled: false, 1736 | implicit: false, 1737 | name: 'top', 1738 | range: { 1739 | endColumn: 11, 1740 | endLine: 2934, 1741 | startColumn: 4, 1742 | startLine: 2934, 1743 | }, 1744 | text: 'top: 0;', 1745 | value: '0', 1746 | }, 1747 | { 1748 | disabled: true, 1749 | name: 'transition', 1750 | range: { 1751 | endColumn: 99, 1752 | endLine: 2935, 1753 | startColumn: 4, 1754 | startLine: 2935, 1755 | }, 1756 | text: 1757 | '/* transition: background-color .25s cubic-bezier(1,0,0,1),color .25s cubic-bezier(1,0,0,1); */', 1758 | value: 1759 | 'background-color .25s cubic-bezier(1,0,0,1),color .25s cubic-bezier(1,0,0,1)', 1760 | }, 1761 | { 1762 | disabled: false, 1763 | implicit: false, 1764 | name: 'width', 1765 | range: { 1766 | endColumn: 16, 1767 | endLine: 2936, 1768 | startColumn: 4, 1769 | startLine: 2936, 1770 | }, 1771 | text: 'width: 100%;', 1772 | value: '100%', 1773 | }, 1774 | { 1775 | disabled: false, 1776 | implicit: false, 1777 | name: 'z-index', 1778 | range: { 1779 | endColumn: 0, 1780 | endLine: 2938, 1781 | startColumn: 4, 1782 | startLine: 2937, 1783 | }, 1784 | text: 'z-index: 300\n', 1785 | value: '300', 1786 | }, 1787 | ], 1788 | cssText: 1789 | '\n /* color: #001837; */\n /* height: 7rem; */\n /* left: 0; */\n position: fixed;\n top: 0;\n /* transition: background-color .25s cubic-bezier(1,0,0,1),color .25s cubic-bezier(1,0,0,1); */\n width: 100%;\n z-index: 300\n', 1790 | range: { 1791 | endColumn: 0, 1792 | endLine: 2938, 1793 | startColumn: 211, 1794 | startLine: 2929, 1795 | }, 1796 | shorthandEntries: [], 1797 | styleSheetId: '75164.32', 1798 | }, 1799 | styleSheetId: '75164.32', 1800 | }, 1801 | }, 1802 | { 1803 | matchingSelectors: [0], 1804 | rule: { 1805 | media: [], 1806 | origin: 'regular', 1807 | selectorList: { 1808 | selectors: [ 1809 | { 1810 | range: { 1811 | endColumn: 13, 1812 | endLine: 219, 1813 | startColumn: 0, 1814 | startLine: 219, 1815 | }, 1816 | text: '.u-colorWhite', 1817 | }, 1818 | ], 1819 | text: '.u-colorWhite', 1820 | }, 1821 | style: { 1822 | cssProperties: [ 1823 | { 1824 | disabled: true, 1825 | name: 'color', 1826 | range: { 1827 | endColumn: 31, 1828 | endLine: 220, 1829 | startColumn: 4, 1830 | startLine: 220, 1831 | }, 1832 | text: '/* color: #fff!important */', 1833 | value: '#fff!important', 1834 | }, 1835 | ], 1836 | cssText: '\n /* color: #fff!important */\n', 1837 | range: { 1838 | endColumn: 0, 1839 | endLine: 221, 1840 | startColumn: 15, 1841 | startLine: 219, 1842 | }, 1843 | shorthandEntries: [], 1844 | styleSheetId: '75164.32', 1845 | }, 1846 | styleSheetId: '75164.32', 1847 | }, 1848 | }, 1849 | { 1850 | matchingSelectors: [0], 1851 | rule: { 1852 | media: [], 1853 | origin: 'regular', 1854 | selectorList: { 1855 | selectors: [ 1856 | { 1857 | range: { 1858 | endColumn: 19, 1859 | endLine: 133, 1860 | startColumn: 0, 1861 | startLine: 133, 1862 | }, 1863 | text: '.u-bgOscarLightBlue', 1864 | }, 1865 | ], 1866 | text: '.u-bgOscarLightBlue', 1867 | }, 1868 | style: { 1869 | cssProperties: [ 1870 | { 1871 | disabled: false, 1872 | implicit: false, 1873 | important: true, 1874 | name: 'background-color', 1875 | range: { 1876 | endColumn: 0, 1877 | endLine: 135, 1878 | startColumn: 4, 1879 | startLine: 134, 1880 | }, 1881 | text: 'background-color: #0031e2!important\n', 1882 | value: '#0031e2!important', 1883 | }, 1884 | ], 1885 | cssText: '\n background-color: #0031e2!important\n', 1886 | range: { 1887 | endColumn: 0, 1888 | endLine: 135, 1889 | startColumn: 21, 1890 | startLine: 133, 1891 | }, 1892 | shorthandEntries: [], 1893 | styleSheetId: '75164.32', 1894 | }, 1895 | styleSheetId: '75164.32', 1896 | }, 1897 | }, 1898 | { 1899 | matchingSelectors: [0], 1900 | rule: { 1901 | media: [], 1902 | origin: 'regular', 1903 | selectorList: { 1904 | selectors: [ 1905 | { 1906 | range: { 1907 | endColumn: 1, 1908 | endLine: 1151, 1909 | startColumn: 0, 1910 | startLine: 1151, 1911 | }, 1912 | text: '*', 1913 | }, 1914 | ], 1915 | text: '*', 1916 | }, 1917 | style: { 1918 | cssProperties: [ 1919 | { 1920 | disabled: false, 1921 | implicit: false, 1922 | name: 'box-sizing', 1923 | range: { 1924 | endColumn: 24, 1925 | endLine: 1152, 1926 | startColumn: 4, 1927 | startLine: 1152, 1928 | }, 1929 | text: 'box-sizing: inherit;', 1930 | value: 'inherit', 1931 | }, 1932 | { 1933 | disabled: true, 1934 | name: 'pointer-events', 1935 | range: { 1936 | endColumn: 34, 1937 | endLine: 1153, 1938 | startColumn: 4, 1939 | startLine: 1153, 1940 | }, 1941 | text: '/* pointer-events: inherit; */', 1942 | value: 'inherit', 1943 | }, 1944 | { 1945 | disabled: true, 1946 | name: 'outline', 1947 | range: { 1948 | endColumn: 23, 1949 | endLine: 1154, 1950 | startColumn: 4, 1951 | startLine: 1154, 1952 | }, 1953 | text: '/* outline: none */', 1954 | value: 'none', 1955 | }, 1956 | ], 1957 | cssText: 1958 | '\n box-sizing: inherit;\n /* pointer-events: inherit; */\n /* outline: none */\n', 1959 | range: { 1960 | endColumn: 0, 1961 | endLine: 1155, 1962 | startColumn: 3, 1963 | startLine: 1151, 1964 | }, 1965 | shorthandEntries: [], 1966 | styleSheetId: '75164.32', 1967 | }, 1968 | styleSheetId: '75164.32', 1969 | }, 1970 | }, 1971 | { 1972 | matchingSelectors: [0], 1973 | rule: { 1974 | media: [], 1975 | origin: 'user-agent', 1976 | selectorList: { 1977 | selectors: [ 1978 | { 1979 | text: 'div', 1980 | }, 1981 | ], 1982 | text: 'div', 1983 | }, 1984 | style: { 1985 | cssProperties: [ 1986 | { 1987 | name: 'display', 1988 | value: 'block', 1989 | }, 1990 | ], 1991 | shorthandEntries: [], 1992 | }, 1993 | }, 1994 | }, 1995 | ]; 1996 | 1997 | const beforeMask = createStyleMask(before); 1998 | const afterMask = createStyleMask(after); 1999 | const nodeId = 1; 2000 | const diff = diffStyleMasks(nodeId, beforeMask)(afterMask); 2001 | 2002 | const expected = { 2003 | disabled: [[1, 3, 5], [1, 3, 8], [1, 3, 9], [1, 3, 10], [1, 3, 11]], 2004 | enabled: [[1, 1, 0]], 2005 | }; 2006 | 2007 | t.deepEqual(diff.disabled, expected.disabled); 2008 | }); 2009 | 2010 | test('check if property is active in style mask', t => { 2011 | const input = [ 2012 | { 2013 | matchingSelectors: [0], 2014 | rule: { 2015 | media: [ 2016 | { 2017 | mediaList: [ 2018 | { 2019 | active: true, 2020 | expressions: [ 2021 | { 2022 | computedLength: 734, 2023 | feature: 'min-width', 2024 | unit: 'px', 2025 | value: 734, 2026 | }, 2027 | ], 2028 | }, 2029 | ], 2030 | range: { 2031 | endColumn: 35, 2032 | endLine: 2967, 2033 | startColumn: 7, 2034 | startLine: 2967, 2035 | }, 2036 | source: 'mediaRule', 2037 | sourceURL: 'http://localhost:7999/oscar/index_files/bundle.css', 2038 | styleSheetId: '75164.19', 2039 | text: 'screen and (min-width: 734px)', 2040 | }, 2041 | ], 2042 | origin: 'regular', 2043 | selectorList: { 2044 | selectors: [ 2045 | { 2046 | range: { 2047 | endColumn: 49, 2048 | endLine: 2968, 2049 | startColumn: 4, 2050 | startLine: 2968, 2051 | }, 2052 | text: '.navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 2053 | }, 2054 | ], 2055 | text: '.navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 2056 | }, 2057 | style: { 2058 | cssProperties: [ 2059 | { 2060 | disabled: false, 2061 | implicit: false, 2062 | name: 'height', 2063 | range: { 2064 | endColumn: 4, 2065 | endLine: 2970, 2066 | startColumn: 8, 2067 | startLine: 2969, 2068 | }, 2069 | text: 'height: 10rem\n ', 2070 | value: '10rem', 2071 | }, 2072 | ], 2073 | cssText: '\n height: 10rem\n ', 2074 | range: { 2075 | endColumn: 4, 2076 | endLine: 2970, 2077 | startColumn: 51, 2078 | startLine: 2968, 2079 | }, 2080 | shorthandEntries: [], 2081 | styleSheetId: '75164.19', 2082 | }, 2083 | styleSheetId: '75164.19', 2084 | }, 2085 | }, 2086 | { 2087 | matchingSelectors: [0], 2088 | rule: { 2089 | media: [], 2090 | origin: 'regular', 2091 | selectorList: { 2092 | selectors: [ 2093 | { 2094 | range: { 2095 | endColumn: 45, 2096 | endLine: 2962, 2097 | startColumn: 0, 2098 | startLine: 2962, 2099 | }, 2100 | text: '.navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 2101 | }, 2102 | ], 2103 | text: '.navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 2104 | }, 2105 | style: { 2106 | cssProperties: [ 2107 | { 2108 | disabled: true, 2109 | name: 'color', 2110 | range: { 2111 | endColumn: 22, 2112 | endLine: 2963, 2113 | startColumn: 4, 2114 | startLine: 2963, 2115 | }, 2116 | text: '/* color: #fff; */', 2117 | value: '#fff', 2118 | }, 2119 | { 2120 | disabled: false, 2121 | implicit: false, 2122 | name: 'height', 2123 | range: { 2124 | endColumn: 0, 2125 | endLine: 2965, 2126 | startColumn: 4, 2127 | startLine: 2964, 2128 | }, 2129 | text: 'height: 7rem\n', 2130 | value: '7rem', 2131 | }, 2132 | ], 2133 | cssText: '\n /* color: #fff; */\n height: 7rem\n', 2134 | range: { 2135 | endColumn: 0, 2136 | endLine: 2965, 2137 | startColumn: 47, 2138 | startLine: 2962, 2139 | }, 2140 | shorthandEntries: [], 2141 | styleSheetId: '75164.19', 2142 | }, 2143 | styleSheetId: '75164.19', 2144 | }, 2145 | }, 2146 | { 2147 | matchingSelectors: [4], 2148 | rule: { 2149 | media: [ 2150 | { 2151 | mediaList: [ 2152 | { 2153 | active: true, 2154 | expressions: [ 2155 | { 2156 | computedLength: 734, 2157 | feature: 'min-width', 2158 | unit: 'px', 2159 | value: 734, 2160 | }, 2161 | ], 2162 | }, 2163 | ], 2164 | range: { 2165 | endColumn: 35, 2166 | endLine: 2940, 2167 | startColumn: 7, 2168 | startLine: 2940, 2169 | }, 2170 | source: 'mediaRule', 2171 | sourceURL: 'http://localhost:7999/oscar/index_files/bundle.css', 2172 | styleSheetId: '75164.19', 2173 | text: 'screen and (min-width: 734px)', 2174 | }, 2175 | ], 2176 | origin: 'regular', 2177 | selectorList: { 2178 | selectors: [ 2179 | { 2180 | range: { 2181 | endColumn: 46, 2182 | endLine: 2941, 2183 | startColumn: 4, 2184 | startLine: 2941, 2185 | }, 2186 | text: '.navHeaderHasBanner-3rQVtTrEkCWdKFqj2Lu8lS', 2187 | }, 2188 | { 2189 | range: { 2190 | endColumn: 88, 2191 | endLine: 2941, 2192 | startColumn: 47, 2193 | startLine: 2941, 2194 | }, 2195 | text: '.navHeaderNoCenter-1-PoTwgY-CGMSNMmCL5uRP', 2196 | }, 2197 | { 2198 | range: { 2199 | endColumn: 126, 2200 | endLine: 2941, 2201 | startColumn: 89, 2202 | startLine: 2941, 2203 | }, 2204 | text: '.navHeaderShort-eXOoccIdsiS38H7RjPxao', 2205 | }, 2206 | { 2207 | range: { 2208 | endColumn: 167, 2209 | endLine: 2941, 2210 | startColumn: 127, 2211 | startLine: 2941, 2212 | }, 2213 | text: '.navHeaderWrapper-3GbasrtkfUf0mLlgFmMTv5', 2214 | }, 2215 | { 2216 | range: { 2217 | endColumn: 213, 2218 | endLine: 2941, 2219 | startColumn: 168, 2220 | startLine: 2941, 2221 | }, 2222 | text: '.navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 2223 | }, 2224 | ], 2225 | text: 2226 | '.navHeaderHasBanner-3rQVtTrEkCWdKFqj2Lu8lS, .navHeaderNoCenter-1-PoTwgY-CGMSNMmCL5uRP, .navHeaderShort-eXOoccIdsiS38H7RjPxao, .navHeaderWrapper-3GbasrtkfUf0mLlgFmMTv5, .navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 2227 | }, 2228 | style: { 2229 | cssProperties: [ 2230 | { 2231 | disabled: true, 2232 | name: 'box-shadow', 2233 | range: { 2234 | endColumn: 31, 2235 | endLine: 2942, 2236 | startColumn: 8, 2237 | startLine: 2942, 2238 | }, 2239 | text: '/* box-shadow: none; */', 2240 | value: 'none', 2241 | }, 2242 | { 2243 | disabled: true, 2244 | name: 'height', 2245 | range: { 2246 | endColumn: 28, 2247 | endLine: 2943, 2248 | startColumn: 8, 2249 | startLine: 2943, 2250 | }, 2251 | text: '/* height: 10rem; */', 2252 | value: '10rem', 2253 | }, 2254 | { 2255 | disabled: true, 2256 | name: 'overflow', 2257 | range: { 2258 | endColumn: 31, 2259 | endLine: 2944, 2260 | startColumn: 8, 2261 | startLine: 2944, 2262 | }, 2263 | text: '/* overflow: visible */', 2264 | value: 'visible', 2265 | }, 2266 | ], 2267 | cssText: 2268 | '\n /* box-shadow: none; */\n /* height: 10rem; */\n /* overflow: visible */\n ', 2269 | range: { 2270 | endColumn: 4, 2271 | endLine: 2945, 2272 | startColumn: 215, 2273 | startLine: 2941, 2274 | }, 2275 | shorthandEntries: [], 2276 | styleSheetId: '75164.19', 2277 | }, 2278 | styleSheetId: '75164.19', 2279 | }, 2280 | }, 2281 | { 2282 | matchingSelectors: [4], 2283 | rule: { 2284 | media: [], 2285 | origin: 'regular', 2286 | selectorList: { 2287 | selectors: [ 2288 | { 2289 | range: { 2290 | endColumn: 42, 2291 | endLine: 2929, 2292 | startColumn: 0, 2293 | startLine: 2929, 2294 | }, 2295 | text: '.navHeaderHasBanner-3rQVtTrEkCWdKFqj2Lu8lS', 2296 | }, 2297 | { 2298 | range: { 2299 | endColumn: 84, 2300 | endLine: 2929, 2301 | startColumn: 43, 2302 | startLine: 2929, 2303 | }, 2304 | text: '.navHeaderNoCenter-1-PoTwgY-CGMSNMmCL5uRP', 2305 | }, 2306 | { 2307 | range: { 2308 | endColumn: 122, 2309 | endLine: 2929, 2310 | startColumn: 85, 2311 | startLine: 2929, 2312 | }, 2313 | text: '.navHeaderShort-eXOoccIdsiS38H7RjPxao', 2314 | }, 2315 | { 2316 | range: { 2317 | endColumn: 163, 2318 | endLine: 2929, 2319 | startColumn: 123, 2320 | startLine: 2929, 2321 | }, 2322 | text: '.navHeaderWrapper-3GbasrtkfUf0mLlgFmMTv5', 2323 | }, 2324 | { 2325 | range: { 2326 | endColumn: 209, 2327 | endLine: 2929, 2328 | startColumn: 164, 2329 | startLine: 2929, 2330 | }, 2331 | text: '.navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 2332 | }, 2333 | ], 2334 | text: 2335 | '.navHeaderHasBanner-3rQVtTrEkCWdKFqj2Lu8lS, .navHeaderNoCenter-1-PoTwgY-CGMSNMmCL5uRP, .navHeaderShort-eXOoccIdsiS38H7RjPxao, .navHeaderWrapper-3GbasrtkfUf0mLlgFmMTv5, .navHeaderWrapperWhite-3oby4f18yRexsaOYq-K9TX', 2336 | }, 2337 | style: { 2338 | cssProperties: [ 2339 | { 2340 | disabled: true, 2341 | name: 'color', 2342 | range: { 2343 | endColumn: 25, 2344 | endLine: 2930, 2345 | startColumn: 4, 2346 | startLine: 2930, 2347 | }, 2348 | text: '/* color: #001837; */', 2349 | value: '#001837', 2350 | }, 2351 | { 2352 | disabled: true, 2353 | name: 'height', 2354 | range: { 2355 | endColumn: 23, 2356 | endLine: 2931, 2357 | startColumn: 4, 2358 | startLine: 2931, 2359 | }, 2360 | text: '/* height: 7rem; */', 2361 | value: '7rem', 2362 | }, 2363 | { 2364 | disabled: true, 2365 | name: 'left', 2366 | range: { 2367 | endColumn: 18, 2368 | endLine: 2932, 2369 | startColumn: 4, 2370 | startLine: 2932, 2371 | }, 2372 | text: '/* left: 0; */', 2373 | value: '0', 2374 | }, 2375 | { 2376 | disabled: false, 2377 | implicit: false, 2378 | name: 'position', 2379 | range: { 2380 | endColumn: 20, 2381 | endLine: 2933, 2382 | startColumn: 4, 2383 | startLine: 2933, 2384 | }, 2385 | text: 'position: fixed;', 2386 | value: 'fixed', 2387 | }, 2388 | { 2389 | disabled: false, 2390 | implicit: false, 2391 | name: 'top', 2392 | range: { 2393 | endColumn: 11, 2394 | endLine: 2934, 2395 | startColumn: 4, 2396 | startLine: 2934, 2397 | }, 2398 | text: 'top: 0;', 2399 | value: '0', 2400 | }, 2401 | { 2402 | disabled: false, 2403 | implicit: false, 2404 | name: 'transition', 2405 | range: { 2406 | endColumn: 93, 2407 | endLine: 2935, 2408 | startColumn: 4, 2409 | startLine: 2935, 2410 | }, 2411 | text: 2412 | 'transition: background-color .25s cubic-bezier(1,0,0,1),color .25s cubic-bezier(1,0,0,1);', 2413 | value: 2414 | 'background-color .25s cubic-bezier(1,0,0,1),color .25s cubic-bezier(1,0,0,1)', 2415 | }, 2416 | { 2417 | disabled: false, 2418 | implicit: false, 2419 | name: 'width', 2420 | range: { 2421 | endColumn: 16, 2422 | endLine: 2936, 2423 | startColumn: 4, 2424 | startLine: 2936, 2425 | }, 2426 | text: 'width: 100%;', 2427 | value: '100%', 2428 | }, 2429 | { 2430 | disabled: false, 2431 | implicit: false, 2432 | name: 'z-index', 2433 | range: { 2434 | endColumn: 0, 2435 | endLine: 2938, 2436 | startColumn: 4, 2437 | startLine: 2937, 2438 | }, 2439 | text: 'z-index: 300\n', 2440 | value: '300', 2441 | }, 2442 | { 2443 | name: 'transition-duration', 2444 | value: '0.25s, 0.25s', 2445 | }, 2446 | { 2447 | name: 'transition-timing-function', 2448 | value: 'cubic-bezier(1, 0, 0, 1), cubic-bezier(1, 0, 0, 1)', 2449 | }, 2450 | { 2451 | name: 'transition-delay', 2452 | value: 'initial, initial', 2453 | }, 2454 | { 2455 | name: 'transition-property', 2456 | value: 'background-color, color', 2457 | }, 2458 | ], 2459 | cssText: 2460 | '\n /* color: #001837; */\n /* height: 7rem; */\n /* left: 0; */\n position: fixed;\n top: 0;\n transition: background-color .25s cubic-bezier(1,0,0,1),color .25s cubic-bezier(1,0,0,1);\n width: 100%;\n z-index: 300\n', 2461 | range: { 2462 | endColumn: 0, 2463 | endLine: 2938, 2464 | startColumn: 211, 2465 | startLine: 2929, 2466 | }, 2467 | shorthandEntries: [ 2468 | { 2469 | name: 'transition', 2470 | value: 2471 | 'background-color 0.25s cubic-bezier(1, 0, 0, 1), color 0.25s cubic-bezier(1, 0, 0, 1)', 2472 | }, 2473 | ], 2474 | styleSheetId: '75164.19', 2475 | }, 2476 | styleSheetId: '75164.19', 2477 | }, 2478 | }, 2479 | { 2480 | matchingSelectors: [0], 2481 | rule: { 2482 | media: [], 2483 | origin: 'regular', 2484 | selectorList: { 2485 | selectors: [ 2486 | { 2487 | range: { 2488 | endColumn: 13, 2489 | endLine: 219, 2490 | startColumn: 0, 2491 | startLine: 219, 2492 | }, 2493 | text: '.u-colorWhite', 2494 | }, 2495 | ], 2496 | text: '.u-colorWhite', 2497 | }, 2498 | style: { 2499 | cssProperties: [ 2500 | { 2501 | disabled: true, 2502 | name: 'color', 2503 | range: { 2504 | endColumn: 31, 2505 | endLine: 220, 2506 | startColumn: 4, 2507 | startLine: 220, 2508 | }, 2509 | text: '/* color: #fff!important */', 2510 | value: '#fff!important', 2511 | }, 2512 | ], 2513 | cssText: '\n /* color: #fff!important */\n', 2514 | range: { 2515 | endColumn: 0, 2516 | endLine: 221, 2517 | startColumn: 15, 2518 | startLine: 219, 2519 | }, 2520 | shorthandEntries: [], 2521 | styleSheetId: '75164.19', 2522 | }, 2523 | styleSheetId: '75164.19', 2524 | }, 2525 | }, 2526 | { 2527 | matchingSelectors: [0], 2528 | rule: { 2529 | media: [], 2530 | origin: 'regular', 2531 | selectorList: { 2532 | selectors: [ 2533 | { 2534 | range: { 2535 | endColumn: 19, 2536 | endLine: 133, 2537 | startColumn: 0, 2538 | startLine: 133, 2539 | }, 2540 | text: '.u-bgOscarLightBlue', 2541 | }, 2542 | ], 2543 | text: '.u-bgOscarLightBlue', 2544 | }, 2545 | style: { 2546 | cssProperties: [ 2547 | { 2548 | disabled: false, 2549 | implicit: false, 2550 | important: true, 2551 | name: 'background-color', 2552 | range: { 2553 | endColumn: 0, 2554 | endLine: 135, 2555 | startColumn: 4, 2556 | startLine: 134, 2557 | }, 2558 | text: 'background-color: #0031e2!important\n', 2559 | value: '#0031e2!important', 2560 | }, 2561 | ], 2562 | cssText: '\n background-color: #0031e2!important\n', 2563 | range: { 2564 | endColumn: 0, 2565 | endLine: 135, 2566 | startColumn: 21, 2567 | startLine: 133, 2568 | }, 2569 | shorthandEntries: [], 2570 | styleSheetId: '75164.19', 2571 | }, 2572 | styleSheetId: '75164.19', 2573 | }, 2574 | }, 2575 | { 2576 | matchingSelectors: [0], 2577 | rule: { 2578 | media: [], 2579 | origin: 'regular', 2580 | selectorList: { 2581 | selectors: [ 2582 | { 2583 | range: { 2584 | endColumn: 1, 2585 | endLine: 1151, 2586 | startColumn: 0, 2587 | startLine: 1151, 2588 | }, 2589 | text: '*', 2590 | }, 2591 | ], 2592 | text: '*', 2593 | }, 2594 | style: { 2595 | cssProperties: [ 2596 | { 2597 | disabled: false, 2598 | implicit: false, 2599 | name: 'box-sizing', 2600 | range: { 2601 | endColumn: 24, 2602 | endLine: 1152, 2603 | startColumn: 4, 2604 | startLine: 1152, 2605 | }, 2606 | text: 'box-sizing: inherit;', 2607 | value: 'inherit', 2608 | }, 2609 | { 2610 | disabled: true, 2611 | name: 'pointer-events', 2612 | range: { 2613 | endColumn: 34, 2614 | endLine: 1153, 2615 | startColumn: 4, 2616 | startLine: 1153, 2617 | }, 2618 | text: '/* pointer-events: inherit; */', 2619 | value: 'inherit', 2620 | }, 2621 | { 2622 | disabled: true, 2623 | name: 'outline', 2624 | range: { 2625 | endColumn: 23, 2626 | endLine: 1154, 2627 | startColumn: 4, 2628 | startLine: 1154, 2629 | }, 2630 | text: '/* outline: none */', 2631 | value: 'none', 2632 | }, 2633 | ], 2634 | cssText: 2635 | '\n box-sizing: inherit;\n /* pointer-events: inherit; */\n /* outline: none */\n', 2636 | range: { 2637 | endColumn: 0, 2638 | endLine: 1155, 2639 | startColumn: 3, 2640 | startLine: 1151, 2641 | }, 2642 | shorthandEntries: [], 2643 | styleSheetId: '75164.19', 2644 | }, 2645 | styleSheetId: '75164.19', 2646 | }, 2647 | }, 2648 | { 2649 | matchingSelectors: [0], 2650 | rule: { 2651 | media: [], 2652 | origin: 'user-agent', 2653 | selectorList: { 2654 | selectors: [ 2655 | { 2656 | text: 'div', 2657 | }, 2658 | ], 2659 | text: 'div', 2660 | }, 2661 | style: { 2662 | cssProperties: [ 2663 | { 2664 | name: 'display', 2665 | value: 'block', 2666 | }, 2667 | ], 2668 | shorthandEntries: [], 2669 | }, 2670 | }, 2671 | }, 2672 | ]; 2673 | 2674 | const mask = createStyleMask(input); 2675 | t.true(isPropertyActive(mask)([3, 3])); 2676 | t.false(isPropertyActive(mask)([3, 2])); 2677 | }); 2678 | 2679 | test('build CSS from example', t => { 2680 | const input = { 2681 | '.foo': { 2682 | margin: '10px', 2683 | 'margin-top': '20px', 2684 | 'margin-right': '// 20px', 2685 | 'margin-bottom': '// 20px !important', 2686 | 'margin-left': '20px', 2687 | }, 2688 | '.bar': { 2689 | margin: '30px', 2690 | }, 2691 | }; 2692 | 2693 | const expected = [ 2694 | { 2695 | rule: { 2696 | style: { 2697 | cssProperties: [ 2698 | { name: 'margin', value: '10px' }, 2699 | { name: 'margin-top', value: '20px' }, 2700 | { name: 'margin-right', value: '// 20px', disabled: true }, 2701 | { 2702 | name: 'margin-bottom', 2703 | value: '// 20px !important', 2704 | important: true, 2705 | disabled: true, 2706 | }, 2707 | { name: 'margin-left', value: '20px' }, 2708 | ], 2709 | }, 2710 | }, 2711 | }, 2712 | { 2713 | rule: { 2714 | style: { 2715 | cssProperties: [{ name: 'margin', value: '30px' }], 2716 | }, 2717 | }, 2718 | }, 2719 | ]; 2720 | 2721 | t.deepEqual(buildCSS(input), expected); 2722 | }); 2723 | 2724 | test('find effective property with !important', t => { 2725 | const input = [ 2726 | { 2727 | rule: { 2728 | media: [], 2729 | origin: 'regular', 2730 | selectorList: { 2731 | selectors: [ 2732 | { 2733 | range: { 2734 | endColumn: 6, 2735 | endLine: 22, 2736 | startColumn: 0, 2737 | startLine: 22, 2738 | }, 2739 | text: '.front', 2740 | }, 2741 | ], 2742 | text: '.front', 2743 | }, 2744 | style: { 2745 | cssProperties: [ 2746 | { 2747 | disabled: true, 2748 | name: 'position', 2749 | range: { 2750 | endColumn: 29, 2751 | endLine: 23, 2752 | startColumn: 4, 2753 | startLine: 23, 2754 | }, 2755 | text: '/* position: absolute; */', 2756 | value: 'absolute', 2757 | }, 2758 | ], 2759 | cssText: '\n /* position: absolute; */\n', 2760 | range: { 2761 | endColumn: 0, 2762 | endLine: 24, 2763 | startColumn: 8, 2764 | startLine: 22, 2765 | }, 2766 | shorthandEntries: [], 2767 | styleSheetId: '25533.28', 2768 | }, 2769 | styleSheetId: '25533.28', 2770 | }, 2771 | }, 2772 | { 2773 | rule: { 2774 | media: [], 2775 | origin: 'regular', 2776 | selectorList: { 2777 | selectors: [ 2778 | { 2779 | range: { 2780 | endColumn: 6, 2781 | endLine: 16, 2782 | startColumn: 0, 2783 | startLine: 16, 2784 | }, 2785 | text: '.front', 2786 | }, 2787 | ], 2788 | text: '.front', 2789 | }, 2790 | style: { 2791 | cssProperties: [ 2792 | { 2793 | disabled: false, 2794 | implicit: false, 2795 | important: true, 2796 | name: 'position', 2797 | range: { 2798 | endColumn: 34, 2799 | endLine: 17, 2800 | startColumn: 4, 2801 | startLine: 17, 2802 | }, 2803 | text: 'position: relative !important;', 2804 | value: 'relative !important', 2805 | }, 2806 | { 2807 | disabled: false, 2808 | implicit: false, 2809 | name: 'top', 2810 | range: { 2811 | endColumn: 14, 2812 | endLine: 18, 2813 | startColumn: 4, 2814 | startLine: 18, 2815 | }, 2816 | text: 'top: 25px;', 2817 | value: '25px', 2818 | }, 2819 | { 2820 | disabled: false, 2821 | implicit: false, 2822 | name: 'left', 2823 | range: { 2824 | endColumn: 15, 2825 | endLine: 19, 2826 | startColumn: 4, 2827 | startLine: 19, 2828 | }, 2829 | text: 'left: 25px;', 2830 | value: '25px', 2831 | }, 2832 | ], 2833 | cssText: 2834 | '\n position: relative !important;\n top: 25px;\n left: 25px;\n', 2835 | range: { 2836 | endColumn: 0, 2837 | endLine: 20, 2838 | startColumn: 8, 2839 | startLine: 16, 2840 | }, 2841 | shorthandEntries: [], 2842 | styleSheetId: '25533.28', 2843 | }, 2844 | styleSheetId: '25533.28', 2845 | }, 2846 | }, 2847 | { 2848 | rule: { 2849 | media: [], 2850 | origin: 'regular', 2851 | selectorList: { 2852 | selectors: [ 2853 | { 2854 | range: { 2855 | endColumn: 6, 2856 | endLine: 12, 2857 | startColumn: 0, 2858 | startLine: 12, 2859 | }, 2860 | text: '.front', 2861 | }, 2862 | ], 2863 | text: '.front', 2864 | }, 2865 | style: { 2866 | cssProperties: [ 2867 | { 2868 | disabled: true, 2869 | name: 'position', 2870 | range: { 2871 | endColumn: 26, 2872 | endLine: 13, 2873 | startColumn: 4, 2874 | startLine: 13, 2875 | }, 2876 | text: '/* position: fixed; */', 2877 | value: 'fixed', 2878 | }, 2879 | ], 2880 | cssText: '\n /* position: fixed; */\n', 2881 | range: { 2882 | endColumn: 0, 2883 | endLine: 14, 2884 | startColumn: 8, 2885 | startLine: 12, 2886 | }, 2887 | shorthandEntries: [], 2888 | styleSheetId: '25533.28', 2889 | }, 2890 | styleSheetId: '25533.28', 2891 | }, 2892 | }, 2893 | { 2894 | rule: { 2895 | media: [], 2896 | origin: 'regular', 2897 | selectorList: { 2898 | selectors: [ 2899 | { 2900 | range: { 2901 | endColumn: 6, 2902 | endLine: 0, 2903 | startColumn: 0, 2904 | startLine: 0, 2905 | }, 2906 | text: '.front', 2907 | }, 2908 | ], 2909 | text: '.front', 2910 | }, 2911 | style: { 2912 | cssProperties: [ 2913 | { 2914 | disabled: false, 2915 | implicit: false, 2916 | name: 'background-color', 2917 | range: { 2918 | endColumn: 26, 2919 | endLine: 1, 2920 | startColumn: 4, 2921 | startLine: 1, 2922 | }, 2923 | text: 'background-color: red;', 2924 | value: 'red', 2925 | }, 2926 | { 2927 | disabled: false, 2928 | implicit: false, 2929 | name: 'height', 2930 | range: { 2931 | endColumn: 18, 2932 | endLine: 2, 2933 | startColumn: 4, 2934 | startLine: 2, 2935 | }, 2936 | text: 'height: 150px;', 2937 | value: '150px', 2938 | }, 2939 | { 2940 | disabled: false, 2941 | implicit: false, 2942 | name: 'width', 2943 | range: { 2944 | endColumn: 16, 2945 | endLine: 3, 2946 | startColumn: 4, 2947 | startLine: 3, 2948 | }, 2949 | text: 'width: 60px;', 2950 | value: '60px', 2951 | }, 2952 | ], 2953 | cssText: 2954 | '\n background-color: red;\n height: 150px;\n width: 60px;\n', 2955 | range: { 2956 | endColumn: 0, 2957 | endLine: 4, 2958 | startColumn: 8, 2959 | startLine: 0, 2960 | }, 2961 | shorthandEntries: [], 2962 | styleSheetId: '25533.28', 2963 | }, 2964 | styleSheetId: '25533.28', 2965 | }, 2966 | }, 2967 | { 2968 | rule: { 2969 | media: [], 2970 | origin: 'user-agent', 2971 | selectorList: { 2972 | selectors: [ 2973 | { 2974 | text: 'div', 2975 | }, 2976 | ], 2977 | text: 'div', 2978 | }, 2979 | style: { 2980 | cssProperties: [ 2981 | { 2982 | name: 'display', 2983 | value: 'block', 2984 | }, 2985 | ], 2986 | shorthandEntries: [], 2987 | }, 2988 | }, 2989 | }, 2990 | ]; 2991 | 2992 | const values = getEffectiveValueForProperty(input)('position').map( 2993 | ({ value }) => value, 2994 | ); 2995 | 2996 | t.deepEqual(values, ['relative !important']); 2997 | }); 2998 | 2999 | test('find effective property with shorthands', t => { 3000 | const input = [ 3001 | { 3002 | rule: { 3003 | media: [], 3004 | origin: 'regular', 3005 | selectorList: { 3006 | selectors: [ 3007 | { 3008 | range: { 3009 | endColumn: 6, 3010 | endLine: 4, 3011 | startColumn: 0, 3012 | startLine: 4, 3013 | }, 3014 | text: '.front', 3015 | }, 3016 | ], 3017 | text: '.front', 3018 | }, 3019 | style: { 3020 | cssProperties: [ 3021 | { 3022 | disabled: false, 3023 | implicit: false, 3024 | name: 'margin-left', 3025 | range: { 3026 | endColumn: 22, 3027 | endLine: 5, 3028 | startColumn: 4, 3029 | startLine: 5, 3030 | }, 3031 | text: 'margin-left: 40px;', 3032 | value: '40px', 3033 | }, 3034 | { 3035 | disabled: false, 3036 | implicit: false, 3037 | name: 'margin-right', 3038 | range: { 3039 | endColumn: 23, 3040 | endLine: 6, 3041 | startColumn: 4, 3042 | startLine: 6, 3043 | }, 3044 | text: 'margin-right: 40px;', 3045 | value: '40px', 3046 | }, 3047 | ], 3048 | cssText: '\n margin-left: 40px;\n margin-right: 40px;\n', 3049 | range: { 3050 | endColumn: 0, 3051 | endLine: 7, 3052 | startColumn: 8, 3053 | startLine: 4, 3054 | }, 3055 | shorthandEntries: [], 3056 | styleSheetId: '25533.39', 3057 | }, 3058 | styleSheetId: '25533.39', 3059 | }, 3060 | }, 3061 | { 3062 | rule: { 3063 | media: [], 3064 | origin: 'regular', 3065 | selectorList: { 3066 | selectors: [ 3067 | { 3068 | range: { 3069 | endColumn: 6, 3070 | endLine: 0, 3071 | startColumn: 0, 3072 | startLine: 0, 3073 | }, 3074 | text: '.front', 3075 | }, 3076 | ], 3077 | text: '.front', 3078 | }, 3079 | style: { 3080 | cssProperties: [ 3081 | { 3082 | disabled: true, 3083 | name: 'margin', 3084 | range: { 3085 | endColumn: 25, 3086 | endLine: 1, 3087 | startColumn: 4, 3088 | startLine: 1, 3089 | }, 3090 | text: '/* margin: 0 20px; */', 3091 | value: '0 20px', 3092 | }, 3093 | ], 3094 | cssText: '\n /* margin: 0 20px; */\n', 3095 | range: { 3096 | endColumn: 0, 3097 | endLine: 2, 3098 | startColumn: 8, 3099 | startLine: 0, 3100 | }, 3101 | shorthandEntries: [], 3102 | styleSheetId: '25533.39', 3103 | }, 3104 | styleSheetId: '25533.39', 3105 | }, 3106 | }, 3107 | { 3108 | rule: { 3109 | media: [], 3110 | origin: 'user-agent', 3111 | selectorList: { 3112 | selectors: [ 3113 | { 3114 | text: 'div', 3115 | }, 3116 | ], 3117 | text: 'div', 3118 | }, 3119 | style: { 3120 | cssProperties: [ 3121 | { 3122 | name: 'display', 3123 | value: 'block', 3124 | }, 3125 | ], 3126 | shorthandEntries: [], 3127 | }, 3128 | }, 3129 | }, 3130 | ]; 3131 | 3132 | const values = getEffectiveValueForProperty(input)('margin').map( 3133 | ({ value }) => value, 3134 | ); 3135 | t.deepEqual(values, ['40px', '40px']); 3136 | }); 3137 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const ChromeExtensionReloader = require('webpack-chrome-extension-reloader'); 4 | 5 | const baseConfig = { 6 | context: path.resolve(__dirname), 7 | entry: './src/background.js', 8 | output: { 9 | path: path.resolve(__dirname, 'build'), 10 | filename: 'background.js', 11 | }, 12 | plugins: [ 13 | new HtmlWebpackPlugin({ 14 | title: 'Background Page', 15 | filename: 'background.html', 16 | }), 17 | ], 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.js$/, 22 | exclude: /node_modules/, 23 | loader: 'babel-loader', 24 | include: path.join(__dirname, 'src'), 25 | }, 26 | ], 27 | }, 28 | target: 'web', 29 | devtool: 'source-map', 30 | }; 31 | 32 | const devConfig = { 33 | devtool: 'inline-source-map', 34 | plugins: [new ChromeExtensionReloader()], 35 | watch: true, 36 | }; 37 | 38 | module.exports = function(env) { 39 | return env.dev ? Object.assign(baseConfig, devConfig) : baseConfig; 40 | }; 41 | --------------------------------------------------------------------------------