diff --git a/CHANGELOG.md b/CHANGELOG.md index ec4c02ed..05879851 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,11 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic -## Unreleased +## [Unreleased] ### Added +- `spoof-css` scriptlet [#317](https://github.com/AdguardTeam/Scriptlets/issues/317) - New values `t`, `f`, `necessary`, `required` for `set-cookie` and `set-cookie-reload` [#379](https://github.com/AdguardTeam/Scriptlets/issues/379) @@ -333,6 +334,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/scripts/compatibility-table.json b/scripts/compatibility-table.json index 539ffe8b..30ff0e1a 100644 --- a/scripts/compatibility-table.json +++ b/scripts/compatibility-table.json @@ -270,6 +270,7 @@ "ubo": "window.name-defuser.js" }, { + "adg": "spoof-css", "ubo": "spoof-css.js" }, { diff --git a/src/helpers/converter.ts b/src/helpers/converter.ts index cfa0b834..b985d110 100644 --- a/src/helpers/converter.ts +++ b/src/helpers/converter.ts @@ -61,8 +61,10 @@ const UBO_NO_FETCH_IF_WILDCARD = '/^/'; const ESCAPED_COMMA_SEPARATOR = '\\,'; const COMMA_SEPARATOR = ','; +const SPOOF_CSS_METHOD = 'spoofCSS'; const REMOVE_ATTR_METHOD = 'removeAttr'; const REMOVE_CLASS_METHOD = 'removeClass'; +const SPOOF_CSS_ALIASES = scriptletList[SPOOF_CSS_METHOD].names; const REMOVE_ATTR_ALIASES = scriptletList[REMOVE_ATTR_METHOD].names; const REMOVE_CLASS_ALIASES = scriptletList[REMOVE_CLASS_METHOD].names; const REMOVE_ATTR_CLASS_APPLYING = ['asap', 'stay', 'complete']; @@ -157,7 +159,7 @@ const validateRemoveAttrClassArgs = (parsedArgs: string[]): string[] => { // https://github.com/AdguardTeam/Scriptlets/issues/133 const lastArg = restArgs.pop() as string; // https://github.com/microsoft/TypeScript/issues/30406 let applying; - // check the last parsed arg for matching possible 'applying' vale + // check the last parsed arg for matching possible 'applying' value if (REMOVE_ATTR_CLASS_APPLYING.some((el) => lastArg.includes(el))) { applying = lastArg; } else { @@ -180,6 +182,25 @@ const validateRemoveAttrClassArgs = (parsedArgs: string[]): string[] => { return validArgs; }; +/** + * Convert uBO spoof-css scriptlet selectors argument to AdGuard syntax + * + * @param parsedArgs scriptlet arguments + * @returns converted args + */ +const convertSpoofCssArgs = (parsedArgs: string[]): string[] => { + const [name, selectors, ...restArgs] = parsedArgs; + // in uBO selectors are separated by escaped commas + // so it's necessary to replace it with just commas + const selector = replaceAll( + selectors, + ESCAPED_COMMA_SEPARATOR, + COMMA_SEPARATOR, + ); + const convertedArgs = [name, selector, ...restArgs]; + return convertedArgs; +}; + /** * Converts string of UBO scriptlet rule to AdGuard scriptlet rule * @@ -206,6 +227,10 @@ export const convertUboScriptletToAdg = (rule: string): string[] => { parsedArgs = validateRemoveAttrClassArgs(parsedArgs); } + if (SPOOF_CSS_ALIASES.includes(scriptletName)) { + parsedArgs = convertSpoofCssArgs(parsedArgs); + } + const args = parsedArgs .map((arg, index) => { let outputArg = arg; diff --git a/src/scriptlets/scriptlets-list.js b/src/scriptlets/scriptlets-list.js index 4da4f759..380cb896 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 00000000..d4725add --- /dev/null +++ b/src/scriptlets/spoof-css.js @@ -0,0 +1,259 @@ +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, cssNameProperty, cssNameValue) + * ``` + * + * - `selectors` — string of comma-separated selectors to match + * - `cssPropertyName` — CSS property name + * - `cssPropertyValue` — CSS property value + * + * > Call with `debug` as `cssPropertyName` and `truthy` value as `cssPropertyValue` will trigger debugger statement + * > when `getComputedStyle()` or `getBoundingClientRect()` methods is called. + * > It may be useful for debugging but it is not allowed for prod versions of filter lists. + * + * ### 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 `height` to `100` for all elements with class `adsbygoogle` and `advert`: + * + * ```adblock + * example.org#%#//scriptlet('spoof-css', '.adsbygoogle, .advert', 'height', '100') + * ``` + * + * 3. To invoke debugger statement: + * + * ```adblock + * example.org#%#//scriptlet('spoof-css', '.adsbygoogle', 'debug', 'true') + * ``` + * + * + * @added unknown. + */ +/* eslint-enable max-len */ + +export function spoofCSS(source, selectors, cssPropertyName, cssPropertyValue) { + if (!selectors) { + return; + } + + const uboAliases = [ + 'spoof-css.js', + 'ubo-spoof-css.js', + 'ubo-spoof-css', + ]; + + /** + * 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 + * + * @param {string} cssProperty + * @returns {string} camelCase version of CSS property + */ + function convertToCamelCase(cssProperty) { + if (!cssProperty.includes('-')) { + return cssProperty; + } + const splittedProperty = cssProperty.split('-'); + const firstPart = splittedProperty[0]; + const secondPart = splittedProperty[1]; + return `${firstPart}${secondPart[0].toUpperCase()}${secondPart.slice(1)}`; + } + + const shouldDebug = !!(cssPropertyName === 'debug' && cssPropertyValue); + + const propToValueMap = new Map(); + + /** + * UBO spoof-css analog has it's own args sequence: + * (selectors, ...arguments) + * arguments contains property-name/property-value pairs, all separated by commas + * + * example.com##+js(spoof-css, a[href="x.com"]\, .ads\, .bottom, clip-path, none) + * example.com##+js(spoof-css, .ad, clip-path, none, display, block) + * example.com##+js(spoof-css, .ad, debug, 1) + */ + if (uboAliases.includes(source.name)) { + const { args } = source; + let arrayOfProperties = []; + // Check if one before last argument is 'debug' + const isDebug = args.at(-2); + if (isDebug === 'debug') { + // If it's debug, then we need to skip first (selectors) and last two arguments + arrayOfProperties = args.slice(1, -2); + } else { + // If it's not debug, then we need to skip only first (selectors) argument + arrayOfProperties = args.slice(1); + } + for (let i = 0; i < arrayOfProperties.length; i += 2) { + if (arrayOfProperties[i] === '') { + break; + } + propToValueMap.set(convertToCamelCase(arrayOfProperties[i]), arrayOfProperties[i + 1]); + } + } else if (cssPropertyName && cssPropertyValue && !shouldDebug) { + propToValueMap.set(convertToCamelCase(cssPropertyName), cssPropertyValue); + } + + const spoofStyle = (cssProperty, realCssValue) => { + return propToValueMap.has(cssProperty) + ? propToValueMap.get(cssProperty) + : realCssValue; + }; + + 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); + if (!args[0].matches(selectors)) { + return style; + } + const proxiedStyle = new Proxy(style, { + get(target, prop) { + const CSSStyleProp = target[prop]; + + if (typeof CSSStyleProp !== 'function') { + return spoofStyle(prop, CSSStyleProp || ''); + } + + if (prop !== 'getPropertyValue') { + return CSSStyleProp.bind(target); + } + + 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; + }, + 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); + if (!thisArg.matches(selectors)) { + 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/api/index.spec.js b/tests/api/index.spec.js index a8617da5..d38ca590 100644 --- a/tests/api/index.spec.js +++ b/tests/api/index.spec.js @@ -191,6 +191,15 @@ describe('Test scriptlet api methods', () => { actual: 'example.com##+js(set-session-storage-item, acceptCookies, false)', expected: "example.com#%#//scriptlet('ubo-set-session-storage-item.js', 'acceptCookies', 'false')", }, + { + actual: 'example.com##+js(spoof-css, .advert, display, block)', + expected: "example.com#%#//scriptlet('ubo-spoof-css.js', '.advert', 'display', 'block')", + }, + { + actual: 'example.com##+js(spoof-css, .adsbygoogle\\, #ads\\, .adTest, visibility, visible)', + // eslint-disable-next-line max-len + expected: "example.com#%#//scriptlet('ubo-spoof-css.js', '.adsbygoogle, #ads, .adTest', 'visibility', 'visible')", + }, ]; test.each(validTestCases)('$actual', ({ actual, expected }) => { expect(convertScriptletToAdg(actual)[0]).toStrictEqual(expected); @@ -308,6 +317,15 @@ describe('Test scriptlet api methods', () => { actual: String.raw`example.com#%#//scriptlet('adjust-setInterval', ',dataType:_', '1000', '0.02')`, expected: String.raw`example.com##+js(nano-setInterval-booster, \,dataType:_, 1000, 0.02)`, }, + { + actual: "example.com#%#//scriptlet('spoof-css', '.advert', 'display', 'block')", + expected: 'example.com##+js(spoof-css, .advert, display, block)', + }, + { + // eslint-disable-next-line max-len + actual: "example.com#%#//scriptlet('spoof-css', '.adsbygoogle, #ads, .adTest', 'visibility', 'visible')", + expected: 'example.com##+js(spoof-css, .adsbygoogle\\, #ads\\, .adTest, visibility, visible)', + }, ]; test.each(testCases)('$actual', ({ actual, expected }) => { expect(convertAdgScriptletToUbo(actual)).toStrictEqual(expected); diff --git a/tests/scriptlets/spoof-css.test.js b/tests/scriptlets/spoof-css.test.js new file mode 100644 index 00000000..98b00c1a --- /dev/null +++ b/tests/scriptlets/spoof-css.test.js @@ -0,0 +1,498 @@ +/* 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'); +}); + +// uBO has a different arguments sequence, so we need to test it separately +test('Check uBO alias', (assert) => { + const matchClassName = 'testClassUboAlias'; + + const matchElem = createElem(matchClassName); + const cssProperty = 'display: none !important;'; + const matchStyle = addStyle(`.${matchClassName} { ${cssProperty} }`); + + const cssNamePropertyFirst = 'display'; + const cssValuePropertyFirst = 'block'; + const cssNamePropertySecond = 'height'; + const cssValuePropertySecond = '233px'; + + const scriptletArgs = [ + `.${matchClassName}`, + cssNamePropertyFirst, + cssValuePropertyFirst, + cssNamePropertySecond, + cssValuePropertySecond, + ]; + runScriptlet('ubo-spoof-css', scriptletArgs); + + const elStyleDisplay = window.getComputedStyle(matchElem).display; + const elStyleHeight = window.getComputedStyle(matchElem).height; + + assert.strictEqual(elStyleDisplay, cssValuePropertyFirst, `display style is set to ${cssValuePropertyFirst}`); + assert.strictEqual(elStyleHeight, cssValuePropertySecond, `height style is set to ${cssValuePropertySecond}`); + assert.strictEqual(window.hit, 'FIRED'); + + clearGlobalProps('hit'); + matchElem.remove(); + matchStyle.remove(); +}); + +test('Check uBO alias - two elements', (assert) => { + const matchClassNameFirst = 'testClassUboFirst'; + const matchClassNameSecond = 'testClassUboSecond'; + + const matchElemFirst = createElem(matchClassNameFirst); + const matchElemSecond = createElem(matchClassNameSecond); + const cssProperty = 'display: none !important;'; + const matchStyle = addStyle(`.${matchClassNameFirst} { ${cssProperty} }`); + + const cssNamePropertyFirst = 'display'; + const cssValuePropertyFirst = 'block'; + const cssNamePropertySecond = 'height'; + const cssValuePropertySecond = '233px'; + + const scriptletArgs = [ + // eslint-disable-next-line no-useless-escape + `.${matchClassNameFirst}\, .${matchClassNameSecond}`, + cssNamePropertyFirst, + cssValuePropertyFirst, + cssNamePropertySecond, + cssValuePropertySecond, + ]; + runScriptlet('ubo-spoof-css', scriptletArgs); + + const elStyleDisplayFirst = window.getComputedStyle(matchElemFirst).display; + const elStyleHeightFirst = window.getComputedStyle(matchElemFirst).height; + + const elStyleDisplaySecond = window.getComputedStyle(matchElemSecond).display; + const elStyleHeightSecond = window.getComputedStyle(matchElemSecond).height; + + assert.strictEqual(elStyleDisplayFirst, cssValuePropertyFirst, `display style is set to ${cssValuePropertyFirst}`); + assert.strictEqual(elStyleHeightFirst, cssValuePropertySecond, `height style is set to ${cssValuePropertySecond}`); + assert.strictEqual(elStyleDisplaySecond, cssValuePropertyFirst, `display style is set to ${cssValuePropertyFirst}`); + assert.strictEqual(elStyleHeightSecond, cssValuePropertySecond, `height style is set to ${cssValuePropertySecond}`); + assert.strictEqual(window.hit, 'FIRED'); + + clearGlobalProps('hit'); + matchElemFirst.remove(); + matchElemSecond.remove(); + matchStyle.remove(); +}); + +test('Check uBO alias with debug', (assert) => { + const matchClassName = 'testClassUboDebug'; + + const matchElem = createElem(matchClassName); + const cssProperty = 'display: none !important;'; + const matchStyle = addStyle(`.${matchClassName} { ${cssProperty} }`); + + const cssNamePropertyFirst = 'display'; + const cssValuePropertyFirst = 'block'; + const cssNamePropertySecond = 'height'; + const cssValuePropertySecond = '233px'; + const debug = 'debug'; + + const scriptletArgs = [ + `.${matchClassName}`, + cssNamePropertyFirst, + cssValuePropertyFirst, + cssNamePropertySecond, + cssValuePropertySecond, + debug, + '1', + ]; + runScriptlet('ubo-spoof-css', scriptletArgs); + + const elStyleDisplay = window.getComputedStyle(matchElem).display; + const elStyleHeight = window.getComputedStyle(matchElem).height; + + assert.strictEqual(elStyleDisplay, cssValuePropertyFirst, `display style is set to ${cssValuePropertyFirst}`); + assert.strictEqual(elStyleHeight, cssValuePropertySecond, `height style is set to ${cssValuePropertySecond}`); + assert.strictEqual(window.hit, 'FIRED'); + + clearGlobalProps('hit'); + matchElem.remove(); + matchStyle.remove(); +}); + +test('One selector and one property - getComputedStyle', (assert) => { + const matchClassName = 'testClassGetComputedStyle'; + + const matchElem = createElem(matchClassName); + const cssProperty = 'display: none !important;'; + const matchStyle = addStyle(`.${matchClassName} { ${cssProperty} }`); + + const cssNameProperty = 'display'; + const cssValueProperty = 'block'; + + const scriptletArgs = [`.${matchClassName}`, cssNameProperty, cssValueProperty]; + 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('Only selector - do nothing', (assert) => { + const matchClassName = 'testClassNothing'; + + const matchElem = createElem(matchClassName); + const cssProperty = 'display: none !important;'; + const matchStyle = addStyle(`.${matchClassName} { ${cssProperty} }`); + + const scriptletArgs = [`.${matchClassName}`]; + runScriptlet(name, scriptletArgs); + + const elStyle = window.getComputedStyle(matchElem).display; + + assert.strictEqual(elStyle, 'none', 'display style is set to none'); + 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 cssNameProperty = 'display'; + const cssValueProperty = 'block'; + + const scriptletArgs = [`.${matchClassNameChange}`, cssNameProperty, cssValueProperty]; + 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 = 'testClassNotExistedProperty'; + + const matchElem = createElem(matchClassName); + const cssProperty = 'display: none !important;'; + const matchStyle = addStyle(`.${matchClassName} { ${cssProperty} }`); + + const cssNameProperty = 'display'; + const cssValueProperty = 'block'; + + const scriptletArgs = [`.${matchClassName}`, cssNameProperty, cssValueProperty]; + 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 cssNameProperty = 'clip-path'; + const cssValueProperty = 'circle(0%)'; + + const scriptletArgs = [`.${matchClassName}`, cssNameProperty, cssValueProperty]; + 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 cssNamePropertyOne = 'display'; + const cssValuePropertyOne = 'block'; + + const cssNamePropertyTwo = 'visibility'; + const cssValuePropertyTwo = 'visible'; + + const scriptletArgsOne = [`.${matchClassName}`, cssNamePropertyOne, cssValuePropertyOne]; + runScriptlet(name, scriptletArgsOne); + + const scriptletArgsTwo = [`.${matchClassName}`, cssNamePropertyTwo, cssValuePropertyTwo]; + 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('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 cssNameProperty = 'display'; + const cssValueProperty = 'block'; + + const scriptletArgs = [`.${matchClassNameFirst}, .${matchClassNameSecond}`, cssNameProperty, cssValueProperty]; + 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 cssNameProperty = 'display'; + const cssValueProperty = 'block'; + + // eslint-disable-next-line no-useless-escape + const scriptletArgs = [`.${matchClassNameFirst}\, .${matchClassNameSecond}`, cssNameProperty, cssValueProperty]; + 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('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 cssNameProperty = 'display'; + const cssValueProperty = 'block'; + + const scriptletArgs = [`.${matchClassName}`, cssNameProperty, cssValueProperty]; + 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 cssNameProperty = 'height'; + const cssValueProperty = `${EXPECTED_HEIGHT}`; + + const scriptletArgs = [`.${matchClassName}`, cssNameProperty, cssValueProperty]; + 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 cssNameProperty = 'top'; + const cssValueProperty = `${EXPECTED_TOP}`; + + const scriptletArgs = [`.${matchClassName}`, cssNameProperty, cssValueProperty]; + 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('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'); +}); diff --git a/wiki/compatibility-table.md b/wiki/compatibility-table.md index 3ccbd9a8..d6aa7fdd 100644 --- a/wiki/compatibility-table.md +++ b/wiki/compatibility-table.md @@ -36,7 +36,7 @@ | [prevent-element-src-loading](../wiki/about-scriptlets.md#prevent-element-src-loading) | | | | [prevent-eval-if](../wiki/about-scriptlets.md#prevent-eval-if) | noeval-if.js (prevent-eval-if.js) | | | [prevent-fab-3.2.0](../wiki/about-scriptlets.md#prevent-fab-3.2.0) | | | -| [prevent-fetch](../wiki/about-scriptlets.md#prevent-fetch) | no-fetch-if.js (prevent-fetch.js) | | +| [prevent-fetch](../wiki/about-scriptlets.md#prevent-fetch) | prevent-fetch.js (no-fetch-if.js) | | | [prevent-xhr](../wiki/about-scriptlets.md#prevent-xhr) | no-xhr-if.js (prevent-xhr.js) | | | [prevent-popads-net](../wiki/about-scriptlets.md#prevent-popads-net) | | | | [prevent-refresh](../wiki/about-scriptlets.md#prevent-refresh) | prevent-refresh.js (refresh-defuser.js) | | @@ -84,7 +84,7 @@ | | | trace | | | | race | | | window.name-defuser.js | | -| | spoof-css.js | | +| [spoof-css](../wiki/about-scriptlets.md#spoof-css) | spoof-css.js | | | | trusted-set-constant.js (trusted-set.js) | | | | trusted-set-cookie.js | | | | trusted-set-local-storage-item.js | | @@ -101,6 +101,7 @@ | | trusted-prune-outbound-object.js | | | | trusted-set-session-storage-item.js | | | | trusted-replace-node-text.js (trusted-rpnt.js, replace-node-text.js, rpnt.js) | | +| | trusted-replace-argument.js | | ## Redirects compatibility table