Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-work explicit-cross-domain-links.js #2502

Merged
merged 1 commit into from
Dec 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
useful summary for people upgrading their application, not a replication
of the commit log.

## Unreleased
## Unreleased

* Alter use of pseudo-underline mixin to allow for different button sizes ([#2501](https://github.com/alphagov/govuk_publishing_components/pull/2501))
* Re-work explicit-cross-domain-links.js ([PR #2502](https://github.com/alphagov/govuk_publishing_components/pull/2502))

## 27.16.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,78 +6,79 @@

GOVUK.Modules.ExplicitCrossDomainLinks = function () {
this.start = function ($module) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While you're at it... 😁

It seems like this module doesn't contain any jQuery, so you could switch it from using a start function to an init function. Shouldn't be a huge change - you'll need to change line 9 to this.element = $module and update how the code is called in the test, but that should be about it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion, but do you mind if we do this as a separate piece of work as it's actually not so straightforward? Happy to record it as an issue in the meanwhile

var element = $module[0]
this.element = $module[0]
this.attribute = 'href'
this.attributeValue = this.element.getAttribute(this.attribute)
this.eventType = 'click'
if (!this.attributeValue) {
this.attribute = 'action'
this.attributeValue = this.element.getAttribute(this.attribute)
this.eventType = 'submit'
}

var cookieBannerEngaged = GOVUK.cookie('cookies_preferences_set')
this.handleEvent = this.handleEvent.bind(this)
this.handleCookiesAccepted = this.handleCookiesAccepted.bind(this)
// Listens for the 'submit' event if the element is a form, and the 'click' event if it is a link
this.element.addEventListener(this.eventType, this.handleEvent)
}

// If not engaged, append only ?cookie_consent=not-engaged
// If engaged and rejected, append only ?cookie_consent=reject
// If engaged and accepted usage, append ?_ga=clientid if available and cookie_consent=accept
this.handleEvent = function (e) {
// prevent default: we want the link href and/or form action to be decorated before we navigate away
e.preventDefault()
var cookieBannerEngaged = GOVUK.cookie('cookies_preferences_set')
var cookieConsent = GOVUK.getConsentCookie()

if (cookieBannerEngaged !== 'true') {
this.decorate(element, 'cookie_consent=not-engaged')
this.start = this.start.bind(this, $module)

// if the user has not engaged with the cookie banner yet, listen for the cookie consent accept/reject events
// re-start the module if cookies are accepted or rejected on the current page – setting cookie preferences does not reload the page
window.addEventListener('cookie-consent', this.start)
window.addEventListener('cookie-reject', this.start)
return
// If not engaged, append only ?cookie_consent=not-engaged
this.decorate(this.element, 'cookie_consent=not-engaged', this.attribute)
} else if (cookieConsent && cookieConsent.usage === true) {
this.handleCookiesAccepted()
} else {
this.decorate(this.element, 'cookie_consent=reject', this.attribute)
}
var cookieConsent = GOVUK.getConsentCookie()
if (cookieConsent && cookieConsent.usage === false) {
this.decorate(element, 'cookie_consent=reject')
return

// remove the event listener to avoid an infinite loop
this.element.removeEventListener(this.eventType, this.handleEvent)

// if the element is a form, submit it. If it is a link, click it
if (this.eventType === 'submit') {
this.element.submit()
} else {
this.element.click()
}
}

this.decorate(element, 'cookie_consent=accept')
this.handleCookiesAccepted = function () {
// If the cookie banner was engaged and usage cookie accepted, append ?_ga=clientid if available and cookie_consent=accept
var element = this.element
var attribute = this.attribute
this.decorate(element, 'cookie_consent=accept', attribute)

if (!global.ga) { return }
if (!global.ga) {
return
}

global.ga(function () {
var trackers = global.ga.getAll()

if (!trackers.length) { return }

var linker = new global.gaplugins.Linker(trackers[0])
var attrValue = element.getAttribute(attribute)

var attrAction = element.getAttribute('action')
if (attrAction) {
element.setAttribute('action', linker.decorate(attrAction))
}

var attrHref = element.getAttribute('href')
if (attrHref) {
element.href = linker.decorate(attrHref)
}
element.setAttribute(attribute, linker.decorate(attrValue))
})
}

this.decorate = function (element, param) {
var attribute = 'href'
this.decorate = function (element, param, attribute) {
var attributeValue = element.getAttribute(attribute)
var cookieConsentParameterPattern = /cookie_consent=[^&]*/
var paramIsCookieConsent = param.match(cookieConsentParameterPattern)

if (!attributeValue) {
attribute = 'action'
attributeValue = element.getAttribute(attribute)
}

if (!attributeValue) { return }

var attributeHasCookieConsent = attributeValue.match(cookieConsentParameterPattern)

if (attributeHasCookieConsent && paramIsCookieConsent) {
// if the decorate function has received a cookie_consent parameter, but the target element already has a cookie_consent parameter, replace the existing parameter with the new value
attributeValue = attributeValue.replace(cookieConsentParameterPattern, param)
if (attributeValue.includes('?')) {
attributeValue += '&' + param
} else {
// otherwise, simply append the parameter to the target element href query string
if (attributeValue.includes('?')) {
attributeValue += '&' + param
} else {
attributeValue += '?' + param
}
attributeValue += '?' + param
}

element.setAttribute(attribute, attributeValue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ window.GOVUK.Modules = window.GOVUK.Modules || {};
this.$module.cookieBannerConfirmationMessage.focus()
window.GOVUK.cookie('cookies_preferences_set', 'true', { days: 365 })
window.GOVUK.setDefaultConsentCookie()
window.GOVUK.triggerEvent(window, 'cookie-reject')
}

CookieBanner.prototype.showConfirmationMessage = function () {
Expand Down
2 changes: 0 additions & 2 deletions docs/javascript-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,6 @@ This functionality runs like this:
- if cookies have been consented, the module calls the rest of its code and carries on as normal
- if cookies have not been consented, the listener is created and calls the rest of the module when the `cookie-consent` event is fired by the cookie banner

If a module has functionality which is dependent on rejecting cookies, that module should listen for the `cookie-reject` event. This event is fired by the cookie banner when the user rejects cookies.

### Module structure

A module must add its constructor to `GOVUK.Modules` and it must have an `init` method. The simplest module looks like:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,28 +42,37 @@ describe('Explicit cross-domain linker', function () {
delete window.gaplugins
})

describe('links', function () {
describe('when a cross-domain link is clicked', function () {
beforeEach(function () {
element = $('<a href="/somewhere">')
element = $('<a href="#">')
})

it('modifies the link href to append cookie_consent parameter "not-engaged" if cookies_preferences_set cookie is "false"', function () {
GOVUK.cookie('cookies_preferences_set', 'false')
explicitCrossDomainLinks.start(element)
expect(element.attr('href')).toEqual('/somewhere?cookie_consent=not-engaged')
expect(element.attr('href')).toEqual('#')
window.GOVUK.triggerEvent(element[0], 'click')
expect(element.attr('href')).toEqual('#?cookie_consent=not-engaged')
expect(window.location.href).toContain('?cookie_consent=not-engaged')
})

it('modifies the link href to append cookie_consent parameter "not-engaged" if cookies_preferences_set cookie is not set', function () {
GOVUK.cookie('cookies_preferences_set', null)
explicitCrossDomainLinks.start(element)
expect(element.attr('href')).toEqual('/somewhere?cookie_consent=not-engaged')
expect(element.attr('href')).toEqual('#')
window.GOVUK.triggerEvent(element[0], 'click')
expect(element.attr('href')).toEqual('#?cookie_consent=not-engaged')
expect(window.location.href).toContain('?cookie_consent=not-engaged')
})

it('modifies the link href to append cookie_consent parameter "reject" if usage cookies have been rejected', function () {
GOVUK.cookie('cookies_preferences_set', 'true')
GOVUK.setConsentCookie({ usage: false })
explicitCrossDomainLinks.start(element)
expect(element.attr('href')).toEqual('/somewhere?cookie_consent=reject')
expect(element.attr('href')).toEqual('#')
window.GOVUK.triggerEvent(element[0], 'click')
expect(element.attr('href')).toEqual('#?cookie_consent=reject')
expect(window.location.href).toContain('?cookie_consent=reject')
})

describe('user has accepted cookies', function () {
Expand All @@ -73,51 +82,39 @@ describe('Explicit cross-domain linker', function () {
trackers = [{ ga_mock: 'foobar' }]

explicitCrossDomainLinks.start(element)
expect(element.attr('href')).toEqual('#')
window.GOVUK.triggerEvent(element[0], 'click')

expect(element.attr('href')).toEqual('/somewhere?cookie_consent=accept&_ga=abc123')
expect(element.attr('href')).toEqual('#?cookie_consent=accept&_ga=abc123')
expect(window.location.href).toContain('?cookie_consent=accept&_ga=abc123')
})

it('modifies the link href to only append cookie_consent "accept" if there are no trackers', function () {
GOVUK.cookie('cookies_preferences_set', 'true')
GOVUK.setConsentCookie({ usage: true })
trackers = []

explicitCrossDomainLinks.start(element)
expect(element.attr('href')).toEqual('/somewhere?cookie_consent=accept')
expect(element.attr('href')).toEqual('#')
window.GOVUK.triggerEvent(element[0], 'click')
expect(element.attr('href')).toEqual('#?cookie_consent=accept')
expect(window.location.href).toContain('?cookie_consent=accept')
})

it('modifies the link href to only append cookie_consent "accept" if ga is not initalised on window', function () {
GOVUK.cookie('cookies_preferences_set', 'true')
GOVUK.setConsentCookie({ usage: true })
window.ga = undefined
explicitCrossDomainLinks.start(element)
expect(element.attr('href')).toEqual('/somewhere?cookie_consent=accept')
})
})

describe('user has interacted with the cookie banner on the current page', function () {
beforeEach(function () {
GOVUK.cookie('cookies_preferences_set', null)
explicitCrossDomainLinks.start(element)
GOVUK.cookie('cookies_preferences_set', 'true')
})
it('modifies the link href to append cookie_consent parameter "accept" if the cookie-consent event was fired', function () {
GOVUK.setConsentCookie({ usage: true })
window.ga = undefined
window.GOVUK.triggerEvent(window, 'cookie-consent')

expect(element.attr('href')).toEqual('/somewhere?cookie_consent=accept')
})

it('modifies the link href to append cookie_consent parameter "reject" if the cookie-reject event was fired', function () {
GOVUK.setConsentCookie({ usage: false })
window.GOVUK.triggerEvent(window, 'cookie-reject')

expect(element.attr('href')).toEqual('/somewhere?cookie_consent=reject')
expect(element.attr('href')).toEqual('#')
window.GOVUK.triggerEvent(element[0], 'click')
expect(element.attr('href')).toEqual('#?cookie_consent=accept')
expect(window.location.href).toContain('?cookie_consent=accept')
})
})
})

describe('forms', function () {
describe('when a cross-domain form is submitted', function () {
beforeEach(function () {
element = $('<form method="POST" action="/somewhere">' +
'<input type="hidden" name="key" value="value" />' +
Expand All @@ -128,19 +125,26 @@ describe('Explicit cross-domain linker', function () {
it('modifies the form action to append cookie_consent parameter "not-engaged" if cookies_preferences_set cookie is "false"', function () {
GOVUK.cookie('cookies_preferences_set', 'false')
explicitCrossDomainLinks.start(element)
expect(element.attr('action')).toEqual('/somewhere')
window.GOVUK.triggerEvent(element[0], 'submit')

expect(element.attr('action')).toEqual('/somewhere?cookie_consent=not-engaged')
})

it('modifies the form action to append cookie_consent parameter "not-engaged" if cookies_preferences_set cookie is not set', function () {
GOVUK.cookie('cookies_preferences_set', null)
explicitCrossDomainLinks.start(element)
expect(element.attr('action')).toEqual('/somewhere')
window.GOVUK.triggerEvent(element[0], 'submit')
expect(element.attr('action')).toEqual('/somewhere?cookie_consent=not-engaged')
})

it('modifies the form action to append cookie_consent parameter "reject" if usage cookies have been rejected', function () {
GOVUK.cookie('cookies_preferences_set', 'true')
GOVUK.setConsentCookie({ usage: false })
explicitCrossDomainLinks.start(element)
expect(element.attr('action')).toEqual('/somewhere')
window.GOVUK.triggerEvent(element[0], 'submit')
expect(element.attr('action')).toEqual('/somewhere?cookie_consent=reject')
})

Expand All @@ -150,6 +154,8 @@ describe('Explicit cross-domain linker', function () {
GOVUK.setConsentCookie({ usage: true })
trackers = [{ ga_mock: 'foobar' }]
explicitCrossDomainLinks.start(element)
expect(element.attr('action')).toEqual('/somewhere')
window.GOVUK.triggerEvent(element[0], 'submit')
expect(element.attr('action')).toEqual('/somewhere?cookie_consent=accept&_ga=abc123')
})

Expand All @@ -158,6 +164,8 @@ describe('Explicit cross-domain linker', function () {
GOVUK.setConsentCookie({ usage: true })
trackers = []
explicitCrossDomainLinks.start(element)
expect(element.attr('action')).toEqual('/somewhere')
window.GOVUK.triggerEvent(element[0], 'submit')
expect(element.attr('action')).toEqual('/somewhere?cookie_consent=accept')
})

Expand All @@ -166,30 +174,10 @@ describe('Explicit cross-domain linker', function () {
GOVUK.setConsentCookie({ usage: true })
window.ga = undefined
explicitCrossDomainLinks.start(element)
expect(element.attr('action')).toEqual('/somewhere')
window.GOVUK.triggerEvent(element[0], 'submit')
expect(element.attr('action')).toEqual('/somewhere?cookie_consent=accept')
})
})

describe('user has interacted with the cookie banner on the current page', function () {
beforeEach(function () {
GOVUK.cookie('cookies_preferences_set', null)
explicitCrossDomainLinks.start(element)
GOVUK.cookie('cookies_preferences_set', 'true')
})
it('modifies the form action to append cookie_consent parameter "accept" if the cookie-consent event was fired', function () {
GOVUK.setConsentCookie({ usage: true })
window.ga = undefined
window.GOVUK.triggerEvent(window, 'cookie-consent')

expect(element.attr('action')).toEqual('/somewhere?cookie_consent=accept')
})

it('modifies the form action to append cookie_consent parameter "reject" if the cookie-reject event was fired', function () {
GOVUK.setConsentCookie({ usage: false })
window.GOVUK.triggerEvent(window, 'cookie-reject')

expect(element.attr('action')).toEqual('/somewhere?cookie_consent=reject')
})
})
})
})
26 changes: 0 additions & 26 deletions spec/javascripts/govuk_publishing_components/modules.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,20 +140,6 @@ describe('GOVUK Modules', function () {
}
GOVUK.Modules.TestCookieDependencyModule = TestCookieDependencyModule

// GOV.UK Frontend Module that depends on rejected cookies to start
function TestCookieRejectDependencyModule (element) {
this.element = element
}
TestCookieRejectDependencyModule.prototype.init = function () {
this.startModule = this.startModule.bind(this)
window.addEventListener('cookie-reject', this.startModule)
}
TestCookieRejectDependencyModule.prototype.startModule = function () {
window.removeEventListener('cookie-reject', this.startModule)
callbackFrontendModule(this.element)
}
GOVUK.Modules.TestCookieRejectDependencyModule = TestCookieRejectDependencyModule

container = $('<div></div>')
})

Expand All @@ -165,7 +151,6 @@ describe('GOVUK Modules', function () {
delete GOVUK.Modules.GovukTestAlertPublishingAndFrontendModule
delete GOVUK.Modules.TestAlertPublishingAndFrontendModule
delete GOVUK.Modules.TestCookieDependencyModule
delete GOVUK.Modules.TestCookieRejectDependencyModule

container.remove()
})
Expand Down Expand Up @@ -243,17 +228,6 @@ describe('GOVUK Modules', function () {
expect(callbackFrontendModule.calls.count()).toBe(1)
})

it('starts delayed modules once cookies have been rejected', function () {
var module = $('<div data-module="test-cookie-reject-dependency-module"></div>')
container.append(module)
$('body').append(container)

GOVUK.modules.start(container)
expect(callbackFrontendModule.calls.count()).toBe(0)
window.GOVUK.triggerEvent(window, 'cookie-reject')
expect(callbackFrontendModule.calls.count()).toBe(1)
})

it('starts multiple delayed modules once cookies have been consented', function () {
var module1 = $('<div data-module="test-cookie-dependency-module"></div>')
var module2 = $('<div data-module="test-cookie-dependency-module"></div>')
Expand Down