diff --git a/lib/commands/update.js b/lib/commands/update.js index be9750f5b..c1a8f1ff2 100644 --- a/lib/commands/update.js +++ b/lib/commands/update.js @@ -19,6 +19,7 @@ class UpdateCommand extends Command { const MigrateCommand = require('./migrate'); const migrator = require('../tasks/migrator'); + const majorUpdate = require('../tasks/major-update'); const instance = this.system.getInstance(); @@ -66,6 +67,18 @@ class UpdateCommand extends Command { title: 'Downloading and updating Ghost', skip: (ctx) => ctx.rollback, task: this.downloadAndUpdate + }, { + title: 'Updating to a major version', + task: majorUpdate, + // CASE: Skip if you are already on ^2 or you update from v1 to v1. + enabled: () => { + if (semver.satisfies(instance.cliConfig.get('active-version'), '^2.0.0') || + !semver.satisfies(context.version, '^2.0.0')) { + return false; + } + + return true; + } }, { title: 'Stopping Ghost', enabled: () => isRunning, @@ -82,7 +95,7 @@ class UpdateCommand extends Command { skip: (ctx) => ctx.rollback, task: migrator.migrate, // CASE: We have moved the execution of knex-migrator into Ghost 2.0.0. - // If you are already on ^2 or you update from ^1 to ^2, then skip the task. + // If you are already on v2 or you update from v1 to v2, then skip the task. enabled: () => { if (semver.satisfies(instance.cliConfig.get('active-version'), '^2.0.0') || semver.satisfies(context.version, '^2.0.0')) { diff --git a/lib/errors.js b/lib/errors.js index aa66642da..b777aa31b 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -25,6 +25,7 @@ class CliError extends Error { Error.captureStackTrace(this, this.constructor); this.options = options; + this.logMessageOnly = options.logMessageOnly; this.help = 'Please refer to https://docs.ghost.org/v1/docs/troubleshooting#section-cli-errors for troubleshooting.' diff --git a/lib/tasks/major-update/data.js b/lib/tasks/major-update/data.js new file mode 100644 index 000000000..486791190 --- /dev/null +++ b/lib/tasks/major-update/data.js @@ -0,0 +1,75 @@ +'use strict'; + +module.exports = function getData(options = {}) { + const path = require('path'); + const errors = require('../../errors'); + + if (!options.dir) { + return Promise.reject(new errors.CliError({ + message: '`dir` is required.' + })) + } + + if (!options.database) { + return Promise.reject(new errors.CliError({ + message: '`database` is required.' + })) + } + + const knexPath = path.resolve(options.dir, options.version, 'node_modules', 'knex'); + const gscanPath = path.resolve(options.dir, options.version, 'node_modules', 'gscan'); + + const knex = require(knexPath); + const gscan = require(gscanPath); + + const connection = knex(Object.assign({useNullAsDefault: true}, options.database)); + + const themeFolder = path.resolve(options.dir, 'content', 'themes'); + let gscanReport; + + return connection.raw('SELECT * FROM settings WHERE `key`="active_theme";') + .then((response) => { + let activeTheme; + + if (options.database.client === 'mysql') { + activeTheme = response[0][0].value; + } else { + activeTheme = response[0].value; + } + + return gscan.check(path.resolve(themeFolder, activeTheme)); + }) + .then((report) => { + gscanReport = gscan.format(report, {sortByFiles: true}); + + return connection.raw('SELECT uuid FROM posts WHERE slug="v2-demo-post";') + }) + .then((response) => { + let demoPost; + + if (options.database.client === 'mysql') { + demoPost = response[0][0]; + } else { + demoPost = response[0]; + } + + return { + gscanReport: gscanReport, + demoPost: demoPost + }; + }) + .then((response) => { + return new Promise((resolve) => { + connection.destroy(() => { + resolve(response); + }); + }); + }) + .catch((err) => { + return new Promise((resolve, reject) => { + connection.destroy(() => { + reject(err); + }); + }); + }); +}; diff --git a/lib/tasks/major-update/index.js b/lib/tasks/major-update/index.js new file mode 100644 index 000000000..0dfff7302 --- /dev/null +++ b/lib/tasks/major-update/index.js @@ -0,0 +1,2 @@ +'use strict'; +module.exports = require('./ui'); diff --git a/lib/tasks/major-update/ui.js b/lib/tasks/major-update/ui.js new file mode 100644 index 000000000..81f008463 --- /dev/null +++ b/lib/tasks/major-update/ui.js @@ -0,0 +1,160 @@ +'use strict'; + +module.exports = function ui(ctx) { + const chalk = require('chalk'); + const logSymbols = require('log-symbols'); + const each = require('lodash/each'); + const errors = require('../../errors'); + const getData = require('./data'); + let gscanReport; + let demoPost; + + return getData({ + dir: ctx.instance.dir, + database: ctx.instance.config.get('database'), + version: `versions/${ctx.version}` + }).then((response) => { + gscanReport = response.gscanReport; + demoPost = response.demoPost; + + ctx.ui.log(chalk.bold.underline.white(`\n\nChecking theme compatibility for Ghost ${ctx.version}\n`)); + if (!gscanReport.results.error.all.length && !gscanReport.results.warning.all.length) { + ctx.ui.log(`${logSymbols.success} Your theme is compatible.\n`); + + if (demoPost && demoPost.uuid) { + const demoLink = `${ctx.instance.config.get('url')}p/${demoPost.uuid}/`; + ctx.ui.log(`Visit the demo post at ${chalk.cyan(demoLink)} to see how your theme looks like in Ghost 2.0`); + } + + ctx.ui.log(`You can also check theme compatibility at ${chalk.cyan('https://gscan.ghost.org')}\n`); + } else { + let message = ''; + + if (gscanReport.results.warning.all.length && !gscanReport.results.error.all.length) { + message += `${chalk.yellow('⚠')} Your theme has `; + + let text = 'warning'; + + if (gscanReport.results.warning.all.length > 1) { + text = 'warnings'; + } + + message += chalk.bold.yellow(`${gscanReport.results.warning.all.length} ${text}`); + } else if (!gscanReport.results.warning.all.length && gscanReport.results.error.all.length) { + message += `${chalk.red('⚠')} Your theme has `; + + let text = 'error'; + + if (gscanReport.results.error.all.length > 1) { + text = 'errors'; + } + + message += chalk.bold.red(`${gscanReport.results.error.all.length} ${text}`); + } else if (gscanReport.results.warning.all.length && gscanReport.results.error.all.length) { + message += `${chalk.red('⚠')} Your theme has `; + + let text1 = 'error'; + let text2 = 'warning'; + + if (gscanReport.results.error.all.length > 1) { + text1 = 'errors'; + } + + if (gscanReport.results.warning.all.length > 1) { + text2 = 'warnings'; + } + + message += chalk.bold.red(`${gscanReport.results.error.all.length} ${text1}`); + message += ' and '; + message += chalk.bold.yellow(`${gscanReport.results.warning.all.length} ${text2}`); + } + + message += '\n'; + ctx.ui.log(message); + + return ctx.ui.confirm('View error and warning details?', null, {prefix: chalk.cyan('?')}); + } + }).then((answer) => { + if (answer) { + const spaces = ' '; + + if (gscanReport.results.error.all.length) { + ctx.ui.log(chalk.bold.red('\nErrors')); + + each(gscanReport.results.error.byFiles, (errors, fileName) => { + if (!errors.length) { + return; + } + + let message = chalk.bold.white(`${spaces}File: `); + message += chalk.white(`${fileName}`); + message += '\n'; + + errors.forEach((error, index) => { + if (error.fatal) { + message += `${spaces}- ${chalk.bold.red('Fatal error:')} ${error.rule.replace(/(<([^>]+)>)/ig, '')}`; + } else { + message += `${spaces}- ${error.rule.replace(/(<([^>]+)>)/ig, '')}`; + } + + if (index < (errors.length - 1)) { + message += '\n'; + } + }); + + message += '\n'; + ctx.ui.log(message); + }); + } + + if (gscanReport.results.warning.all.length) { + ctx.ui.log(chalk.bold.yellow('\nWarnings')); + + each(gscanReport.results.warning.byFiles, (warnings, fileName) => { + if (!warnings.length) { + return; + } + + let message = chalk.bold.white(`${spaces}File: `); + message += chalk.white(`${fileName}`); + message += '\n'; + + warnings.forEach((warning, index) => { + message += `${spaces}- ${warning.rule.replace(/(<([^>]+)>)/ig, '')}`; + + if (index < (warnings.length - 1)) { + message += '\n'; + } + }); + + message += '\n'; + ctx.ui.log(message); + }); + } + + if (demoPost && demoPost.uuid) { + const demoLink = `${ctx.instance.config.get('url')}p/${demoPost.uuid}/`; + ctx.ui.log(`Visit the demo post at ${chalk.cyan(demoLink)} to see how your theme looks like in Ghost 2.0`); + } + + ctx.ui.log(`You can also check theme compatibility at ${chalk.cyan('https://gscan.ghost.org')}\n`); + } + + if (gscanReport.results.hasFatalErrors) { + return Promise.reject(new errors.CliError({ + message: 'Migration failed. Your theme has fatal errors.\n For additional theme help visit https://themes.ghost.org/docs/changelog', + logMessageOnly: true + })); + } + + return ctx.ui.confirm(`Are you sure you want to proceed with migrating to Ghost ${ctx.version}?`, null, {prefix: chalk.cyan('?')}) + .then((answer) => { + if (!answer) { + return Promise.reject(new errors.CliError({ + message: `Update aborted. Your blog is still on ${ctx.activeVersion}.`, + logMessageOnly: true + })); + } + }); + }); +}; diff --git a/lib/ui/index.js b/lib/ui/index.js index 8718c06ff..8b25cf82b 100644 --- a/lib/ui/index.js +++ b/lib/ui/index.js @@ -144,7 +144,7 @@ class UI { * @method confirm * @public */ - confirm(question, defaultAnswer) { + confirm(question, defaultAnswer, options = {}) { if (!this.allowPrompt) { return Promise.resolve(defaultAnswer); } @@ -153,7 +153,8 @@ class UI { type: 'confirm', name: 'yes', message: question, - default: defaultAnswer + default: defaultAnswer, + prefix: options.prefix }).then((answer) => { return answer.yes; }); @@ -328,6 +329,10 @@ class UI { const debugInfo = this._formatDebug(system); if (error instanceof errors.CliError) { + if (error.logMessageOnly) { + return this.fail(error.message); + } + // Error is one that is generated by CLI usage (as in, the CLI itself // manually generates this error) this.log(`A ${error.type} occurred.\n`, 'red'); diff --git a/test/unit/commands/update-spec.js b/test/unit/commands/update-spec.js index 27a6e172a..590117ba8 100644 --- a/test/unit/commands/update-spec.js +++ b/test/unit/commands/update-spec.js @@ -46,12 +46,23 @@ describe('Unit: Commands > Update', function () { migrate: sinon.stub().resolves(), rollback: sinon.stub().resolves() }; + + const majorUpdateStub = sinon.stub(); + const UpdateCommand = proxyquire(modulePath, { - '../tasks/migrator': migratorStub + '../tasks/migrator': migratorStub, + '../tasks/major-update': majorUpdateStub }); - const config = configStub(); - config.get.withArgs('cli-version').returns('1.8.0'); - config.get.withArgs('active-version').returns('2.0.0'); + + const cliConfig = configStub(); + cliConfig.get.withArgs('cli-version').returns('1.8.0'); + cliConfig.get.withArgs('active-version').returns('2.0.0'); + + const ghostConfig = configStub(); + ghostConfig.get.withArgs('database').returns({ + client: 'sqlite3' + }); + const ui = {log: sinon.stub(), listr: sinon.stub(), run: sinon.stub()}; const system = {getInstance: sinon.stub()}; @@ -67,7 +78,8 @@ describe('Unit: Commands > Update', function () { }); class TestInstance extends Instance { - get cliConfig() { return config; } + get cliConfig() { return cliConfig; } + get config() { return ghostConfig; } } const fakeInstance = sinon.stub(new TestInstance(ui, system, '/var/www/ghost')); system.getInstance.returns(fakeInstance); @@ -105,6 +117,8 @@ describe('Unit: Commands > Update', function () { expect(migratorStub.migrate.called).to.be.false; expect(migratorStub.rollback.called).to.be.false; + + expect(majorUpdateStub.called).to.be.false; }); }); @@ -113,12 +127,23 @@ describe('Unit: Commands > Update', function () { migrate: sinon.stub().resolves(), rollback: sinon.stub().resolves() }; + + const majorUpdateStub = sinon.stub(); + const UpdateCommand = proxyquire(modulePath, { - '../tasks/migrator': migratorStub + '../tasks/migrator': migratorStub, + '../tasks/major-update': majorUpdateStub }); - const config = configStub(); - config.get.withArgs('cli-version').returns('1.8.0'); - config.get.withArgs('active-version').returns('1.25.0'); + + const cliConfig = configStub(); + cliConfig.get.withArgs('cli-version').returns('1.8.0'); + cliConfig.get.withArgs('active-version').returns('1.25.0'); + + const ghostConfig = configStub(); + ghostConfig.get.withArgs('database').returns({ + client: 'sqlite3' + }); + const ui = {log: sinon.stub(), listr: sinon.stub(), run: sinon.stub()}; const system = {getInstance: sinon.stub()}; @@ -134,7 +159,8 @@ describe('Unit: Commands > Update', function () { }); class TestInstance extends Instance { - get cliConfig() { return config; } + get cliConfig() { return cliConfig; } + get config() { return ghostConfig; } } const fakeInstance = sinon.stub(new TestInstance(ui, system, '/var/www/ghost')); system.getInstance.returns(fakeInstance); @@ -172,6 +198,8 @@ describe('Unit: Commands > Update', function () { expect(migratorStub.migrate.called).to.be.false; expect(migratorStub.rollback.called).to.be.false; + + expect(majorUpdateStub.called).to.be.true; }); }); @@ -298,9 +326,14 @@ describe('Unit: Commands > Update', function () { migrate: sinon.stub().resolves(), rollback: sinon.stub().resolves() }; + + const majorUpdateStub = sinon.stub(); + const UpdateCommand = proxyquire(modulePath, { - '../tasks/migrator': migratorStub + '../tasks/migrator': migratorStub, + '../tasks/major-update': majorUpdateStub }); + const config = configStub(); config.get.withArgs('cli-version').returns('1.0.0'); config.get.withArgs('active-version').returns('1.0.0'); @@ -309,7 +342,7 @@ describe('Unit: Commands > Update', function () { ui.run.callsFake(fn => fn()); ui.listr.callsFake((tasks, ctx) => { return Promise.each(tasks, (task) => { - if (task.skip && task.skip(ctx)) { + if (task.skip && task.skip(ctx) || (task.enabled && !task.enabled(ctx))) { return; } @@ -344,20 +377,26 @@ describe('Unit: Commands > Update', function () { expect(stopStub.calledOnce).to.be.true; expect(linkStub.calledOnce).to.be.true; expect(migratorStub.migrate.calledOnce).to.be.true; - expect(migratorStub.rollback.calledOnce).to.be.true; + expect(migratorStub.rollback.calledOnce).to.be.false; expect(restartStub.calledOnce).to.be.true; expect(removeOldVersionsStub.calledOnce).to.be.true; + + expect(majorUpdateStub.called).to.be.false; }); - }) + }); it('skips download, migrate, and removeOldVersion tasks if rollback is true', function () { const migratorStub = { migrate: sinon.stub().resolves(), rollback: sinon.stub().resolves() }; + const majorUpdateStub = sinon.stub(); + const UpdateCommand = proxyquire(modulePath, { - '../tasks/migrator': migratorStub + '../tasks/migrator': migratorStub, + '../tasks/major-update': majorUpdateStub }); + const config = configStub(); config.get.withArgs('cli-version').returns('1.0.0'); config.get.withArgs('active-version').returns('1.1.0'); @@ -367,7 +406,7 @@ describe('Unit: Commands > Update', function () { ui.run.callsFake(fn => fn()); ui.listr.callsFake((tasks, ctx) => { return Promise.each(tasks, (task) => { - if (task.skip && task.skip(ctx)) { + if (task.skip && task.skip(ctx) || (task.enabled && !task.enabled(ctx))) { return; } @@ -414,6 +453,8 @@ describe('Unit: Commands > Update', function () { expect(migratorStub.migrate.called).to.be.false; expect(migratorStub.rollback.called).to.be.true; expect(removeOldVersionsStub.called).to.be.false; + + expect(majorUpdateStub.called).to.be.false; }); }); diff --git a/test/unit/tasks/major-update/data-spec.js b/test/unit/tasks/major-update/data-spec.js new file mode 100644 index 000000000..5f95dc40a --- /dev/null +++ b/test/unit/tasks/major-update/data-spec.js @@ -0,0 +1,123 @@ +'use strict'; +const expect = require('chai').expect; +const sinon = require('sinon'); +const proxyquire = require('proxyquire'); +const errors = require('../../../../lib/errors'); + +describe('Unit: Tasks > Major Update > Data', function () { + let knexMock, gscanMock, data, connection; + + beforeEach(function () { + connection = sinon.stub(); + connection.raw = sinon.stub(); + connection.destroy = sinon.stub().callsFake((cb) => { + cb(); + }); + + knexMock = sinon.stub().returns(connection); + + gscanMock = sinon.stub(); + gscanMock.check = sinon.stub(); + gscanMock.format = sinon.stub(); + + knexMock['@noCallThru'] = true; + gscanMock['@noCallThru'] = true; + + data = proxyquire('../../../../lib/tasks/major-update/data', { + '/var/www/ghost/versions/2.0.0/node_modules/knex': knexMock, + '/var/www/ghost/versions/2.0.0/node_modules/gscan': gscanMock + }); + }); + + it('requires `options.dir`', function () { + return data().catch((err) => { + expect(err).to.be.an.instanceOf(errors.CliError); + }); + }); + + it('requires `options.database`', function () { + return data({dir: 'dir'}).catch((err) => { + expect(err).to.be.an.instanceOf(errors.CliError); + }); + }); + + it('sqlite3: success', function () { + connection.raw.withArgs('SELECT * FROM settings WHERE `key`="active_theme";').resolves([{value: 'casper'}]); + connection.raw.withArgs('SELECT uuid FROM posts WHERE slug="v2-demo-post";').resolves([{uuid: 'uuid'}]); + + gscanMock.check.resolves({unformatted: true}); + gscanMock.format.returns({formatted: true}); + + return data({ + database: { + client: 'sqlite3' + }, + dir: '/var/www/ghost/', + version: 'versions/2.0.0/' + }).then((response) => { + expect(response.gscanReport.formatted).to.be.true; + expect(response.demoPost.uuid).to.eql('uuid'); + + expect(connection.destroy.calledOnce).to.be.true; + expect(connection.raw.calledTwice).to.be.true; + + expect(gscanMock.check.calledOnce).to.be.true; + expect(gscanMock.format.calledOnce).to.be.true; + + expect(knexMock.calledOnce).to.be.true; + }); + }); + + it('mysql: success', function () { + connection.raw.withArgs('SELECT * FROM settings WHERE `key`="active_theme";').resolves([[{value: 'casper'}]]); + connection.raw.withArgs('SELECT uuid FROM posts WHERE slug="v2-demo-post";').resolves([[{uuid: 'uuid'}]]); + + gscanMock.check.resolves({unformatted: true}); + gscanMock.format.returns({formatted: true}); + + return data({ + database: { + client: 'mysql' + }, + dir: '/var/www/ghost/', + version: 'versions/2.0.0/' + }).then((response) => { + expect(response.gscanReport.formatted).to.be.true; + expect(response.demoPost.uuid).to.eql('uuid'); + + expect(connection.destroy.calledOnce).to.be.true; + expect(connection.raw.calledTwice).to.be.true; + + expect(gscanMock.check.calledOnce).to.be.true; + expect(gscanMock.format.calledOnce).to.be.true; + + expect(knexMock.calledOnce).to.be.true; + }); + }); + + it('fails', function () { + connection.raw.withArgs('SELECT * FROM settings WHERE `key`="active_theme";').resolves([[{value: 'casper'}]]); + connection.raw.withArgs('SELECT uuid FROM posts WHERE slug="v2-demo-post";').resolves([[{uuid: 'uuid'}]]); + + gscanMock.check.rejects(new Error('oops')); + + return data({ + database: { + client: 'mysql' + }, + dir: '/var/www/ghost/', + version: 'versions/2.0.0/' + }).then(() => { + expect('1').to.eql(1, 'Expected error'); + }).catch((err) => { + expect(err.message).to.eql('oops'); + expect(connection.destroy.calledOnce).to.be.true; + expect(connection.raw.calledOnce).to.be.true; + + expect(gscanMock.check.calledOnce).to.be.true; + expect(gscanMock.format.called).to.be.false; + + expect(knexMock.calledOnce).to.be.true; + }); + }); +}); diff --git a/test/unit/tasks/major-update/ui-spec.js b/test/unit/tasks/major-update/ui-spec.js new file mode 100644 index 000000000..c427d2b3b --- /dev/null +++ b/test/unit/tasks/major-update/ui-spec.js @@ -0,0 +1,220 @@ +'use strict'; + +const expect = require('chai').expect; +const sinon = require('sinon'); +const proxyquire = require('proxyquire'); +const configStub = require('../../../utils/config-stub'); +// From https://github.com/chalk/ansi-regex/blob/v3.0.0/index.js +const pattern = [ + '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\\u0007)', + '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))' +].join('|'); + +const COLOR_REGEX = new RegExp(pattern, 'g'); + +describe('Unit: Tasks > Major Update > UI', function () { + let ui, dataMock, ctx; + + beforeEach(function () { + ctx = { + instance: sinon.stub(), + ui: sinon.stub(), + version: '1.25.4' + }; + + ctx.ui.log = sinon.stub(); + ctx.ui.confirm = sinon.stub(); + + ctx.instance.config = configStub(); + ctx.instance.config.get.withArgs('url').returns('http://localhost:2368/'); + dataMock = sinon.stub(); + + ui = proxyquire('../../../../lib/tasks/major-update/ui', { + './data': dataMock + }); + }); + + it('theme is compatible', function () { + ctx.ui.confirm.resolves(true); + + dataMock.resolves({ + gscanReport: { + results: { + error: { + all: [] + }, + warning: { + all: [] + } + } + }, + demoPost: { + uuid: '1234' + } + }); + + return ui(ctx).then(() => { + expect(ctx.ui.log.callCount).to.eql(4); + expect(ctx.ui.confirm.calledOnce).to.be.true; + }); + }); + + it('theme has warnings', function () { + ctx.ui.confirm.resolves(true); + + dataMock.resolves({ + gscanReport: { + results: { + error: { + all: [] + }, + warning: { + all: [{RULE_01: {failures: [{ref: 'index.hbs'}]}}], + byFiles: {'index.hbs': [{rule: 'Replace this.'}]} + } + } + }, + demoPost: { + uuid: '1234' + } + }); + + return ui(ctx).then(() => { + expect(ctx.ui.log.callCount).to.eql(6); + expect(ctx.ui.confirm.calledTwice).to.be.true; + + const output = ctx.ui.log.args.join(' ').replace(COLOR_REGEX, ''); + + expect(output.match(/Your theme has 1 warning/)).to.exist; + expect(output.match(/File: index.hbs/)).to.exist; + expect(output.match(/- Replace this/)).to.exist; + expect(output.match(/Visit the demo post at http:\/\/localhost:2368\/p\/1234\//)).to.exist; + expect(output.match(/check theme compatibility at https:\/\/gscan.ghost.org/)).to.exist; + }); + }); + + it('theme has errors', function () { + ctx.ui.confirm.resolves(true); + + dataMock.resolves({ + gscanReport: { + results: { + error: { + all: [{RULE_10: {failures: [{ref: 'post.hbs'}, {ref: 'page.hbs'}]}}, {RULE_20: {failures: [{ref: 'page.hbs'}]}}], + byFiles: { + 'post.hbs': [{rule: 'This is an Error.'}], + 'page.hbs': [{rule: 'This is another Error.'}, {rule: 'This is an Error.'}] + } + }, + warning: { + all: [] + } + } + }, + demoPost: { + uuid: '1234' + } + }); + + return ui(ctx).then(() => { + expect(ctx.ui.log.callCount).to.eql(7); + expect(ctx.ui.confirm.calledTwice).to.be.true; + + const output = ctx.ui.log.args.join(' ').replace(COLOR_REGEX, ''); + + expect(output.match(/Your theme has 2 errors/)).to.exist; + expect(output.match(/File: post.hbs/)).to.exist; + expect(output.match(/File: page.hbs/)).to.exist; + expect(output.match(/This is an Error./g).length).to.eql(2); + expect(output.match(/This is another Error./g).length).to.eql(1); + }); + }); + + it('theme has errors and warnings', function () { + ctx.ui.confirm.resolves(true); + + dataMock.resolves({ + gscanReport: { + results: { + error: { + all: [{RULE_10: {failures: [{ref: 'post.hbs'}, {ref: 'page.hbs'}]}}, {RULE_20: {failures: [{ref: 'page.hbs'}]}}], + byFiles: { + 'post.hbs': [{rule: 'This is an Error.'}], + 'page.hbs': [{rule: 'This is another Error.'}, {rule: 'This is an Error.'}] + } + }, + warning: { + all: [{RULE_01: {failures: [{ref: 'package.json'}]}}], + byFiles: {'package.json': [{rule: 'This attribute is important.'}]} + } + } + }, + demoPost: { + uuid: '1234' + } + }); + + return ui(ctx).then(() => { + expect(ctx.ui.log.callCount).to.eql(9); + expect(ctx.ui.confirm.calledTwice).to.be.true; + + const output = ctx.ui.log.args.join(' ').replace(COLOR_REGEX, ''); + + expect(output.match(/Your theme has 2 errors and 1 warning/)).to.exist; + expect(output.match(/File: post.hbs/)).to.exist; + expect(output.match(/File: page.hbs/)).to.exist; + expect(output.match(/File: package.json/)).to.exist; + expect(output.match(/This is an Error./g).length).to.eql(2); + expect(output.match(/This is another Error./g).length).to.eql(1); + expect(output.match(/This attribute is important./g).length).to.eql(1); + }); + }); + + it('theme has fatal errors and warnings', function () { + ctx.ui.confirm.resolves(true); + + dataMock.resolves({ + gscanReport: { + results: { + hasFatalErrors: true, + error: { + all: [{RULE_10: {failures: [{ref: 'post.hbs'}, {ref: 'page.hbs'}]}}, {RULE_20: {failures: [{ref: 'page.hbs'}]}}], + byFiles: { + 'post.hbs': [{rule: 'This is an Error.', fatal: true}], + 'page.hbs': [{rule: 'This is another Error.'}, {rule: 'This is an Error.'}] + } + }, + warning: { + all: [{RULE_01: {failures: [{ref: 'package.json'}]}}], + byFiles: {'package.json': [{rule: 'This attribute is important.'}]} + } + } + }, + demoPost: { + uuid: '1234' + } + }); + + return ui(ctx) + .then(() => { + expect('1').to.eql(1, 'Test should fail'); + }) + .catch((err) => { + expect(err.message).to.match(/Migration failed. Your theme has fatal errors/); + expect(err.logMessageOnly).to.exist; + + expect(ctx.ui.log.callCount).to.eql(9); + expect(ctx.ui.confirm.calledOnce).to.be.true; + + const output = ctx.ui.log.args.join(' ').replace(COLOR_REGEX, ''); + + expect(output.match(/Your theme has 2 errors and 1 warning/)).to.exist; + expect(output.match(/File: post.hbs/)).to.exist; + expect(output.match(/File: page.hbs/)).to.exist; + expect(output.match(/File: package.json/)).to.exist; + expect(output.match(/This is an Error./g).length).to.eql(2); + expect(output.match(/This is another Error./g).length).to.eql(1); + expect(output.match(/This attribute is important./g).length).to.eql(1); + }); + }); +}); diff --git a/test/unit/ui/index-spec.js b/test/unit/ui/index-spec.js index adc5e4c1d..3a495a488 100644 --- a/test/unit/ui/index-spec.js +++ b/test/unit/ui/index-spec.js @@ -290,13 +290,15 @@ describe('Unit: UI', function () { type: 'confirm', name: 'yes', message: 'Is the sky blue', - default: true + default: true, + prefix: undefined }; const testB = { type: 'confirm', name: 'yes', message: 'Is ghost just a blogging platform', - default: undefined + default: undefined, + prefix: undefined }; return ui.confirm('Is the sky blue', true).then((result) => {