diff --git a/package-lock.json b/package-lock.json index 2abdd222..87b1af4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1756,6 +1756,12 @@ "url-template": "^2.0.8" }, "dependencies": { + "deepmerge": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-3.2.0.tgz", + "integrity": "sha512-6+LuZGU7QCNUnAJyX8cIrlzoEgggTM6B7mm+znKOX4t5ltluT9KLjN6g61ECMS0LTsLW7yDpNoxhix5FZcrIow==", + "dev": true + }, "is-plain-object": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz", @@ -2186,6 +2192,7 @@ "version": "1.17.0", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", + "dev": true, "requires": { "@types/connect": "*", "@types/node": "*" @@ -2217,10 +2224,20 @@ "@types/node": "*" } }, + "@types/colors": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/colors/-/colors-1.2.1.tgz", + "integrity": "sha512-7jNkpfN2lVO07nJ1RWzyMnNhH/I5N9iWuMPx9pedptxJ4MODf8rRV0lbJi6RakQ4sKQk231Fw4e2W9n3D7gZ3w==", + "dev": true, + "requires": { + "colors": "*" + } + }, "@types/connect": { "version": "3.4.32", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", + "dev": true, "requires": { "@types/node": "*" } @@ -2249,6 +2266,15 @@ "@types/node": "*" } }, + "@types/deepmerge": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/deepmerge/-/deepmerge-2.2.0.tgz", + "integrity": "sha512-FEQYDHh6+Q+QXKSrIY46m+/lAmAj/bk4KpLaam+hArmzaVpMBHLcfwOH2Q2UOkWM7XsdY9PmZpGyPAjh/JRGhQ==", + "dev": true, + "requires": { + "deepmerge": "*" + } + }, "@types/events": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", @@ -2258,6 +2284,7 @@ "version": "4.17.0", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.0.tgz", "integrity": "sha512-CjaMu57cjgjuZbh9DpkloeGxV45CnMGlVd+XpG7Gm9QgVrd7KFq+X4HY0vM+2v0bczS48Wg7bvnMY5TN+Xmcfw==", + "dev": true, "requires": { "@types/body-parser": "*", "@types/express-serve-static-core": "*", @@ -2268,6 +2295,7 @@ "version": "4.16.6", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.6.tgz", "integrity": "sha512-8wr3CA/EMybyb6/V8qvTRKiNkPmgUA26uA9XWD6hlA0yFDuqi4r2L0C2B0U2HAYltJamoYJszlkaWM31vrKsHg==", + "dev": true, "requires": { "@types/node": "*", "@types/range-parser": "*" @@ -2304,7 +2332,8 @@ "@types/js-yaml": { "version": "3.12.1", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.1.tgz", - "integrity": "sha512-SGGAhXLHDx+PK4YLNcNGa6goPf9XRWQNAUUbffkwVGGXIxmDKWyGGL4inzq2sPmExu431Ekb9aEMn9BkPqEYFA==" + "integrity": "sha512-SGGAhXLHDx+PK4YLNcNGa6goPf9XRWQNAUUbffkwVGGXIxmDKWyGGL4inzq2sPmExu431Ekb9aEMn9BkPqEYFA==", + "dev": true }, "@types/lodash": { "version": "4.14.123", @@ -2315,7 +2344,8 @@ "@types/mime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", - "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==" + "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==", + "dev": true }, "@types/minimatch": { "version": "3.0.3", @@ -2352,6 +2382,7 @@ "version": "1.12.4", "resolved": "https://registry.npmjs.org/@types/puppeteer/-/puppeteer-1.12.4.tgz", "integrity": "sha512-aaGbJaJ9TuF9vZfTeoh876sBa+rYJWPwtsmHmYr28pGr42ewJnkDTq2aeSKEmS39SqUdkwLj73y/d7rBSp7mDQ==", + "dev": true, "requires": { "@types/node": "*" } @@ -2359,12 +2390,14 @@ "@types/range-parser": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", - "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", + "dev": true }, "@types/serve-static": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz", "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==", + "dev": true, "requires": { "@types/express-serve-static-core": "*", "@types/mime": "*" @@ -3491,7 +3524,7 @@ }, "browserify-aes": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "dev": true, "requires": { @@ -3536,7 +3569,7 @@ }, "browserify-rsa": { "version": "4.0.1", - "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "dev": true, "requires": { @@ -4572,7 +4605,7 @@ }, "create-hash": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "dev": true, "requires": { @@ -4585,7 +4618,7 @@ }, "create-hmac": { "version": "1.1.7", - "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "dev": true, "requires": { @@ -4774,10 +4807,9 @@ "dev": true }, "deepmerge": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-3.2.0.tgz", - "integrity": "sha512-6+LuZGU7QCNUnAJyX8cIrlzoEgggTM6B7mm+znKOX4t5ltluT9KLjN6g61ECMS0LTsLW7yDpNoxhix5FZcrIow==", - "dev": true + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-3.2.1.tgz", + "integrity": "sha512-+hbDSzTqEW0fWgnlKksg7XAOtT+ddZS5lHZJ6f6MdixRs9wQy+50fm1uUCVb1IkvjLUYX/SfFO021ZNwriURTw==" }, "define-properties": { "version": "1.1.3", @@ -4933,7 +4965,7 @@ }, "diffie-hellman": { "version": "5.0.3", - "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", "dev": true, "requires": { @@ -7974,7 +8006,7 @@ }, "is-obj": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "dev": true }, @@ -9239,7 +9271,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "requires": { "minimist": "0.0.8" @@ -15277,7 +15309,7 @@ }, "sha.js": { "version": "2.4.11", - "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "dev": true, "requires": { @@ -15296,7 +15328,7 @@ }, "shasum": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz", "integrity": "sha1-5wEjENj0F/TetXEhUOVni4euVl8=", "dev": true, "requires": { @@ -16253,7 +16285,7 @@ }, "through": { "version": "2.3.8", - "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, diff --git a/package.json b/package.json index 43061d2c..ac763a8c 100644 --- a/package.json +++ b/package.json @@ -22,14 +22,12 @@ "@oclif/config": "^1", "@oclif/plugin-help": "^2", "@oclif/plugin-not-found": "^1.2", - "@types/express": "^4.16.0", - "@types/js-yaml": "^3.11.2", - "@types/puppeteer": "^1.6.0", "axios": "^0.18.1", "body-parser": "^1.18.3", "colors": "^1.3.2", "cors": "^2.8.4", "cross-spawn": "^6.0.5", + "deepmerge": "^3.2.1", "express": "^4.16.3", "generic-pool": "^3.7.1", "globby": "^9.2.0", @@ -54,13 +52,18 @@ "@types/chai": "^4.1.4", "@types/chai-http": "^3.0.5", "@types/cheerio": "^0.22.11", + "@types/colors": "^1.2.1", "@types/cors": "^2.8.4", + "@types/deepmerge": "^2.2.0", + "@types/express": "^4.16.0", "@types/cross-spawn": "^6.0.0", "@types/generic-pool": "^3.1.9", "@types/http-server": "^0.10.0", + "@types/js-yaml": "^3.11.2", "@types/mocha": "^5.2.5", "@types/nock": "^10.0.2", "@types/node": "^12.0.0", + "@types/puppeteer": "^1.6.0", "@types/sinon": "^7.0.12", "@types/sinon-chai": "^3.2.0", "babel-loader": "^8.0.6", diff --git a/src/commands/exec.ts b/src/commands/exec.ts index befe3909..26af05f4 100644 --- a/src/commands/exec.ts +++ b/src/commands/exec.ts @@ -1,6 +1,7 @@ import {flags} from '@oclif/command' import * as spawn from 'cross-spawn' -import Constants from '../services/constants' +import { DEFAULT_CONFIGURATION } from '../configuration/configuration' +import ConfigurationService from '../services/configuration-service' import PercyCommand from './percy-command' export default class Exec extends PercyCommand { @@ -16,12 +17,12 @@ export default class Exec extends PercyCommand { static flags = { 'network-idle-timeout': flags.integer({ char: 't', - default: Constants.NETWORK_IDLE_TIMEOUT, + default: DEFAULT_CONFIGURATION.agent['asset-discovery']['network-idle-timeout'], description: 'asset discovery network idle timeout (in milliseconds)', }), 'port': flags.integer({ char: 'p', - default: Constants.PORT, + default: DEFAULT_CONFIGURATION.agent.port, description: 'port', }), } @@ -32,8 +33,6 @@ export default class Exec extends PercyCommand { const {argv} = this.parse(Exec) const {flags} = this.parse(Exec) - const port = flags.port as number - const networkIdleTimeout = flags['network-idle-timeout'] as number const command = argv.shift() if (!command) { @@ -44,7 +43,8 @@ export default class Exec extends PercyCommand { } if (this.percyWillRun()) { - await this.agentService.start({port, networkIdleTimeout}) + const configuration = new ConfigurationService().applyFlags(flags) + await this.agentService.start(configuration) this.logStart() } diff --git a/src/commands/health-check.ts b/src/commands/health-check.ts index b34a23d1..110d980e 100644 --- a/src/commands/health-check.ts +++ b/src/commands/health-check.ts @@ -1,6 +1,5 @@ import {Command, flags} from '@oclif/command' -import * as colors from 'colors' -import Constants from '../services/constants' +import {DEFAULT_CONFIGURATION} from '../configuration/configuration' import healthCheck from '../utils/health-checker' export default class HealthCheck extends Command { @@ -10,7 +9,7 @@ export default class HealthCheck extends Command { static flags = { port: flags.integer({ char: 'p', - default: Constants.PORT, + default: DEFAULT_CONFIGURATION.agent.port, description: 'port', }), } diff --git a/src/commands/percy-command.ts b/src/commands/percy-command.ts index e8a61fcf..941ac277 100644 --- a/src/commands/percy-command.ts +++ b/src/commands/percy-command.ts @@ -1,6 +1,6 @@ import {Command} from '@oclif/command' import * as winston from 'winston' -import AgentService from '../services/agent-service' +import {AgentService} from '../services/agent-service' import ProcessService from '../services/process-service' import logger from '../utils/logger' diff --git a/src/commands/snapshot.ts b/src/commands/snapshot.ts index 96b1e63f..2b9e15ac 100644 --- a/src/commands/snapshot.ts +++ b/src/commands/snapshot.ts @@ -1,8 +1,6 @@ import {flags} from '@oclif/command' -import configuration from '../configuration/configuration' -import {StaticSnapshotsConfiguration} from '../configuration/static-snapshots-configuration' -import Constants from '../services/constants' -import {StaticSnapshotOptions} from '../services/static-snapshot-options' +import { DEFAULT_CONFIGURATION } from '../configuration/configuration' +import ConfigurationService from '../services/configuration-service' import StaticSnapshotService from '../services/static-snapshot-service' import logger from '../utils/logger' import PercyCommand from './percy-command' @@ -27,28 +25,28 @@ export default class Snapshot extends PercyCommand { 'snapshot-files': flags.string({ char: 's', description: 'Glob or comma-seperated string of globs for matching the files and directories to snapshot.', - default: '**/*.html,**/*.htm', + default: DEFAULT_CONFIGURATION['static-snapshots']['snapshot-files'], }), 'ignore-files': flags.string({ char: 'i', description: 'Glob or comma-seperated string of globs for matching the files and directories to ignore.', - default: '', + default: DEFAULT_CONFIGURATION['static-snapshots']['ignore-files'], }), 'base-url': flags.string({ char: 'b', description: 'If your static files will be hosted in a subdirectory, instead \n' + 'of the webserver\'s root path, set that subdirectory with this flag.', - default: '/', + default: DEFAULT_CONFIGURATION['static-snapshots']['base-url'], }), // from exec command. needed to start the agent service. 'network-idle-timeout': flags.integer({ char: 't', - default: Constants.NETWORK_IDLE_TIMEOUT, + default: DEFAULT_CONFIGURATION.agent['asset-discovery']['network-idle-timeout'], description: 'Asset discovery network idle timeout (in milliseconds)', }), 'port': flags.integer({ char: 'p', - default: Constants.PORT, + default: DEFAULT_CONFIGURATION.agent.port, description: 'Port', }), } @@ -58,28 +56,15 @@ export default class Snapshot extends PercyCommand { const {args, flags} = this.parse(Snapshot) - const snapshotDirectory = args.snapshotDirectory as string - const port = flags.port as number - const staticServerPort = port + 1 - const networkIdleTimeout = flags['network-idle-timeout'] as number - - const baseUrlFlag = flags['base-url'] as string - const rawIgnoreGlobFlag = flags['ignore-files'] as string - const rawSnapshotGlobFlag = flags['snapshot-files'] as string + const configurationService = new ConfigurationService() + configurationService.applyFlags(flags) + configurationService.applyArgs(args) + const configuration = configurationService.configuration // exit gracefully if percy will not run if (!this.percyWillRun()) { this.exit(0) } - // read configurations from the percy.yml file - const conf = (configuration()['static-snapshots'] || {}) as StaticSnapshotsConfiguration - const baseUrl = conf['base-url'] || baseUrlFlag - const rawSnapshotFiles = conf['snapshot-files'] || rawSnapshotGlobFlag - const rawIgnoreFiles = conf['ignore-files'] || rawIgnoreGlobFlag - - const snapshotGlobs = rawSnapshotFiles.split(',') - - // if it is an empty string then convert it to an empty array instead of an array of an empty string - const ignoreGlobs = rawIgnoreFiles ? rawIgnoreFiles.split(',') : [] + const baseUrl = configuration['static-snapshots']['base-url'] // check that base url starts with a slash and exit if it is missing if (baseUrl[0] !== '/') { @@ -87,19 +72,10 @@ export default class Snapshot extends PercyCommand { this.exit(1) } - // start the agent service - await this.agentService.start({port, networkIdleTimeout}) + await this.agentService.start(configuration) this.logStart() - const options: StaticSnapshotOptions = { - port: staticServerPort, - snapshotDirectory, - baseUrl, - snapshotGlobs, - ignoreGlobs, - } - - const staticSnapshotService = new StaticSnapshotService(options) + const staticSnapshotService = new StaticSnapshotService(configuration['static-snapshots']) // start the snapshot service await staticSnapshotService.start() diff --git a/src/commands/start.ts b/src/commands/start.ts index bfee6b61..b9405063 100644 --- a/src/commands/start.ts +++ b/src/commands/start.ts @@ -1,7 +1,7 @@ import {flags} from '@oclif/command' import * as path from 'path' -import {AgentOptions} from '../services/agent-options' -import Constants from '../services/constants' +import { DEFAULT_CONFIGURATION } from '../configuration/configuration' +import ConfigurationService from '../services/configuration-service' import healthCheck from '../utils/health-checker' import PercyCommand from './percy-command' @@ -11,7 +11,7 @@ export default class Start extends PercyCommand { static examples = [ '$ percy start\n' + - `info: percy has started on port ${Constants.PORT}.`, + `info: percy has started on port ${DEFAULT_CONFIGURATION.agent.port}.`, ] static flags = { @@ -21,12 +21,12 @@ export default class Start extends PercyCommand { }), 'network-idle-timeout': flags.integer({ char: 't', - default: Constants.NETWORK_IDLE_TIMEOUT, + default: DEFAULT_CONFIGURATION.agent['asset-discovery']['network-idle-timeout'], description: 'asset discovery network idle timeout (in milliseconds)', }), 'port': flags.integer({ char: 'p', - default: Constants.PORT, + default: DEFAULT_CONFIGURATION.agent.port, description: 'port', }), } @@ -38,19 +38,19 @@ export default class Start extends PercyCommand { if (!this.percyWillRun()) { this.exit(0) } const {flags} = this.parse(Start) - const port = flags.port as number - const networkIdleTimeout = flags['network-idle-timeout'] as number if (flags.detached) { - this.runDetached({port, networkIdleTimeout}) + this.runDetached() } else { - await this.runAttached({port, networkIdleTimeout}) + await this.runAttached() } - await healthCheck(port) + await healthCheck(flags.port!) } - private async runAttached(options: AgentOptions = {}) { + private async runAttached() { + const {flags} = this.parse(Start) + process.on('SIGHUP', async () => { await this.agentService.stop() process.exit(0) @@ -66,17 +66,20 @@ export default class Start extends PercyCommand { process.exit(0) }) - await this.agentService.start(options) + const configuration = new ConfigurationService().applyFlags(flags) + await this.agentService.start(configuration) this.logStart() } - private runDetached(options: AgentOptions = {}) { + private runDetached() { + const {flags} = this.parse(Start) + const pid = this.processService.runDetached( [ path.resolve(`${__dirname}/../../bin/run`), 'start', - '-p', String(options.port), - '-t', String(options.networkIdleTimeout), + '-p', String(flags.port!), + '-t', String(flags['network-idle-timeout']), ], ) diff --git a/src/commands/stop.ts b/src/commands/stop.ts index 27f6c06f..68652cf1 100644 --- a/src/commands/stop.ts +++ b/src/commands/stop.ts @@ -1,6 +1,8 @@ import {flags} from '@oclif/command' import Axios from 'axios' -import Constants from '../services/constants' +import {DEFAULT_CONFIGURATION} from '../configuration/configuration' +import {STOP_PATH} from '../services/agent-service-constants' +import ConfigurationService from '../services/configuration-service' import {logError} from '../utils/logger' import PercyCommand from './percy-command' @@ -16,7 +18,7 @@ export default class Stop extends PercyCommand { static flags = { port: flags.integer({ char: 'p', - default: Constants.PORT, + default: DEFAULT_CONFIGURATION.agent.port, description: 'port', }), } @@ -28,10 +30,10 @@ export default class Stop extends PercyCommand { if (!this.percyWillRun()) { this.exit(0) } const {flags} = this.parse(Stop) - const port = flags.port ? flags.port : Constants.PORT + const configuration = new ConfigurationService().applyFlags(flags).agent if (this.processService.isRunning()) { - await this.postToRunningAgent(Constants.STOP_PATH, port) + await this.postToRunningAgent(STOP_PATH, configuration.port) } else { this.logger.warn('percy is already stopped.') } diff --git a/src/configuration/agent-configuration.ts b/src/configuration/agent-configuration.ts new file mode 100644 index 00000000..e89bc8b4 --- /dev/null +++ b/src/configuration/agent-configuration.ts @@ -0,0 +1,6 @@ +import { AssetDiscoveryConfiguration } from './asset-discovery-configuration' + +export interface AgentConfiguration { + 'port': number, + 'asset-discovery': AssetDiscoveryConfiguration +} diff --git a/src/configuration/asset-discovery-configuration.ts b/src/configuration/asset-discovery-configuration.ts new file mode 100644 index 00000000..e5f04f04 --- /dev/null +++ b/src/configuration/asset-discovery-configuration.ts @@ -0,0 +1,5 @@ +export interface AssetDiscoveryConfiguration { + 'network-idle-timeout': number, + 'page-pool-size-min': number, + 'page-pool-size-max': number +} diff --git a/src/configuration/configuration.ts b/src/configuration/configuration.ts index 3f105473..d05c4736 100644 --- a/src/configuration/configuration.ts +++ b/src/configuration/configuration.ts @@ -1,6 +1,5 @@ -import * as fs from 'fs' -import * as yaml from 'js-yaml' -import * as path from 'path' +import { DEFAULT_PORT } from '../services/agent-service-constants' +import { AgentConfiguration } from './agent-configuration' import { SnapshotConfiguration } from './snapshot-configuration' import { StaticSnapshotsConfiguration } from './static-snapshots-configuration' @@ -8,25 +7,28 @@ export interface Configuration { version: number, snapshot: SnapshotConfiguration 'static-snapshots': StaticSnapshotsConfiguration + agent: AgentConfiguration } -const configuration = (relativePath = '.percy.yml'): Configuration => { - const configFilePath = path.join(process.cwd(), relativePath) - - try { - return yaml.safeLoad(fs.readFileSync(configFilePath, 'utf8')) - } catch { - // this is ok because we just use this configuration as one of the fallbacks - // in a chain. snapshot specific options -> agent configuration -> default values - - const defaultConfiguration: Configuration = { - 'version': 1.0, - 'snapshot': {}, - 'static-snapshots': {}, - } - - return defaultConfiguration - } +export const DEFAULT_CONFIGURATION: Configuration = { + 'version': 1.0, + 'snapshot': { + 'widths': [1280, 375], // px + 'min-height': 1024, // px + }, + 'agent': { + 'port': DEFAULT_PORT, + 'asset-discovery': { + 'network-idle-timeout': 50, // ms + 'page-pool-size-min': 1, // pages + 'page-pool-size-max': 5, // pages + }, + }, + 'static-snapshots': { + 'path': '.', + 'base-url': '/', + 'snapshot-files': '**/*.html,**/*.htm', + 'ignore-files': '', + 'port': DEFAULT_PORT + 1, + }, } - -export default configuration diff --git a/src/configuration/snapshot-configuration.ts b/src/configuration/snapshot-configuration.ts index 10a11811..1e4f0028 100644 --- a/src/configuration/snapshot-configuration.ts +++ b/src/configuration/snapshot-configuration.ts @@ -1,4 +1,4 @@ export interface SnapshotConfiguration { - widths?: [number], - 'min-height'?: number, + widths: number[], + 'min-height': number, } diff --git a/src/configuration/static-snapshots-configuration.ts b/src/configuration/static-snapshots-configuration.ts index c323338e..3ecf259d 100644 --- a/src/configuration/static-snapshots-configuration.ts +++ b/src/configuration/static-snapshots-configuration.ts @@ -1,5 +1,7 @@ export interface StaticSnapshotsConfiguration { - 'base-url'?: string, - 'snapshot-files'?: string, - 'ignore-files'?: string, + path: string, + port: number, + 'base-url': string, + 'snapshot-files': string, + 'ignore-files': string, } diff --git a/src/percy-agent-client/percy-agent-client.ts b/src/percy-agent-client/percy-agent-client.ts index 10c4a5a2..81a20857 100644 --- a/src/percy-agent-client/percy-agent-client.ts +++ b/src/percy-agent-client/percy-agent-client.ts @@ -1,4 +1,4 @@ -import Constants from '../services/constants' +import {HEALTHCHECK_PATH} from '../services/agent-service-constants' export class PercyAgentClient { xhr: XMLHttpRequest @@ -24,7 +24,7 @@ export class PercyAgentClient { healthCheck() { try { - this.xhr.open('get', `${this.agentHost}${Constants.HEALTHCHECK_PATH}`, false) + this.xhr.open('get', `${this.agentHost}${HEALTHCHECK_PATH}`, false) this.xhr.onload = () => { if (this.xhr.status === 200) { this.agentConnected = true diff --git a/src/percy-agent-client/percy-agent.ts b/src/percy-agent-client/percy-agent.ts index ea704a83..448549b2 100644 --- a/src/percy-agent-client/percy-agent.ts +++ b/src/percy-agent-client/percy-agent.ts @@ -1,4 +1,4 @@ -import Constants from '../services/constants' +import {DEFAULT_PORT, SNAPSHOT_PATH} from '../services/agent-service-constants' import {ClientOptions} from './client-options' import DOM from './dom' import {PercyAgentClient} from './percy-agent-client' @@ -19,7 +19,7 @@ export default class PercyAgent { // Default to 'true' unless explicitly disabled. this.handleAgentCommunication = options.handleAgentCommunication !== false this.domTransformation = options.domTransformation || null - this.port = options.port || Constants.PORT + this.port = options.port || DEFAULT_PORT if (this.handleAgentCommunication) { this.xhr = options.xhr || XMLHttpRequest @@ -35,7 +35,7 @@ export default class PercyAgent { const domSnapshot = this.domSnapshot(documentObject, options) if (this.handleAgentCommunication && this.client) { - this.client.post(Constants.SNAPSHOT_PATH, { + this.client.post(SNAPSHOT_PATH, { name, url: documentObject.URL, // enableJavascript is deprecated. Use enableJavaScript diff --git a/src/services/agent-options.ts b/src/services/agent-options.ts deleted file mode 100644 index c17096d3..00000000 --- a/src/services/agent-options.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface AgentOptions { - port?: number, - networkIdleTimeout?: number -} diff --git a/src/services/agent-service-constants.ts b/src/services/agent-service-constants.ts new file mode 100644 index 00000000..14c1169a --- /dev/null +++ b/src/services/agent-service-constants.ts @@ -0,0 +1,5 @@ +export const DEFAULT_PORT: number = 5338 // BEES + +export const SNAPSHOT_PATH = '/percy/snapshot' +export const STOP_PATH = '/percy/stop' +export const HEALTHCHECK_PATH = '/percy/healthcheck' diff --git a/src/services/agent-service.ts b/src/services/agent-service.ts index 1ee55305..b80129c0 100644 --- a/src/services/agent-service.ts +++ b/src/services/agent-service.ts @@ -2,17 +2,17 @@ import * as bodyParser from 'body-parser' import * as cors from 'cors' import * as express from 'express' import {Server} from 'http' -import configuration from '../configuration/configuration' -import {SnapshotConfiguration} from '../configuration/snapshot-configuration' +import { Configuration } from '../configuration/configuration' import {SnapshotOptions} from '../percy-agent-client/snapshot-options' import logger, {profile} from '../utils/logger' -import {AgentOptions} from './agent-options' +import {HEALTHCHECK_PATH, SNAPSHOT_PATH, STOP_PATH} from './agent-service-constants' import BuildService from './build-service' +import ConfigurationService from './configuration-service' import Constants from './constants' import ProcessService from './process-service' import SnapshotService from './snapshot-service' -export default class AgentService { +export class AgentService { buildService: BuildService snapshotService: SnapshotService | null = null @@ -30,20 +30,23 @@ export default class AgentService { this.app.use(bodyParser.json({limit: '50mb'})) this.app.use(express.static(this.publicDirectory)) - this.app.post(Constants.SNAPSHOT_PATH, this.handleSnapshot.bind(this)) - this.app.post(Constants.STOP_PATH, this.handleStop.bind(this)) - - this.app.get(Constants.HEALTHCHECK_PATH, this.handleHealthCheck.bind(this)) + this.app.post(SNAPSHOT_PATH, this.handleSnapshot.bind(this)) + this.app.post(STOP_PATH, this.handleStop.bind(this)) + this.app.get(HEALTHCHECK_PATH, this.handleHealthCheck.bind(this)) this.buildService = new BuildService() } - async start(options: AgentOptions = {}) { + async start(configuration: Configuration) { this.buildId = await this.buildService.create() if (this.buildId !== null) { - this.server = this.app.listen(options.port) - this.snapshotService = new SnapshotService(this.buildId, {networkIdleTimeout: options.networkIdleTimeout}) + this.server = this.app.listen(configuration.agent.port) + this.snapshotService = new SnapshotService( + this.buildId, + configuration.agent['asset-discovery'], + ) + await this.snapshotService.assetDiscoveryService.setup() return } @@ -86,11 +89,11 @@ export default class AgentService { if (!this.snapshotService) { return response.json({success: false}) } - const snapshotConfiguration = (configuration().snapshot || {}) as SnapshotConfiguration + const configuration = new ConfigurationService().configuration const snapshotOptions: SnapshotOptions = { - widths: request.body.widths || snapshotConfiguration.widths, + widths: request.body.widths || configuration.snapshot.widths, enableJavaScript: request.body.enableJavaScript, - minHeight: request.body.minHeight || snapshotConfiguration['min-height'], + minHeight: request.body.minHeight || configuration.snapshot['min-height'], } const domSnapshot = request.body.domSnapshot diff --git a/src/services/asset-discovery-service.ts b/src/services/asset-discovery-service.ts index a51cdb83..b68eb380 100644 --- a/src/services/asset-discovery-service.ts +++ b/src/services/asset-discovery-service.ts @@ -1,46 +1,38 @@ import * as pool from 'generic-pool' import * as puppeteer from 'puppeteer' +import { AssetDiscoveryConfiguration } from '../configuration/asset-discovery-configuration' +import { DEFAULT_CONFIGURATION } from '../configuration/configuration' import { SnapshotOptions } from '../percy-agent-client/snapshot-options' import logger, {logError, profile} from '../utils/logger' import waitForNetworkIdle from '../utils/wait-for-network-idle' import PercyClientService from './percy-client-service' import ResponseService from './response-service' -interface AssetDiscoveryOptions { - networkIdleTimeout?: number -} - -const DEFAULT_PAGE_POOL_SIZE = process.env.PERCY_POOL_SIZE +export const MAX_SNAPSHOT_WIDTHS: number = 10 -export default class AssetDiscoveryService extends PercyClientService { +export class AssetDiscoveryService extends PercyClientService { responseService: ResponseService browser: puppeteer.Browser | null pagePool: pool.Pool | null - readonly DEFAULT_NETWORK_IDLE_TIMEOUT: number = 50 // ms - networkIdleTimeout: number // ms - - readonly MAX_SNAPSHOT_WIDTHS: number = 10 - readonly PAGE_POOL_SIZE_MIN: number = 2 - readonly PAGE_POOL_SIZE_MAX: number = DEFAULT_PAGE_POOL_SIZE ? parseInt(DEFAULT_PAGE_POOL_SIZE) : 10 + configuration: AssetDiscoveryConfiguration - // Default widths to use for asset discovery. Must match Percy service defaults. - readonly DEFAULT_WIDTHS: number[] = [1280, 375] - - constructor(buildId: number, options: AssetDiscoveryOptions = {}) { + constructor(buildId: number, configuration?: AssetDiscoveryConfiguration) { super() this.responseService = new ResponseService(buildId) - this.networkIdleTimeout = options.networkIdleTimeout || this.DEFAULT_NETWORK_IDLE_TIMEOUT this.browser = null this.pagePool = null + this.configuration = configuration || DEFAULT_CONFIGURATION.agent['asset-discovery'] } async setup() { profile('-> assetDiscoveryService.setup') + const browser = this.browser = await this.createBrowser() this.pagePool = await this.createPagePool(() => { return this.createPage(browser) - }, this.PAGE_POOL_SIZE_MIN, this.PAGE_POOL_SIZE_MAX) + }, this.configuration['page-pool-size-min'], + this.configuration['page-pool-size-max']) profile('-> assetDiscoveryService.setup') } @@ -96,8 +88,8 @@ export default class AssetDiscoveryService extends PercyClientService { return [] } - if (options.widths && options.widths.length > this.MAX_SNAPSHOT_WIDTHS) { - logger.error(`Too many widths requested. Max is ${this.MAX_SNAPSHOT_WIDTHS}. Requested: ${options.widths}`) + if (options.widths && options.widths.length > MAX_SNAPSHOT_WIDTHS) { + logger.error(`Too many widths requested. Max is ${MAX_SNAPSHOT_WIDTHS}. Requested: ${options.widths}`) return [] } @@ -106,7 +98,7 @@ export default class AssetDiscoveryService extends PercyClientService { logger.debug(`discovering assets for URL: ${rootResourceUrl}`) const enableJavaScript = options.enableJavaScript || false - const widths = options.widths || this.DEFAULT_WIDTHS + const widths = options.widths || DEFAULT_CONFIGURATION.snapshot.widths // Do asset discovery for each requested width in parallel. We don't keep track of which page // is doing work, and instead rely on the fact that we always have fewer widths to work on than @@ -223,7 +215,7 @@ export default class AssetDiscoveryService extends PercyClientService { profile('--> assetDiscoveryService.page.goto') profile('--> assetDiscoveryService.waitForNetworkIdle') - await waitForNetworkIdle(page, this.networkIdleTimeout) + await waitForNetworkIdle(page, this.configuration['network-idle-timeout']) profile('--> assetDiscoveryService.waitForNetworkIdle') profile('--> assetDiscoveryServer.waitForResourceProcessing') diff --git a/src/services/configuration-service.ts b/src/services/configuration-service.ts new file mode 100644 index 00000000..cdd58082 --- /dev/null +++ b/src/services/configuration-service.ts @@ -0,0 +1,67 @@ +import * as deepmerge from 'deepmerge' +import * as fs from 'fs' +import * as yaml from 'js-yaml' +import * as path from 'path' +import logger from '../utils/logger' +import { Configuration, DEFAULT_CONFIGURATION } from './../configuration/configuration' + +export default class ConfigurationService { + static DEFAULT_FILE = '.percy.yml' + + configuration: Configuration + + constructor(configurationFile: string = ConfigurationService.DEFAULT_FILE) { + // We start with the default configuration + this.configuration = DEFAULT_CONFIGURATION + + // Next we merge in configuration from .percy.yml if we have it + this.applyFile(configurationFile) + } + + applyFile(configurationFile: string): Configuration { + try { + const userConfigFilePath = path.join(process.cwd(), configurationFile) + const userConf = yaml.safeLoad(fs.readFileSync(userConfigFilePath, 'utf8')) + + // apply a deep overwrite merge to userConf and this.configuration + const overwriteMerge = (destinationArray: any, sourceArray: any, options: any) => sourceArray + this.configuration = deepmerge(this.configuration, userConf, { arrayMerge: overwriteMerge }) + } catch { + logger.debug('.percy.yml configuration file not supplied or failed to be loaded and parsed.') + } + + return this.configuration + } + + applyFlags(flags: any): Configuration { + if (flags.port) { + this.configuration.agent.port = flags.port + } + + if (flags['network-idle-timeout']) { + this.configuration.agent['asset-discovery']['network-idle-timeout'] = flags['network-idle-timeout'] + } + + if (flags['base-url']) { + this.configuration['static-snapshots']['base-url'] = flags['base-url'] + } + + if (flags['snapshot-files']) { + this.configuration['static-snapshots']['snapshot-files'] = flags['snapshot-files'] + } + + if (flags['ignore-files']) { + this.configuration['static-snapshots']['ignore-files'] = flags['ignore-files'] + } + + return this.configuration + } + + applyArgs(args: any): Configuration { + if (args.snapshotDirectory) { + this.configuration['static-snapshots'].path = args.snapshotDirectory + } + + return this.configuration + } +} diff --git a/src/services/constants.ts b/src/services/constants.ts index 286e4a28..9d2d0817 100644 --- a/src/services/constants.ts +++ b/src/services/constants.ts @@ -1,12 +1,4 @@ export default class Constants { - static readonly PORT: number = 5338 - static readonly NETWORK_IDLE_TIMEOUT: number = 50 // in milliseconds - - // Agent Service paths - static readonly SNAPSHOT_PATH = '/percy/snapshot' - static readonly STOP_PATH = '/percy/stop' - static readonly HEALTHCHECK_PATH = '/percy/healthcheck' - static readonly MAX_FILE_SIZE_BYTES = 15728640 // 15MB static readonly MAX_LOG_LENGTH = 1024 } diff --git a/src/services/snapshot-service.ts b/src/services/snapshot-service.ts index 4501e9ef..15601998 100644 --- a/src/services/snapshot-service.ts +++ b/src/services/snapshot-service.ts @@ -1,29 +1,22 @@ -import { SnapshotOptions } from '../percy-agent-client/snapshot-options' +import { AssetDiscoveryConfiguration } from '../configuration/asset-discovery-configuration' +import {SnapshotOptions} from '../percy-agent-client/snapshot-options' import {logError, profile} from '../utils/logger' -import AssetDiscoveryService from './asset-discovery-service' +import {AssetDiscoveryService} from './asset-discovery-service' import PercyClientService from './percy-client-service' import ResourceService from './resource-service' -interface SnapshotServiceOptions { - networkIdleTimeout?: number -} - export default class SnapshotService extends PercyClientService { assetDiscoveryService: AssetDiscoveryService resourceService: ResourceService buildId: number - constructor(buildId: number, options: SnapshotServiceOptions = {}) { + constructor(buildId: number, configuration?: AssetDiscoveryConfiguration) { super() this.buildId = buildId - this.assetDiscoveryService = new AssetDiscoveryService( - buildId, - {networkIdleTimeout: options.networkIdleTimeout}, - ) - this.resourceService = new ResourceService(buildId) + this.assetDiscoveryService = new AssetDiscoveryService(buildId, configuration) } async buildResources( diff --git a/src/services/static-snapshot-options.ts b/src/services/static-snapshot-options.ts deleted file mode 100644 index c6b2882c..00000000 --- a/src/services/static-snapshot-options.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface StaticSnapshotOptions { - snapshotDirectory: string, - port: number, - baseUrl: string, - snapshotGlobs: string[], - ignoreGlobs: string[], - } diff --git a/src/services/static-snapshot-service.ts b/src/services/static-snapshot-service.ts index 0a95ba6e..657ea49b 100644 --- a/src/services/static-snapshot-service.ts +++ b/src/services/static-snapshot-service.ts @@ -4,32 +4,33 @@ import * as express from 'express' import * as globby from 'globby' import {Server} from 'http' import * as puppeteer from 'puppeteer' +import { DEFAULT_CONFIGURATION } from '../configuration/configuration' +import { StaticSnapshotsConfiguration } from '../configuration/static-snapshots-configuration' import logger from '../utils/logger' import {agentJsFilename} from '../utils/sdk-utils' -import {StaticSnapshotOptions} from './static-snapshot-options' // Use this instead of importing PercyAgent - we only want the compiled version declare var PercyAgent: any export default class StaticSnapshotService { - readonly options: StaticSnapshotOptions + readonly configuration: StaticSnapshotsConfiguration private readonly app: express.Application private server: Server | null = null - constructor(options: StaticSnapshotOptions) { + constructor(configuration?: StaticSnapshotsConfiguration) { this.app = express() - this.options = options + this.configuration = configuration || DEFAULT_CONFIGURATION['static-snapshots'] this.app.use(cors()) this.app.use(bodyParser.urlencoded({extended: true})) this.app.use(bodyParser.json({limit: '50mb'})) - this.app.use(options.baseUrl, express.static(options.snapshotDirectory)) + this.app.use(this.configuration['base-url'], express.static(this.configuration.path)) } async start() { logger.info(`serving static site at ${this._buildLocalUrl()}`) - this.server = await this.app.listen(this.options.port) + this.server = await this.app.listen(this.configuration.port) } async snapshotAll() { @@ -73,19 +74,27 @@ export default class StaticSnapshotService { } _buildLocalUrl() { - return `http://localhost:${this.options.port}${this.options.baseUrl}` + return `http://localhost:${this.configuration.port}${this.configuration['base-url']}` } async _buildPageUrls() { - const baseUrl = this._buildLocalUrl() - const pageUrls = [] as any + // We very intentially remove '' values from these globs because that matches every file + const ignoreGlobs = this.configuration['ignore-files'] + .split(',') + .filter((value) => value !== '') + + const snapshotGlobs = this.configuration['snapshot-files'] + .split(',') + .filter((value) => value !== '') const globOptions = { - cwd: this.options.snapshotDirectory, - ignore: this.options.ignoreGlobs, + cwd: this.configuration.path, + ignore: ignoreGlobs, } - const paths = await globby(this.options.snapshotGlobs, globOptions) + const paths = await globby(snapshotGlobs, globOptions) + const pageUrls = [] as any + const baseUrl = this._buildLocalUrl() for (const path of paths) { pageUrls.push(baseUrl + path) diff --git a/src/utils/sdk-utils.ts b/src/utils/sdk-utils.ts index c8245919..3c5ff56b 100644 --- a/src/utils/sdk-utils.ts +++ b/src/utils/sdk-utils.ts @@ -1,6 +1,6 @@ import Axios from 'axios' import * as path from 'path' -import Constants from '../services/constants' +import {DEFAULT_PORT, HEALTHCHECK_PATH} from '../services/agent-service-constants' import {logError} from './logger' export function agentJsFilename() { @@ -14,7 +14,7 @@ export function agentJsFilename() { export async function isAgentRunning() { return Axios({ method: 'get', - url: `http://localhost:${Constants.PORT}${Constants.HEALTHCHECK_PATH}`, + url: `http://localhost:${DEFAULT_PORT}${HEALTHCHECK_PATH}`, } as any).then(() => { return true }).catch((error) => { @@ -23,7 +23,7 @@ export async function isAgentRunning() { } export async function postSnapshot(body: any) { - const URL = `http://localhost:${Constants.PORT}${Constants.SNAPSHOT_PATH}` + const URL = `http://localhost:${DEFAULT_PORT}${HEALTHCHECK_PATH}` const ONE_HUNDRED_MB_IN_BYTES = 100_000_000 return Axios({ diff --git a/test/commands/exec.test.ts b/test/commands/exec.test.ts index 3d81439d..6111000d 100644 --- a/test/commands/exec.test.ts +++ b/test/commands/exec.test.ts @@ -3,8 +3,8 @@ import {describe} from 'mocha' import * as nock from 'nock' import * as sinon from 'sinon' import Exec from '../../src/commands/exec' -import AgentService from '../../src/services/agent-service' -import Constants from '../../src/services/constants' +import {AgentService} from '../../src/services/agent-service' +import {DEFAULT_PORT} from '../../src/services/agent-service-constants' import {captureStdOut} from '../helpers/stdout' const expect = chai.expect @@ -39,7 +39,7 @@ describe('Exec', () => { await Exec.run(['--', 'echo', 'hello']) }) - expect(agentServiceStub.start).to.calledWithMatch(Constants.PORT) + expect(agentServiceStub.start).to.calledWithMatch(DEFAULT_PORT) expect(stdout).to.match(/\[percy\] percy has started on port \d+./) }) diff --git a/test/commands/snapshot.test.ts b/test/commands/snapshot.test.ts index e63decac..3cb00bc3 100644 --- a/test/commands/snapshot.test.ts +++ b/test/commands/snapshot.test.ts @@ -1,13 +1,13 @@ +import {expect, test} from '@oclif/test' import * as chai from 'chai' import {describe} from 'mocha' import * as sinon from 'sinon' import Snapshot from '../../src/commands/snapshot' -import AgentService from '../../src/services/agent-service' +import { DEFAULT_CONFIGURATION } from '../../src/configuration/configuration' +import {AgentService} from '../../src/services/agent-service' import StaticSnapshotService from '../../src/services/static-snapshot-service' import {captureStdOut} from '../helpers/stdout' -import {expect, test} from '@oclif/test' - describe('snapshot', () => { describe('#run', () => { const sandbox = sinon.createSandbox() @@ -35,8 +35,6 @@ describe('snapshot', () => { } it('starts the static snapshot service', async () => { - const expectedAgentOptions = {networkIdleTimeout: 50, port: 5338} - const agentServiceStub = AgentServiceStub() const staticSnapshotServiceStub = StaticSnapshotServiceStub() @@ -44,7 +42,7 @@ describe('snapshot', () => { await Snapshot.run(['./dummy-test-dir']) }) - chai.expect(agentServiceStub.start).to.be.calledWith(expectedAgentOptions) + chai.expect(agentServiceStub.start).to.be.calledWith(DEFAULT_CONFIGURATION) chai.expect(staticSnapshotServiceStub.start).to.have.callCount(1) chai.expect(staticSnapshotServiceStub.snapshotAll).to.have.callCount(1) chai.expect(stdout).to.match(/\[percy\] percy has started./) diff --git a/test/commands/start.test.ts b/test/commands/start.test.ts index c9ca7095..0e54701b 100644 --- a/test/commands/start.test.ts +++ b/test/commands/start.test.ts @@ -4,8 +4,8 @@ import * as nock from 'nock' import * as path from 'path' import * as sinon from 'sinon' import Start from '../../src/commands/start' -import AgentService from '../../src/services/agent-service' -import Constants from '../../src/services/constants' +import { DEFAULT_CONFIGURATION } from '../../src/configuration/configuration' +import {AgentService} from '../../src/services/agent-service' import ProcessService from '../../src/services/process-service' import {captureStdOut} from '../helpers/stdout' @@ -51,7 +51,7 @@ describe('Start', () => { await Start.run([]) }) - expect(agentServiceStub.start).to.calledWithMatch({port: Constants.PORT, networkIdleTimeout: 50}) + expect(agentServiceStub.start).to.calledWithMatch(DEFAULT_CONFIGURATION) expect(stdout).to.contain('[percy] percy has started.') }) @@ -63,7 +63,12 @@ describe('Start', () => { }) expect(processService.runDetached).to.calledWithMatch( - [path.resolve(`${__dirname}/../../bin/run`), 'start', '-p', String(Constants.PORT), '-t', '50'], + [ + path.resolve(`${__dirname}/../../bin/run`), + 'start', + '-p', String(DEFAULT_CONFIGURATION.agent.port), + '-t', '50', + ], ) }) @@ -77,7 +82,10 @@ describe('Start', () => { await Start.run(options) }) - expect(agentServiceStub.start).to.calledWithMatch({port: +port, networkIdleTimeout: 50}) + const expectedConfiguration = DEFAULT_CONFIGURATION + expectedConfiguration.agent.port = +port + + expect(agentServiceStub.start).to.calledWithMatch(expectedConfiguration) expect(stdout).to.contain('[percy] percy has started.') }) diff --git a/test/configuration/configuration.test.ts b/test/configuration/configuration.test.ts deleted file mode 100644 index 78840ab0..00000000 --- a/test/configuration/configuration.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {expect} from 'chai' -import configuration from '../../src/configuration/configuration' - -describe('Configuration', () => { - it('parses valid configuration', () => { - const subject = configuration('test/support/.percy.yml') - - expect(subject.version).to.eql(1) - expect(subject.snapshot.widths).to.eql([375, 1280]) - expect(subject.snapshot['min-height']).to.eql(1024) - expect(subject['static-snapshots']['base-url']).to.eql('/blog/') - expect(subject['static-snapshots']['snapshot-files']).to.eql('**/*.html') - expect(subject['static-snapshots']['ignore-files']).to.eql('**/*.htm') - }) - - it('gracefully handles a missing file', () => { - const subject = configuration('test/support/.file-does-not-exist.yml') - - expect(subject).to.eql({'version': 1.0, 'snapshot': {}, 'static-snapshots': {}}) - }) -}) diff --git a/test/percy-agent-client/percy-agent.test.ts b/test/percy-agent-client/percy-agent.test.ts index 9cbee56d..65f9ec34 100644 --- a/test/percy-agent-client/percy-agent.test.ts +++ b/test/percy-agent-client/percy-agent.test.ts @@ -1,8 +1,7 @@ import {expect} from 'chai' import * as sinon from 'sinon' import PercyAgent from '../../src/percy-agent-client/percy-agent' -import Constants from '../../src/services/constants' -import { htmlWithoutSelector } from '../helpers/html-string' +import {DEFAULT_PORT, SNAPSHOT_PATH} from '../../src/services/agent-service-constants' describe('PercyAgent', () => { let requests: sinon.SinonFakeXMLHttpRequest[] = [] @@ -37,7 +36,7 @@ describe('PercyAgent', () => { const request = requests[0] const requestBody = JSON.parse(request.requestBody) - expect(request.url).to.equal(`http://localhost:${Constants.PORT}${Constants.SNAPSHOT_PATH}`) + expect(request.url).to.equal(`http://localhost:${DEFAULT_PORT}${SNAPSHOT_PATH}`) expect(request.method).to.equal('post') expect(requestBody.name).to.equal('test snapshot') }) @@ -51,7 +50,7 @@ describe('PercyAgent', () => { const request = requests[0] const requestBody = JSON.parse(request.requestBody) - expect(request.url).to.equal(`http://localhost:${Constants.PORT}${Constants.SNAPSHOT_PATH}`) + expect(request.url).to.equal(`http://localhost:${DEFAULT_PORT}${SNAPSHOT_PATH}`) expect(request.method).to.equal('post') expect(requestBody.name).to.equal('test snapshot with options') expect(requestBody.enableJavaScript).to.equal(true) diff --git a/test/services/agent-service.test.ts b/test/services/agent-service.test.ts index 3494d398..1d2009a4 100644 --- a/test/services/agent-service.test.ts +++ b/test/services/agent-service.test.ts @@ -1,15 +1,14 @@ import {describe} from 'mocha' import * as nock from 'nock' -import AgentService from '../../src/services/agent-service' -import Constants from '../../src/services/constants' +import { DEFAULT_CONFIGURATION } from '../../src/configuration/configuration' +import {AgentService} from '../../src/services/agent-service' import {captureStdOut} from '../helpers/stdout' import chai from '../support/chai' const expect = chai.expect describe('AgentService', () => { const subject = new AgentService() - const port = Constants.PORT - const host = `localhost:${port}` + const configuration = DEFAULT_CONFIGURATION const buildCreateResponse = require('../fixtures/build-create.json') const buildId = buildCreateResponse.data.id @@ -31,9 +30,9 @@ describe('AgentService', () => { }) it('starts serving dist/public on supplied port', async () => { - await captureStdOut(() => subject.start({port})) + await captureStdOut(() => subject.start(configuration)) - chai.request(`http://${host}`) + chai.request(`http://localhost:${configuration.agent.port}`) .get('/percy-agent.js') .end((error, response) => { expect(error).to.be.eq(null) @@ -43,7 +42,7 @@ describe('AgentService', () => { }) it('logs to stdout that it created a build', async () => { - const stdout = await captureStdOut(() => subject.start({port})) + const stdout = await captureStdOut(() => subject.start(configuration)) expect(stdout).to.match(/\[percy\] created build #\d+: https:\/\/percy\.io\/test\/test\/builds\/\d+/) }) }) @@ -51,19 +50,19 @@ describe('AgentService', () => { describe('#stop', () => { it('stops serving dist/public on supplied port', async () => { await captureStdOut(async () => { - await subject.start({port}) + await subject.start(configuration) await subject.stop() }) - chai.request(`http://${host}`) + chai.request(`http://localhost:${configuration.agent.port}`) .get('/percy-agent.js') .catch(async (error) => { - await expect(error).to.have.property('message', `connect ECONNREFUSED 127.0.0.1:${port}`) + await expect(error).to.have.property('message', `connect ECONNREFUSED 127.0.0.1:${configuration.agent.port}`) }) }) it('logs to stdout that it finalized a build', async () => { - await captureStdOut(() => subject.start({port})) + await captureStdOut(() => subject.start(configuration)) const stdout = await captureStdOut(async () => { await subject.stop() diff --git a/test/services/configuration-service.test.ts b/test/services/configuration-service.test.ts new file mode 100644 index 00000000..2571a560 --- /dev/null +++ b/test/services/configuration-service.test.ts @@ -0,0 +1,64 @@ +import { expect } from 'chai' +import { DEFAULT_CONFIGURATION } from '../../src/configuration/configuration' +import ConfigurationService from '../../src/services/configuration-service' + +describe('ConfigurationService', () => { + describe('#configuration', () => { + it('returns default configuration by default', () => { + const subject = new ConfigurationService().configuration + expect(subject).to.eql(DEFAULT_CONFIGURATION) + }) + }) + + describe('#applyFile', () => { + it('parses valid configuration', () => { + const subject = new ConfigurationService().applyFile('test/support/.percy.yml') + + expect(subject.version).to.eql(1) + expect(subject.snapshot.widths).to.eql([375, 1280]) + expect(subject.snapshot['min-height']).to.eql(1024) + expect(subject['static-snapshots'].path).to.eql('_site/') + expect(subject['static-snapshots'].port).to.eql(9999) + expect(subject['static-snapshots']['base-url']).to.eql('/blog/') + expect(subject['static-snapshots']['snapshot-files']).to.eql('**/*.html') + expect(subject['static-snapshots']['ignore-files']).to.eql('**/*.htm') + expect(subject.agent.port).to.eql(1111) + expect(subject.agent['asset-discovery']['network-idle-timeout']).to.eql(50) + expect(subject.agent['asset-discovery']['page-pool-size-min']).to.eql(5) + expect(subject.agent['asset-discovery']['page-pool-size-max']).to.eql(20) + }) + + it('gracefully falls back to default configuration when file does not exist', () => { + const subject = new ConfigurationService().applyFile('test/support/.file-does-not-exist.yml') + expect(subject).to.eql(DEFAULT_CONFIGURATION) + }) + }) + + describe('#applyFlags', () => { + it('applies flags', () => { + const flags = { + 'network-idle-timeout': 51, + 'base-url': '/flag/', + 'snapshot-files': 'flags/*.html', + 'ignore-files': 'ignore-flags/*.html', + } + const subject = new ConfigurationService('test/support/.percy.yml').applyFlags(flags) + + expect(subject['static-snapshots']['base-url']).to.eql('/flag/') + expect(subject['static-snapshots']['snapshot-files']).to.eql('flags/*.html') + expect(subject['static-snapshots']['ignore-files']).to.eql('ignore-flags/*.html') + expect(subject.agent['asset-discovery']['network-idle-timeout']).to.eql(51) + }) + }) + + describe('#applyArgs', () => { + it('applies args', () => { + const args = { + snapshotDirectory: '/from/arg', + } + const subject = new ConfigurationService('test/support/.percy.yml').applyArgs(args) + + expect(subject['static-snapshots'].path).to.eql('/from/arg') + }) + }) +}) diff --git a/test/services/static-snapshot-service.test.ts b/test/services/static-snapshot-service.test.ts index fd4cdc98..77c11464 100644 --- a/test/services/static-snapshot-service.test.ts +++ b/test/services/static-snapshot-service.test.ts @@ -1,6 +1,6 @@ import {describe} from 'mocha' -import Constants from '../../src/services/constants' -import {StaticSnapshotOptions} from '../../src/services/static-snapshot-options' +import { StaticSnapshotsConfiguration } from '../../src/configuration/static-snapshots-configuration' +import {DEFAULT_PORT} from '../../src/services/agent-service-constants' import StaticSnapshotService from '../../src/services/static-snapshot-service' import {captureStdOut} from '../helpers/stdout' import chai from '../support/chai' @@ -8,22 +8,22 @@ import chai from '../support/chai' const expect = chai.expect describe('StaticSnapshotService', () => { - const staticSitePort = Constants.PORT + 1 - - const options: StaticSnapshotOptions = { - port: staticSitePort, - snapshotDirectory: './test/fixtures/services/static-snapshot-service/_dummy-testing-app/', - snapshotGlobs: ['**/*.html', '**/*.htm'], - ignoreGlobs: ['**/blog/*'], - baseUrl: '/', + const staticSitePort = DEFAULT_PORT + 1 + + const configuration: StaticSnapshotsConfiguration = { + 'port': staticSitePort, + 'path': './test/fixtures/services/static-snapshot-service/_dummy-testing-app/', + 'snapshot-files': '**/*.html,**/*.htm', + 'ignore-files': '**/blog/*', + 'base-url': '/', } - const subject = new StaticSnapshotService(options) + const subject = new StaticSnapshotService(configuration) const localUrl = subject._buildLocalUrl() describe('#constructor', () => { it('creates a static snapshot service with the given arguments', () => { - expect(subject.options).to.eq(options) + expect(subject.configuration).to.eq(configuration) }) }) @@ -63,15 +63,15 @@ describe('StaticSnapshotService', () => { }) describe('#_buildPageUrls without the ignore flag set', () => { - const options: StaticSnapshotOptions = { - port: staticSitePort, - snapshotDirectory: './test/fixtures/services/static-snapshot-service/_dummy-testing-app/', - snapshotGlobs: ['**/*.html', '**/*.htm'], - ignoreGlobs: [], - baseUrl: '/', + const configuration: StaticSnapshotsConfiguration = { + 'port': staticSitePort, + 'path': './test/fixtures/services/static-snapshot-service/_dummy-testing-app/', + 'snapshot-files': '**/*.html,**/*.htm', + 'ignore-files': '', + 'base-url': '/', } - const subject = new StaticSnapshotService(options) + const subject = new StaticSnapshotService(configuration) it('ignores the correct files', async () => { const pages = await subject._buildPageUrls() diff --git a/test/support/.percy.yml b/test/support/.percy.yml index 2ddae6a0..8089da1b 100644 --- a/test/support/.percy.yml +++ b/test/support/.percy.yml @@ -1,8 +1,16 @@ version: 1 snapshot: widths: [375, 1280] - min-height: 1024 + min-height: 1024 # px static-snapshots: + path: _site/ + port: 9999 base-url: /blog/ snapshot-files: '**/*.html' ignore-files: '**/*.htm' +agent: + port: 1111 + asset-discovery: + network-idle-timeout: 50 # ms + page-pool-size-min: 5 # pages + page-pool-size-max: 20 # pages