diff --git a/package.json b/package.json index 2f5e72ad3..aef2545cb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "testcafe-hammerhead", "description": "A powerful web-proxy used as a core for the TestCafe testing framework (https://github.com/DevExpress/testcafe).", - "version": "9.1.0", + "version": "9.2.0", "homepage": "https://github.com/DevExpress/testcafe-hammerhead", "bugs": { "url": "https://github.com/DevExpress/testcafe-hammerhead/issues" diff --git a/src/client/index.js b/src/client/index.js index 5c6d0b516..c5876dbcd 100644 --- a/src/client/index.js +++ b/src/client/index.js @@ -17,6 +17,8 @@ import * as styleUtils from './utils/style'; import trim from '../utils/string-trim'; import { getProxyUrl } from './utils/url'; import { processScript } from '../processing/script'; +import { SCRIPT_PROCESSING_START_COMMENT, SCRIPT_PROCESSING_END_HEADER_COMMENT, SCRIPT_PROCESSING_END_COMMENT } from '../processing/script/header'; +import { STYLESHEET_PROCESSING_START_COMMENT, STYLESHEET_PROCESSING_END_COMMENT } from '../processing/style'; import isJQueryObj from './utils/is-jquery-object'; import extend from './utils/extend'; @@ -42,6 +44,14 @@ class Hammerhead { fetchSend: this.sandbox.fetch.FETCH_REQUEST_SEND_EVENT }; + this.PROCESSING_COMMENTS = { + stylesheetStart: STYLESHEET_PROCESSING_START_COMMENT, + stylesheetEnd: STYLESHEET_PROCESSING_END_COMMENT, + scriptStart: SCRIPT_PROCESSING_START_COMMENT, + scriptEndHeader: SCRIPT_PROCESSING_END_HEADER_COMMENT, + scriptEnd: SCRIPT_PROCESSING_END_COMMENT + }; + this.EventEmitter = EventEmitter; // Methods diff --git a/src/processing/script/header.js b/src/processing/script/header.js index 6b36dcac6..956f018f3 100644 --- a/src/processing/script/header.js +++ b/src/processing/script/header.js @@ -7,11 +7,13 @@ import reEscape from '../../utils/regexp-escape'; import INTERNAL_PROPS from '../../processing/dom/internal-properties'; import INSTRUCTION from './instruction'; -const PREFIX = '/*hammerhead|script-processing-header|start*/'; -const POSTFIX = '/*hammerhead|script-processing-header|end*/'; -export const HEADER = [ - PREFIX, +export const SCRIPT_PROCESSING_START_COMMENT = '/*hammerhead|script|start*/'; +export const SCRIPT_PROCESSING_END_COMMENT = '/*hammerhead|script|end*/'; +export const SCRIPT_PROCESSING_END_HEADER_COMMENT = '/*hammerhead|script|processing-header-end*/'; + +const HEADER = [ + SCRIPT_PROCESSING_START_COMMENT, 'var __w$= typeof window!=="undefined"&&window;', `__w$ && __w$["${INTERNAL_PROPS.processDomMethodName}"] && __w$["${INTERNAL_PROPS.processDomMethodName}"]();`, `var ${ INSTRUCTION.getLocation }=__w$?__w$.${ INSTRUCTION.getLocation }:function(l){return l},`, @@ -21,14 +23,21 @@ export const HEADER = [ `${ INSTRUCTION.callMethod }=__w$?__w$.${ INSTRUCTION.callMethod }:function(o,p,a){return o[p].apply(o,a)},`, `${ INSTRUCTION.getEval }=__w$?__w$.${ INSTRUCTION.getEval }:function(e){return e},`, `${ INSTRUCTION.processScript }=__w$?__w$.${ INSTRUCTION.processScript }:function(s){return s};`, - POSTFIX, + SCRIPT_PROCESSING_END_HEADER_COMMENT, '\n' ].join(''); // NOTE: IE removes trailing newlines in script.textContent, // so a trailing newline in RegExp is optional -const HEADER_RE = new RegExp(`${reEscape(PREFIX)}[\\S\\s]+?${reEscape(POSTFIX)}\n?`, 'i'); +const HEADER_RE = new RegExp(`${reEscape(SCRIPT_PROCESSING_START_COMMENT)}[\\S\\s]+?${reEscape(SCRIPT_PROCESSING_END_HEADER_COMMENT)}\n?`, 'i'); +const PROCESSING_END_COMMENT_RE = new RegExp(`\n?${ reEscape(SCRIPT_PROCESSING_END_COMMENT) }\\s*$`, 'gi'); export function remove (code) { - return code.replace(HEADER_RE, ''); + return code + .replace(HEADER_RE, '') + .replace(PROCESSING_END_COMMENT_RE, ''); +} + +export function add (code) { + return HEADER + code + '\n' + SCRIPT_PROCESSING_END_COMMENT; } diff --git a/src/processing/script/index.js b/src/processing/script/index.js index 864cccf3d..153e49afa 100644 --- a/src/processing/script/index.js +++ b/src/processing/script/index.js @@ -5,7 +5,7 @@ import transform from './transform'; import INSTRUCTION from './instruction'; -import { HEADER, remove as removeHeader } from './header'; +import { add as addHeader, remove as removeHeader } from './header'; import { parse } from './tools/acorn'; import { generate, Syntax } from './tools/esotope'; import reEscape from '../../utils/regexp-escape'; @@ -50,7 +50,7 @@ function preprocess (code) { function postprocess (processed, withHeader, bom) { if (withHeader) - processed = HEADER + processed; + processed = addHeader(processed); return bom ? bom + processed : processed; } diff --git a/src/processing/style.js b/src/processing/style.js index f0928ebd5..85027e796 100644 --- a/src/processing/style.js +++ b/src/processing/style.js @@ -4,51 +4,55 @@ // ------------------------------------------------------------- /* eslint hammerhead/proto-methods: 2 */ +import reEscape from '../utils/regexp-escape'; import INTERNAL_ATTRS from '../processing/dom/internal-attributes'; -const SOURCE_MAP_REG_EX = /#\s*sourceMappingURL\s*=\s*[^\s]+(\s|\*\/)/i; -const CSS_URL_PROPERTY_VALUE_PATTERN = /(url\s*\(\s*)(?:(')([^\s']*)(')|(")([^\s"]*)(")|([^\s\)]*))(\s*\))|(@import\s+)(?:(')([^\s']*)(')|(")([^\s"]*)("))/g; +const SOURCE_MAP_RE = /#\s*sourceMappingURL\s*=\s*[^\s]+(\s|\*\/)/i; +const CSS_URL_PROPERTY_VALUE_PATTERN = /(url\s*\(\s*)(?:(')([^\s']*)(')|(")([^\s"]*)(")|([^\s\)]*))(\s*\))|(@import\s+)(?:(')([^\s']*)(')|(")([^\s"]*)("))/g; +const STYLESHEET_PROCESSING_START_COMMENT = '/*hammerhead|stylesheet|start*/'; +const STYLESHEET_PROCESSING_END_COMMENT = '/*hammerhead|stylesheet|end*/'; +const HOVER_PSEUDO_CLASS_RE = /\s*:\s*hover(\W)/gi; +const PSEUDO_CLASS_RE = new RegExp(`\\[${ INTERNAL_ATTRS.hoverPseudoClass }\\](\\W)`, 'ig'); +const IS_STYLE_SHEET_PROCESSED_RE = new RegExp(`^\\s*${ reEscape(STYLESHEET_PROCESSING_START_COMMENT) }`, 'gi'); +const STYLESHEET_PROCESSING_COMMENTS_RE = new RegExp(`^\\s*${ reEscape(STYLESHEET_PROCESSING_START_COMMENT) }\n?|` + + `\n?${ reEscape(STYLESHEET_PROCESSING_END_COMMENT) }\\s*$`, 'gi'); class StyleProcessor { constructor () { - this.IS_STYLESHEET_PROCESSED_COMMENT = '/* stylesheet processed via hammerhead */'; + this.STYLESHEET_TEXT_START_COMMENT = STYLESHEET_PROCESSING_START_COMMENT; + this.STYLESHEET_TEXT_END_COMMENT = STYLESHEET_PROCESSING_END_COMMENT; } process (css, urlReplacer, isStylesheetTable) { - var isStyleSheetProcessingRegEx = new RegExp('^\\s*' + - this.IS_STYLESHEET_PROCESSED_COMMENT.replace(/\/|\*/g, '\\$&')); - var isStylesheetProcessed = isStyleSheetProcessingRegEx.test(css); + if (typeof css !== 'string' || IS_STYLE_SHEET_PROCESSED_RE.test(css)) + return css; - if (typeof css === 'string' && !isStylesheetProcessed) { - var prefix = isStylesheetTable ? this.IS_STYLESHEET_PROCESSED_COMMENT + '\n' : ''; + var prefix = isStylesheetTable ? STYLESHEET_PROCESSING_START_COMMENT + '\n' : ''; + var postfix = isStylesheetTable ? '\n' + STYLESHEET_PROCESSING_END_COMMENT : ''; - // NOTE: Replace the :hover pseudo-class. - css = css.replace(/\s*:\s*hover(\W)/gi, '[' + INTERNAL_ATTRS.hoverPseudoClass + ']$1'); + // NOTE: Replace the :hover pseudo-class. + css = css.replace(HOVER_PSEUDO_CLASS_RE, '[' + INTERNAL_ATTRS.hoverPseudoClass + ']$1'); - // NOTE: Remove the ‘source map’ directive. - css = css.replace(SOURCE_MAP_REG_EX, '$1'); + // NOTE: Remove the ‘source map’ directive. + css = css.replace(SOURCE_MAP_RE, '$1'); - // NOTE: Replace URLs in CSS rules with proxy URLs. - return prefix + this._replaceStylsheetUrls(css, urlReplacer); - } - - return css; + // NOTE: Replace URLs in CSS rules with proxy URLs. + return prefix + this._replaceStylsheetUrls(css, urlReplacer) + postfix; } cleanUp (css, parseProxyUrl) { - if (typeof css === 'string') { - css = css - .replace(new RegExp('\\[' + INTERNAL_ATTRS.hoverPseudoClass + '\\](\\W)', 'ig'), ':hover$1') - .replace(new RegExp('^\\s*' + this.IS_STYLESHEET_PROCESSED_COMMENT.replace(/\/|\*/g, '\\$&') + '\n?'), ''); + if (typeof css !== 'string') + return css; - return this._replaceStylsheetUrls(css, url => { - var parsedProxyUrl = parseProxyUrl(url); + css = css + .replace(PSEUDO_CLASS_RE, ':hover$1') + .replace(STYLESHEET_PROCESSING_COMMENTS_RE, ''); - return parsedProxyUrl ? parsedProxyUrl.destUrl : url; - }); - } + return this._replaceStylsheetUrls(css, url => { + var parsedProxyUrl = parseProxyUrl(url); - return css; + return parsedProxyUrl ? parsedProxyUrl.destUrl : url; + }); } _replaceStylsheetUrls (css, processor) { diff --git a/test/client/fixtures/sandbox/node/document-write-test.js b/test/client/fixtures/sandbox/node/document-write-test.js index e6a3f4638..b25713379 100644 --- a/test/client/fixtures/sandbox/node/document-write-test.js +++ b/test/client/fixtures/sandbox/node/document-write-test.js @@ -141,8 +141,9 @@ test('write style', function () { strictEqual(getElems(iframeForWrite, 'style').length, getElems(iframeForNativeWrite, 'style').length); strictEqual(innerHTML(getElems(iframeForWrite, 'style')[0], true), innerHTML(getElems(iframeForNativeWrite, 'style')[0])); - strictEqual(innerHTML(getElems(iframeForWrite, 'style')[0]), styleProcessor.IS_STYLESHEET_PROCESSED_COMMENT + - '\n\ndiv {}\n'); + strictEqual(innerHTML(getElems(iframeForWrite, 'style')[0]), styleProcessor.STYLESHEET_TEXT_START_COMMENT + + '\n\ndiv {}\n\n' + + styleProcessor.STYLESHEET_TEXT_END_COMMENT); strictEqual(innerHTML(getElems(iframeForWrite, 'style')[1], true), innerHTML(getElems(iframeForNativeWrite, 'style')[1])); strictEqual(innerHTML(getElems(iframeForWrite, 'style')[1]), ''); diff --git a/test/client/fixtures/sandbox/node/dom-processor-test.js b/test/client/fixtures/sandbox/node/dom-processor-test.js index 226ee418d..94fced6dc 100644 --- a/test/client/fixtures/sandbox/node/dom-processor-test.js +++ b/test/client/fixtures/sandbox/node/dom-processor-test.js @@ -335,8 +335,8 @@ test('stylesheet after innerHTML', function () { nativeMethods.appendChild.call(document.body, style); var check = function (cssText) { - strictEqual(cssText.indexOf(styleProcessor.IS_STYLESHEET_PROCESSED_COMMENT), 0); - strictEqual(cssText.indexOf(styleProcessor.IS_STYLESHEET_PROCESSED_COMMENT, 1), -1); + strictEqual(cssText.indexOf(styleProcessor.STYLESHEET_TEXT_START_COMMENT), 0); + strictEqual(cssText.indexOf(styleProcessor.STYLESHEET_TEXT_START_COMMENT, 1), -1); strictEqual(cssText.replace(/^[\s\S]+url\(([\s\S]+)\)[\s\S]+$/, '$1'), urlUtils.getProxyUrl('http://domain.com')); }; diff --git a/test/server/data/page/expected.html b/test/server/data/page/expected.html index 78e1734a8..d4f9efd7f 100644 --- a/test/server/data/page/expected.html +++ b/test/server/data/page/expected.html @@ -9,7 +9,7 @@ - +/*hammerhead|stylesheet|end*/ +/*hammerhead|script|start*/var __w$= typeof window!=="undefined"&&window;__w$ && __w$["hammerhead|process-dom-method"] && __w$["hammerhead|process-dom-method"]();var __get$Loc=__w$?__w$.__get$Loc:function(l){return l},__set$Loc=__w$?__w$.__set$Loc:function(l,v){return l = v},__set$=__w$?__w$.__set$:function(o,p,v){return o[p] = v},__get$=__w$?__w$.__get$:function(o,p){return o[p]},__call$=__w$?__w$.__call$:function(o,p,a){return o[p].apply(o,a)},__get$Eval=__w$?__w$.__get$Eval:function(e){return e},__proc$Script=__w$?__w$.__proc$Script:function(s){return s};/*hammerhead|script|processing-header-end*/ +__set$(window,"location",'test'); (function(){return __set$Loc(location,'test')||(location='test');}.apply(this)); __set$(document,"location",'test'); __set$(document,"domain",'test'); __set$(document,"cookie",'test'); +/*hammerhead|script|end*///]]> +/*hammerhead|script|end*/