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) => {