diff --git a/src/commands/exec.ts b/src/commands/exec.ts index 2165df46..dd2dc6d1 100644 --- a/src/commands/exec.ts +++ b/src/commands/exec.ts @@ -16,7 +16,7 @@ export default class Exec extends PercyCommand { static flags = { 'network-idle-timeout': flags.integer({ char: 't', - default: 50, + default: Constants.NETWORK_IDLE_TIMEOUT, description: 'asset discovery network idle timeout (in milliseconds)', }), 'port': flags.integer({ diff --git a/src/commands/snapshot.ts b/src/commands/snapshot.ts new file mode 100644 index 00000000..2f777b24 --- /dev/null +++ b/src/commands/snapshot.ts @@ -0,0 +1,102 @@ +import {flags} from '@oclif/command' +import Constants from '../services/constants' +import {StaticSnapshotOptions} from '../services/static-snapshot-options' +import StaticSnapshotService from '../services/static-snapshot-service' +import logger from '../utils/logger' +import PercyCommand from './percy-command' + +export default class Snapshot extends PercyCommand { + static description = 'Snapshot a directory containing a pre-built static website' + static hidden = true + + static args = [{ + name: 'snapshotDirectory', + description: 'A path to the directory you would like to snapshot', + required: true, + }] + + static examples = [ + '$ percy snapshot _site/', + '$ percy snapshot _site/ --base-url "/blog"', + '$ percy snapshot _site/ --ignore-files "\.(blog|docs)$"', + ] + + static flags = { + 'snapshot-files': flags.string({ + char: 'c', + description: 'Regular expression for matching the files to snapshot.', + default: '\.(html|htm)$', + }), + 'ignore-files': flags.string({ + char: 'i', + description: 'Regular expression for matching the files to ignore.', + default: '', + }), + '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: '/', + }), + // from exec command. needed to start the agent service. + 'network-idle-timeout': flags.integer({ + char: 't', + default: Constants.NETWORK_IDLE_TIMEOUT, + description: 'Asset discovery network idle timeout (in milliseconds)', + }), + 'port': flags.integer({ + char: 'p', + default: Constants.PORT, + description: 'Port', + }), + } + + async run() { + await super.run() + + const {args, flags} = this.parse(Snapshot) + + const isWindows = process.platform === 'win32' + + 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 baseUrl = flags['base-url'] as string + const ignoreFilesRegex = flags['ignore-files'] as string + const snapshotFilesRegex = flags['snapshot-files'] as string + + // exit gracefully if percy will not run + if (!this.percyWillRun()) { this.exit(0) } + + // check that base url starts with a slash and exit if it is missing + if (baseUrl[0] !== '/') { + logger.warn('The base-url flag must begin with a slash.') + this.exit(1) + } + + // start the agent service + await this.agentService.start({port, networkIdleTimeout}) + this.logStart() + + const options: StaticSnapshotOptions = { + port: staticServerPort, + snapshotDirectory, + baseUrl, + snapshotFilesRegex, + ignoreFilesRegex, + } + + const staticSnapshotService = new StaticSnapshotService(options) + + // start the snapshot service + await staticSnapshotService.start() + + // take the snapshots + await staticSnapshotService.snapshotAll() + + // stop the static snapshot and agent services + await staticSnapshotService.stop() + await this.agentService.stop() + } +} diff --git a/src/commands/start.ts b/src/commands/start.ts index 5c5d5fb7..bfee6b61 100644 --- a/src/commands/start.ts +++ b/src/commands/start.ts @@ -21,7 +21,7 @@ export default class Start extends PercyCommand { }), 'network-idle-timeout': flags.integer({ char: 't', - default: 50, + default: Constants.NETWORK_IDLE_TIMEOUT, description: 'asset discovery network idle timeout (in milliseconds)', }), 'port': flags.integer({ diff --git a/src/services/constants.ts b/src/services/constants.ts index 72cab3f3..1263f9da 100644 --- a/src/services/constants.ts +++ b/src/services/constants.ts @@ -1,5 +1,6 @@ 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' diff --git a/src/services/static-snapshot-options.ts b/src/services/static-snapshot-options.ts new file mode 100644 index 00000000..d936e2a4 --- /dev/null +++ b/src/services/static-snapshot-options.ts @@ -0,0 +1,7 @@ +export interface StaticSnapshotOptions { + snapshotDirectory: string, + port: number, + baseUrl: string, + snapshotFilesRegex: string, + ignoreFilesRegex?: string, + } diff --git a/src/services/static-snapshot-service.ts b/src/services/static-snapshot-service.ts new file mode 100644 index 00000000..65dded53 --- /dev/null +++ b/src/services/static-snapshot-service.ts @@ -0,0 +1,20 @@ +import logger from '../utils/logger' +import {StaticSnapshotOptions} from './static-snapshot-options' + +export default class StaticSnapshotService { + constructor(options: StaticSnapshotOptions) { + // logger.info('calling constructor...') + } + + async start() { + // logger.info('starting static snapshot service...') + } + + async snapshotAll() { + // logger.info('taking snapshots of the static site...') + } + + async stop() { + // logger.info('stopping static snapshot service...') + } +} diff --git a/test/commands/snapshot.test.ts b/test/commands/snapshot.test.ts new file mode 100644 index 00000000..93a1be4a --- /dev/null +++ b/test/commands/snapshot.test.ts @@ -0,0 +1,73 @@ +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 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() + + afterEach(() => { + sandbox.restore() + }) + + function AgentServiceStub(): AgentService { + const agentService = AgentService.prototype as AgentService + sandbox.stub(agentService, 'start') + + const start = new Snapshot([], '') as Snapshot + sandbox.stub(start, 'agentService').returns(agentService) + + return agentService + } + + function StaticSnapshotServiceStub(): StaticSnapshotService { + const staticSnapshotService = StaticSnapshotService.prototype as StaticSnapshotService + sandbox.stub(staticSnapshotService, 'snapshotAll') + sandbox.stub(staticSnapshotService, 'start') + + return staticSnapshotService + } + + it('starts the static snapshot service', async () => { + const expectedAgentOptions = {networkIdleTimeout: 50, port: 5338} + + const agentServiceStub = AgentServiceStub() + const staticSnapshotServiceStub = StaticSnapshotServiceStub() + + const stdout = await captureStdOut(async () => { + await Snapshot.run(['./dummy-test-dir']) + }) + + chai.expect(agentServiceStub.start).to.be.calledWith(expectedAgentOptions) + 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./) + }) + + xit('starts the snapshot service on the correct port') + }) + + describe('snapshot command', () => { + test + .stub(process, 'env', {PERCY_TOKEN: ''}) + .stderr() + .command(['snapshot', './test_dir']) + .exit(0) + .do((output) => expect(output.stderr).to.contain( + 'Warning: Skipping visual tests. PERCY_TOKEN was not provided.', + )) + .it('warns about PERCY_TOKEN not being set and exits gracefully') + + test + .env({PERCY_TOKEN: 'abc'}) + .command(['snapshot']) + .exit(2) + .it('exits when the asset directory arg is missing') + }) +})