cypress-io / cypress

Fast, easy and reliable testing for anything that runs in a browser.
https://cypress.io
MIT License
46.85k stars 3.17k forks source link

XML Parsing Error when attempting to visit XHTML pages #15962

Closed vodlogic closed 1 year ago

vodlogic commented 3 years ago

Current behaviour

As of version 7.x, it is no longer possible to test web apps that use XHTML (support added in #15741). It seems that the browser is now trying to parse the parent cypress window for valid XHTML in addition to the app itself and falls over as it contains Javascript code that is not escaped e.g. && is not &&

The testing works fine in 6.8.0 with PR #15741 merged in. I'm not sure if the problem is a fundamental change in how the test runner now works in cypress 7.0 or whether the Javascript that its now tripping up on is a new feature added in that release.

XML Parsing Error: not well-formed
Location: http://localhost:5000/index.xhtml?sid=4164
Line Number 5, Column 366:

<head> <script type='text/javascript'> document.domain = 'localhost'; !function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}({"./injection/index.js":function(e,t,n){"use strict";n.r(t);const r=window.Cypress=parent.Cypress;if(!r)throw new Error("Something went terribly wrong and we cannot proceed. We expected to find the global Cypress in the parent window but it is missing!. This should never happen and likely is a bug. Please open an issue!");const o=(()=>{let e,t,n,r;const o=()=>{e=!1,t=!1,n=[],r={}},i=(e,t=[])=>"function"==typeof e?e.apply(window,t):window.eval(e);return o(),{wrap:()=>{const o={setTimeout:window.setTimeout,setInterval:window.setInterval,requestAnimationFrame:window.requestAnimationFrame,clearTimeout:window.clearTimeout,clearInterval:window.clearInterval},a=(e,t)=>o[e].apply(window,t),s=e=>n=>(t&&(r[n]=!0),a(e,[n])),u=t=>(...r)=>{let o,[s,u,...l]=r;return o=a(t,[(...r)=>{if(!e)return i(s,r);n.push({timerId:o,fnOrCode:s,params:r,type:t})},u,...l]),o};window.setTimeout=u("setTimeout"),window.setInterval=u("setInterval"),window.requestAnimationFrame=u("requestAnimationFrame"),window.clearTimeout=s("clearTimeout"),window.clearInterval=s("clearInterval")},reset:o,pause:a=>{e=Boolean(a),e||(t=!0,n.forEach(e=>{const{timerId:t,type:n,fnOrCode:o,params:a}=e;"setInterval"===n&&r[t]||i(o,a)}),o())}}})();r.removeAllListeners("app:timers:reset"),r.removeAllListeners("app:timers:pause"),r.on("app:timers:reset",o.reset),r.on("app:timers:pause",o.pause),o.wrap(),r.action("app:window:before:load",window)},0:function(e,t,n){e.exports=n("./injection/index.js")}}); </script>
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------^

The "prettified" version of the code where it falls over is as follows

<head>
    <script type='text/javascript'> document.domain = 'localhost';
    !function (e) {
        var t = {};

        function n(r) {
            if (t[r]) return t[r].exports;
            var o = t[r] = {i: r, l: !1, exports: {}};
            return e[r].call(o.exports, o, o.exports, n), o.l = !0, o.exports
        }

        n.m = e, n.c = t, n.d = function (e, t, r) {
            n.o(e, t) || Object.defineProperty(e, t, {enumerable: !0, get: r})
        }, n.r = function (e) {
            "undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, {value: "Module"}), Object.defineProperty(e, "__esModule", {value: !0})
        }, n.t = function (e, t) {
            if (1 & t && (e = n(e)), 8 & t) return e;
            if (4 & t && "object" == typeof e && e && e.__esModule) return e;
            var r = Object.create(null);
            if (n.r(r), Object.defineProperty(r, "default", {
                enumerable: !0,
                value: e
            }), 2 & t && "string" != typeof e) for (var o in e) n.d(r, o, function (t) {
                return e[t]
            }.bind(null, o));
            return r
        }, n.n = function (e) {
            var t = e && e.__esModule ? function () {
                return e.default
            } : function () {
                return e
            };
            return n.d(t, "a", t), t
        }, n.o = function (e, t) {
            return Object.prototype.hasOwnProperty.call(e, t)
        }, n.p = "", n(n.s = 0)
    }({
        "./injection/index.js": function (e, t, n) {
            "use strict";
            n.r(t);
            const r = window.Cypress = parent.Cypress;
            if (!r) throw new Error("Something went terribly wrong and we cannot proceed. We expected to find the global Cypress in the parent window but it is missing!. This should never happen and likely is a bug. Please open an issue!");
            const o = (() => {
                let e, t, n, r;
                const o = () => {
                    e = !1, t = !1, n = [], r = {}
                }, i = (e, t = []) => "function" == typeof e ? e.apply(window, t) : window.eval(e);
                return o(), {
                    wrap: () => {
                        const o = {
                                setTimeout: window.setTimeout,
                                setInterval: window.setInterval,
                                requestAnimationFrame: window.requestAnimationFrame,
                                clearTimeout: window.clearTimeout,
                                clearInterval: window.clearInterval
                            }, a = (e, t) => o[e].apply(window, t), s = e => n => (t && (r[n] = !0), a(e, [n])),
                            u = t => (...r) => {
                                let o, [s, u, ...l] = r;
                                return o = a(t, [(...r) => {
                                    if (!e) return i(s, r);
                                    n.push({timerId: o, fnOrCode: s, params: r, type: t})
                                }, u, ...l]), o
                            };
                        window.setTimeout = u("setTimeout"), window.setInterval = u("setInterval"), window.requestAnimationFrame = u("requestAnimationFrame"), window.clearTimeout = s("clearTimeout"), window.clearInterval = s("clearInterval")
                    }, reset: o, pause: a => {
                        e = Boolean(a), e || (t = !0, n.forEach(e => {
                            const {timerId: t, type: n, fnOrCode: o, params: a} = e;
                            "setInterval" === n && r[t] || i(o, a)
                        }), o())
                    }
                }
            })();
            r.removeAllListeners("app:timers:reset"), r.removeAllListeners("app:timers:pause"), r.on("app:timers:reset", o.reset), r.on("app:timers:pause", o.pause), o.wrap(), r.action("app:window:before:load", window)
        }, 0: function (e, t, n) {
            e.exports = n("./injection/index.js")
        }
    }); </script>

