diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 41dd920a8..ae82209f9 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -1,2 +1,4 @@ +//= require_directory ./modules + //= require govuk_publishing_components/dependencies //= require govuk_publishing_components/all_components diff --git a/app/assets/javascripts/legacy_modules/downtime_message.js b/app/assets/javascripts/legacy_modules/legacy_downtime_message.js similarity index 98% rename from app/assets/javascripts/legacy_modules/downtime_message.js rename to app/assets/javascripts/legacy_modules/legacy_downtime_message.js index 76b6ffd03..e381851d7 100644 --- a/app/assets/javascripts/legacy_modules/downtime_message.js +++ b/app/assets/javascripts/legacy_modules/legacy_downtime_message.js @@ -2,7 +2,7 @@ (function (Modules) { 'use strict' - Modules.DowntimeMessage = function () { + Modules.LegacyDowntimeMessage = function () { this.start = function (element) { var $startTimeFields = element.find('.js-start-time select') var $stopTimeFields = element.find('.js-stop-time select') diff --git a/app/assets/javascripts/modules/downtime_message.js b/app/assets/javascripts/modules/downtime_message.js new file mode 100644 index 000000000..45e914c2d --- /dev/null +++ b/app/assets/javascripts/modules/downtime_message.js @@ -0,0 +1,97 @@ +//= require govuk_publishing_components/vendor/polyfills/closest + +window.GOVUK = window.GOVUK || {} +window.GOVUK.Modules = window.GOVUK.Modules || {}; + +(function (Modules) { + function DowntimeMessage ($module) { + this.module = $module + } + + DowntimeMessage.prototype.init = function () { + const form = this.module + + form.addEventListener('change', updateMessage) + + function updateMessage () { + const fromDate = getDate('from') + const toDate = getDate('to') + + let message = '' + if (isValidSchedule(fromDate, toDate)) { + message = downtimeMessage(fromDate, toDate) + } + form.elements.message.value = message + } + + function getDate (selector) { + const day = form.elements[`${selector}-date[day]`].value + const month = form.elements[`${selector}-date[month]`].value + const year = form.elements[`${selector}-date[year]`].value + const hours = form.elements[`${selector}-time[hour]`].value + const minutes = form.elements[`${selector}-time[minute]`].value + + // The Date class treats 1 as February, but in the UI we expect 1 to be January + const zeroIndexedMonth = parseInt(month) - 1 + return new Date(year, zeroIndexedMonth, day, hours, minutes) + } + + function isValidSchedule (fromDate, toDate) { + return toDate.getTime() > fromDate.getTime() + } + + function downtimeMessage (fromDate, toDate) { + let message = 'This service will be unavailable from' + const fromDayText = getDayText(fromDate) + const fromTime = getTime(fromDate) + const toTime = getTime(toDate) + const toDayText = getDayText(toDate) + const sameDay = isSameDay(fromDate, toDate) + + if (!isValidSchedule(fromDate, toDate)) { + return '' + } + + if (sameDay) { + message = `${message} ${fromTime} to ${toTime} on ${fromDayText}.` + } else { + message = `${message} ${fromTime} on ${fromDayText} to ${toTime} on ${toDayText}.` + } + + return message + } + + function getTime (date) { + const time = date.toLocaleString( + 'en-GB', + { hour: 'numeric', minute: 'numeric', hourCycle: 'h12' }) + return time.replace(/:00/, '') + .replace(/ am/, 'am') + .replace(/ pm/, 'pm') + .replace(/12am/, 'midnight') + .replace(/12pm/, 'midday') + } + + function getDayText (date) { + const dayText = date.toLocaleDateString( + 'en-GB', + { weekday: 'long', month: 'long', day: 'numeric' } + ) + return dayText.replace(/,/, '') + } + + function isSameDay (fromDate, toDate) { + // Treat a midnight stop date as being on the same day as + // the hours before it. eg + // Unavailable from 10pm to midnight on Thursday 8 January + const toDateOneMinuteEarlier = new Date(toDate.valueOf()) + toDateOneMinuteEarlier.setMinutes(toDate.getMinutes() - 1) + + return fromDate.getFullYear() === toDateOneMinuteEarlier.getFullYear() && + fromDate.getMonth() === toDateOneMinuteEarlier.getMonth() && + fromDate.getDate() === toDateOneMinuteEarlier.getDate() + } + } + + Modules.DowntimeMessage = DowntimeMessage +})(window.GOVUK.Modules) diff --git a/app/views/legacy_downtimes/edit.html.erb b/app/views/legacy_downtimes/edit.html.erb index 9d263c669..e2f850b37 100644 --- a/app/views/legacy_downtimes/edit.html.erb +++ b/app/views/legacy_downtimes/edit.html.erb @@ -12,7 +12,7 @@ -<%= form_for @downtime, url: edition_downtime_path(@edition), html: { class: 'form well remove-top-margin', 'data-module': 'downtime-message' } do |f| %> +<%= form_for @downtime, url: edition_downtime_path(@edition), html: { class: 'form well remove-top-margin', 'data-module': 'legacy-downtime-message' } do |f| %> <%= render 'form', f: f %> <%= f.submit 'Re-schedule downtime message', class: 'js-submit btn btn-success' %> <%= f.submit 'Cancel downtime', class: 'add-left-margin btn btn-danger' %> diff --git a/app/views/legacy_downtimes/new.html.erb b/app/views/legacy_downtimes/new.html.erb index e7e64545c..3fdd6b95e 100644 --- a/app/views/legacy_downtimes/new.html.erb +++ b/app/views/legacy_downtimes/new.html.erb @@ -12,7 +12,7 @@ -<%= form_for @downtime, url: edition_downtime_path(@edition), html: { class: 'form well remove-top-margin', 'data-module' => 'downtime-message' } do |f| %> +<%= form_for @downtime, url: edition_downtime_path(@edition), html: { class: 'form well remove-top-margin', 'data-module' => 'legacy-downtime-message' } do |f| %> <%= render 'form', f: f %> <%= f.submit 'Schedule downtime message', class: 'js-submit btn btn-success' %> <% end %> diff --git a/spec/javascripts/spec/downtime_message.spec.js b/spec/javascripts/spec/downtime_message.spec.js index 4a360e0c0..cfc4908e9 100644 --- a/spec/javascripts/spec/downtime_message.spec.js +++ b/spec/javascripts/spec/downtime_message.spec.js @@ -1,261 +1,316 @@ +/* global GOVUK */ + describe('A downtime message module', function () { 'use strict' - var downtimeMessage, - form + let downtimeMessage, form + + const formHtml = `
` beforeEach(function () { - var formHTML = '' + - '' + - '' + - '' + - '' - - form = $( - '' - ) - - $('body').append(form) + form = document.createElement('form') + form.innerHTML = formHtml + document.body.appendChild(form) - downtimeMessage = new GOVUKAdmin.Modules.DowntimeMessage() - downtimeMessage.start(form) + downtimeMessage = new GOVUK.Modules.DowntimeMessage(form) + downtimeMessage.init(form) }) afterEach(function () { form.remove() }) - describe('when starting', function () { + describe('when initialising', function () { it('leaves any existing downtime message alone', function () { expectDowntimeMessageToMatch('starting message') }) - - it('disables the form if the preloaded dates aren’t valid', function () { - expectDisabledForm() - }) }) - describe('when selecting dates', function () { - it('generates a downtime message and a schedule message', function () { - selectStartDate() - selectStopDate({ hour: '02' }) + describe('when entering dates', function () { + it('generates a downtime message', function () { + enterFromDate() + enterToDate({ hour: '02' }) expectDowntimeMessageToMatch('This service will be unavailable from 1am to 2am on Thursday 1 January.') - expectScheduleMessageToMatch('A downtime message will show from midnight on Wednesday 31 December') - selectStopDate({ hour: '03' }) + enterToDate({ hour: '03' }) expectDowntimeMessageToMatch('from 1am to 3am on Thursday 1 January.') - expectScheduleMessageToMatch('from midnight on Wednesday 31 December') - - expectEnabledForm() }) describe('that are the same', function () { beforeEach(function () { - selectStartDate() - selectStopDate() + enterFromDate() + enterToDate() }) - it('doesn’t generate a message', function () { + it('sets an empty message', function () { expectDowntimeMessageToBe('') }) - - it('disables the form', function () { - expectDisabledForm() - }) }) describe('with a stop date before the start date', function () { beforeEach(function () { - selectStartDate({ hour: '03' }) - selectStopDate({ hour: '01' }) + enterFromDate({ hour: '03' }) + enterToDate({ hour: '01' }) }) - it('doesn’t generate a message', function () { + it('sets an empty message', function () { expectDowntimeMessageToBe('') }) - - it('disables the form', function () { - expectDisabledForm() - }) }) describe('the generated messages', function () { it('use a 12 hour clock', function () { - selectStartDate({ hour: '11' }) - selectStopDate({ hour: '15' }) + enterFromDate({ hour: '11' }) + enterToDate({ hour: '15' }) expectDowntimeMessageToMatch('from 11am to 3pm on Thursday 1 January.') - expectScheduleMessageToMatch('from midnight on Wednesday 31 December') - expectEnabledForm() }) it('use midnight instead of 12am', function () { - selectStartDate({ hour: '00' }) - selectStopDate({ hour: '02' }) + enterFromDate({ hour: '00' }) + enterToDate({ hour: '02' }) expectDowntimeMessageToMatch('from midnight to 2am on Thursday 1 January.') - expectScheduleMessageToMatch('from midnight on Wednesday 31 December') }) it('use midday instead of 12pm', function () { - selectStartDate({ hour: '12' }) - selectStopDate({ hour: '14' }) + enterFromDate({ hour: '12' }) + enterToDate({ hour: '14' }) expectDowntimeMessageToMatch('from midday to 2pm on Thursday 1 January.') - expectScheduleMessageToMatch('from midnight on Wednesday 31 December') }) it('includes minutes when they are not 0', function () { - selectStartDate({ hour: '00', minutes: '15' }) - selectStopDate({ hour: '02', minutes: '45' }) + enterFromDate({ hour: '00', minutes: '15' }) + enterToDate({ hour: '02', minutes: '45' }) expectDowntimeMessageToMatch('from 12:15am to 2:45am on Thursday 1 January.') - expectScheduleMessageToMatch('from midnight on Wednesday 31 December') }) it('includes both dates when they differ', function () { - selectStartDate() - selectStopDate({ day: '2', hour: '03' }) + enterFromDate() + enterToDate({ day: '2', hour: '03' }) expectDowntimeMessageToMatch('from 1am on Thursday 1 January to 3am on Friday 2 January.') }) it('treats midnight on the next consecutive day as the same date', function () { - selectStartDate({ hour: '22', day: '1' }) - selectStopDate({ hour: '00', day: '2' }) + enterFromDate({ hour: '22', day: '1' }) + enterToDate({ hour: '00', day: '2' }) expectDowntimeMessageToMatch('from 10pm to midnight on Thursday 1 January.') - expectScheduleMessageToMatch('from midnight on Wednesday 31 December') }) it('handles incorrect dates in the same way as rails', function () { - selectStartDate({ day: '29', month: '2' }) - selectStopDate({ day: '5', month: '3' }) + enterFromDate({ day: '29', month: '2' }) + enterToDate({ day: '5', month: '3' }) expectDowntimeMessageToMatch('from 1am on Sunday 1 March to 1am on Thursday 5 March.') - expectScheduleMessageToMatch('from midnight on Saturday 28 February') - expectEnabledForm() }) }) }) function expectDowntimeMessageToMatch (text) { - expect(form.find('.js-downtime-message').val()).toMatch(text) + expect(form.elements.message.value).toMatch(text) } function expectDowntimeMessageToBe (text) { - expect(form.find('.js-downtime-message').val()).toBe(text) - } - - function expectScheduleMessageToMatch (text) { - expect(form.find('.js-schedule-message').text()).toMatch(text) - } - - function expectDisabledForm () { - expect(form.find('.js-submit:disabled').length).toBe(1) - expect(form.find('.js-downtime-message:disabled').length).toBe(1) - expectScheduleMessageToMatch('Please select a valid date range') - } - - function expectEnabledForm () { - expect(form.find('.js-submit:disabled').length).toBe(0) - expect(form.find('.js-downtime-message:disabled').length).toBe(0) + expect(form.elements.message.value).toBe(text) } - function selectStartDate (dateObj) { - selectDate('.js-start-time', dateObj) + function enterFromDate (dateObj) { + enterDate('from', dateObj) } - function selectStopDate (dateObj) { - selectDate('.js-stop-time', dateObj) + function enterToDate (dateObj) { + enterDate('to', dateObj) } - function selectDate (selector, dateObj) { - var selects = $(selector + ' select') + function enterDate (selector, dateObj) { + const day = form.elements[`${selector}-date[day]`] + const month = form.elements[`${selector}-date[month]`] + const year = form.elements[`${selector}-date[year]`] + const hour = form.elements[`${selector}-time[hour]`] + const minute = form.elements[`${selector}-time[minute]`] dateObj = dateObj || {} - selects.eq(0).val(dateObj.hour || '01') - selects.eq(1).val(dateObj.minutes || '00') - selects.eq(2).val(dateObj.day || '1') - selects.eq(3).val(dateObj.month || '1') - selects.eq(4).val(dateObj.year || '2015').trigger('change') + day.value = dateObj.day || '1' + month.value = dateObj.month || '1' + year.value = dateObj.year || '2015' + hour.value = dateObj.hour || '1' + minute.value = dateObj.minutes || '0' + + const event = new Event('change') + form.dispatchEvent(event) } }) diff --git a/spec/javascripts/spec/legacy_downtime_message.spec.js b/spec/javascripts/spec/legacy_downtime_message.spec.js new file mode 100644 index 000000000..b92c0a8c8 --- /dev/null +++ b/spec/javascripts/spec/legacy_downtime_message.spec.js @@ -0,0 +1,261 @@ +describe('A downtime message module', function () { + 'use strict' + + var downtimeMessage, + form + + beforeEach(function () { + var formHTML = '' + + '' + + '' + + '' + + '' + + form = $( + '' + ) + + $('body').append(form) + + downtimeMessage = new GOVUKAdmin.Modules.LegacyDowntimeMessage() + downtimeMessage.start(form) + }) + + afterEach(function () { + form.remove() + }) + + describe('when starting', function () { + it('leaves any existing downtime message alone', function () { + expectDowntimeMessageToMatch('starting message') + }) + + it('disables the form if the preloaded dates aren’t valid', function () { + expectDisabledForm() + }) + }) + + describe('when selecting dates', function () { + it('generates a downtime message and a schedule message', function () { + selectStartDate() + selectStopDate({ hour: '02' }) + expectDowntimeMessageToMatch('This service will be unavailable from 1am to 2am on Thursday 1 January.') + expectScheduleMessageToMatch('A downtime message will show from midnight on Wednesday 31 December') + + selectStopDate({ hour: '03' }) + expectDowntimeMessageToMatch('from 1am to 3am on Thursday 1 January.') + expectScheduleMessageToMatch('from midnight on Wednesday 31 December') + + expectEnabledForm() + }) + + describe('that are the same', function () { + beforeEach(function () { + selectStartDate() + selectStopDate() + }) + + it('doesn’t generate a message', function () { + expectDowntimeMessageToBe('') + }) + + it('disables the form', function () { + expectDisabledForm() + }) + }) + + describe('with a stop date before the start date', function () { + beforeEach(function () { + selectStartDate({ hour: '03' }) + selectStopDate({ hour: '01' }) + }) + + it('doesn’t generate a message', function () { + expectDowntimeMessageToBe('') + }) + + it('disables the form', function () { + expectDisabledForm() + }) + }) + + describe('the generated messages', function () { + it('use a 12 hour clock', function () { + selectStartDate({ hour: '11' }) + selectStopDate({ hour: '15' }) + expectDowntimeMessageToMatch('from 11am to 3pm on Thursday 1 January.') + expectScheduleMessageToMatch('from midnight on Wednesday 31 December') + expectEnabledForm() + }) + + it('use midnight instead of 12am', function () { + selectStartDate({ hour: '00' }) + selectStopDate({ hour: '02' }) + expectDowntimeMessageToMatch('from midnight to 2am on Thursday 1 January.') + expectScheduleMessageToMatch('from midnight on Wednesday 31 December') + }) + + it('use midday instead of 12pm', function () { + selectStartDate({ hour: '12' }) + selectStopDate({ hour: '14' }) + expectDowntimeMessageToMatch('from midday to 2pm on Thursday 1 January.') + expectScheduleMessageToMatch('from midnight on Wednesday 31 December') + }) + + it('includes minutes when they are not 0', function () { + selectStartDate({ hour: '00', minutes: '15' }) + selectStopDate({ hour: '02', minutes: '45' }) + expectDowntimeMessageToMatch('from 12:15am to 2:45am on Thursday 1 January.') + expectScheduleMessageToMatch('from midnight on Wednesday 31 December') + }) + + it('includes both dates when they differ', function () { + selectStartDate() + selectStopDate({ day: '2', hour: '03' }) + expectDowntimeMessageToMatch('from 1am on Thursday 1 January to 3am on Friday 2 January.') + }) + + it('treats midnight on the next consecutive day as the same date', function () { + selectStartDate({ hour: '22', day: '1' }) + selectStopDate({ hour: '00', day: '2' }) + expectDowntimeMessageToMatch('from 10pm to midnight on Thursday 1 January.') + expectScheduleMessageToMatch('from midnight on Wednesday 31 December') + }) + + it('handles incorrect dates in the same way as rails', function () { + selectStartDate({ day: '29', month: '2' }) + selectStopDate({ day: '5', month: '3' }) + expectDowntimeMessageToMatch('from 1am on Sunday 1 March to 1am on Thursday 5 March.') + expectScheduleMessageToMatch('from midnight on Saturday 28 February') + expectEnabledForm() + }) + }) + }) + + function expectDowntimeMessageToMatch (text) { + expect(form.find('.js-downtime-message').val()).toMatch(text) + } + + function expectDowntimeMessageToBe (text) { + expect(form.find('.js-downtime-message').val()).toBe(text) + } + + function expectScheduleMessageToMatch (text) { + expect(form.find('.js-schedule-message').text()).toMatch(text) + } + + function expectDisabledForm () { + expect(form.find('.js-submit:disabled').length).toBe(1) + expect(form.find('.js-downtime-message:disabled').length).toBe(1) + expectScheduleMessageToMatch('Please select a valid date range') + } + + function expectEnabledForm () { + expect(form.find('.js-submit:disabled').length).toBe(0) + expect(form.find('.js-downtime-message:disabled').length).toBe(0) + } + + function selectStartDate (dateObj) { + selectDate('.js-start-time', dateObj) + } + + function selectStopDate (dateObj) { + selectDate('.js-stop-time', dateObj) + } + + function selectDate (selector, dateObj) { + var selects = $(selector + ' select') + + dateObj = dateObj || {} + + selects.eq(0).val(dateObj.hour || '01') + selects.eq(1).val(dateObj.minutes || '00') + selects.eq(2).val(dateObj.day || '1') + selects.eq(3).val(dateObj.month || '1') + selects.eq(4).val(dateObj.year || '2015').trigger('change') + } +})