From e3c26835fae88a478baad477d537bd0ff1424db9 Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Fri, 2 Apr 2021 19:52:47 +0300 Subject: [PATCH] feat: improve the `open` option, you can specify `target` and `app` options BREAKING CHANGE: the `openPage` option and `--open-page` CLI option was removed in favor `open: boolean | string | (string | { target?: string | string[], app: string | string[] })[]` and `--open-target [URL]` and `--open-app ` for CLI --- bin/cli-flags.js | 32 +- lib/Server.js | 4 +- lib/options.json | 79 ++- lib/utils/runOpen.js | 82 ++- test/__snapshots__/Validation.test.js.snap | 2 +- test/cli/cli.test.js | 72 ++ test/options.test.js | 319 +++------ test/server/open-option.test.js | 627 +++++++++++++++++- .../utils/__snapshots__/runOpen.test.js.snap | 68 -- test/server/utils/runOpen.test.js | 350 ---------- 10 files changed, 959 insertions(+), 676 deletions(-) delete mode 100644 test/server/utils/__snapshots__/runOpen.test.js.snap delete mode 100644 test/server/utils/runOpen.test.js diff --git a/bin/cli-flags.js b/bin/cli-flags.js index bf70ee415d..5f7ad9ce4f 100644 --- a/bin/cli-flags.js +++ b/bin/cli-flags.js @@ -111,9 +111,11 @@ module.exports = { 'Do not close and exit the process on SIGNIT and SIGTERM.', negative: true, }, + // TODO remove in the next major release in favor `--open-target` { name: 'open', type: [Boolean, String], + multiple: true, configs: [ { type: 'boolean', @@ -122,18 +124,40 @@ module.exports = { type: 'string', }, ], - description: - 'Open the default browser, or optionally specify a browser name.', + description: 'Open the default browser.', }, { - name: 'open-page', + name: 'open-app', type: String, configs: [ { type: 'string', }, ], - description: 'Open default browser with the specified page.', + description: 'Open specified browser.', + processor(opts) { + opts.open = opts.open || {}; + opts.open.app = opts.openApp.split(' '); + delete opts.openApp; + }, + }, + { + name: 'open-target', + type: String, + configs: [ + { + type: 'boolean', + }, + { + type: 'string', + }, + ], + description: 'Open specified browser.', + processor(opts) { + opts.open = opts.open || {}; + opts.open.target = opts.openTarget; + delete opts.openTarget; + }, multiple: true, }, { diff --git a/lib/Server.js b/lib/Server.js index 33f7a025b9..f7db641318 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -719,10 +719,10 @@ class Server { ); } - if (this.options.open || this.options.openPage) { + if (this.options.open) { const openTarget = prettyPrintUrl(this.hostname || 'localhost'); - runOpen(openTarget, this.options, this.logger); + runOpen(openTarget, this.options.open, this.logger); } } diff --git a/lib/options.json b/lib/options.json index b6f83c72a5..8d9dac1cec 100644 --- a/lib/options.json +++ b/lib/options.json @@ -10,7 +10,8 @@ "minLength": 1 }, "staticOptions": { - "type": "object" + "type": "object", + "additionalProperties": true }, "publicPath": { "anyOf": [ @@ -34,7 +35,8 @@ "type": "boolean" }, { - "type": "object" + "type": "object", + "additionalProperties": true } ] }, @@ -53,6 +55,54 @@ "StaticString": { "type": "string", "minLength": 1 + }, + "OpenBoolean": { + "type": "boolean" + }, + "OpenString": { + "type": "string", + "minLength": 1 + }, + "OpenObject": { + "type": "object", + "additionalProperties": false, + "properties": { + "target": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "minLength": 1 + }, + { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 1 + } + ] + }, + "app": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 1 + } + ] + } + } } }, "properties": { @@ -258,25 +308,25 @@ "open": { "anyOf": [ { - "type": "string" + "$ref": "#/definitions/OpenBoolean" }, { - "type": "boolean" + "$ref": "#/definitions/OpenString" }, { - "type": "object" - } - ] - }, - "openPage": { - "anyOf": [ - { - "type": "string" + "$ref": "#/definitions/OpenObject" }, { "type": "array", "items": { - "type": "string" + "anyOf": [ + { + "$ref": "#/definitions/OpenString" + }, + { + "$ref": "#/definitions/OpenObject" + } + ] }, "minItems": 1 } @@ -393,8 +443,7 @@ "onAfterSetupMiddleware": "should be {Function} (https://webpack.js.org/configuration/dev-server/#devserverafter)", "onBeforeSetupMiddleware": "should be {Function} (https://webpack.js.org/configuration/dev-server/#devserverbefore)", "onListening": "should be {Function} (https://webpack.js.org/configuration/dev-server/#onlistening)", - "open": "should be {String|Boolean|Object} (https://webpack.js.org/configuration/dev-server/#devserveropen)", - "openPage": "should be {String|Array} (https://webpack.js.org/configuration/dev-server/#devserveropenpage)", + "open": "should be {Boolean|String|(STRING | Object)[]} (https://webpack.js.org/configuration/dev-server/#devserveropen)", "port": "should be {Number|String|Null} (https://webpack.js.org/configuration/dev-server/#devserverport)", "proxy": "should be {Object|Array} (https://webpack.js.org/configuration/dev-server/#devserverproxy)", "public": "should be {String} (https://webpack.js.org/configuration/dev-server/#devserverpublic)", diff --git a/lib/utils/runOpen.js b/lib/utils/runOpen.js index daa0b3b4c5..dbfdf5cd41 100644 --- a/lib/utils/runOpen.js +++ b/lib/utils/runOpen.js @@ -5,29 +5,75 @@ const isAbsoluteUrl = require('is-absolute-url'); function runOpen(uri, options, logger) { // /~https://github.com/webpack/webpack-dev-server/issues/1990 - let openOptions = { wait: false }; - let openOptionValue = ''; - - if (typeof options.open === 'string') { - openOptions = Object.assign({}, openOptions, { app: options.open }); - openOptionValue = `: "${options.open}"`; - } else if (typeof options.open === 'object') { - openOptions = options.open; - openOptionValue = `: "${JSON.stringify(options.open)}"`; - } + const defaultOpenOptions = { wait: false }; + const openTasks = []; + + const getOpenTask = (item) => { + if (typeof item === 'boolean') { + return [{ target: uri, options: defaultOpenOptions }]; + } + + if (typeof item === 'string') { + return [{ target: item, options: defaultOpenOptions }]; + } + + let targets; + + if (item.target) { + targets = Array.isArray(item.target) ? item.target : [item.target]; + } else { + targets = [uri]; + } - const pages = - typeof options.openPage === 'string' - ? [options.openPage] - : options.openPage || ['']; + return targets.map((target) => { + const openOptions = defaultOpenOptions; + + if (item.app) { + openOptions.app = item.app; + } + + return { target, options: openOptions }; + }); + }; + + if (Array.isArray(options)) { + options.forEach((item) => { + openTasks.push(...getOpenTask(item)); + }); + } else { + openTasks.push(...getOpenTask(options)); + } return Promise.all( - pages.map((page) => { - const pageUrl = page && isAbsoluteUrl(page) ? page : `${uri}${page}`; + openTasks.map((openTask) => { + let openTarget; + + if (openTask.target) { + if (typeof openTask.target === 'boolean') { + openTarget = uri; + } else { + openTarget = isAbsoluteUrl(openTask.target) + ? openTask.target + : new URL(openTask.target, uri).toString(); + } + } else { + openTarget = uri; + } - return open(pageUrl, openOptions).catch(() => { + return open(openTarget, openTask.options).catch(() => { logger.warn( - `Unable to open "${pageUrl}" in browser${openOptionValue}. If you are running in a headless environment, please do not use the --open flag` + `Unable to open "${openTarget}" page${ + // eslint-disable-next-line no-nested-ternary + openTask.options.app + ? Array.isArray(openTask.options.app) + ? ` in "${ + openTask.options.app[0] + }" app with "${openTask.options.app + .slice(1) + .join(' ')}" arguments` + : ` in "${openTask.options.app}" app` + : '' + }. If you are running in a headless environment, please do not use the "--open" flag or the "open" option.` ); }); }) diff --git a/test/__snapshots__/Validation.test.js.snap b/test/__snapshots__/Validation.test.js.snap index 7a24b7771a..e2f492f407 100644 --- a/test/__snapshots__/Validation.test.js.snap +++ b/test/__snapshots__/Validation.test.js.snap @@ -43,5 +43,5 @@ exports[`Validation validation should fail validation for invalid \`static\` con exports[`Validation validation should fail validation for no additional properties 1`] = ` "Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. - configuration has an unknown property 'additional'. These properties are valid: - object { bonjour?, client?, compress?, dev?, firewall?, headers?, historyApiFallback?, host?, hot?, http2?, https?, liveReload?, onAfterSetupMiddleware?, onBeforeSetupMiddleware?, onListening?, open?, openPage?, port?, proxy?, public?, setupExitSignals?, static?, transportMode? }" + object { bonjour?, client?, compress?, dev?, firewall?, headers?, historyApiFallback?, host?, hot?, http2?, https?, liveReload?, onAfterSetupMiddleware?, onBeforeSetupMiddleware?, onListening?, open?, port?, proxy?, public?, setupExitSignals?, static?, transportMode? }" `; diff --git a/test/cli/cli.test.js b/test/cli/cli.test.js index 23a9d364a5..3cafd9eecb 100644 --- a/test/cli/cli.test.js +++ b/test/cli/cli.test.js @@ -219,6 +219,78 @@ describe('CLI', () => { .catch(done); }); + it('--open', (done) => { + testBin('--open') + .then((output) => { + expect(output.exitCode).toEqual(0); + done(); + }) + .catch(done); + }); + + it('--open /index.html', (done) => { + testBin('--open /index.html') + .then((output) => { + expect(output.exitCode).toEqual(0); + done(); + }) + .catch(done); + }); + + it('--open /first.html second.html', (done) => { + testBin('--open /first.html second.html') + .then((output) => { + expect(output.exitCode).toEqual(0); + done(); + }) + .catch(done); + }); + + it('--open-app google-chrome', (done) => { + testBin('--open-app google-chrome') + .then((output) => { + expect(output.exitCode).toEqual(0); + done(); + }) + .catch(done); + }); + + it('--open-target', (done) => { + testBin('--open-target') + .then((output) => { + expect(output.exitCode).toEqual(0); + done(); + }) + .catch(done); + }); + + it('--open-target index.html', (done) => { + testBin('--open-target index.html') + .then((output) => { + expect(output.exitCode).toEqual(0); + done(); + }) + .catch(done); + }); + + it('--open-target /first.html second.html', (done) => { + testBin('--open-target /first.html second.html') + .then((output) => { + expect(output.exitCode).toEqual(0); + done(); + }) + .catch(done); + }); + + it('--open-target /index.html --open-app google-chrome', (done) => { + testBin('--open-target /index.html --open-app google-chrome') + .then((output) => { + expect(output.exitCode).toEqual(0); + done(); + }) + .catch(done); + }); + it('should log public path', (done) => { testBin( '--no-color', diff --git a/test/options.test.js b/test/options.test.js index 526f68d55e..455baa517a 100644 --- a/test/options.test.js +++ b/test/options.test.js @@ -65,18 +65,7 @@ describe('options', () => { return p .then(() => { - const opts = - Object.prototype.toString.call(value) === '[object Object]' && - Object.keys(value).length !== 0 - ? value - : { - [propertyName]: value, - }; - - if (typeof opts.static === 'undefined') { - opts.static = false; - } - server = new Server(compiler, opts); + server = new Server(compiler, { [propertyName]: value }); }) .then(() => { if (current < successCount) { @@ -99,6 +88,7 @@ describe('options', () => { server.close(() => { compiler = null; server = null; + resolve(); }); } else { @@ -127,198 +117,132 @@ describe('options', () => { failure: [false], }, bonjour: { - success: [false], + success: [false, true], failure: [''], }, client: { success: [ + {}, { - client: {}, - }, - { - client: { - host: '', - }, + host: '', }, { - client: { - path: '', - }, + path: '', }, { - client: { - port: '', - }, + port: '', }, { - client: { - logging: 'none', - }, + logging: 'none', }, { - client: { - logging: 'error', - }, + logging: 'error', }, { - client: { - logging: 'warn', - }, + logging: 'warn', }, { - client: { - logging: 'info', - }, + logging: 'info', }, { - client: { - logging: 'log', - }, + logging: 'log', }, { - client: { - logging: 'verbose', - }, + logging: 'verbose', }, { - client: { - host: '', - path: '', - port: 8080, - logging: 'none', - }, + host: '', + path: '', + port: 8080, + logging: 'none', }, { - client: { - host: '', - path: '', - port: '', - }, + host: '', + path: '', + port: '', }, { - client: { - host: '', - path: '', - port: null, - }, + host: '', + path: '', + port: null, }, { - client: { - progress: false, - }, + progress: false, }, { - client: { - overlay: true, - }, + overlay: true, }, { - client: { - overlay: {}, - }, + overlay: {}, }, { - client: { - overlay: { - error: true, - }, + overlay: { + error: true, }, }, { - client: { - overlay: { - warnings: true, - }, + overlay: { + warnings: true, }, }, { - client: { - overlay: { - arbitrary: '', - }, + overlay: { + arbitrary: '', }, }, { - client: { - needClientEntry: true, - }, + needClientEntry: true, }, { - client: { - needHotEntry: true, - }, + needHotEntry: true, }, ], failure: [ 'whoops!', { - client: { - unknownOption: true, - }, + unknownOption: true, }, { - client: { - host: true, - path: '', - port: 8080, - }, + host: true, + path: '', + port: 8080, }, { - client: { - logging: 'whoops!', - }, + logging: 'whoops!', }, { - client: { - logging: 'silent', - }, + logging: 'silent', }, { - client: { - progress: '', - }, + progress: '', }, { - client: { - overlay: '', - }, + overlay: '', }, { - client: { - overlay: { - errors: '', - }, + overlay: { + errors: '', }, }, { - client: { - overlay: { - warnings: '', - }, + overlay: { + warnings: '', }, }, { - client: { - needClientEntry: [''], - }, + needClientEntry: [''], }, { - client: { - needHotEntry: [''], - }, + needHotEntry: [''], }, ], }, compress: { - success: [true], + success: [false, true], failure: [''], }, dev: { - success: [ - { - dev: {}, - }, - ], + success: [{}], failure: [''], }, firewall: { @@ -326,7 +250,7 @@ describe('options', () => { failure: ['', []], }, headers: { - success: [{}], + success: [{}, { foo: 'bar' }], failure: [false], }, historyApiFallback: { @@ -334,7 +258,7 @@ describe('options', () => { failure: [''], }, host: { - success: ['', null], + success: ['', 'localhost', null], failure: [false], }, hot: { @@ -342,38 +266,33 @@ describe('options', () => { failure: ['', 'foo'], }, http2: { - success: [true], + success: [false, true], failure: [''], }, https: { success: [ false, + true, { - https: { - ca: join(httpsCertificateDirectory, 'ca.pem'), - key: join(httpsCertificateDirectory, 'server.key'), - pfx: join(httpsCertificateDirectory, 'server.pfx'), - cert: join(httpsCertificateDirectory, 'server.crt'), - requestCert: true, - passphrase: 'webpack-dev-server', - }, + ca: join(httpsCertificateDirectory, 'ca.pem'), + key: join(httpsCertificateDirectory, 'server.key'), + pfx: join(httpsCertificateDirectory, 'server.pfx'), + cert: join(httpsCertificateDirectory, 'server.crt'), + requestCert: true, + passphrase: 'webpack-dev-server', }, { - https: { - ca: readFileSync(join(httpsCertificateDirectory, 'ca.pem')), - pfx: readFileSync(join(httpsCertificateDirectory, 'server.pfx')), - key: readFileSync(join(httpsCertificateDirectory, 'server.key')), - cert: readFileSync(join(httpsCertificateDirectory, 'server.crt')), - passphrase: 'webpack-dev-server', - }, + ca: readFileSync(join(httpsCertificateDirectory, 'ca.pem')), + pfx: readFileSync(join(httpsCertificateDirectory, 'server.pfx')), + key: readFileSync(join(httpsCertificateDirectory, 'server.key')), + cert: readFileSync(join(httpsCertificateDirectory, 'server.crt')), + passphrase: 'webpack-dev-server', }, ], failure: [ '', { - https: { - foo: 'bar', - }, + foo: 'bar', }, ], }, @@ -382,12 +301,20 @@ describe('options', () => { failure: [''], }, open: { - success: [true, '', {}], - failure: [[]], - }, - openPage: { - success: [''], - failure: [false], + success: [ + true, + 'foo', + ['foo', 'bar'], + { target: true }, + { target: 'foo' }, + { target: ['foo', 'bar'] }, + { app: 'google-chrome' }, + { app: ['google-chrome', '--incognito'] }, + { target: 'foo', app: 'google-chrome' }, + { target: ['foo', 'bar'], app: ['google-chrome', '--incognito'] }, + {}, + ], + failure: ['', [], { foo: 'bar' }], }, port: { success: ['', 0, null], @@ -396,15 +323,13 @@ describe('options', () => { proxy: { success: [ { - proxy: { - '/api': 'http://localhost:3000', - }, + '/api': 'http://localhost:3000', }, ], failure: [[], () => {}, false], }, public: { - success: [''], + success: ['', 'foo', 'auto'], failure: [false], }, static: { @@ -412,35 +337,29 @@ describe('options', () => { 'path', false, { - static: { - directory: 'path', + directory: 'path', + staticOptions: {}, + publicPath: '/', + serveIndex: true, + watch: true, + }, + { + directory: 'path', + staticOptions: {}, + publicPath: ['/public1/', '/public2/'], + serveIndex: {}, + watch: {}, + }, + [ + 'path1', + { + directory: 'path2', staticOptions: {}, publicPath: '/', serveIndex: true, watch: true, }, - }, - { - static: { - directory: 'path', - staticOptions: {}, - publicPath: ['/public1/', '/public2/'], - serveIndex: {}, - watch: {}, - }, - }, - { - static: [ - 'path1', - { - directory: 'path2', - staticOptions: {}, - publicPath: '/', - serveIndex: true, - watch: true, - }, - ], - }, + ], ], failure: [0, null, ''], }, @@ -449,54 +368,36 @@ describe('options', () => { 'ws', 'sockjs', { - transportMode: { - server: 'sockjs', - }, + server: 'sockjs', }, { - transportMode: { - server: require.resolve('../lib/servers/SockJSServer'), - }, + server: require.resolve('../lib/servers/SockJSServer'), }, { - transportMode: { - server: SockJSServer, - }, + server: SockJSServer, }, { - transportMode: { - client: 'sockjs', - }, + client: 'sockjs', }, { - transportMode: { - client: require.resolve('../client/clients/SockJSClient'), - }, + client: require.resolve('../client/clients/SockJSClient'), }, { - transportMode: { - server: SockJSServer, - client: require.resolve('../client/clients/SockJSClient'), - }, + server: SockJSServer, + client: require.resolve('../client/clients/SockJSClient'), }, ], failure: [ 'nonexistent-implementation', null, { - transportMode: { - notAnOption: true, - }, + notAnOption: true, }, { - transportMode: { - server: false, - }, + server: false, }, { - transportMode: { - client: () => {}, - }, + client: () => {}, }, ], }, diff --git a/test/server/open-option.test.js b/test/server/open-option.test.js index 75a33032ae..b142ba6dff 100644 --- a/test/server/open-option.test.js +++ b/test/server/open-option.test.js @@ -45,7 +45,30 @@ describe('"open" option', () => { server.listen(port); }); - it('should work with "0.0.0.0" host', (done) => { + it("should work with the 'https' option", (done) => { + const compiler = webpack(config); + const server = new Server(compiler, { + open: true, + port, + https: true, + static: false, + }); + + compiler.hooks.done.tap('webpack-dev-server', () => { + server.close(() => { + expect(open).toHaveBeenCalledWith('https://localhost:8117/', { + wait: false, + }); + + done(); + }); + }); + + compiler.run(() => {}); + server.listen(port); + }); + + it("should work with '0.0.0.0' host", (done) => { const compiler = webpack(config); const server = new Server(compiler, { open: true, @@ -67,7 +90,7 @@ describe('"open" option', () => { server.listen(port, '0.0.0.0'); }); - it('should work with "::" host', (done) => { + it("should work with '::' host", (done) => { const compiler = webpack(config); const server = new Server(compiler, { open: true, @@ -89,7 +112,7 @@ describe('"open" option', () => { server.listen(port, '::'); }); - it('should work with "localhost" host', (done) => { + it("should work with 'localhost' host", (done) => { const compiler = webpack(config); const server = new Server(compiler, { open: true, @@ -111,7 +134,7 @@ describe('"open" option', () => { server.listen(port, 'localhost'); }); - it('should work with "127.0.0.1" host', (done) => { + it("should work with '127.0.0.1' host", (done) => { const compiler = webpack(config); const server = new Server(compiler, { open: true, @@ -133,7 +156,7 @@ describe('"open" option', () => { server.listen(port, '127.0.0.1'); }); - it('should work with "::1" host', (done) => { + it("should work with '::1' host", (done) => { const compiler = webpack(config); const server = new Server(compiler, { open: true, @@ -155,7 +178,7 @@ describe('"open" option', () => { server.listen(port, '::1'); }); - it(`should work with "${internalIPv4}" host`, (done) => { + it(`should work with '${internalIPv4}' host`, (done) => { const compiler = webpack(config); const server = new Server(compiler, { open: true, @@ -179,7 +202,7 @@ describe('"open" option', () => { // TODO need improve // if (internalIPv6) { - // it(`should work with "${internalIPv6}" host`, (done) => { + // it(`should work with '${internalIPv6}' host`, (done) => { // const compiler = webpack(config); // const server = new Server(compiler, { // open: true, @@ -202,10 +225,52 @@ describe('"open" option', () => { // }); // } - it('should work with unspecified the `open` option and specified the `openPage` option', (done) => { + it('should work with boolean', (done) => { + const compiler = webpack(config); + const server = new Server(compiler, { + open: true, + port, + static: false, + }); + + compiler.hooks.done.tap('webpack-dev-server', () => { + server.close(() => { + expect(open).toHaveBeenCalledWith('http://localhost:8117/', { + wait: false, + }); + + done(); + }); + }); + + compiler.run(() => {}); + server.listen(port, 'localhost'); + }); + + it("should work with boolean but don't close with 'false' value", (done) => { + const compiler = webpack(config); + const server = new Server(compiler, { + open: false, + port, + static: false, + }); + + compiler.hooks.done.tap('webpack-dev-server', () => { + server.close(() => { + expect(open).not.toHaveBeenCalled(); + + done(); + }); + }); + + compiler.run(() => {}); + server.listen(port, 'localhost'); + }); + + it('should work with relative string', (done) => { const compiler = webpack(config); const server = new Server(compiler, { - openPage: 'index.html', + open: 'index.html', port, static: false, }); @@ -223,4 +288,548 @@ describe('"open" option', () => { compiler.run(() => {}); server.listen(port, 'localhost'); }); + + it('should work with relative string starting with "/"', (done) => { + const compiler = webpack(config); + const server = new Server(compiler, { + open: '/index.html', + port, + static: false, + }); + + compiler.hooks.done.tap('webpack-dev-server', () => { + server.close(() => { + expect(open).toHaveBeenCalledWith('http://localhost:8117/index.html', { + wait: false, + }); + + done(); + }); + }); + + compiler.run(() => {}); + server.listen(port, 'localhost'); + }); + + it('should work with absolute string', (done) => { + const compiler = webpack(config); + const server = new Server(compiler, { + open: 'http://localhost:8117/index.html', + port, + static: false, + }); + + compiler.hooks.done.tap('webpack-dev-server', () => { + server.close(() => { + expect(open).toHaveBeenCalledWith('http://localhost:8117/index.html', { + wait: false, + }); + + done(); + }); + }); + + compiler.run(() => {}); + server.listen(port, 'localhost'); + }); + + it('should work with multiple relative strings', (done) => { + const compiler = webpack(config); + const server = new Server(compiler, { + open: ['first.html', 'second.html'], + port, + static: false, + }); + + compiler.hooks.done.tap('webpack-dev-server', () => { + server.close(() => { + expect(open).toHaveBeenNthCalledWith( + 1, + 'http://localhost:8117/first.html', + { + wait: false, + } + ); + expect(open).toHaveBeenNthCalledWith( + 2, + 'http://localhost:8117/second.html', + { + wait: false, + } + ); + + done(); + }); + }); + + compiler.run(() => {}); + server.listen(port, 'localhost'); + }); + + it('should work with multiple absolute strings', (done) => { + const compiler = webpack(config); + const server = new Server(compiler, { + open: [ + 'http://localhost:8117/first.html', + 'http://localhost:8117/second.html', + ], + port, + static: false, + }); + + compiler.hooks.done.tap('webpack-dev-server', () => { + server.close(() => { + expect(open).toHaveBeenNthCalledWith( + 1, + 'http://localhost:8117/first.html', + { + wait: false, + } + ); + expect(open).toHaveBeenNthCalledWith( + 2, + 'http://localhost:8117/second.html', + { + wait: false, + } + ); + + done(); + }); + }); + + compiler.run(() => {}); + server.listen(port, 'localhost'); + }); + + it('should work with empty object', (done) => { + const compiler = webpack(config); + const server = new Server(compiler, { + open: {}, + port, + static: false, + }); + + compiler.hooks.done.tap('webpack-dev-server', () => { + server.close(() => { + expect(open).toHaveBeenCalledWith('http://localhost:8117/', { + wait: false, + }); + + done(); + }); + }); + + compiler.run(() => {}); + server.listen(port, 'localhost'); + }); + + it("should work with object and with the boolean value of 'target' option", (done) => { + const compiler = webpack(config); + const server = new Server(compiler, { + open: { + target: true, + }, + port, + static: false, + }); + + compiler.hooks.done.tap('webpack-dev-server', () => { + server.close(() => { + expect(open).toHaveBeenCalledWith('http://localhost:8117/', { + wait: false, + }); + + done(); + }); + }); + + compiler.run(() => {}); + server.listen(port, 'localhost'); + }); + + it("should work with object and with the 'target' option", (done) => { + const compiler = webpack(config); + const server = new Server(compiler, { + open: { + target: 'index.html', + }, + port, + static: false, + }); + + compiler.hooks.done.tap('webpack-dev-server', () => { + server.close(() => { + expect(open).toHaveBeenCalledWith('http://localhost:8117/index.html', { + wait: false, + }); + + done(); + }); + }); + + compiler.run(() => {}); + server.listen(port, 'localhost'); + }); + + it("should work with object and with multiple values of the 'target' option", (done) => { + const compiler = webpack(config); + const server = new Server(compiler, { + open: { + target: ['first.html', 'second.html'], + }, + port, + static: false, + }); + + compiler.hooks.done.tap('webpack-dev-server', () => { + server.close(() => { + expect(open).toHaveBeenNthCalledWith( + 1, + 'http://localhost:8117/first.html', + { + wait: false, + } + ); + expect(open).toHaveBeenNthCalledWith( + 2, + 'http://localhost:8117/second.html', + { + wait: false, + } + ); + + done(); + }); + }); + + compiler.run(() => {}); + server.listen(port, 'localhost'); + }); + + it("should work with object and with the 'app' option", (done) => { + const compiler = webpack(config); + const server = new Server(compiler, { + open: { + app: 'google-chrome', + }, + port, + static: false, + }); + + compiler.hooks.done.tap('webpack-dev-server', () => { + server.close(() => { + expect(open).toHaveBeenCalledWith('http://localhost:8117/', { + app: 'google-chrome', + wait: false, + }); + + done(); + }); + }); + + compiler.run(() => {}); + server.listen(port, 'localhost'); + }); + + it("should work with object and with the 'app' option with arguments", (done) => { + const compiler = webpack(config); + const server = new Server(compiler, { + open: { + app: ['google-chrome', '--incognito'], + }, + port, + static: false, + }); + + compiler.hooks.done.tap('webpack-dev-server', () => { + server.close(() => { + expect(open).toHaveBeenCalledWith('http://localhost:8117/', { + app: ['google-chrome', '--incognito'], + wait: false, + }); + + done(); + }); + }); + + compiler.run(() => {}); + server.listen(port, 'localhost'); + }); + + it('should work with object with "target" and "app" options', (done) => { + const compiler = webpack(config); + const server = new Server(compiler, { + open: { + target: 'index.html', + app: 'google-chrome', + }, + port, + static: false, + }); + + compiler.hooks.done.tap('webpack-dev-server', () => { + server.close(() => { + expect(open).toHaveBeenCalledWith('http://localhost:8117/index.html', { + app: 'google-chrome', + wait: false, + }); + + done(); + }); + }); + + compiler.run(() => {}); + server.listen(port, 'localhost'); + }); + + it("should work with object, with multiple value of the 'target' option and with the 'app' option with arguments", (done) => { + const compiler = webpack(config); + const server = new Server(compiler, { + open: { + target: ['first.html', 'second.html'], + app: ['google-chrome', '--incognito'], + }, + port, + static: false, + }); + + compiler.hooks.done.tap('webpack-dev-server', () => { + server.close(() => { + expect(open).toHaveBeenNthCalledWith( + 1, + 'http://localhost:8117/first.html', + { + wait: false, + app: ['google-chrome', '--incognito'], + } + ); + expect(open).toHaveBeenNthCalledWith( + 2, + 'http://localhost:8117/second.html', + { + wait: false, + app: ['google-chrome', '--incognito'], + } + ); + + done(); + }); + }); + + compiler.run(() => {}); + server.listen(port, 'localhost'); + }); + + it("should work with object, with multiple value of the 'target' option (relative and absolute URLs) and with the 'app' option with arguments", (done) => { + const compiler = webpack(config); + const server = new Server(compiler, { + open: { + target: ['first.html', 'http://localhost:8117/second.html'], + app: ['google-chrome', '--incognito'], + }, + port, + static: false, + }); + + compiler.hooks.done.tap('webpack-dev-server', () => { + server.close(() => { + expect(open).toHaveBeenNthCalledWith( + 1, + 'http://localhost:8117/first.html', + { + wait: false, + app: ['google-chrome', '--incognito'], + } + ); + expect(open).toHaveBeenNthCalledWith( + 2, + 'http://localhost:8117/second.html', + { + wait: false, + app: ['google-chrome', '--incognito'], + } + ); + + done(); + }); + }); + + compiler.run(() => {}); + server.listen(port, 'localhost'); + }); + + it("should log warning when can't open", (done) => { + open.mockImplementation(() => Promise.reject()); + + const compiler = webpack(config); + const server = new Server(compiler, { + open: true, + port, + static: false, + }); + const loggerWarnSpy = jest.spyOn(server.logger, 'warn'); + + compiler.hooks.done.tap('webpack-dev-server', () => { + server.close(() => { + expect(open).toHaveBeenCalledWith('http://localhost:8117/', { + wait: false, + }); + expect(loggerWarnSpy).toHaveBeenCalledWith( + 'Unable to open "http://localhost:8117/" page. If you are running in a headless environment, please do not use the "--open" flag or the "open" option.' + ); + + loggerWarnSpy.mockRestore(); + done(); + }); + }); + + compiler.run(() => {}); + server.listen(port); + }); + + it("should log warning when can't open with string", (done) => { + open.mockImplementation(() => Promise.reject()); + + const compiler = webpack(config); + const server = new Server(compiler, { + open: 'index.html', + port, + static: false, + }); + const loggerWarnSpy = jest.spyOn(server.logger, 'warn'); + + compiler.hooks.done.tap('webpack-dev-server', () => { + server.close(() => { + expect(open).toHaveBeenCalledWith('http://localhost:8117/index.html', { + wait: false, + }); + expect(loggerWarnSpy).toHaveBeenCalledWith( + 'Unable to open "http://localhost:8117/index.html" page. If you are running in a headless environment, please do not use the "--open" flag or the "open" option.' + ); + + loggerWarnSpy.mockRestore(); + done(); + }); + }); + + compiler.run(() => {}); + server.listen(port); + }); + + it("should log warning when can't open with object", (done) => { + open.mockImplementation(() => Promise.reject()); + + const compiler = webpack(config); + const server = new Server(compiler, { + open: { + target: 'index.html', + app: 'google-chrome', + }, + port, + static: false, + }); + const loggerWarnSpy = jest.spyOn(server.logger, 'warn'); + + compiler.hooks.done.tap('webpack-dev-server', () => { + server.close(() => { + expect(open).toHaveBeenCalledWith('http://localhost:8117/index.html', { + app: 'google-chrome', + wait: false, + }); + expect(loggerWarnSpy).toHaveBeenCalledWith( + 'Unable to open "http://localhost:8117/index.html" page in "google-chrome" app. If you are running in a headless environment, please do not use the "--open" flag or the "open" option.' + ); + + loggerWarnSpy.mockRestore(); + done(); + }); + }); + + compiler.run(() => {}); + server.listen(port); + }); + + it("should log warning when can't open with object with the 'app' option with arguments", (done) => { + open.mockImplementation(() => Promise.reject()); + + const compiler = webpack(config); + const server = new Server(compiler, { + open: { + target: 'index.html', + app: ['google-chrome', '--incognito', '--new-window'], + }, + port, + static: false, + }); + const loggerWarnSpy = jest.spyOn(server.logger, 'warn'); + + compiler.hooks.done.tap('webpack-dev-server', () => { + server.close(() => { + expect(open).toHaveBeenCalledWith('http://localhost:8117/index.html', { + app: ['google-chrome', '--incognito', '--new-window'], + wait: false, + }); + expect(loggerWarnSpy).toHaveBeenCalledWith( + 'Unable to open "http://localhost:8117/index.html" page in "google-chrome" app with "--incognito --new-window" arguments. If you are running in a headless environment, please do not use the "--open" flag or the "open" option.' + ); + + loggerWarnSpy.mockRestore(); + done(); + }); + }); + + compiler.run(() => {}); + server.listen(port); + }); + + it("should log warning when can't open with object with the 'app' option with arguments", (done) => { + open.mockImplementation(() => Promise.reject()); + + const compiler = webpack(config); + const server = new Server(compiler, { + open: { + target: ['first.html', 'http://localhost:8117/second.html'], + app: ['google-chrome', '--incognito', '--new-window'], + }, + port, + static: false, + }); + const loggerWarnSpy = jest.spyOn(server.logger, 'warn'); + + compiler.hooks.done.tap('webpack-dev-server', () => { + server.close(() => { + expect(open).toHaveBeenNthCalledWith( + 1, + 'http://localhost:8117/first.html', + { + wait: false, + app: ['google-chrome', '--incognito', '--new-window'], + } + ); + expect(open).toHaveBeenNthCalledWith( + 2, + 'http://localhost:8117/second.html', + { + wait: false, + app: ['google-chrome', '--incognito', '--new-window'], + } + ); + expect(loggerWarnSpy).toHaveBeenNthCalledWith( + 1, + 'Unable to open "http://localhost:8117/first.html" page in "google-chrome" app with "--incognito --new-window" arguments. If you are running in a headless environment, please do not use the "--open" flag or the "open" option.' + ); + expect(loggerWarnSpy).toHaveBeenNthCalledWith( + 2, + 'Unable to open "http://localhost:8117/second.html" page in "google-chrome" app with "--incognito --new-window" arguments. If you are running in a headless environment, please do not use the "--open" flag or the "open" option.' + ); + + loggerWarnSpy.mockRestore(); + done(); + }); + }); + + compiler.run(() => {}); + server.listen(port); + }); }); diff --git a/test/server/utils/__snapshots__/runOpen.test.js.snap b/test/server/utils/__snapshots__/runOpen.test.js.snap deleted file mode 100644 index af9cecdd6f..0000000000 --- a/test/server/utils/__snapshots__/runOpen.test.js.snap +++ /dev/null @@ -1,68 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`runOpen util on specify multiple absolute https URLs with pages in Google Chrome 1`] = ` -Array [ - "https://example2.com", - Object { - "app": "Google Chrome", - "wait": false, - }, -] -`; - -exports[`runOpen util on specify multiple absolute https URLs with pages in Google Chrome 2`] = ` -Array [ - "https://example3.com", - Object { - "app": "Google Chrome", - "wait": false, - }, -] -`; - -exports[`runOpen util on specify one relative URL and one absolute URL with pages in Google Chrome 1`] = ` -Array [ - "https://example.com/index.html", - Object { - "app": "Google Chrome", - "wait": false, - }, -] -`; - -exports[`runOpen util on specify one relative URL and one absolute URL with pages in Google Chrome 2`] = ` -Array [ - "https://example2.com", - Object { - "app": "Google Chrome", - "wait": false, - }, -] -`; - -exports[`runOpen util should open browser on specify URL with multiple pages inside array 1`] = ` -Array [ - "https://example.com/index.html", - Object { - "wait": false, - }, -] -`; - -exports[`runOpen util should open browser on specify URL with multiple pages inside array 2`] = ` -Array [ - "https://example.com/index2.html", - Object { - "wait": false, - }, -] -`; - -exports[`runOpen util should open browser on specify URL with page inside array 1`] = ` -Array [ - "https://example.com/index.html", - Object { - "wait": false, - }, -] -`; diff --git a/test/server/utils/runOpen.test.js b/test/server/utils/runOpen.test.js deleted file mode 100644 index 9e3ea64299..0000000000 --- a/test/server/utils/runOpen.test.js +++ /dev/null @@ -1,350 +0,0 @@ -'use strict'; - -const open = require('open'); -const runOpen = require('../../../lib/utils/runOpen'); - -jest.mock('open'); - -describe('runOpen util', () => { - afterEach(() => { - open.mockClear(); - }); - - describe('should open browser', () => { - beforeEach(() => { - open.mockImplementation(() => Promise.resolve()); - }); - - it('on specify URL', () => - runOpen('https://example.com', {}, console).then(() => { - expect(open).toBeCalledWith('https://example.com', { wait: false }); - - expect(open.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "https://example.com", - Object { - "wait": false, - }, - ] - `); - })); - - it('on specify URL with page', () => - runOpen('https://example.com', { openPage: '/index.html' }, console).then( - () => { - expect(open).toBeCalledWith('https://example.com/index.html', { - wait: false, - }); - - expect(open.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "https://example.com/index.html", - Object { - "wait": false, - }, - ] - `); - } - )); - - it('on specify URL with page inside array', () => - runOpen( - 'https://example.com', - { openPage: ['/index.html'] }, - console - ).then(() => { - expect(open).toBeCalledWith('https://example.com/index.html', { - wait: false, - }); - - expect(open.mock.calls[0]).toMatchSnapshot(); - })); - - it('on specify URL with multiple pages inside array', () => - runOpen( - 'https://example.com', - { openPage: ['/index.html', '/index2.html'] }, - console - ).then(() => { - expect(open).toBeCalledWith('https://example.com/index.html', { - wait: false, - }); - expect(open).toBeCalledWith('https://example.com/index2.html', { - wait: false, - }); - - expect(open.mock.calls[0]).toMatchSnapshot(); - expect(open.mock.calls[1]).toMatchSnapshot(); - })); - - it('on specify URL in Google Chrome', () => - runOpen('https://example.com', { open: 'Google Chrome' }, console).then( - () => { - expect(open).toBeCalledWith('https://example.com', { - app: 'Google Chrome', - wait: false, - }); - - expect(open.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "https://example.com", - Object { - "app": "Google Chrome", - "wait": false, - }, - ] - `); - } - )); - - it('on specify URL with page in Google Chrome ', () => - runOpen( - 'https://example.com', - { open: 'Google Chrome', openPage: '/index.html' }, - console - ).then(() => { - expect(open).toBeCalledWith('https://example.com/index.html', { - app: 'Google Chrome', - wait: false, - }); - - expect(open.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "https://example.com/index.html", - Object { - "app": "Google Chrome", - "wait": false, - }, - ] - `); - })); - - it('on specify URL with openPage option only ', () => - runOpen('https://example.com', { openPage: '/index.html' }, console).then( - () => { - expect(open).toBeCalledWith('https://example.com/index.html', { - wait: false, - }); - - expect(open.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "https://example.com/index.html", - Object { - "wait": false, - }, - ] - `); - } - )); - - it('on specify absolute https URL with page in Google Chrome ', () => - runOpen( - 'https://example.com', - { open: 'Google Chrome', openPage: 'https://example2.com' }, - console - ).then(() => { - expect(open).toBeCalledWith('https://example2.com', { - app: 'Google Chrome', - wait: false, - }); - - expect(open.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "https://example2.com", - Object { - "app": "Google Chrome", - "wait": false, - }, - ] - `); - })); - - it('on specify absolute http URL with page in Google Chrome ', () => - runOpen( - 'https://example.com', - { open: 'Google Chrome', openPage: 'http://example2.com' }, - console - ).then(() => { - expect(open).toBeCalledWith('http://example2.com', { - app: 'Google Chrome', - wait: false, - }); - expect(open.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "http://example2.com", - Object { - "app": "Google Chrome", - "wait": false, - }, - ] - `); - })); - }); - - it('on specify multiple absolute https URLs with pages in Google Chrome ', () => - runOpen( - 'https://example.com', - { - open: 'Google Chrome', - openPage: ['https://example2.com', 'https://example3.com'], - }, - console - ).then(() => { - expect(open).toBeCalledWith('https://example2.com', { - app: 'Google Chrome', - wait: false, - }); - expect(open).toBeCalledWith('https://example3.com', { - app: 'Google Chrome', - wait: false, - }); - expect(open.mock.calls[0]).toMatchSnapshot(); - expect(open.mock.calls[1]).toMatchSnapshot(); - })); - - it('on specify one relative URL and one absolute URL with pages in Google Chrome ', () => - runOpen( - 'https://example.com', - { - open: 'Google Chrome', - openPage: ['/index.html', 'https://example2.com'], - }, - console - ).then(() => { - expect(open).toBeCalledWith('https://example.com/index.html', { - app: 'Google Chrome', - wait: false, - }); - expect(open).toBeCalledWith('https://example2.com', { - app: 'Google Chrome', - wait: false, - }); - - expect(open.mock.calls[0]).toMatchSnapshot(); - expect(open.mock.calls[1]).toMatchSnapshot(); - })); - - describe('should not open browser', () => { - const logMock = { warn: jest.fn() }; - - beforeEach(() => { - open.mockImplementation(() => Promise.reject()); - }); - - afterEach(() => { - logMock.warn.mockClear(); - }); - - it('on specify URL and log error', () => - runOpen('https://example.com', {}, logMock).then(() => { - expect(logMock.warn.mock.calls[0][0]).toMatchInlineSnapshot( - `"Unable to open \\"https://example.com\\" in browser. If you are running in a headless environment, please do not use the --open flag"` - ); - expect(open).toBeCalledWith('https://example.com', { wait: false }); - - expect(open.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "https://example.com", - Object { - "wait": false, - }, - ] - `); - })); - - it('on specify URL with page and log error', () => - runOpen('https://example.com', { openPage: '/index.html' }, logMock).then( - () => { - expect(logMock.warn.mock.calls[0][0]).toMatchInlineSnapshot( - `"Unable to open \\"https://example.com/index.html\\" in browser. If you are running in a headless environment, please do not use the --open flag"` - ); - expect(open).toBeCalledWith('https://example.com/index.html', { - wait: false, - }); - - expect(open.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "https://example.com/index.html", - Object { - "wait": false, - }, - ] - `); - } - )); - - it('on specify URL in Google Chrome and log error', () => - runOpen('https://example.com', { open: 'Google Chrome' }, logMock).then( - () => { - expect(logMock.warn.mock.calls[0][0]).toMatchInlineSnapshot( - `"Unable to open \\"https://example.com\\" in browser: \\"Google Chrome\\". If you are running in a headless environment, please do not use the --open flag"` - ); - expect(open).toBeCalledWith('https://example.com', { - app: 'Google Chrome', - wait: false, - }); - - expect(open.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "https://example.com", - Object { - "app": "Google Chrome", - "wait": false, - }, - ] - `); - } - )); - - it('on specify URL with page in Google Chrome and log error ', () => - runOpen( - 'https://example.com', - { open: 'Google Chrome', openPage: '/index.html' }, - logMock - ).then(() => { - expect(logMock.warn.mock.calls[0][0]).toMatchInlineSnapshot( - `"Unable to open \\"https://example.com/index.html\\" in browser: \\"Google Chrome\\". If you are running in a headless environment, please do not use the --open flag"` - ); - expect(open).toBeCalledWith('https://example.com/index.html', { - app: 'Google Chrome', - wait: false, - }); - - expect(open.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "https://example.com/index.html", - Object { - "app": "Google Chrome", - "wait": false, - }, - ] - `); - })); - - it('on specify URL with page in Google Chrome incognito mode and log error ', () => - runOpen( - 'https://example.com', - { - open: { app: ['Google Chrome', '--incognito'] }, - openPage: '/index.html', - }, - logMock - ).then(() => { - expect(open).toBeCalledWith('https://example.com/index.html', { - app: ['Google Chrome', '--incognito'], - }); - - expect(open.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "https://example.com/index.html", - Object { - "app": Array [ - "Google Chrome", - "--incognito", - ], - }, - ] - `); - })); - }); -});