Versions

Affects 7.x

vodlogic commented 3 years ago

Manually replacing all & with &amp; in packages/runner/dist/injection.js makes the tests execute again against XHTML pages.

It seems that we would need to generate an XML escaped version of this Javascript in the dist folder aswell and use that as the content variable in packages/proxy/lib/http/util/inject.ts when visiting XHTML pages.

Would be good if you could advise if there is a better way of fixing this problem and any thoughts on what changes in 7.0 have contributed to this problem so that it can be better understood.

bahmutov commented 3 years ago

@vodlogic can you create a tiny repo (maybe by cloning the cypress-test-tiny) to show the problem?

vodlogic commented 3 years ago

Sure i have setup a test here https://github.com/vodlogic/cypress-test-tiny/tree/xhtml-test

To run it, just do

npm install
npm run serve
npm run cypress:open

If port 5000 is already used then sirv will start the webserver on a new random port, so just replace that in the test spec file

Firefox shows the error in detail as described above. Chrome and Electron obfuscate the error a little and complain about iframe cross origin problem, but the underlying error is the fact that the test runner embedded JS code isnt XML escaped.

bahmutov commented 3 years ago

great!

github-actions[bot] commented 3 years ago

Internal Jira issue: TR-761

vodlogic commented 3 years ago

You guys probably already have a handle on this but for my own curiosity I found that injection of code Into the AUT test runner is new functionality introduced in 7.0.0.

The injected code begins here packages/runner/injection/index.js and it’s tripping up on the JS code in packages/runner/injection/timers.js

As this code is injected into the AUT we need to ensure that when the test runner loads XHTML sites it injects XML escaped versions of the minified dist/injection.js

JohnLukeBentley commented 2 years ago

To make script safe in XHTML use a CDATA escape as follows.

<script>/*<![CDATA[*/
  if (true && true) {
    console.log("Very true");
  }
/*]]>*/</script>  

Further info: https://www.w3.org/TR/html-polyglot/#safe-CDATA-content

JohnLukeBentley commented 2 years ago

So my untested assumption is that changing https://github.com/cypress-io/cypress/blob/develop/packages/proxy/lib/http/util/inject.ts to the following will solve the problem

import { oneLine } from 'common-tags'
import { getRunnerInjectionContents } from '@packages/resolve-dist'

export function partial (domain) {
  return oneLine`
    <script type='text/javascript'>/*<![CDATA[*/
      document.domain = '${domain}';
    /*]]>*/</script>
  `
}

export function full (domain) {
  return getRunnerInjectionContents().then((contents) => {
    return oneLine`
      <script type='text/javascript'>/*<![CDATA[*/
        document.domain = '${domain}';
        ${contents}
      /*]]>*/</script>
    `
  })
}
cypress-app-bot commented 1 year ago

This issue has not had any activity in 180 days. Cypress evolves quickly and the reported behavior should be tested on the latest version of Cypress to verify the behavior is still occurring. It will be closed in 14 days if no updates are provided.

cypress-app-bot commented 1 year ago

This issue has been closed due to inactivity.