diff --git a/packages/driver/src/cy/commands/navigation.coffee b/packages/driver/src/cy/commands/navigation.coffee index 3658b8453f89..620a8fbf3478 100644 --- a/packages/driver/src/cy/commands/navigation.coffee +++ b/packages/driver/src/cy/commands/navigation.coffee @@ -577,7 +577,7 @@ module.exports = (Commands, Cypress, cy, state, config) -> $utils.iframeSrc($autIframe, url) - onLoad = ({runOnLoadCallback}) -> + onLoad = ({runOnLoadCallback, totalTime}) -> ## reset window on load win = state("window") @@ -585,7 +585,11 @@ module.exports = (Commands, Cypress, cy, state, config) -> if runOnLoadCallback isnt false options.onLoad?.call(runnable.ctx, win) - options._log.set({url: url}) if options._log + if options._log + options._log.set({ + url + totalTime + }) return Promise.resolve(win) @@ -680,7 +684,8 @@ module.exports = (Commands, Cypress, cy, state, config) -> url = $Location.fullyQualifyUrl(url) changeIframeSrc(url, "window:load") - .then(onLoad) + .then -> + onLoad(resp) else ## if we've already visited a new superDomain ## then die else we'd be in a terrible endless loop @@ -784,8 +789,6 @@ module.exports = (Commands, Cypress, cy, state, config) -> go() visit() - .then -> - state("window") .timeout(options.timeout, "visit") .catch Promise.TimeoutError, (err) => timedOutWaitingForPageLoad(options.timeout, options._log) diff --git a/packages/network/lib/agent.ts b/packages/network/lib/agent.ts index 8c841ef5c11e..ebc099762df9 100644 --- a/packages/network/lib/agent.ts +++ b/packages/network/lib/agent.ts @@ -138,7 +138,18 @@ export class CombinedAgent { } // called by Node.js whenever a new request is made internally - addRequest(req: http.ClientRequest, options: http.RequestOptions) { + addRequest(req: http.ClientRequest, options: http.RequestOptions, port?: number, localAddress?: string) { + // Legacy API: addRequest(req, host, port, localAddress) + // https://github.com/nodejs/node/blob/cb68c04ce1bc4534b2d92bc7319c6ff6dda0180d/lib/_http_agent.js#L148-L155 + if (typeof options === 'string') { + // @ts-ignore + options = { + host: options, + port: port!, + localAddress + } + } + const isHttps = isRequestHttps(options) if (!options.href) { @@ -156,11 +167,13 @@ export class CombinedAgent { } } - debug(`addRequest called for ${options.href}`) + debug('addRequest called %o', { isHttps, ..._.pick(options, 'href') }) return getFirstWorkingFamily(options, this.familyCache, (family: net.family) => { options.family = family + debug('got family %o', _.pick(options, 'family', 'href')) + if (isHttps) { return this.httpsAgent.addRequest(req, options) } @@ -180,22 +193,22 @@ class HttpAgent extends http.Agent { this.httpsAgent = new https.Agent({ keepAlive: true }) } - createSocket (req: http.ClientRequest, options: http.RequestOptions, cb: http.SocketCallback) { + addRequest (req: http.ClientRequest, options: http.RequestOptions) { if (process.env.HTTP_PROXY) { const proxy = getProxyForUrl(options.href) if (proxy) { options.proxy = proxy - return this._createProxiedSocket(req, options, cb) + return this._addProxiedRequest(req, options) } } - super.createSocket(req, options, cb) + super.addRequest(req, options) } - _createProxiedSocket (req: http.ClientRequest, options: RequestOptionsWithProxy, cb: http.SocketCallback) { - debug(`Creating proxied socket for ${options.href} through ${options.proxy}`) + _addProxiedRequest (req: http.ClientRequest, options: RequestOptionsWithProxy) { + debug(`Creating proxied request for ${options.href} through ${options.proxy}`) const proxy = url.parse(options.proxy) @@ -226,7 +239,7 @@ class HttpAgent extends http.Agent { return this.httpsAgent.addRequest(req, options) } - super.createSocket(req, options, cb) + super.addRequest(req, options) } } diff --git a/packages/network/lib/connect.ts b/packages/network/lib/connect.ts index 4875314422c9..0d580ef1bf3a 100644 --- a/packages/network/lib/connect.ts +++ b/packages/network/lib/connect.ts @@ -22,6 +22,8 @@ export function byPortAndAddress (port: number, address: net.Address) { } export function getAddress (port: number, hostname: string) { + debug('beginning getAddress %o', { hostname, port }) + const fn = byPortAndAddress.bind({}, port) // promisify at the very last second which enables us to @@ -35,9 +37,13 @@ export function getAddress (port: number, hostname: string) { // @ts-ignore return lookupAsync(hostname, { all: true }) .then((addresses: net.Address[]) => { + debug('got addresses %o', { hostname, port, addresses }) // convert to an array if string return Array.prototype.concat.call(addresses).map(fn) }) + .tapCatch((err) => { + debug('error getting address', { hostname, port, err }) + }) .any() } diff --git a/packages/network/test/integration/connect_spec.ts b/packages/network/test/integration/connect_spec.ts new file mode 100644 index 000000000000..cd85b5c6dbf6 --- /dev/null +++ b/packages/network/test/integration/connect_spec.ts @@ -0,0 +1,53 @@ +import _ from 'lodash' +import Bluebird from 'bluebird' +import chai from 'chai' +import net from 'net' +import sinonChai from 'sinon-chai' +import * as connect from '../../lib/connect' + +const expect = chai.expect +chai.use(sinonChai) + +describe('lib/connect', function() { + context('.getAddress', function() { + it('resolves localhost on 127.0.0.1 immediately', function() { + this.timeout(50) + + const server = net.createServer(_.partialRight(_.invoke, 'close')) + + return Bluebird.fromCallback(cb => { + server.listen(0, '127.0.0.1', cb) + }) + .then(() => { + return connect.getAddress(server.address().port, 'localhost') + }) + .then((address) => { + expect(address).to.deep.eq({ + family: 4, + address: '127.0.0.1' + }) + }) + }) + + // Error: listen EADDRNOTAVAIL ::1 + // TODO: add an ipv6 lo if to the docker container + it.skip('resolves localhost on ::1 immediately', function() { + this.timeout(50) + + const server = net.createServer(_.partialRight(_.invoke, 'close')) + + return Bluebird.fromCallback(cb => { + server.listen(0, '::1', cb) + }) + .then(() => { + return connect.getAddress(server.address().port, 'localhost') + }) + .then((address) => { + expect(address).to.deep.eq({ + family: 6, + address: '::1' + }) + }) + }) + }) +}) diff --git a/packages/network/test/mocha.opts b/packages/network/test/mocha.opts index 3622a281707e..7ef9f2476490 100644 --- a/packages/network/test/mocha.opts +++ b/packages/network/test/mocha.opts @@ -1,4 +1,5 @@ test/unit +test/integration --compilers ts:@packages/ts/register --timeout 10000 --recursive diff --git a/packages/network/test/unit/agent_spec.ts b/packages/network/test/unit/agent_spec.ts index 687c53d1f398..5fe314b3da25 100644 --- a/packages/network/test/unit/agent_spec.ts +++ b/packages/network/test/unit/agent_spec.ts @@ -1,6 +1,5 @@ import Bluebird from 'bluebird' import chai from 'chai' -import { EventEmitter } from 'events' import http from 'http' import https from 'https' import net from 'net' diff --git a/packages/server/__snapshots__/6_visit_spec.coffee.js b/packages/server/__snapshots__/6_visit_spec.coffee.js index 30aa55b179e5..df8c81e561e3 100644 --- a/packages/server/__snapshots__/6_visit_spec.coffee.js +++ b/packages/server/__snapshots__/6_visit_spec.coffee.js @@ -819,3 +819,123 @@ Error: ESOCKETTIMEDOUT ` + +exports['e2e visit resolves visits quickly in chrome (headed) 1'] = ` + +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (fast_visit_spec.coffee) │ + │ Searched: cypress/integration/fast_visit_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: fast_visit_spec.coffee... (1 of 1) + +Warning: Cypress can only record videos when using the built in 'electron' browser. + +You have set the browser to: 'chrome' + +A video will not be recorded when using this browser. + + + ✓ always finishes in less than XX:XX on localhost with connection: close + ✓ always finishes in less than XX:XX on localhost with connection: keep-alive + + 2 passing + + + (Results) + + ┌──────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 2 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: false │ + │ Duration: X seconds │ + │ Spec Ran: fast_visit_spec.coffee │ + └──────────────────────────────────────┘ + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ fast_visit_spec.coffee XX:XX 2 2 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! XX:XX 2 2 - - - + + +` + +exports['e2e visit resolves visits quickly in electron (headless) 1'] = ` + +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (fast_visit_spec.coffee) │ + │ Searched: cypress/integration/fast_visit_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: fast_visit_spec.coffee... (1 of 1) + + + ✓ always finishes in less than XX:XX on localhost with connection: close + ✓ always finishes in less than XX:XX on localhost with connection: keep-alive + + 2 passing + + + (Results) + + ┌──────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 2 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: fast_visit_spec.coffee │ + └──────────────────────────────────────┘ + + + (Video) + + - Started processing: Compressing to 32 CRF + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ fast_visit_spec.coffee XX:XX 2 2 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! XX:XX 2 2 - - - + + +` diff --git a/packages/server/index.js b/packages/server/index.js index 283e80622b28..5eb0509688fd 100644 --- a/packages/server/index.js +++ b/packages/server/index.js @@ -1,12 +1,6 @@ // override tty if we're being forced to require('./lib/util/tty').override() -// if we are running in electron -// we must hack around busted timers -if (process.versions.electron) { - require('./timers/parent').fix() -} - if (process.env.CY_NET_PROFILE && process.env.CYPRESS_ENV) { const netProfiler = require('./lib/util/net_profiler')() diff --git a/packages/server/lib/controllers/proxy.coffee b/packages/server/lib/controllers/proxy.coffee index c355878686f4..7e2d6c972aaa 100644 --- a/packages/server/lib/controllers/proxy.coffee +++ b/packages/server/lib/controllers/proxy.coffee @@ -348,7 +348,7 @@ module.exports = { return if res.headersSent ## omit problematic headers - headers = _.omit incomingRes.headers, "set-cookie", "x-frame-options", "content-length", "content-security-policy" + headers = _.omit incomingRes.headers, "set-cookie", "x-frame-options", "content-length", "content-security-policy", "connection" ## do not cache when we inject content into responses ## later on we should switch to an etag system so we dont diff --git a/packages/server/lib/plugins/child/index.js b/packages/server/lib/plugins/child/index.js index 6de9649b0b4d..34b755b7e6f1 100644 --- a/packages/server/lib/plugins/child/index.js +++ b/packages/server/lib/plugins/child/index.js @@ -1,8 +1,3 @@ -// if we are running in electron -// we must hack around busted timers -if (process.versions.electron) { - require('../../timers/parent').fix() -} require('graceful-fs').gracefulify(require('fs')) require('@packages/coffee/register') require && require.extensions && delete require.extensions['.litcoffee'] diff --git a/packages/server/lib/server.coffee b/packages/server/lib/server.coffee index 760686cff864..99744679ceba 100644 --- a/packages/server/lib/server.coffee +++ b/packages/server/lib/server.coffee @@ -317,6 +317,8 @@ class Server options }) + startTime = new Date() + ## if we have an existing url resolver ## in flight then cancel it if @_urlResolver @@ -452,6 +454,8 @@ class Server ## connection hangs before receiving a body. debug("resolve:url response ended, setting buffer %o", { newUrl, details }) + details.totalTime = new Date() - startTime + ## TODO: think about moving this logic back into the ## frontend so that the driver can be in control of ## when the server should cache the request buffer diff --git a/packages/server/test/e2e/6_visit_spec.coffee b/packages/server/test/e2e/6_visit_spec.coffee index d9ddb00de35d..1043ab617f13 100644 --- a/packages/server/test/e2e/6_visit_spec.coffee +++ b/packages/server/test/e2e/6_visit_spec.coffee @@ -150,3 +150,39 @@ describe "e2e visit", -> snapshot: true expectedExitCode: 2 }) + + ## https://github.com/cypress-io/cypress/issues/4313 + context "resolves visits quickly", -> + e2e.setup({ + servers: { + port: 3434 + onServer: (app) -> + app.get '/keepalive', (req, res) -> + res + .type('html').send('hi') + + app.get '/close', (req, res) -> + res + .set('connection', 'close') + .type('html').send('hi') + } + settings: { + baseUrl: 'http://localhost:3434' + } + }) + + it "in chrome (headed)", -> + e2e.exec(@, { + spec: "fast_visit_spec.coffee" + snapshot: true + expectedExitCode: 0 + browser: 'chrome' + }) + + it "in electron (headless)", -> + e2e.exec(@, { + spec: "fast_visit_spec.coffee" + snapshot: true + expectedExitCode: 0 + browser: 'electron' + }) diff --git a/packages/server/test/integration/server_spec.coffee b/packages/server/test/integration/server_spec.coffee index 70aa890fb947..b97b1a7128ab 100644 --- a/packages/server/test/integration/server_spec.coffee +++ b/packages/server/test/integration/server_spec.coffee @@ -13,6 +13,11 @@ Fixtures = require("#{root}test/support/helpers/fixtures") s3StaticHtmlUrl = "https://s3.amazonaws.com/internal-test-runner-assets.cypress.io/index.html" +expectToEqDetails = (actual, expected) -> + actual = _.omit(actual, 'totalTime') + + expect(actual).to.deep.eq(expected) + describe "Server", -> beforeEach -> sinon.stub(Server.prototype, "reset") @@ -116,7 +121,7 @@ describe "Server", -> it "can serve static assets", -> @server._onResolveUrl("/index.html", {}, @automationRequest) .then (obj = {}) -> - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" @@ -142,7 +147,7 @@ describe "Server", -> it "sends back the content type", -> @server._onResolveUrl("/assets/foo.json", {}, @automationRequest) .then (obj = {}) -> - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: false contentType: "application/json" @@ -160,7 +165,7 @@ describe "Server", -> @server._onResolveUrl("/index.html", {}, @automationRequest) .then (obj = {}) => - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" @@ -177,7 +182,7 @@ describe "Server", -> .then => @server._onResolveUrl("/index.html", {}, @automationRequest) .then (obj = {}) => - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" @@ -203,7 +208,7 @@ describe "Server", -> it "can follow static file redirects", -> @server._onResolveUrl("/sub", {}, @automationRequest) .then (obj = {}) -> - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" @@ -233,7 +238,7 @@ describe "Server", -> it "gracefully handles 404", -> @server._onResolveUrl("/does-not-exist", {}, @automationRequest) .then (obj = {}) -> - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: false isHtml: false contentType: undefined @@ -256,7 +261,7 @@ describe "Server", -> it "handles urls with hashes", -> @server._onResolveUrl("/index.html#/foo/bar", {}, @automationRequest) .then (obj = {}) => - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" @@ -322,7 +327,7 @@ describe "Server", -> ## fire the 2nd request now that the first one has had some time to reach out @server._onResolveUrl("http://localhost:#{@httpPort}/#{path}/100", {}, @automationRequest) .then (obj) => - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" @@ -360,7 +365,7 @@ describe "Server", -> @server._onResolveUrl("http://getbootstrap.com/", headers, @automationRequest) .then (obj = {}) -> - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" @@ -389,7 +394,7 @@ describe "Server", -> @server._onResolveUrl("http://getbootstrap.com/user.json", {}, @automationRequest) .then (obj = {}) -> - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: false contentType: "application/json" @@ -420,7 +425,7 @@ describe "Server", -> @server._onResolveUrl("http://espn.com/", {}, @automationRequest) .then (obj = {}) -> - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" @@ -477,7 +482,7 @@ describe "Server", -> @server._onResolveUrl("http://espn.com/", {}, @automationRequest) .then (obj = {}) => - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" @@ -496,7 +501,7 @@ describe "Server", -> .then => @server._onResolveUrl("http://espn.com/", {}, @automationRequest) .then (obj = {}) => - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" @@ -544,7 +549,7 @@ describe "Server", -> @server._onResolveUrl("http://espn.com/", {}, @automationRequest) .then (obj = {}) => - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: false isHtml: false contentType: undefined @@ -558,7 +563,7 @@ describe "Server", -> @server._onResolveUrl("http://espn.com/", {}, @automationRequest) .then (obj = {}) => - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" @@ -590,7 +595,7 @@ describe "Server", -> @server._onResolveUrl("http://mlb.com/", {}, @automationRequest) .then (obj = {}) -> - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: false isHtml: true contentType: "text/html" @@ -619,7 +624,7 @@ describe "Server", -> @server._onResolveUrl("http://getbootstrap.com/#/foo", {}, @automationRequest) .then (obj = {}) -> - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" @@ -655,7 +660,7 @@ describe "Server", -> @server._onResolveUrl("http://google.com/foo", headers, @automationRequest, { failOnStatusCode: false }) .then (obj = {}) -> - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" @@ -705,7 +710,7 @@ describe "Server", -> @server._onResolveUrl("http://google.com/index", headers, @automationRequest, { auth }) .then (obj = {}) -> - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" @@ -756,7 +761,7 @@ describe "Server", -> @server._onResolveUrl("/index.html", {}, @automationRequest) .then (obj = {}) -> - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" @@ -775,7 +780,7 @@ describe "Server", -> .then => @server._onResolveUrl("http://www.google.com/", {}, @automationRequest) .then (obj = {}) -> - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" @@ -807,7 +812,7 @@ describe "Server", -> .then => @server._onResolveUrl("/index.html", {}, @automationRequest) .then (obj = {}) -> - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" @@ -847,7 +852,7 @@ describe "Server", -> @server._onResolveUrl("http://www.google.com/", {}, @automationRequest) .then (obj = {}) -> - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" @@ -882,7 +887,7 @@ describe "Server", -> .then => @server._onResolveUrl("/index.html", {}, @automationRequest) .then (obj = {}) -> - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" @@ -914,7 +919,7 @@ describe "Server", -> .then => @server._onResolveUrl("http://www.google.com/", {}, @automationRequest) .then (obj = {}) -> - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" @@ -952,7 +957,7 @@ describe "Server", -> @server._onResolveUrl("https://www.foobar.com:8443/", {}, @automationRequest) .then (obj = {}) -> - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" @@ -987,7 +992,7 @@ describe "Server", -> .then => @server._onResolveUrl("/index.html", {}, @automationRequest) .then (obj = {}) -> - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" @@ -1019,7 +1024,7 @@ describe "Server", -> .then => @server._onResolveUrl("https://www.foobar.com:8443/", {}, @automationRequest) .then (obj = {}) -> - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" @@ -1057,7 +1062,7 @@ describe "Server", -> @server._onResolveUrl(s3StaticHtmlUrl, {}, @automationRequest) .then (obj = {}) -> - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" @@ -1101,7 +1106,7 @@ describe "Server", -> .then => @server._onResolveUrl("/index.html", {}, @automationRequest) .then (obj = {}) -> - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" @@ -1133,7 +1138,7 @@ describe "Server", -> .then => @server._onResolveUrl(s3StaticHtmlUrl, {}, @automationRequest) .then (obj = {}) -> - expect(obj).to.deep.eq({ + expectToEqDetails(obj, { isOkStatusCode: true isHtml: true contentType: "text/html" diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/fast_visit_spec.coffee b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/fast_visit_spec.coffee new file mode 100644 index 000000000000..30c739eda404 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/fast_visit_spec.coffee @@ -0,0 +1,26 @@ +beforeEach -> + cy.on "log:added", (attrs, log) => + if attrs.name is "visit" + @lastLog = log + + return null + +it "always finishes in less than 150ms on localhost with connection: close", -> + cy.visit('/close') + + Cypress._.times 100, -> + cy.visit('/close') + .then -> + expect(@lastLog.get("totalTime")).to.be.lte(150) + + return undefined + +it "always finishes in less than 150ms on localhost with connection: keep-alive", -> + cy.visit('/close') + + Cypress._.times 100, -> + cy.visit('/keepalive') + .then -> + expect(@lastLog.get("totalTime")).to.be.lte(150) + + return undefined diff --git a/packages/server/test/support/helpers/e2e.coffee b/packages/server/test/support/helpers/e2e.coffee index c61a68daad54..1118defcdfa2 100644 --- a/packages/server/test/support/helpers/e2e.coffee +++ b/packages/server/test/support/helpers/e2e.coffee @@ -356,14 +356,14 @@ module.exports = { env: _.chain(process.env) .omit("CYPRESS_DEBUG") .extend({ - DEBUG_COLORS: "1" - ## FYI: color will already be disabled ## because we are piping the child process COLUMNS: 100 LINES: 24 }) .defaults({ + DEBUG_COLORS: "1" + ## prevent any Compression progress ## messages from showing up VIDEO_COMPRESSION_THROTTLE: 120000 diff --git a/packages/server/test/unit/timers_spec.coffee b/packages/server/test/unit/timers_spec.coffee deleted file mode 100644 index a3a93bf88db2..000000000000 --- a/packages/server/test/unit/timers_spec.coffee +++ /dev/null @@ -1,112 +0,0 @@ -require("../spec_helper") - -_ = require("lodash") - -parent = require("#{root}timers/parent") - -describe "timers/parent", -> - context ".fix", -> - beforeEach -> - parent.restore() - @timer = parent.fix() - - describe "setTimeout", -> - it "returns timer object", (done) -> - obj = setTimeout(done, 10) - - expect(obj.id).to.eq(1) - expect(obj.ref).to.be.a("function") - expect(obj.unref).to.be.a("function") - - it "increments timer id", (done) -> - fn = _.after(2, done) - - obj1 = setTimeout(fn, 10) - obj2 = setTimeout(fn, 10) - - expect(obj2.id).to.eq(2) - - it "slices out of queue once cb is invoked", (done) -> - fn = => - expect(@timer.queue).to.deep.eq({}) - done() - - setTimeout(fn, 10) - - expect(@timer.queue[1].cb).to.eq(fn) - - describe "clearTimeout", -> - it "does not explode when passing null", -> - clearTimeout(null) - - it "can clear the timeout and prevent the cb from being invoked", (done) -> - fn = => - done(new Error("should not have been invoked")) - - timer = setTimeout(fn, 10) - - expect(@timer.queue[1].cb).to.eq(fn) - - clearTimeout(timer) - - expect(@timer.queue).to.deep.eq({}) - - setTimeout -> - done() - , 20 - - describe "setInterval", -> - it "returns timer object", (done) -> - obj = setInterval -> - clearInterval(obj) - - done() - , 10 - - expect(obj.id).to.eq(1) - expect(obj.ref).to.be.a("function") - expect(obj.unref).to.be.a("function") - - it "increments timer id", (done) -> - fn = _.after 2, -> - clearInterval(obj1) - clearInterval(obj2) - done() - - obj1 = setInterval(fn, 10) - obj2 = setInterval(fn, 10000) - - expect(obj2.id).to.eq(2) - - it "continuously polls until cleared", (done) -> - poller = _.after 3, => - clearInterval(t) - - setTimeout -> - expect(fn).to.be.calledThrice - done() - , 100 - - fn = sinon.spy(poller) - - t = setInterval(fn, 10) - - describe "clearInterval", -> - it "does not explode when passing null", -> - clearInterval(null) - - it "can clear the interval and prevent the cb from being invoked", (done) -> - fn = => - done(new Error("should not have been invoked")) - - timer = setInterval(fn, 10) - - expect(@timer.queue[1].cb).to.exist - - clearInterval(timer) - - expect(@timer.queue).to.deep.eq({}) - - setTimeout -> - done() - , 20 diff --git a/packages/server/timers/child.js b/packages/server/timers/child.js deleted file mode 100644 index f57b0d5a64d5..000000000000 --- a/packages/server/timers/child.js +++ /dev/null @@ -1,22 +0,0 @@ -const log = require('debug')('cypress:server:timers') - -process.on('message', (obj = {}) => { - const { id, ms } = obj - - log('child received timer id %d', id) - - setTimeout(() => { - try { - log('child sending timer id %d', id) - - // process.send could throw if - // parent process has already exited - process.send({ - id, - ms, - }) - } catch (err) { - // eslint-disable no-empty - } - }, ms) -}) diff --git a/packages/server/timers/parent.js b/packages/server/timers/parent.js deleted file mode 100644 index bd44be74672e..000000000000 --- a/packages/server/timers/parent.js +++ /dev/null @@ -1,166 +0,0 @@ -// electron has completely busted timers resulting in -// all kinds of bizarre timeouts and unresponsive UI -// https://github.com/electron/electron/issues/7079 -// -// this fixes this problem by replacing all the global -// timers and implementing a lightweight queuing mechanism -// involving a forked process - -const cp = require('child_process') -const path = require('path') -const log = require('debug')('cypress:server:timers') - -const st = global.setTimeout -const si = global.setInterval -const ct = global.clearTimeout -const ci = global.clearInterval - -let child = null - -function noop () {} - -function restore () { - // restore - global.setTimeout = st - global.setInterval = si - global.clearTimeout = ct - global.clearInterval = ci - - if (child) { - child.kill() - } - - child = null -} - -function fix () { - const queue = {} - let idCounter = 0 - - function sendAndQueue (id, cb, ms, args) { - // const started = Date.now() - log('queuing timer id %d after %d ms', id, ms) - - queue[id] = { - // started, - args, - ms, - cb, - } - - child.send({ - id, - ms, - }) - - // return the timer object - return { - id, - ref: noop, - unref: noop, - } - } - - function clear (id) { - log('clearing timer id %d from queue %o', id, queue) - - delete queue[id] - } - - // fork the child process - let child = cp.fork(path.join(__dirname, 'child.js'), [], { - stdio: 'inherit', - }) - .on('message', (obj = {}) => { - const { id } = obj - - const msg = queue[id] - - // if we didn't get a msg - // that means we must have - // cleared the timeout already - if (!msg) { - return - } - - const { cb, args } = msg - - clear(id) - - cb(...args) - }) - - // In linux apparently the child process is never - // exiting which causes cypress to hang indefinitely. - // It would **SEEM** as if we... - // 1. dont need to manually kill our child process - // because it should naturally exit. - // (but of course it doesn't in linux) - // 2. use our restore function already defined above. - // however when using the restore function above - // the 'child' reference is null. how is it null? - // it makes no sense. there must be a rip in the - // space time continuum, obviously. that or the - // child reference as the rest of the matter of - // the universe has succumbed to entropy. - process.on('exit', () => { - child && child.kill() - - restore() - }) - - global.setTimeout = function (cb, ms, ...args) { - idCounter += 1 - - return sendAndQueue(idCounter, cb, ms, args) - } - - global.clearTimeout = function (timer) { - if (!timer) { - return - } - - // return undefined per the spec - clear(timer.id) - } - - global.clearInterval = function (timer) { - if (!timer) { - return - } - - // return undefined per the spec - clear(timer.id) - } - - global.setInterval = function (fn, ms, ...args) { - const permId = idCounter += 1 - - function cb () { - // we want to immediately poll again - // because our permId was just cleared - // from the queue stack - poll() - - fn() - } - - function poll () { - return sendAndQueue(permId, cb, ms, args) - } - - return poll() - } - - return { - child, - - queue, - } -} - -module.exports = { - restore, - - fix, -}