diff --git a/tests/javascripts/templateFolderForm.test.js b/tests/javascripts/templateFolderForm.test.js
new file mode 100644
index 000000000..d04be551c
--- /dev/null
+++ b/tests/javascripts/templateFolderForm.test.js
@@ -0,0 +1,645 @@
+const helpers = require('./support/helpers');
+
+function setFixtures (hierarchy) {
+
+ function templatesAndFoldersCheckboxesHTML () {
+ let result = '';
+
+ hierarchy.forEach((node, idx) => {
+
+ result += `
+
+
+
+
+
+
+ ${node.meta}
+
`;
+
+ });
+
+ return result;
+
+ };
+
+ const foldersCheckboxesHTML = function (filter) {
+ let count = 0;
+
+ // use closure to give all calls access to count
+ return function (nodes) {
+ let result = '';
+
+ nodes
+ .filter(node => node.type === 'folder')
+ .forEach(node => {
+ result += `
+
+
+ ${node.children ? foldersCheckboxesHTML(node.children) : ''}
+ `;
+ count++;
+ });
+
+ return ``;
+ };
+
+ }();
+
+ function controlsHTML () {
+
+ return ``
+ };
+
+ document.body.innerHTML = `
+ `;
+
+};
+
+beforeAll(() => {
+ require('../../app/assets/javascripts/templateFolderForm.js');
+});
+
+afterAll(() => {
+ require('./support/teardown.js');
+});
+
+describe('TemplateFolderForm', () => {
+
+ const hierarchy = [
+ {
+ 'label': 'Folder 1',
+ 'type': 'folder',
+ 'meta': '1 template, 1 folder',
+ 'children': [
+ {
+ 'label': 'Template 3',
+ 'type': 'template',
+ 'meta': 'Email template'
+ },
+ {
+ 'label': 'Folder 2',
+ 'type': 'folder',
+ 'meta': 'Empty',
+ 'children': []
+ }
+ ]
+ },
+ {
+ 'label': 'Template 1',
+ 'type': 'Email template',
+ 'meta': 'Email template'
+ },
+ {
+ 'label': 'Template 2',
+ 'type': 'template',
+ 'meta': 'Email template'
+ }
+ ];
+
+ let templateFolderForm;
+ let formControls;
+ let visibleCounter;
+ let hiddenCounter;
+
+ beforeAll(() => {
+
+ // stub out calls to sticky JS
+ GOVUK.stickAtBottomWhenScrolling = {
+ setMode: jest.fn(),
+ recalculate: jest.fn()
+ };
+
+ });
+
+ afterAll(() => {
+
+ GOVUK.stickAtBottomWhenScrolling = undefined;
+
+ });
+
+ beforeEach(() => {
+
+ setFixtures(hierarchy);
+
+ templateFolderForm = document.querySelector('form[data-module=template-folder-form]');
+
+ });
+
+ afterEach(() => {
+
+ document.body.innerHTML = '';
+
+ });
+
+ function getTemplateFolderCheckboxes () {
+ return templateFolderForm.querySelectorAll('input[type=checkbox]');
+ };
+
+ function getVisibleCounter () {
+ return formControls.querySelector('.template-list-selected-counter__count');
+ };
+
+ function getHiddenCounter () {
+ return formControls.querySelector('[role=status]');
+ };
+
+ describe("When the page loads", () => {
+
+ beforeEach(() => {
+
+ // start module
+ window.GOVUK.modules.start();
+
+ formControls = templateFolderForm.querySelector('#sticky_template_forms');
+ visibleCounter = getVisibleCounter();
+
+ });
+
+ test("the default controls and the counter should be showing", () => {
+
+ expect(document.querySelector('button[value=add-new-template]')).not.toBeNull();
+ expect(document.querySelector('button[value=add-new-folder]')).not.toBeNull();
+ expect(visibleCounter).not.toBeNull();
+
+ });
+
+ // Our counter needs to be wrapped in an ARIA live region so changes to its content are
+ // communicated to assistive tech'.
+ // ARIA live regions need to be in the HTML before JS loads.
+ // Because of this, we have a counter, in a live region, in the page when it loads, and
+ // a duplicate, visible, one in the HTML the module adds to the page.
+ // We hide the one in the live region to avoid duplication of it's content.
+ describe("Selection counter", () => {
+
+ beforeEach(() => {
+
+ hiddenCounter = getHiddenCounter();
+
+ })
+
+ test("the visible counter should be hidden from assistive tech", () => {
+
+ expect(visibleCounter.getAttribute('aria-hidden')).toEqual("true");
+
+ });
+
+ test("the content of both visible and hidden counters should match", () => {
+
+ expect(visibleCounter.textContent.trim()).toEqual(hiddenCounter.textContent.trim());
+
+ });
+
+ });
+
+ });
+
+ describe("Clicking 'New template'", () => {
+
+ beforeEach(() => {
+
+ // start module
+ window.GOVUK.modules.start();
+
+ formControls = templateFolderForm.querySelector('#sticky_template_forms');
+
+ helpers.triggerEvent(formControls.querySelector('[value=add-new-template]'), 'click');
+
+ });
+
+ test("should show options for all the types of template", () => {
+
+ const options = [
+ 'Email', 'Text message', 'Letter', 'Copy an existing template'
+ ];
+
+ const labels = Array.from(formControls.querySelectorAll('label'));
+ const radios = Array.from(formControls.querySelectorAll('input[type=radio]'));
+
+ options.forEach(option => {
+ let matchingLabels = labels.filter(label => label.textContent.trim() === option);
+
+ expect(matchingLabels.length > 0).toBe(true);
+
+ let matchingRadio = formControls.querySelector(`#${matchingLabels[0].getAttribute('for')}`)
+
+ expect(matchingRadio).not.toBeNull();
+ });;
+
+ });
+
+ test("should show a 'Cancel' link", () => {
+
+ const cancelLink = formControls.querySelector('.js-cancel');
+
+ expect(cancelLink).not.toBeNull();
+
+ });
+
+ test("should focus the fieldset", () => {
+
+ expect(document.activeElement).toBe(formControls.querySelector('fieldset'));
+
+ });
+
+ describe("When the 'Cancel' link is clicked", () => {
+
+ let addNewTemplateButton;
+
+ beforeEach(() => {
+
+ helpers.triggerEvent(formControls.querySelector('.js-cancel'), 'click');
+
+ addNewTemplateButton = formControls.querySelector('[value=add-new-template]');
+
+ });
+
+ test("the controls should reset", () => {
+
+ expect(addNewTemplateButton).not.toBeNull();
+
+ });
+
+ test("the add new template control should be focused", () => {
+
+ expect(document.activeElement).toBe(addNewTemplateButton);
+
+ });
+
+ });
+
+ });
+
+ describe("Clicking 'New folder'", () => {
+
+ let textbox;
+
+ beforeEach(() => {
+
+ // start module
+ window.GOVUK.modules.start();
+
+ formControls = templateFolderForm.querySelector('#sticky_template_forms');
+
+ helpers.triggerEvent(formControls.querySelector('[value=add-new-folder]'), 'click');
+
+ textbox = formControls.querySelector('input[type=text]');
+
+ });
+
+ test("should show a textbox for the folder name", () => {
+
+ expect(textbox).not.toBeNull();
+
+ // check textbox has a label
+ expect(formControls.querySelector(`label[for=${textbox.getAttribute('id')}]`)).not.toBeNull();
+
+ });
+
+ test("should focus the textbox", () => {
+
+ expect(document.activeElement).toBe(textbox);
+
+ });
+
+ describe("When the 'Cancel' link is clicked", () => {
+
+ let addNewFolderButton;
+
+ beforeEach(() => {
+
+ helpers.triggerEvent(formControls.querySelector('.js-cancel'), 'click');
+
+ addNewFolderButton = formControls.querySelector('button[value=add-new-folder]');
+
+ });
+
+ test("the controls should reset", () => {
+
+ expect(addNewFolderButton).not.toBeNull();
+
+ });
+
+ test("the control for adding a new folder should be focused", () => {
+
+ expect(document.activeElement).toBe(addNewFolderButton);
+
+ });
+
+ });
+
+ });
+
+ describe("When some templates/folders are selected", () => {
+
+ let templateFolderCheckboxes;
+
+ beforeEach(() => {
+
+ // start module
+ window.GOVUK.modules.start();
+
+ templateFolderCheckboxes = getTemplateFolderCheckboxes();
+
+ formControls = templateFolderForm.querySelector('#sticky_template_forms');
+
+ helpers.triggerEvent(templateFolderCheckboxes[0], 'click');
+ helpers.triggerEvent(templateFolderCheckboxes[2], 'click');
+
+ });
+
+ test("the buttons for moving to a new or existing folder are showing", () => {
+
+ expect(formControls.querySelector('button[value=move-to-new-folder]')).not.toBeNull();
+ expect(formControls.querySelector('button[value=move-to-existing-folder]')).not.toBeNull();
+
+ });
+
+ describe("Clear link", () => {
+
+ let clearLink;
+
+ beforeEach(() => {
+
+ clearLink = formControls.querySelector('.js-cancel');
+
+ });
+
+ test("the link has been added with the right text", () => {
+
+ expect(clearLink).not.toBeNull();
+ expect(clearLink.textContent.trim()).toEqual('Clear selection');
+
+ });
+
+ test("clicking the link clears the selection", () => {
+
+ helpers.triggerEvent(clearLink, 'click');
+
+ const checkedCheckboxes = Array.from(templateFolderCheckboxes).filter(checkbox => checkbox.checked);
+
+ expect(checkedCheckboxes.length === 0).toBe(true);
+
+ });
+
+ });
+
+ describe("Selection counter", () => {
+
+ let visibleCounterText;
+ let hiddenCounterText;
+
+ beforeEach(() => {
+
+ visibleCounterText = getVisibleCounter().textContent.trim();
+ hiddenCounterText = getHiddenCounter().textContent.trim();
+
+ });
+
+ test("the content of both visible and hidden counters should match", () => {
+
+ expect(visibleCounterText).toEqual(hiddenCounterText);
+
+ });
+
+ test("the content of the counter should reflect the selection", () => {
+
+ expect(visibleCounterText).toEqual('2 selected');
+
+ });
+
+ });
+
+ describe("Clicking the 'Move' button", () => {
+
+ beforeEach(() => {
+
+ helpers.triggerEvent(formControls.querySelector('[value=move-to-existing-folder]'), 'click');
+
+ });
+
+ test("should show radios for all the folders in the hierarchy", () => {
+
+ const foldersInHierarchy = [];
+
+ function getFolders (nodes) {
+
+ nodes.forEach(node => {
+ if (node.type === 'folder') {
+
+ foldersInHierarchy.push(node.label);
+ if (node.children.length) { getFolders(node.children) }
+
+ }
+ });
+
+ };
+
+ getFolders(hierarchy);
+
+ const folderLabels = Array.from(formControls.querySelectorAll('#move_to label'))
+ .filter(label => label.textContent.trim() !== 'Templates');
+
+ expect(folderLabels.map(label => label.textContent.trim())).toEqual(foldersInHierarchy);
+
+ const radiosForLabels = folderLabels
+ .map(label => formControls.querySelector(`#${label.getAttribute('for')}`))
+ .filter(radio => radio !== null);
+
+ expect(radiosForLabels.length).toEqual(foldersInHierarchy.length);
+
+ });
+
+ test("focus the 'Choose a folder' fieldset", () => {
+
+ expect(document.activeElement).toBe(formControls.querySelector('#move_to'));
+
+ });
+
+ describe("When the 'Cancel' link is clicked", () => {
+
+ let moveToFolderButton;
+
+ beforeEach(() => {
+
+ helpers.triggerEvent(formControls.querySelector('.js-cancel'), 'click');
+
+ moveToFolderButton = formControls.querySelector('button[value=move-to-existing-folder]');
+
+ });
+
+ test("the controls should reset", () => {
+
+ expect(moveToFolderButton).not.toBeNull();
+
+ });
+
+ test("the control for moving to an existing folder should be focused", () => {
+
+ expect(document.activeElement).toBe(moveToFolderButton);
+
+ });
+
+ });
+
+ });
+
+ describe("Clicking the 'Add to new folder' button", () => {
+
+ let textbox;
+
+ beforeEach(() => {
+
+ helpers.triggerEvent(formControls.querySelector('[value=move-to-new-folder]'), 'click');
+
+ textbox = formControls.querySelector('input[type=text]');
+
+ });
+
+ test("should show a textbox for the folder name", () => {
+
+ expect(textbox).not.toBeNull();
+
+ // check textbox has a label
+ expect(formControls.querySelector(`label[for=${textbox.getAttribute('id')}]`)).not.toBeNull();
+
+ });
+
+ test("should focus the textbox", () => {
+
+ expect(document.activeElement).toBe(textbox);
+
+ });
+
+ describe("When the 'Cancel' link is clicked", () => {
+
+ let moveToNewFolderButton;
+
+ beforeEach(() => {
+
+ helpers.triggerEvent(formControls.querySelector('.js-cancel'), 'click');
+
+ moveToNewFolderButton = formControls.querySelector('button[value=move-to-new-folder]');
+
+ });
+
+ test("the controls should reset", () => {
+
+ expect(moveToNewFolderButton).not.toBeNull();
+
+ });
+
+ test("the control for adding a new folder should be focused", () => {
+
+ expect(document.activeElement).toBe(moveToNewFolderButton);
+
+ });
+
+ });
+
+ });
+
+ });
+
+});