From ccd09b9fd74b27f4df4574b3e9c656aec0b88acb Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Wed, 15 May 2019 14:26:51 +0100 Subject: [PATCH] Revert "Merge pull request #2969 from alphagov/revert-2956-progressively-enhance-folder-permissions" This reverts commit 8266f3d65cf2f0a81de92fc9a178b95aa13a798a, reversing changes made to b2a38fe2222fa54973a330931ad13029e2ef744b. --- app/assets/images/folder-black.png | Bin 0 -> 321 bytes app/assets/images/folder-black.svg | 1 + .../javascripts/collapsibleCheckboxes.js | 170 ++++++ .../stick-to-window-when-scrolling.js | 5 +- app/assets/stylesheets/app.scss | 10 + .../stylesheets/components/checkboxes.scss | 60 +++ app/templates/components/checkbox.html | 8 +- app/templates/components/select-input.html | 20 +- .../views/manage-users/permissions.html | 11 +- .../templates/manage-template-folder.html | 2 +- gulpfile.js | 1 + .../javascripts/collapsibleCheckboxes.test.js | 500 ++++++++++++++++++ tests/javascripts/support/helpers.js | 84 +++ 13 files changed, 850 insertions(+), 22 deletions(-) create mode 100644 app/assets/images/folder-black.png create mode 100644 app/assets/images/folder-black.svg create mode 100644 app/assets/javascripts/collapsibleCheckboxes.js create mode 100644 tests/javascripts/collapsibleCheckboxes.test.js create mode 100644 tests/javascripts/support/helpers.js diff --git a/app/assets/images/folder-black.png b/app/assets/images/folder-black.png new file mode 100644 index 0000000000000000000000000000000000000000..3c586bc69524f06f12ce1fc572f194ee0e3a67da GIT binary patch literal 321 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)i!3HEJJLv8MQY^(zo*^7SP{WbZ0pxQQctjR6 zFz_7#VaBQ2e9}Nc$r9IylHmNblJdl&REF~Ma=pyF?Be9af>gcyqV(DCY(U2D^K@|x z@#uUz)sd^oL8NWJ=kCS_Nhge5gf8ml+*R6;98&&*>FT1W^41Q8Ma3Lm<_1T^E*^5K z$a?#%nSJw*nf)^>_b{GlEnUFKbwc;FQ+BLW*W(!`zVZ!h?@C?F8XjB7_&>jNA;~Uz z|Ltv!h8xx>XL&j~8dT}4m#QQMp8x)`Le4?_=WMZ4(>kUEwLF}1wkq_kgoB7{O60dJ zw(F%kWAdk(Pc)GE;yKBstaSf@j-C@8Jujp$9>3mlDBy4R!)A5W#ZD^@N&L?M`h>yL L)z4*}Q$iB}QvZ7E literal 0 HcmV?d00001 diff --git a/app/assets/images/folder-black.svg b/app/assets/images/folder-black.svg new file mode 100644 index 000000000..d6de0a261 --- /dev/null +++ b/app/assets/images/folder-black.svg @@ -0,0 +1 @@ + diff --git a/app/assets/javascripts/collapsibleCheckboxes.js b/app/assets/javascripts/collapsibleCheckboxes.js new file mode 100644 index 000000000..3eea8693d --- /dev/null +++ b/app/assets/javascripts/collapsibleCheckboxes.js @@ -0,0 +1,170 @@ +(function (global) { + "use strict"; + + const GOVUK = global.GOVUK; + + function Summary (module) { + this.module = module; + this.$el = module.$formGroup.find('.selection-summary'); + this.fieldLabel = module.fieldLabel; + this.total = module.total; + this.addContent(); + this.update(module.getSelection()); + } + Summary.prototype.templates = { + all: (selection, total, field) => `All ${field}s`, + some: (selection, total, field) => `${selection} of ${total} ${field}s`, + none: (selection, total, field) => ({ + "folder": "No folders (only templates outside a folder)", + "team member": "No team members (only you)" + }[field] || `No ${field}s`) + }; + Summary.prototype.addContent = function() { + this.$text = $(`

`); + + if (this.fieldLabel === 'folder') { this.$text.addClass('selection-summary__text--folders'); } + + this.$el.append(this.$text); + }; + Summary.prototype.update = function(selection) { + let template; + + if (selection === this.total) { + template = 'all'; + } else if (selection > 0) { + template = 'some'; + } else { + template = 'none'; + } + + this.$text.html(this.templates[template](selection, this.total, this.fieldLabel)); + }; + Summary.prototype.bindEvents = function () { + // take summary out of tab order when focus moves + this.$el.on('blur', (e) => $(this).attr('tabindex', '-1')); + }; + + function Footer (module) { + this.module = module; + this.fieldLabel = module.fieldLabel; + this.fieldsetId = module.$fieldset.attr('id'); + this.$el = this.getEl(this.module.expanded); + this.module.$formGroup.append(this.$el); + } + Footer.prototype.buttonContent = { + change: (fieldLabel) => `Choose ${fieldLabel}s`, + done: (fieldLabel) => `Done choosing ${fieldLabel}s` + }; + Footer.prototype.getEl = function (expanded) { + const buttonState = expanded ? 'done' : 'change'; + const buttonContent = this.buttonContent[buttonState](this.fieldLabel); + const stickyClass = expanded ? ' js-stick-at-bottom-when-scrolling' : ''; + + return $(`

+ +
`); + }; + Footer.prototype.update = function (expanded) { + this.$el.remove(); + this.$el = this.getEl(expanded); + + this.module.$formGroup.append(this.$el); + + // make footer sticky if expanded, clear up from it being sticky if not + GOVUK.stickAtBottomWhenScrolling.recalculate(); + }; + + function CollapsibleCheckboxes () {} + CollapsibleCheckboxes.prototype._focusTextElement = ($el) => { + $el + .attr('tabindex', '-1') + .focus(); + }; + CollapsibleCheckboxes.prototype.start = function(component) { + this.$formGroup = $(component); + this.$fieldset = this.$formGroup.find('fieldset'); + this.$checkboxes = this.$fieldset.find('input[type=checkbox]'); + this.fieldLabel = this.$formGroup.data('fieldLabel'); + this.total = this.$checkboxes.length; + this.legendText = this.$fieldset.find('legend').text().trim(); + this.expanded = false; + + this.addHeadingHideLegend(); + + // generate summary and footer + this.footer = new Footer(this); + this.summary = new Summary(this); + + this.$fieldset.before(this.summary.$el); + + // add custom classes + this.$formGroup.addClass('selection-wrapper'); + this.$fieldset.addClass('selection-content'); + + // hide checkboxes + this.$fieldset.hide(); + + this.bindEvents(); + }; + CollapsibleCheckboxes.prototype.getSelection = function() { return this.$checkboxes.filter(':checked').length; }; + CollapsibleCheckboxes.prototype.addHeadingHideLegend = function() { + const headingLevel = this.$formGroup.data('heading-level') || '2'; + + this.$heading = $(`${this.legendText}`); + this.$fieldset.before(this.$heading); + + this.$fieldset.find('legend').addClass('visuallyhidden'); + }; + CollapsibleCheckboxes.prototype.expand = function(e) { + if (e !== undefined) { e.preventDefault(); } + + if (!this.expanded) { + this.$fieldset.show(); + this.expanded = true; + this.summary.update(this.getSelection()); + this.footer.update(this.expanded); + } + + // shift focus whether expanded or not + this._focusTextElement(this.$fieldset); + }; + CollapsibleCheckboxes.prototype.collapse = function(e) { + if (e !== undefined) { e.preventDefault(); } + + if (this.expanded) { + this.$fieldset.hide(); + this.expanded = false; + this.summary.update(this.getSelection()); + this.footer.update(this.expanded); + } + + // shift focus whether expanded or not + this._focusTextElement(this.summary.$text); + }; + CollapsibleCheckboxes.prototype.handleClick = function(e) { + if (this.expanded) { + this.collapse(e); + } else { + this.expand(e); + } + }; + CollapsibleCheckboxes.prototype.handleSelection = function(e) { + this.summary.update(this.getSelection(), this.total, this.fieldLabel); + }; + CollapsibleCheckboxes.prototype.bindEvents = function() { + const self = this; + + this.$formGroup.on('click', '.button', this.handleClick.bind(this)); + this.$checkboxes.on('click', this.handleSelection.bind(this)); + + this.summary.bindEvents(this); + }; + + GOVUK.Modules.CollapsibleCheckboxes = CollapsibleCheckboxes; + +}(window)); diff --git a/app/assets/javascripts/stick-to-window-when-scrolling.js b/app/assets/javascripts/stick-to-window-when-scrolling.js index 7f2066aa5..0a3176beb 100644 --- a/app/assets/javascripts/stick-to-window-when-scrolling.js +++ b/app/assets/javascripts/stick-to-window-when-scrolling.js @@ -11,12 +11,11 @@ var $scrollArea = $el.closest('.sticky-scroll-area'); $scrollArea = $scrollArea.length ? $scrollArea : $el.parent(); - scrollArea = $scrollArea.get(0); this._els = [el]; this.edge = edge; this.selector = selector; - this.node = scrollArea; + this.node = $scrollArea.get(0); this.setEvents(); }; ScrollArea.prototype.addEl = function (el) { @@ -38,7 +37,7 @@ }; ScrollArea.prototype.getFocusedDetails = { forElement: function ($focusedElement) { - focused = { + var focused = { 'top': $focusedElement.offset().top, 'height': $focusedElement.outerHeight(), 'type': 'element' diff --git a/app/assets/stylesheets/app.scss b/app/assets/stylesheets/app.scss index 4bd8db24b..3cb2aa81b 100644 --- a/app/assets/stylesheets/app.scss +++ b/app/assets/stylesheets/app.scss @@ -124,10 +124,20 @@ td { .form-label { margin-bottom: 5px; + + &.heading-small { + @include bold-19(); + } } .hint { color: $secondary-text-colour; + + .form-label + & { + display: block; + margin-top: -5px; + margin-bottom: 5px; + } } .list-bullet { diff --git a/app/assets/stylesheets/components/checkboxes.scss b/app/assets/stylesheets/components/checkboxes.scss index 40ca600d4..46e8269a9 100644 --- a/app/assets/stylesheets/components/checkboxes.scss +++ b/app/assets/stylesheets/components/checkboxes.scss @@ -1,3 +1,37 @@ +.selection-summary { + + .selection-summary__text { + @include core-19($tabular-numbers: true); + padding: 5px 0 0 0; + margin-bottom: $gutter / 2; + + &:focus { + outline: none; + } + } + + .selection-summary__text--folders { + padding: 10px 15px 5px 51px; + background-image: file-url('folder-black.svg'); + background-repeat: no-repeat; + background-size: 39px auto; + background-position: 0px 4px; + + @include ie-lte(8) { + background-image: file-url('folder-black.png'); + } + } + + // revert full-width button for smaller screens + .button { + display: inline-block; + width: auto; + position: relative; + top: 1px; + } + +} + .checkboxes-nested { margin-bottom: 10px; @@ -42,3 +76,29 @@ } } + +.selection-content { + margin-bottom: ($gutter / 3) * 2; + + .checkboxes-nested { + margin-bottom: 0; + } +} + +.selection-footer { + clear: both; + + .button-secondary { + // revert full-width button for smaller screens + display: inline-block; + width: auto; + } +} + +// styles specific to the collapsible checkboxes module +.selection-wrapper { + fieldset:focus { + outline: none; + } + +} diff --git a/app/templates/components/checkbox.html b/app/templates/components/checkbox.html index 51594eea6..3647c4f1c 100644 --- a/app/templates/components/checkbox.html +++ b/app/templates/components/checkbox.html @@ -19,13 +19,13 @@ {% endmacro %} -{% macro checkboxes_nested(field, child_map, hint=None, disable=[], option_hints={}, hide_legend=False) %} - {{ select_nested(field, child_map, hint, disable, option_hints, hide_legend, input="checkbox") }} +{% macro checkboxes_nested(field, child_map, hint=None, disable=[], option_hints={}, hide_legend=False, collapsible_opts={}, legend_style="text") %} + {{ select_nested(field, child_map, hint, disable, option_hints, hide_legend, collapsible_opts, legend_style, input="checkbox") }} {% endmacro %} -{% macro checkboxes(field, hint=None, disable=[], option_hints={}, hide_legend=False) %} - {{ select(field, hint, disable, option_hints, hide_legend, input="checkbox") }} +{% macro checkboxes(field, hint=None, disable=[], option_hints={}, hide_legend=False, collapsible_opts={}) %} + {{ select(field, hint, disable, option_hints, hide_legend, collapsible_opts, input="checkbox") }} {% endmacro %} diff --git a/app/templates/components/select-input.html b/app/templates/components/select-input.html index b3cfbb9e4..f65647647 100644 --- a/app/templates/components/select-input.html +++ b/app/templates/components/select-input.html @@ -1,6 +1,6 @@ -{% macro select(field, hint=None, disable=[], option_hints={}, hide_legend=False, input="radio") %} +{% macro select(field, hint=None, disable=[], option_hints={}, hide_legend=False, collapsible_opts={}, legend_style="text", input="radio") %} {% call select_wrapper( - field, hint, disable, option_hints, hide_legend + field, hint, disable, option_hints, hide_legend, collapsible_opts, legend_style ) %} {% for option in field %} {{ select_input(option, disable, option_hints, input=input) }} @@ -24,9 +24,9 @@ {% endmacro %} -{% macro select_nested(field, child_map, hint=None, disable=[], option_hints={}, hide_legend=False, input="radio") %} +{% macro select_nested(field, child_map, hint=None, disable=[], option_hints={}, hide_legend=False, collapsible_opts={}, legend_style="text", input="radio") %} {% call select_wrapper( - field, hint, disable, option_hints, hide_legend + field, hint, disable, option_hints, hide_legend, collapsible_opts, legend_style ) %}
{{ select_list(child_map[None], child_map, disable, option_hints, input=input) }} @@ -35,10 +35,14 @@ {% endmacro %} -{% macro select_wrapper(field, hint=None, disable=[], option_hints={}, hide_legend=False) %} -
-
- +{% macro select_wrapper(field, hint=None, disable=[], option_hints={}, hide_legend=False, collapsible_opts={}, legend_style="text") %} + {% set is_collapsible = collapsible_opts|length %} +
+ {% if is_collapsible %} +
+ {% endif %} +
+ {% if hide_legend %}{% endif %} {{ field.label.text|safe }} {% if hide_legend %}{% endif %} diff --git a/app/templates/views/manage-users/permissions.html b/app/templates/views/manage-users/permissions.html index 50648da5e..8d839b9aa 100644 --- a/app/templates/views/manage-users/permissions.html +++ b/app/templates/views/manage-users/permissions.html @@ -2,20 +2,19 @@ {% from "components/radios.html" import radio, radios, conditional_radio_panel %}
- + Permissions + + All team members can see sent messages. + {% for field in form.permissions_fields %} {{ checkbox(field) }} {% endfor %}
-

- All team members can see sent messages. -

- {% if current_service.has_permission("edit_folder_permissions") and form.folder_permissions.all_template_folders %} - {{ checkboxes_nested(form.folder_permissions, form.folder_permissions.children()) }} + {{ checkboxes_nested(form.folder_permissions, form.folder_permissions.children(), hide_legend=True, collapsible_opts={ 'field': 'folder' }) }} {% endif %} {% if service_has_email_auth %} diff --git a/app/templates/views/templates/manage-template-folder.html b/app/templates/views/templates/manage-template-folder.html index 673267062..660e60dc4 100644 --- a/app/templates/views/templates/manage-template-folder.html +++ b/app/templates/views/templates/manage-template-folder.html @@ -27,7 +27,7 @@ {{ textbox(form.name, width='1-1') }} {% if current_service.has_permission("edit_folder_permissions") %} {% if current_user.has_permissions("manage_service") and form.users_with_permission.all_service_users %} - {{ checkboxes(form.users_with_permission) }} + {{ checkboxes(form.users_with_permission, collapsible_opts={ 'field': 'team member' }) }} {% endif %} {% endif %} diff --git a/gulpfile.js b/gulpfile.js index 648bf86eb..305323158 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -95,6 +95,7 @@ const javascripts = () => { paths.src + 'javascripts/previewPane.js', paths.src + 'javascripts/colourPreview.js', paths.src + 'javascripts/templateFolderForm.js', + paths.src + 'javascripts/collapsibleCheckboxes.js', paths.src + 'javascripts/main.js' ]) .pipe(plugins.prettyerror()) diff --git a/tests/javascripts/collapsibleCheckboxes.test.js b/tests/javascripts/collapsibleCheckboxes.test.js new file mode 100644 index 000000000..b29a4d624 --- /dev/null +++ b/tests/javascripts/collapsibleCheckboxes.test.js @@ -0,0 +1,500 @@ +const helpers = require('./support/helpers'); + +beforeAll(() => { + // set up jQuery + window.jQuery = require('jquery'); + $ = window.jQuery; + + // load module code + require('govuk_frontend_toolkit/javascripts/govuk/modules.js'); + + // TODO: remove this when tests for sticky JS are written + require('../../app/assets/javascripts/stick-to-window-when-scrolling.js'); + + require('../../app/assets/javascripts/collapsibleCheckboxes.js'); +}); + +afterAll(() => { + window.jQuery = null; + $ = null; + + delete window.GOVUK; +}); + + +describe('Collapsible fieldset', () => { + + let fieldset; + let checkboxes; + + beforeEach(() => { + const _checkboxes = (start, end, descendents) => { + result = ''; + + for (let num = start; num <= end; num++) { + let id = `folder-permissions-${num}`; + + if (!descendents) { descendents = ''; } + + result += `
  • + + + ${descendents} +
  • `; + } + + return result; + }; + + // set up DOM + document.body.innerHTML = + `
    +
    +
    + + Folders this team member can see + +
    +
      + ${_checkboxes(1, 10)} +
    +
    +
    +
    `; + + formGroup = document.querySelector('.form-group'); + fieldset = formGroup.querySelector('fieldset'); + checkboxesContainer = fieldset.querySelector('.checkboxes-nested'); + checkboxes = checkboxesContainer.querySelectorAll('input[type=checkbox]'); + + }); + + afterEach(() => { + + document.body.innerHTML = ''; + + }); + + describe('when started', () => { + + beforeEach(() => { + + // start module + window.GOVUK.modules.start(); + + }); + + afterEach(() => { + + // reset checkboxes to default state + checkboxes.forEach(el => el.removeAttribute('checked')); + + }); + + test("adds the right classes to the group and fieldset", () => { + + expect(formGroup.classList.contains('selection-wrapper')).toBe(true); + expect(fieldset.classList.contains('selection-content')).toBe(true); + + }); + + test("adds a heading before the selected fieldset", () => { + + const heading = helpers.element(fieldset).getPreviousSibling( + el => (el.nodeName === 'h2') && (el.hasClass('heading-small')) + ); + + expect(heading).not.toBeNull(); + + }); + + test("adds the right content and classes to the summary", () => { + + const summary = formGroup.querySelector('.selection-summary__text'); + + expect(summary).not.toBeNull(); + expect(summary.classList.contains('selection-summary__text--folders')).toBe(true); + + }); + + test("the legend of the fieldset is visually hidden", () => { + + const legend = helpers.element(fieldset.querySelector('legend')); + + expect(legend.hasClass('visuallyhidden')).toBe(true); + + }); + + test("has a button to expand the fieldset", () => { + + const button = formGroup.querySelector('.button'); + + expect(button).not.toBeNull(); + expect(button.textContent.trim()).toEqual('Choose folders'); + + }); + + test("has the correct aria attributes on the button", () => { + + expect(helpers.element(formGroup.querySelector('.button')).hasAttributesSetTo({ + 'aria-controls': fieldset.getAttribute('id'), + 'aria-expanded': 'false' + })).toBe(true); + + }); + + test("hides the checkboxes", () => { + + expect(helpers.element(fieldset).is('hidden')).toEqual(true); + + }); + + }); + + test('has the right summary text when started with no checkboxes selected', () => { + + // start module + window.GOVUK.modules.start(); + + const summaryText = document.querySelector('.selection-summary__text'); + + // default state is for none to be selected + expect(summaryText.textContent.trim()).toEqual("No folders (only templates outside a folder)"); + + }); + + test('has the right summary text when started with some checkboxes selected', () => { + + // select the first 3 checkboxes + checkboxes.forEach((el, idx) => { + if ([0,1,2].includes(idx)) { el.setAttribute('checked', ''); } + }); + + // start module + window.GOVUK.modules.start(); + + const summaryText = document.querySelector('.selection-summary__text'); + + expect(summaryText.textContent.trim()).toEqual("3 of 10 folders"); + + }); + + test('has the right summary text when started with all checkboxes selected', () => { + + // select all the checkboxes + checkboxes.forEach(el => el.setAttribute('checked', '')); + + // start module + window.GOVUK.modules.start(); + + const summaryText = document.querySelector('.selection-summary__text'); + + expect(summaryText.textContent.trim()).toEqual("All folders"); + + }); + + test("the summary doesn't have a folder icon if fields aren't called 'folder'", () => { + + formGroup.dataset.fieldLabel = 'team member'; + + // start module + window.GOVUK.modules.start(); + + const summaryText = document.querySelector('.selection-summary__text'); + + expect(summaryText.classList.contains('.selection-summary__text-label')).toBe(false); + + }); + + describe("when button is clicked while the fieldset is collapsed", () => { + + beforeEach(() => { + + // start module + window.GOVUK.modules.start(); + + helpers.triggerEvent(formGroup.querySelector('.button'), 'click'); + + }); + + test("it shows the checkboxes", () => { + + expect(helpers.element(fieldset).is('hidden')).toBe(false); + + }); + + test("it focuses the fieldset", () => { + + expect(document.activeElement).toBe(fieldset); + + }); + + test("it uses ARIA to mark the checkboxes as expanded", () => { + + expect(formGroup.querySelector('.button').getAttribute('aria-expanded')).toEqual('true'); + + }); + + test("it changes it's text to indicate it's new action", () => { + + expect(formGroup.querySelector('.button').textContent.trim()).toEqual("Done choosing folders"); + + }); + + }); + + describe("when button is clicked when the fieldset is expanded", () => { + + beforeEach(() => { + + // start module + window.GOVUK.modules.start(); + + // show the checkboxes + helpers.triggerEvent(formGroup.querySelector('.button'), 'click'); + + // click the button + helpers.triggerEvent(formGroup.querySelector('.button'), 'click'); + + }); + + test("it hides the checkboxes", () => { + + expect(helpers.element(fieldset).is('hidden')).toBe(true); + + }); + + test("it focuses the summary text", () => { + + expect(document.activeElement).toBe(document.querySelector('.selection-summary__text')); + + }); + + test("it uses ARIA to mark the checkboxes as collapsed", () => { + + expect(formGroup.querySelector('.button').getAttribute('aria-expanded')).toEqual('false'); + + }); + + test("it changes it's text to indicate it's new action", () => { + + expect(formGroup.querySelector('.button').textContent.trim()).toEqual("Choose folders"); + + }); + }); + + describe("the footer (that wraps the button)", () => { + + beforeEach(() => { + + // track calls to sticky JS + window.GOVUK.stickAtBottomWhenScrolling.recalculate = jest.fn(() => {}); + + // start module + window.GOVUK.modules.start(); + + // show the checkboxes + helpers.triggerEvent(formGroup.querySelector('.button'), 'click'); + + }); + + test("is made sticky when the fieldset is expanded", () => { + + expect(formGroup.querySelector('.selection-footer').classList.contains('js-stick-at-bottom-when-scrolling')).toBe(true); + expect(window.GOVUK.stickAtBottomWhenScrolling.recalculate.mock.calls.length).toBe(1); + + }); + + test("has its stickiness removed when the fieldset is collapsed", () => { + + // click the button to collapse the fieldset + helpers.triggerEvent(formGroup.querySelector('.button'), 'click'); + + expect(formGroup.querySelector('.selection-footer').classList.contains('js-stick-at-bottom-when-scrolling')).toBe(false); + expect(window.GOVUK.stickAtBottomWhenScrolling.recalculate.mock.calls.length).toBe(2); + + }); + }); + + describe("when the selection changes", () => { + + const showCheckboxes = () => { + helpers.triggerEvent(formGroup.querySelector('.button'), 'click'); + }; + + const checkFirstCheckbox = () => { + checkboxes[0].setAttribute('checked', ''); + checkboxes[0].checked = true; + }; + + const checkAllCheckboxes = () => { + Array.from(checkboxes).forEach(checkbox => { + checkbox.setAttribute('checked', ''); + checkbox.checked = true; + }); + }; + + const checkAllCheckboxesButTheLast = () => { + Array.from(checkboxes).forEach((checkbox, idx) => { + if (idx > 0) { + checkbox.setAttribute('checked', ''); + checkbox.checked = true; + } + }); + }; + + describe("from some to none the summary updates to reflect that", () => { + + test("if fields are called 'folders'", () => { + + formGroup.dataset.fieldLabel = 'folder'; + + checkFirstCheckbox(); + + // start module + window.GOVUK.modules.start(); + + showCheckboxes(); + + const summaryText = document.querySelector('.selection-summary__text'); + + // click the first checkbox + helpers.triggerEvent(checkboxes[0], 'click'); + + expect(summaryText.textContent.trim()).toEqual("No folders (only templates outside a folder)"); + + }); + + test("if fields are called 'team member'", () => { + + formGroup.dataset.fieldLabel = 'team member'; + + checkFirstCheckbox(); + + // start module + window.GOVUK.modules.start(); + + showCheckboxes(); + + const summaryText = document.querySelector('.selection-summary__text'); + + // click the first checkbox + helpers.triggerEvent(checkboxes[0], 'click'); + + expect(summaryText.textContent.trim()).toEqual("No team members (only you)"); + + }); + + test("if fields are called 'arbitrary thing'", () => { + + formGroup.dataset.fieldLabel = 'arbitrary thing'; + + checkFirstCheckbox(); + + // start module + window.GOVUK.modules.start(); + + showCheckboxes(); + + const summaryText = document.querySelector('.selection-summary__text'); + + // click the first checkbox + helpers.triggerEvent(checkboxes[0], 'click'); + + expect(summaryText.textContent.trim()).toEqual("No arbitrary things"); + + }); + + }); + + describe("from all to some the summary updates to reflect that", () => { + + test("if fields are called 'folder'", () => { + + formGroup.dataset.fieldLabel = 'folder'; + + checkAllCheckboxes(); + + // start module + window.GOVUK.modules.start(); + + showCheckboxes(); + + const summaryText = document.querySelector('.selection-summary__text'); + + // click the first checkbox + helpers.triggerEvent(checkboxes[1], 'click'); + + expect(summaryText.textContent.trim()).toEqual("9 of 10 folders"); + + }); + + test("if fields are called 'team member'", () => { + + formGroup.dataset.fieldLabel = 'team member'; + + checkAllCheckboxes(); + + // start module + window.GOVUK.modules.start(); + + showCheckboxes(); + + const summaryText = document.querySelector('.selection-summary__text'); + + // click the first checkbox + helpers.triggerEvent(checkboxes[1], 'click'); + + expect(summaryText.textContent.trim()).toEqual("9 of 10 team members"); + + }); + + }); + + describe("from some to all the summary updates to reflect that", () => { + + test("if fields are called 'folder'", () => { + + formGroup.dataset.fieldLabel = 'folder'; + + checkAllCheckboxesButTheLast(); + + // start module + window.GOVUK.modules.start(); + + showCheckboxes(); + + const summaryText = document.querySelector('.selection-summary__text'); + + helpers.triggerEvent(checkboxes[0], 'click'); + + expect(summaryText.textContent.trim()).toEqual("All folders"); + + }); + + test("if fields are called 'team member'", () => { + + formGroup.dataset.fieldLabel = 'team member'; + + checkAllCheckboxesButTheLast(); + + // start module + window.GOVUK.modules.start(); + + showCheckboxes(); + + const summaryText = document.querySelector('.selection-summary__text'); + + helpers.triggerEvent(checkboxes[0], 'click'); + + expect(summaryText.textContent.trim()).toEqual("All team members"); + + }); + + }); + + }); + +}); diff --git a/tests/javascripts/support/helpers.js b/tests/javascripts/support/helpers.js new file mode 100644 index 000000000..f951f317b --- /dev/null +++ b/tests/javascripts/support/helpers.js @@ -0,0 +1,84 @@ +const triggerEvent = (el, evtType) => { + const evt = new Event(evtType, { + bubbles: true, + cancelable: true + }); + + el.dispatchEvent(evt); +}; + +class ElementQuery { + constructor (el) { + this.el = el; + } + + get nodeName () { + return this.el.nodeName.toLowerCase(); + } + + get firstTextNodeValue () { + const textNodes = Array.from(this.el.childNodes).filter(el => el.nodeType === 3); + + return textNodes.length ? textNodes[0].nodeValue : undefined; + }; + // returns the elements attributes as an object + hasAttributesSetTo (mappings) { + if (!this.el.hasAttributes()) { return false; } + + const keys = Object.keys(mappings); + let matches = 0; + + keys.forEach(key => { + if (this.el.hasAttribute(key) && (this.el.attributes[key].value === mappings[key])) { + matches++; + } + }); + + return matches === keys.length; + } + + hasClass (classToken) { + return Array.from(this.el.classList).includes(classToken); + } + + is (state) { + const test = `_is${state.charAt(0).toUpperCase()}${state.slice(1)}`; + + if (ElementQuery.prototype.hasOwnProperty(test)) { + return this[test](); + } + } + + // looks for a sibling before the el that matches the supplied test function + // the test function gets sent each sibling, wrapped in an Element instance + getPreviousSibling (test) { + let node = this.el.previousElementSibling; + let el; + + while(node) { + el = element(node); + + if (test(el)) { + return node; + } + + node = node.previousElementSibling; + } + + return null; + } + + _isHidden () { + const display = window.getComputedStyle(this.el).getPropertyValue('display'); + + return display === 'none'; + } +}; + +// function to ask certain questions of a DOM Element +const element = function (el) { + return new ElementQuery(el); +}; + +exports.triggerEvent = triggerEvent; +exports.element = element;