From 1167b3bc70365956d42ff41829223ff54a637957 Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Wed, 11 Sep 2024 10:36:25 +0100 Subject: [PATCH 1/3] Add tests for table of contents JS --- package.json | 2 + spec/javascripts/table-of-contents-spec.js | 289 +++++++++++++++++++++ 2 files changed, 291 insertions(+) create mode 100644 spec/javascripts/table-of-contents-spec.js diff --git a/package.json b/package.json index 7326e95c..46c1bcd6 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,13 @@ }, "standard": { "globals": [ + "jasmine", "describe", "before", "after", "beforeEach", "afterEach", + "beforeAll", "it", "assert", "expect", diff --git a/spec/javascripts/table-of-contents-spec.js b/spec/javascripts/table-of-contents-spec.js new file mode 100644 index 00000000..a99d45b9 --- /dev/null +++ b/spec/javascripts/table-of-contents-spec.js @@ -0,0 +1,289 @@ +describe('Table of contents', function () { + 'use strict' + + // global variables + var $html + var $tocBase + var $toc + var $closeButton + var $openButton + var module + + beforeAll(function () { + $html = $('html') + $tocBase = $( + '' + ) + + // some of the module's logic depends on the display style of the table of contents and the open button (.toc-show) + // this is set in the CSS so replicate that, with classes we control for screen size + $('head').append( + '') + }) + + beforeEach(function () { + $toc = $tocBase.clone() + + $html.find('body') + .append( + '
' + + '' + + '
' + ) + .append($toc) + + $closeButton = $toc.find('.js-toc-close') + $openButton = $html.find('.js-toc-show') + }) + + afterEach(function () { + // clear up any classes left on + $html.removeClass('.toc-open') + $html.find('body #toc-heading').remove() + $html.find('body .toc').remove() + }) + + describe('when the module is started', function () { + it('on a mobile-size screen, it should mark the table of contents as hidden', function () { + // styles applied by this test simulate the styles media-queries will apply on real web pages + // the .mobile-size class hides the table of contents and the open button + $html.addClass('mobile-size') // simulate the styles media-queries will apply on real web pages + + module = new GOVUK.Modules.TableOfContents() + module.start($toc) + + expect($toc.attr('aria-hidden')).toEqual('true') + + $html.removeClass('mobile-size') + }) + + it('on a desktop-size screen, it should mark the table of contents as visible', function () { + // styles applied by this test simulate the styles media-queries will apply on real web pages + // by default, they show the table of contents + + module = new GOVUK.Modules.TableOfContents() + module.start($toc) + + expect($toc.attr('aria-hidden')).toEqual('false') + }) + }) + + describe('when the screen resizes', function () { + describe('on a mobile-size screen', function () { + beforeEach(function () { + $html.addClass('mobile-size') + + module = new GOVUK.Modules.TableOfContents() + module.start($toc) + }) + + afterEach(function () { + $html.removeClass('mobile-size') + }) + + it('if the table of contents is closed, it should mark the buttons as not expanded', function () { + // the table of contents is closed by default, set by CSS styles + + $(window).trigger('resize') + + expect($closeButton.attr('aria-expanded')).toEqual('false') + expect($openButton.attr('aria-expanded')).toEqual('false') + }) + + it('if the table of contents is open, it should mark the buttons as expanded', function () { + $html.addClass('toc-open') + + $(window).trigger('resize') + + expect($closeButton.attr('aria-expanded')).toEqual('true') + expect($openButton.attr('aria-expanded')).toEqual('true') + + $html.removeClass('toc-open') + }) + }) + + }) + + describe('if the open button is clicked', function () { + beforeEach(function () { + module = new GOVUK.Modules.TableOfContents() + module.start($toc) + }) + + it('the click event should be cancelled', function () { + var clickEvt = new $.Event('click') + + $openButton.trigger(clickEvt) + + expect(clickEvt.isDefaultPrevented()).toBe(true) + }) + + it('the table of contents should show and be focused', function () { + // detecting focus has proved unreliable so track calls to $toc.focus instead + var _focus = $.fn.focus + var tocFocusSpy = jasmine.createSpy('tocFocusSpy') + var clickEvt + + $.fn.extend({ + focus: function () { + if (this === $toc) { + tocFocusSpy() + } else { + _focus.call($toc) + } + } + }) + + clickEvt = new $.Event('click') + $openButton.trigger(clickEvt) + + expect($toc.attr('aria-hidden')).toEqual('false') + + expect(tocFocusSpy).toHaveBeenCalled() + + // reset .focus method + $.fn.extend({ focus: _focus }) + }) + }) + + describe('if the close button is clicked', function () { + var clickEvt + + beforeEach(function () { + $html.addClass('mobile-size') + + module = new GOVUK.Modules.TableOfContents() + module.start($toc) + + // tocIsVisible = false // controls what $toc.is(':visible') returns, which will be controlled by CSS in a web page + clickEvt = new $.Event('click') + $closeButton.trigger(clickEvt) + }) + + afterEach(function () { + $html.removeClass('mobile-size') + }) + + it('the click event should be cancelled', function () { + expect(clickEvt.isDefaultPrevented()).toBe(true) + }) + + it('the table of contents should be hidden', function () { + expect($toc.attr('aria-hidden')).toEqual('true') + }) + }) + + it('on mobile-size screens, when the table of contents is open and the escape key is activated, the table of contents should be hidden', function () { + $html.addClass('mobile-size') + + module = new GOVUK.Modules.TableOfContents() + module.start($toc) + + $openButton.trigger('click') + + $(document).trigger(new $.Event('keydown', { + keyCode: 27 + })) + + expect($html.hasClass('toc-open')).toBe(false) + + $html.removeClass('mobile-size') + }) + + describe("Fix for iOS 'rubber banding'", function () { + var _scrollTop + var _prop + var scrollTop + var scrollHeight + var offsetHeight + var scrollTopSpy + + beforeEach(function () { + // stub out jQuery methods + _scrollTop = $.fn.scrollTop + _prop = $.fn.prop + + scrollTopSpy = jasmine.createSpy('scrollTopSpy') + + $.fn.extend({ + scrollTop: function (val) { + if (val !== undefined) { + return scrollTopSpy(val) + } + return scrollTop + } + }) + + $.fn.extend({ + prop: function (key) { + if (key === 'scrollHeight') { + return scrollHeight + } + if (key === 'offsetHeight') { + return offsetHeight + } + return _prop.call($toc, key) + } + }) + + module = new GOVUK.Modules.TableOfContents() + module.start($toc) + }) + + afterEach(function () { + // reset jQuery methods + $.fn.extend({ prop: _prop }) + $.fn.extend({ scrollTop: _scrollTop }) + }) + + it('should stop the scroll reaching the top edge if at the top of the page', function () { + scrollTop = 0 + scrollHeight = 1000 + offsetHeight = 600 + + $toc.trigger('touchstart') + + expect(scrollTopSpy).toHaveBeenCalledWith(1) + }) + + it('should stop the scroll reaching the bottom edge if at the bottom of the page', function () { + scrollTop = 400 + scrollHeight = 1000 + offsetHeight = 600 + + $toc.trigger('touchstart') + + expect(scrollTopSpy).toHaveBeenCalledWith(399) + }) + }) +}) From 46484e7feaf076360774b595d133228e8d7392ae Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Wed, 11 Sep 2024 14:17:08 +0100 Subject: [PATCH 2/3] Make role=dialog for TOC dependant on screen size --- .../javascripts/_modules/table-of-contents.js | 6 +++++- lib/source/layouts/core.erb | 2 +- spec/javascripts/table-of-contents-spec.js | 16 +++++++++++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/assets/javascripts/_modules/table-of-contents.js b/lib/assets/javascripts/_modules/table-of-contents.js index b79dfcee..b48e3503 100644 --- a/lib/assets/javascripts/_modules/table-of-contents.js +++ b/lib/assets/javascripts/_modules/table-of-contents.js @@ -80,11 +80,15 @@ function updateAriaAttributes () { var tocIsVisible = $toc.is(':visible') + var openButtonIsVisible = $openButton.is(':visible') $($openButton).add($closeButton) .attr('aria-expanded', tocIsVisible ? 'true' : 'false') - $toc.attr('aria-hidden', tocIsVisible ? 'false' : 'true') + $toc.attr({ + 'aria-hidden': tocIsVisible ? 'false' : 'true', + role: openButtonIsVisible ? 'dialog' : null + }) } function preventingScrolling (callback) { diff --git a/lib/source/layouts/core.erb b/lib/source/layouts/core.erb index 29fbb481..df22efff 100644 --- a/lib/source/layouts/core.erb +++ b/lib/source/layouts/core.erb @@ -43,7 +43,7 @@
> <% if content_for? :sidebar %>
-