diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b9d52f06..fc2ce69e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic +## [Unreleased] + +### Added + +- `spoof-css` scriptlet [#317](https://github.com/AdguardTeam/Scriptlets/issues/317) + ## [v1.9.105] - 2023-12-25 ### Added @@ -326,6 +332,7 @@ prevent inline `onerror` and match `link` tag [#276](https://github.com/AdguardT - `metrika-yandex-tag` [#254](https://github.com/AdguardTeam/Scriptlets/issues/254) - `googlesyndication-adsbygoogle` [#252](https://github.com/AdguardTeam/Scriptlets/issues/252) +[Unreleased]: https://github.com/AdguardTeam/Scriptlets/compare/v1.9.105...HEAD [v1.9.105]: https://github.com/AdguardTeam/Scriptlets/compare/v1.9.101...v1.9.105 [v1.9.101]: https://github.com/AdguardTeam/Scriptlets/compare/v1.9.96...v1.9.101 [v1.9.96]: https://github.com/AdguardTeam/Scriptlets/compare/v1.9.91...v1.9.96 diff --git a/src/scriptlets/scriptlets-list.js b/src/scriptlets/scriptlets-list.js index 4da4f7598..380cb896d 100644 --- a/src/scriptlets/scriptlets-list.js +++ b/src/scriptlets/scriptlets-list.js @@ -61,3 +61,4 @@ export * from './remove-node-text'; export * from './trusted-replace-node-text'; export * from './evaldata-prune'; export * from './trusted-prune-inbound-object'; +export * from './spoof-css'; diff --git a/src/scriptlets/spoof-css.js b/src/scriptlets/spoof-css.js new file mode 100644 index 000000000..4f0796b8b --- /dev/null +++ b/src/scriptlets/spoof-css.js @@ -0,0 +1,222 @@ +import { + hit, +} from '../helpers/index'; + +/* eslint-disable max-len */ +/** + * @scriptlet spoof-css + * + * @description + * Spoof CSS property value when `getComputedStyle()` or `getBoundingClientRect()` methods is called. + * + * Related UBO scriptlet: + * https://github.com/gorhill/uBlock/wiki/Resources-Library#spoof-cssjs- + * + * ### Syntax + * + * ```text + * example.org#%#//scriptlet('spoof-css'[, selectors[, properties[, shouldDebug]]]) + * ``` + * + * - `selectors` — string of comma-separated selectors to match + * - `properties` — CSS property name and property value, separated by a comma + * - `shouldDebug` — optional, defaults to `false`, if set to `true`, will trigger debugger statement + * when `getComputedStyle()` or `getBoundingClientRect()` methods is called + * + * + * ### Examples + * + * 1. Spoof CSS property value `display` to `block` for all elements with class `adsbygoogle`: + * + * ```adblock + * example.org#%#//scriptlet('spoof-css', '.adsbygoogle', 'display, block') + * ``` + * + * 2. Spoof CSS property value `display` to `block`, `visibility` to `visible` for all elements with class `adsbygoogle`: + * + * ```adblock + * example.org#%#//scriptlet('spoof-css', '.adsbygoogle', 'display, block, visibility, visible') + * ``` + * + * 3. Spoof CSS property value `height` to `100` for all elements with class `adsbygoogle` and `advert`: + * + * ```adblock + * example.org#%#//scriptlet('spoof-css', '.adsbygoogle, .advert', 'height, 100') + * ``` + * + * 4. Spoof CSS property value `height` to `100`, `display` to `block` for all elements with class `adsbygoogle` and `advert`: + * + * ```adblock + * example.org#%#//scriptlet('spoof-css', '.adsbygoogle, .advert', 'height, 100, display, block') + * ``` + * + * @added unknown. + */ +/* eslint-enable max-len */ + +export function spoofCSS(source, selectors, properties, shouldDebug = false) { + if (!selectors) { + return; + } + + const arrayOfProperties = properties.replace(/\s+/g, '').split(','); + + // getComputedStyle uses camelCase version of CSS properties + // for example, "clip-path" is displayed as "clipPath" + // so it's needed to convert CSS property to camelCase + const toCamelCase = (property) => { + const toUpperCase = (text) => text.charAt(1).toUpperCase(); + return property.replace(/-[a-z]/g, toUpperCase); + }; + + const propToValueMap = new Map(); + for (let i = 0; i < arrayOfProperties.length; i += 2) { + if (arrayOfProperties[i] === '') { + break; + } + propToValueMap.set(toCamelCase(arrayOfProperties[i]), arrayOfProperties[i + 1]); + } + + const spoofStyle = (cssProperty, realCssValue) => { + const property = cssProperty; + // For non existing properties return empty string + const realValue = realCssValue ?? ''; + const shouldSpoof = propToValueMap.has(property); + const value = shouldSpoof ? propToValueMap.get(property) : realValue; + return value; + }; + + const setRectValue = (rect, prop, value) => { + Object.defineProperty( + rect, + prop, + { + value: parseFloat(value), + }, + ); + }; + + const getter = (target, prop, receiver) => { + hit(source); + if (prop === 'toString') { + return target.toString.bind(target); + } + return Reflect.get(target, prop, receiver); + }; + + const getComputedStyleWrapper = (target, thisArg, args) => { + if (shouldDebug) { + debugger; // eslint-disable-line no-debugger + } + const style = Reflect.apply(target, thisArg, args); + const targetElements = new WeakSet(document.querySelectorAll(selectors)); + if (!targetElements.has(args[0])) { + return style; + } + const proxiedStyle = new Proxy(style, { + get(target, prop, receiver) { + const CSSStyleProp = target[prop]; + if (typeof CSSStyleProp === 'function') { + if (prop === 'getPropertyValue') { + const getPropertyValueFunc = new Proxy(CSSStyleProp, { + apply(target, thisArg, args) { + const cssName = args[0]; + const cssValue = thisArg[cssName]; + return spoofStyle(cssName, cssValue); + }, + get: getter, + }); + return getPropertyValueFunc; + } + return CSSStyleProp.bind(target); + } + return spoofStyle(prop, Reflect.get(target, prop, receiver)); + }, + getOwnPropertyDescriptor(target, prop) { + if (propToValueMap.has(prop)) { + return { + configurable: true, + enumerable: true, + value: propToValueMap.get(prop), + writable: true, + }; + } + return Reflect.getOwnPropertyDescriptor(target, prop); + }, + }); + hit(source); + return proxiedStyle; + }; + + const getComputedStyleHandler = { + apply: getComputedStyleWrapper, + get: getter, + }; + + window.getComputedStyle = new Proxy(window.getComputedStyle, getComputedStyleHandler); + + const getBoundingClientRectWrapper = (target, thisArg, args) => { + if (shouldDebug) { + debugger; // eslint-disable-line no-debugger + } + const rect = Reflect.apply(target, thisArg, args); + const targetElements = new WeakSet(document.querySelectorAll(selectors)); + if (!targetElements.has(thisArg)) { + return rect; + } + + const { + top, + bottom, + height, + width, + left, + right, + } = rect; + + const newDOMRect = new window.DOMRect(rect.x, rect.y, top, bottom, width, height, left, right); + + if (propToValueMap.has('top')) { + setRectValue(newDOMRect, 'top', propToValueMap.get('top')); + } + if (propToValueMap.has('bottom')) { + setRectValue(newDOMRect, 'bottom', propToValueMap.get('bottom')); + } + if (propToValueMap.has('left')) { + setRectValue(newDOMRect, 'left', propToValueMap.get('left')); + } + if (propToValueMap.has('right')) { + setRectValue(newDOMRect, 'right', propToValueMap.get('right')); + } + if (propToValueMap.has('height')) { + setRectValue(newDOMRect, 'height', propToValueMap.get('height')); + } + if (propToValueMap.has('width')) { + setRectValue(newDOMRect, 'width', propToValueMap.get('width')); + } + hit(source); + return newDOMRect; + }; + + const getBoundingClientRectHandler = { + apply: getBoundingClientRectWrapper, + get: getter, + }; + + window.Element.prototype.getBoundingClientRect = new Proxy( + window.Element.prototype.getBoundingClientRect, + getBoundingClientRectHandler, + ); +} + +spoofCSS.names = [ + 'spoof-css', + // aliases are needed for matching the related scriptlet converted into our syntax + 'spoof-css.js', + 'ubo-spoof-css.js', + 'ubo-spoof-css', +]; + +spoofCSS.injections = [ + hit, +]; diff --git a/tests/scriptlets/spoof-css.test.js b/tests/scriptlets/spoof-css.test.js new file mode 100644 index 000000000..ec5c2a46e --- /dev/null +++ b/tests/scriptlets/spoof-css.test.js @@ -0,0 +1,467 @@ +/* eslint-disable no-underscore-dangle, no-console */ +import { runScriptlet, clearGlobalProps } from '../helpers'; + +const { test, module } = QUnit; +const name = 'spoof-css'; + +const beforeEach = () => { + window.__debug = () => { + window.hit = 'FIRED'; + }; +}; + +const afterEach = () => { + clearGlobalProps('hit', '__debug'); +}; + +module(name, { beforeEach, afterEach }); + +const createElem = (className) => { + const elem = document.createElement('div'); + if (className) { + elem.classList.add(className); + } + document.body.appendChild(elem); + return elem; +}; + +const addStyle = (text) => { + const style = document.createElement('style'); + style.innerText = `${text}`; + document.body.appendChild(style); + return style; +}; + +test('Checking if alias name works', (assert) => { + const adgParams = { + name, + engine: 'test', + verbose: true, + }; + const uboParams = { + name: 'ubo-spoof-css.js', + engine: 'test', + verbose: true, + }; + + const codeByAdgParams = window.scriptlets.invoke(adgParams); + const codeByUboParams = window.scriptlets.invoke(uboParams); + + assert.strictEqual(codeByAdgParams, codeByUboParams, 'ubo name - ok'); +}); + +test('One selector and one property - getComputedStyle', (assert) => { + const matchClassName = 'testClass'; + + const matchElem = createElem(matchClassName); + const cssProperty = 'display: none !important;'; + const matchStyle = addStyle(`.${matchClassName} { ${cssProperty} }`); + + const property = 'display, block'; + + const scriptletArgs = [`.${matchClassName}`, property]; + runScriptlet(name, scriptletArgs); + + const elStyle = window.getComputedStyle(matchElem).display; + + assert.strictEqual(elStyle, 'block', 'display style is set to block'); + assert.strictEqual(window.hit, 'FIRED'); + + clearGlobalProps('hit'); + matchElem.remove(); + matchStyle.remove(); +}); + +test('One selector and one property + test element which style should not be changed - getComputedStyle', (assert) => { + const matchClassNameChange = 'testClassChange'; + const matchClassNameNotChange = 'testClassNotChange'; + + const matchElemChange = createElem(matchClassNameChange); + const matchElemNotChange = createElem(matchClassNameNotChange); + const cssProperty = 'display: none !important;'; + const matchStyle = addStyle(`.${matchClassNameChange}, .${matchClassNameNotChange} { ${cssProperty} }`); + + const property = 'display, block'; + + const scriptletArgs = [`.${matchClassNameChange}`, property]; + runScriptlet(name, scriptletArgs); + + const elStyleChange = window.getComputedStyle(matchElemChange).display; + const elStyleNotChange = window.getComputedStyle(matchElemNotChange).display; + + assert.strictEqual(elStyleChange, 'block', 'display style is set to block'); + assert.strictEqual(elStyleNotChange, 'none', 'display style is set to none'); + assert.strictEqual(window.hit, 'FIRED'); + + clearGlobalProps('hit'); + matchElemChange.remove(); + matchElemNotChange.remove(); + matchStyle.remove(); +}); + +test('One selector and non existed property - getComputedStyle', (assert) => { + const matchClassName = 'testClass'; + + const matchElem = createElem(matchClassName); + const cssProperty = 'display: none !important;'; + const matchStyle = addStyle(`.${matchClassName} { ${cssProperty} }`); + + const property = 'display, block'; + + const scriptletArgs = [`.${matchClassName}`, property]; + runScriptlet(name, scriptletArgs); + + const elStyle = window.getComputedStyle(matchElem); + const elStyleGetPropertyValue = elStyle.getPropertyValue('not_existed_property'); + + assert.strictEqual(elStyleGetPropertyValue, '', 'not_existed_property returns empty string'); + assert.strictEqual(window.hit, 'FIRED'); + + clearGlobalProps('hit'); + matchElem.remove(); + matchStyle.remove(); +}); + +test('One selector and one property - getComputedStyle clip-path', (assert) => { + const matchClassName = 'testClassClipPath'; + + const matchElem = createElem(matchClassName); + const cssProperty = 'clip-path: circle(50%);'; + const matchStyle = addStyle(`.${matchClassName} { ${cssProperty} }`); + + const property = 'clip-path, circle(0%)'; + + const scriptletArgs = [`.${matchClassName}`, property]; + runScriptlet(name, scriptletArgs); + + const elStyle = window.getComputedStyle(matchElem).clipPath; + + assert.strictEqual(elStyle, 'circle(0%)', 'display style is set to block'); + assert.strictEqual(window.hit, 'FIRED'); + + clearGlobalProps('hit'); + matchElem.remove(); + matchStyle.remove(); +}); + +test('One selector and two properties, set by two separate scriptlets - getComputedStyle', (assert) => { + const matchClassName = 'testClassTwoScriptlets'; + + const matchElem = createElem(matchClassName); + const cssProperty = 'display: none !important; visibility: hidden !important;'; + const matchStyle = addStyle(`.${matchClassName} { ${cssProperty} }`); + + const propertyOne = 'display, block'; + const propertyTwo = 'visibility, visible'; + + const scriptletArgsOne = [`.${matchClassName}`, propertyOne]; + runScriptlet(name, scriptletArgsOne); + + const scriptletArgsTwo = [`.${matchClassName}`, propertyTwo]; + runScriptlet(name, scriptletArgsTwo); + + const computedStyle = window.getComputedStyle(matchElem); + const elStyleDisplay = computedStyle.display; + const elStylePropValDisplay = computedStyle.getPropertyValue('display'); + const elStyleVisibility = computedStyle.visibility; + const elStylePropValVisibility = computedStyle.getPropertyValue('visibility'); + + assert.strictEqual(elStyleDisplay, 'block', 'display style is set to block'); + assert.strictEqual(elStylePropValDisplay, 'block', 'display style is set to block - getPropertyValue'); + assert.strictEqual(elStyleVisibility, 'visible', 'visibility style is set to visible'); + assert.strictEqual(elStylePropValVisibility, 'visible', 'visibility style is set to visible - getPropertyValue'); + assert.strictEqual(window.hit, 'FIRED'); + + clearGlobalProps('hit'); + matchElem.remove(); + matchStyle.remove(); +}); + +test('One selector and two properties - getComputedStyle', (assert) => { + const matchClassName = 'testClassFewProps'; + + const matchElem = createElem(matchClassName); + const cssProperty = 'display: none !important; visibility: hidden !important;'; + const matchStyle = addStyle(`.${matchClassName} { ${cssProperty} }`); + + const property = 'display, block, visibility, visible'; + + const scriptletArgs = [`.${matchClassName}`, property]; + runScriptlet(name, scriptletArgs); + + const computedStyle = window.getComputedStyle(matchElem); + const elStyleDisplay = computedStyle.display; + const elStylePropValDisplay = computedStyle.getPropertyValue('display'); + const elStyleVisibility = computedStyle.visibility; + const elStylePropValVisibility = computedStyle.getPropertyValue('visibility'); + + assert.strictEqual(elStyleDisplay, 'block', 'display style is set to block'); + assert.strictEqual(elStylePropValDisplay, 'block', 'display style is set to block - getPropertyValue'); + assert.strictEqual(elStyleVisibility, 'visible', 'visibility style is set to visible'); + assert.strictEqual(elStylePropValVisibility, 'visible', 'visibility style is set to visible - getPropertyValue'); + assert.strictEqual(window.hit, 'FIRED'); + + clearGlobalProps('hit'); + matchElem.remove(); + matchStyle.remove(); +}); + +test('Two selectors and one property - getComputedStyle', (assert) => { + const matchClassNameFirst = 'testClassFirst'; + const matchClassNameSecond = 'testClassSecond'; + + const matchElemFirst = createElem(matchClassNameFirst); + const matchElemSecond = createElem(matchClassNameFirst); + const cssProperty = 'display: none !important;'; + const matchStyle = addStyle(`.${matchClassNameFirst} { ${cssProperty} }`); + + const property = 'display, block'; + + const scriptletArgs = [`.${matchClassNameFirst}, .${matchClassNameSecond}`, property]; + runScriptlet(name, scriptletArgs); + + const computedStyleSecond = window.getComputedStyle(matchElemSecond); + const elStyleDisplaySecond = computedStyleSecond.display; + const elStylePropValDisplaySecond = computedStyleSecond.getPropertyValue('display'); + + const computedStyleFirst = window.getComputedStyle(matchElemFirst); + const elStyleDisplayFirst = computedStyleFirst.display; + const elStylePropValDisplayFirst = computedStyleFirst.getPropertyValue('display'); + + assert.strictEqual(elStyleDisplayFirst, 'block', 'display style is set to block'); + assert.strictEqual(elStylePropValDisplayFirst, 'block', 'display style is set to block - getPropertyValue'); + + assert.strictEqual(elStyleDisplaySecond, 'block', 'display style is set to block'); + assert.strictEqual(elStylePropValDisplaySecond, 'block', 'display style is set to block - getPropertyValue'); + + assert.strictEqual(window.hit, 'FIRED'); + + clearGlobalProps('hit'); + matchElemFirst.remove(); + matchElemSecond.remove(); + matchStyle.remove(); +}); + +test('Two selectors divided by escaped comma and one property - getComputedStyle', (assert) => { + const matchClassNameFirst = 'testClassFirstComma'; + const matchClassNameSecond = 'testClassSecondComma'; + + const matchElemFirst = createElem(matchClassNameFirst); + const matchElemSecond = createElem(matchClassNameFirst); + const cssProperty = 'display: none !important;'; + const matchStyle = addStyle(`.${matchClassNameFirst} { ${cssProperty} }`); + + const property = 'display, block'; + + // eslint-disable-next-line no-useless-escape + const scriptletArgs = [`.${matchClassNameFirst}\, .${matchClassNameSecond}`, property]; + runScriptlet(name, scriptletArgs); + + const computedStyleSecond = window.getComputedStyle(matchElemSecond); + const elStyleDisplaySecond = computedStyleSecond.display; + const elStylePropValDisplaySecond = computedStyleSecond.getPropertyValue('display'); + + const computedStyleFirst = window.getComputedStyle(matchElemFirst); + const elStyleDisplayFirst = computedStyleFirst.display; + const elStylePropValDisplayFirst = computedStyleFirst.getPropertyValue('display'); + + assert.strictEqual(elStyleDisplayFirst, 'block', 'display style is set to block'); + assert.strictEqual(elStylePropValDisplayFirst, 'block', 'display style is set to block - getPropertyValue'); + + assert.strictEqual(elStyleDisplaySecond, 'block', 'display style is set to block'); + assert.strictEqual(elStylePropValDisplaySecond, 'block', 'display style is set to block - getPropertyValue'); + + assert.strictEqual(window.hit, 'FIRED'); + + clearGlobalProps('hit'); + matchElemFirst.remove(); + matchElemSecond.remove(); + matchStyle.remove(); +}); + +test('Two selectors and two properties - getComputedStyle', (assert) => { + const matchClassNameFirst = 'testClassFirst'; + const matchClassNameSecond = 'testClassSecond'; + + const matchElemFirst = createElem(matchClassNameFirst); + const matchElemSecond = createElem(matchClassNameFirst); + const cssProperty = 'display: none !important; visibility: hidden !important;'; + const matchStyle = addStyle( + ` + .${matchClassNameFirst} { ${cssProperty} } + .${matchClassNameSecond} { ${cssProperty} } + `, + ); + + const property = 'display, block, visibility, visible'; + + const scriptletArgs = [`.${matchClassNameFirst}, .${matchClassNameSecond}`, property]; + runScriptlet(name, scriptletArgs); + + const computedStyleSecond = window.getComputedStyle(matchElemSecond); + const elStyleDisplaySecond = computedStyleSecond.display; + const elStylePropValDisplaySecond = computedStyleSecond.getPropertyValue('display'); + const elStyleVisibilitySecond = computedStyleSecond.visibility; + const elStylePropValVisibilitySecond = computedStyleSecond.getPropertyValue('visibility'); + + const computedStyleFirst = window.getComputedStyle(matchElemFirst); + const elStyleDisplayFirst = computedStyleFirst.display; + const elStylePropValDisplayFirst = computedStyleFirst.getPropertyValue('display'); + const elStyleVisibilityFirst = computedStyleFirst.visibility; + const elStylePropValVisibilityFirst = computedStyleFirst.getPropertyValue('visibility'); + + assert.strictEqual(elStyleDisplayFirst, 'block', 'display style is set to block'); + assert.strictEqual(elStylePropValDisplayFirst, 'block', 'display style is set to block - getPropertyValue'); + assert.strictEqual(elStyleVisibilityFirst, 'visible', 'visibility style is set to visible'); + assert.strictEqual( + elStylePropValVisibilityFirst, + 'visible', + 'visibility style is set to visible - getPropertyValue', + ); + + assert.strictEqual(elStyleDisplaySecond, 'block', 'display style is set to block'); + assert.strictEqual(elStylePropValDisplaySecond, 'block', 'display style is set to block - getPropertyValue'); + assert.strictEqual(elStyleVisibilitySecond, 'visible', 'visibility style is set to visible'); + assert.strictEqual( + elStylePropValVisibilitySecond, + 'visible', + 'visibility style is set to visible - getPropertyValue', + ); + + assert.strictEqual(window.hit, 'FIRED'); + + clearGlobalProps('hit'); + matchElemFirst.remove(); + matchElemSecond.remove(); + matchStyle.remove(); +}); + +test('One selector and one property - getComputedStyle getOwnPropertyDescriptor', (assert) => { + const matchClassName = 'testClassGetOwnPropertyDescriptor'; + + const matchElem = createElem(matchClassName); + const cssProperty = 'display: none !important;'; + const matchStyle = addStyle(`.${matchClassName} { ${cssProperty} }`); + + const property = 'display, block'; + + const scriptletArgs = [`.${matchClassName}`, property]; + runScriptlet(name, scriptletArgs); + + const elStyle = window.getComputedStyle(matchElem); + const elStyleDisplay = Object.getOwnPropertyDescriptor(elStyle, 'display').value; + + assert.strictEqual(elStyleDisplay, 'block', 'display style is set to block - getOwnPropertyDescriptor'); + assert.strictEqual(window.hit, 'FIRED'); + + clearGlobalProps('hit'); + matchElem.remove(); + matchStyle.remove(); +}); + +test('One selector and one property - getBoundingClientRect height', (assert) => { + const EXPECTED_HEIGHT = 1024; + const matchClassName = 'testClassClientRect'; + + const matchElem = createElem(matchClassName); + const cssProperty = 'height: 100px !important; width: 100px !important;'; + const matchStyle = addStyle(`.${matchClassName} { ${cssProperty} }`); + + const property = `height, ${EXPECTED_HEIGHT}`; + + const scriptletArgs = [`.${matchClassName}`, property]; + runScriptlet(name, scriptletArgs); + + const boundingClientRect = matchElem.getBoundingClientRect(); + const elStyleHeight = boundingClientRect.height; + + assert.strictEqual(elStyleHeight, EXPECTED_HEIGHT, `height is set to ${EXPECTED_HEIGHT}`); + assert.strictEqual(window.hit, 'FIRED'); + + clearGlobalProps('hit'); + matchElem.remove(); + matchStyle.remove(); +}); + +test('One selector and one property - getBoundingClientRect top', (assert) => { + const EXPECTED_TOP = 2050; + const matchClassName = 'testClassClientRect'; + + const matchElem = createElem(matchClassName); + const cssProperty = 'height: 100px !important; width: 100px !important;'; + const matchStyle = addStyle(`.${matchClassName} { ${cssProperty} }`); + + const property = `top, ${EXPECTED_TOP}`; + + const scriptletArgs = [`.${matchClassName}`, property]; + runScriptlet(name, scriptletArgs); + + const boundingClientRect = matchElem.getBoundingClientRect(); + const elStyleHeight = boundingClientRect.top; + + assert.strictEqual(elStyleHeight, EXPECTED_TOP, `top is set to ${EXPECTED_TOP}`); + assert.strictEqual(window.hit, 'FIRED'); + + clearGlobalProps('hit'); + matchElem.remove(); + matchStyle.remove(); +}); + +test('One selector and two properties - getBoundingClientRect', (assert) => { + const EXPECTED_HEIGHT = 555; + const EXPECTED_WIDTH = 666; + const matchClassName = 'testClassClientRect'; + + const matchElem = createElem(matchClassName); + const cssProperty = 'height: 100px !important; width: 100px !important;'; + const matchStyle = addStyle(`.${matchClassName} { ${cssProperty} }`); + + const property = `height, ${EXPECTED_HEIGHT}, width, ${EXPECTED_WIDTH}`; + + const scriptletArgs = [`.${matchClassName}`, property]; + runScriptlet(name, scriptletArgs); + + const boundingClientRect = matchElem.getBoundingClientRect(); + const elStyleHeight = boundingClientRect.height; + const elStyleWidth = boundingClientRect.width; + + assert.strictEqual(elStyleHeight, EXPECTED_HEIGHT, `height is set to ${EXPECTED_HEIGHT}`); + assert.strictEqual(elStyleWidth, EXPECTED_WIDTH, `width is set to ${EXPECTED_HEIGHT}`); + assert.strictEqual(window.hit, 'FIRED'); + + clearGlobalProps('hit'); + matchElem.remove(); + matchStyle.remove(); +}); + +test('Native code check', (assert) => { + const matchClassName = 'testClassNativeCode'; + const matchElem = createElem(matchClassName); + + const property = 'height, 100, width, 200, display, block'; + + const scriptletArgs = [`.${matchClassName}`, property]; + runScriptlet(name, scriptletArgs); + + const elGetComputedStyle = window.getComputedStyle(matchElem); + const nativeCodeGetComputedStyle = window.getComputedStyle.toString(); + const nativeCodeGetPropertyValue = elGetComputedStyle.getPropertyValue.toString(); + const nativeCodeGetBoundingClientRect = Element.prototype.getBoundingClientRect.toString(); + + assert.ok( + nativeCodeGetComputedStyle.includes('function getComputedStyle() { [native code] }'), + 'getComputedStyle native code is present', + ); + assert.ok( + nativeCodeGetPropertyValue.includes('function getPropertyValue() { [native code] }'), + 'getPropertyValue native code is present', + ); + assert.ok( + nativeCodeGetBoundingClientRect.includes('function getBoundingClientRect() { [native code] }'), + 'getBoundingClientRect native code is present', + ); + assert.strictEqual(window.hit, 'FIRED'); + + matchElem.remove(); + clearGlobalProps('hit'); +});