diff --git a/src/commands/snapshot.ts b/src/commands/snapshot.ts index 19c42da0..3f6d3bfb 100644 --- a/src/commands/snapshot.ts +++ b/src/commands/snapshot.ts @@ -1,8 +1,10 @@ import { flags } from '@oclif/command' import { existsSync } from 'fs' +import * as globby from 'globby' import { DEFAULT_CONFIGURATION } from '../configuration/configuration' +import { StaticSnapshotsConfiguration } from '../configuration/static-snapshots-configuration' import StaticSnapshotService from '../services/static-snapshot-service' -import config from '../utils/configuration' +import config, { parseGlobs } from '../utils/configuration' import logger from '../utils/logger' import PercyCommand from './percy-command' @@ -47,6 +49,10 @@ export default class Snapshot extends PercyCommand { 'of the webserver\'s root path, set that subdirectory with this flag.', ].join(' '), }), + 'dry-run': flags.boolean({ + char: 'd', + description: 'Print the list of paths to snapshot without creating a new build', + }), // from exec command. needed to start the agent service. 'allowed-hostname': flags.string({ char: 'h', @@ -79,6 +85,11 @@ export default class Snapshot extends PercyCommand { const { args, flags } = this.parse(Snapshot) const configuration = config(flags, args) + if (flags['dry-run']) { + await this.dryrun(configuration['static-snapshots']) + this.exit(0) + } + // exit gracefully if percy will not run if (!this.percyWillRun()) { this.exit(0) } @@ -111,4 +122,16 @@ export default class Snapshot extends PercyCommand { await staticSnapshotService.stop() await this.stop() } + + // will print the paths that would have been snapshotted + async dryrun(configuration: StaticSnapshotsConfiguration) { + // this cannot be done in the static snapshot service because not only does + // it map paths to localhost URLs, but it also starts the localhost server + // and creates a new Percy build before parsing any globs + const globs = parseGlobs(configuration['snapshot-files']) + const ignore = parseGlobs(configuration['ignore-files']) + const paths = await globby(globs, { cwd: configuration.path, ignore }) + + console.log(paths.map((p) => configuration['base-url'] + p).join('\n')) + } } diff --git a/src/commands/upload.ts b/src/commands/upload.ts index 66c48fa9..b8556b35 100644 --- a/src/commands/upload.ts +++ b/src/commands/upload.ts @@ -21,21 +21,25 @@ export default class Upload extends Command { ] static flags = { - files: flags.string({ + 'files': flags.string({ char: 'f', description: [ `[default: ${DEFAULT_CONFIGURATION['image-snapshots'].files}]`, 'Glob or comma-seperated string of globs for matching the files and directories to snapshot.', ].join(' '), }), - ignore: flags.string({ + 'ignore': flags.string({ char: 'i', description: [ `[default: ${DEFAULT_CONFIGURATION['image-snapshots'].ignore}]`, 'Glob or comma-seperated string of globs for matching the files and directories to ignore.', ].join(' '), }), - config: flags.string({ + 'dry-run': flags.boolean({ + char: 'd', + description: 'Print the list of images to upload without uploading them', + }), + 'config': flags.string({ char: 'c', description: 'Path to percy config file', }), @@ -59,6 +63,6 @@ export default class Upload extends Command { // upload snapshot images const imageSnapshotService = new ImageSnapshotService(configuration['image-snapshots']) - await imageSnapshotService.snapshotAll() + await imageSnapshotService.snapshotAll({ dry: flags['dry-run'] }) } } diff --git a/src/services/image-snapshot-service.ts b/src/services/image-snapshot-service.ts index 7893c683..b43e8a89 100644 --- a/src/services/image-snapshot-service.ts +++ b/src/services/image-snapshot-service.ts @@ -109,7 +109,7 @@ export default class ImageSnapshotService extends PercyClientService { }).catch(logError) } - async snapshotAll() { + async snapshotAll({ dry = false }: { dry?: boolean } = {}) { const globs = parseGlobs(this.configuration.files) const ignore = parseGlobs(this.configuration.ignore) const paths = await globby(globs, { cwd: this.configuration.path, ignore }) @@ -117,10 +117,14 @@ export default class ImageSnapshotService extends PercyClientService { if (!paths.length) { logger.error(`no matching files found in '${this.configuration.path}''`) - logger.info('exiting') return process.exit(1) } + if (dry) { + console.log(paths.join('\n')) + return + } + await this.buildService.create() logger.debug('uploading snapshots of static images') diff --git a/test/unit/commands/snapshot.test.ts b/test/unit/commands/snapshot.test.ts index 6cb7e616..dca9cd5b 100644 --- a/test/unit/commands/snapshot.test.ts +++ b/test/unit/commands/snapshot.test.ts @@ -59,5 +59,46 @@ describe('snapshot', () => { chai.expect(stderr).to.match(/Warning: Skipping visual tests\. PERCY_TOKEN was not provided\./) }) + + describe('with --dry-run', () => { + let agentServiceStub: AgentService + let staticSnapshotServiceStub: StaticSnapshotService + + beforeEach(() => { + agentServiceStub = AgentServiceStub() + staticSnapshotServiceStub = StaticSnapshotServiceStub() + }) + + it('does not start the static snapshot service', async () => { + const stdout = await captureStdOut(async () => { + await Snapshot.run(['./test/integration/test-static-site', '--dry-run']) + }) + + chai.expect(agentServiceStub.start).to.have.callCount(0) + chai.expect(staticSnapshotServiceStub.start).to.have.callCount(0) + chai.expect(stdout).not.to.match(/\[percy\] percy has started./) + }) + + it('prints paths to snapshot matching the provided options', async () => { + const stdout = await captureStdOut(async () => { + await Snapshot.run([ + './test/integration/test-static-site', + '--base-url=/base-url/', + '--snapshot-files=families/**/*.html', + '--ignore-files=families/targaryen/**/*', + '--dry-run', + ]) + }) + + chai.expect(stdout).to.equal([ + '/base-url/families/greyjoy/members.html', + '/base-url/families/greyjoy/pyke.html', + '/base-url/families/lannister/casterly-rock.html', + '/base-url/families/lannister/members.html', + '/base-url/families/stark/members.html', + '/base-url/families/stark/winterfell.html', + ].join('\n') + '\n') + }) + }) }) }) diff --git a/test/unit/commands/upload.test.ts b/test/unit/commands/upload.test.ts new file mode 100644 index 00000000..50e33934 --- /dev/null +++ b/test/unit/commands/upload.test.ts @@ -0,0 +1,75 @@ +import { expect, test } from '@oclif/test' +import { describe } from 'mocha' +import * as sinon from 'sinon' +import Upload from '../../../src/commands/upload' +import ImageSnapshotService from '../../../src/services/image-snapshot-service' +import { captureStdErr, captureStdOut } from '../helpers/stdout' +import chai from '../support/chai' + +describe('upload', () => { + describe('#run', () => { + const sandbox = sinon.createSandbox() + + afterEach(() => { + sandbox.restore() + // restore token to fake value + process.env.PERCY_TOKEN = 'abc' + }) + + function ImageSnapshotServiceStub(): ImageSnapshotService { + const imageSnapshotService = ImageSnapshotService.prototype as ImageSnapshotService + sandbox.stub(imageSnapshotService, 'snapshotAll') + + return imageSnapshotService + } + + it('starts the static image service', async () => { + const imageSnapshotServiceStub = ImageSnapshotServiceStub() + + const stdout = await captureStdOut(async () => { + await Upload.run(['.']) + }) + + chai.expect(imageSnapshotServiceStub.snapshotAll).to.have.callCount(1) + chai.expect(stdout).not.to.match(/\[percy\] uploading snapshots of static images./) + }) + + it('warns about PERCY_TOKEN not being set and exits gracefully', async () => { + process.env.PERCY_TOKEN = '' + + const stderr = await captureStdErr(async () => { + await Upload.run(['.']) + }) + + chai.expect(stderr).to.match(/Warning: PERCY_TOKEN was not provided\./) + }) + + describe('with --dry-run', () => { + it('does not upload snapshots', async () => { + const imageSnapshotServiceStub = ImageSnapshotServiceStub() + + const stdout = await captureStdOut(async () => { + await Upload.run(['./test/integration/test-static-images', '--dry-run']) + }) + + chai.expect(imageSnapshotServiceStub.snapshotAll).to.be.calledWith({ dry: true }) + chai.expect(stdout).not.to.match(/\[percy\] uploading snapshots of static images./) + }) + + it('prints paths to images matching the provided options', async () => { + const stdout = await captureStdOut(async () => { + await Upload.run([ + './test/integration/test-static-images', + '--files=percy*', + '--ignore=*.jpg', + '--dry-run', + ]) + }) + + chai.expect(stdout).to.equal([ + 'percy logo.png', + ].join('\n') + '\n') + }) + }) + }) +})