Skip to content

Commit

Permalink
Merge pull request #592 from digital-land/551-cookie-banner
Browse files Browse the repository at this point in the history
551 cookie banner
  • Loading branch information
DilwoarH authored Nov 4, 2024
2 parents fb34a92 + 2731c81 commit e927f0d
Show file tree
Hide file tree
Showing 17 changed files with 417 additions and 12 deletions.
6 changes: 6 additions & 0 deletions src/assets/js/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@
as it will be loaded into the base nunjucks template.
*/

import CookieBanner from './components/cookie-banner.js'
import initiateJsHiddenChecks from './js-hidden.js'

const initCookieBanner = () => {
return new CookieBanner(window.document)
}

window.addEventListener('load', () => {
initiateJsHiddenChecks()
initCookieBanner()
})
95 changes: 95 additions & 0 deletions src/assets/js/components/cookie-banner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
const cookieDefaults = {
cookieNames: {
preferenceCookie: 'cookies_preferences_set',
policyCookie: 'cookies_policy'
},
cookieExpiryDays: 365,
cookiePath: '/',
defaultPolicyValue: { essential: true, settings: true, usage: true, campaigns: true }
}

export default class CookieBanner {
constructor (document) {
this.document = document
this.banner = this.document.querySelector('.js-app-c-cookie-banner')
if (!this.banner) {
console.warn('Cookie banner element not found')
return
}

this.form = this.banner.querySelector('.js-app-c-cookie-banner__form')
this.confirmationMessage = this.banner.querySelector('.js-app-c-cookie-banner__confirmation')
this.confirmationDecision = this.banner.querySelector('.js-app-c-cookie-banner__confirmation-decision')
this.acceptButton = this.banner.querySelector('.js-app-c-cookie-banner__accept')
this.rejectButton = this.banner.querySelector('.js-app-c-cookie-banner__reject')
this.hideButton = this.banner.querySelector('.js-app-c-cookie-banner__hide')

this.init()
}

init () {
if (this.getCookie(cookieDefaults.cookieNames.preferenceCookie) !== null) {
this.hideCookieBanner()
}

this.acceptButton.addEventListener('click', this.accept.bind(this))
this.rejectButton.addEventListener('click', this.reject.bind(this))
this.hideButton.addEventListener('click', this.hideCookieBanner.bind(this))
}

accept () {
this.userCookiePolicyDecision(true)
}

reject () {
this.userCookiePolicyDecision(false)
}

userCookiePolicyDecision (userAcceptedCookiePolicy = true) {
this.setCookie(
cookieDefaults.cookieNames.preferenceCookie,
userAcceptedCookiePolicy,
cookieDefaults.cookieExpiryDays)

this.setCookie(
cookieDefaults.cookieNames.policyCookie,
userAcceptedCookiePolicy ? cookieDefaults.defaultPolicyValue : null,
userAcceptedCookiePolicy ? cookieDefaults.cookieExpiryDays : 0
)

this.showConfirmationMessage(userAcceptedCookiePolicy)
}

setCookie (name, value, days) {
const expires = new Date()
expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000))
this.document.cookie = `${name}=${JSON.stringify(value)};expires=${expires.toUTCString()};path=${cookieDefaults.cookiePath}`
}

getCookie (name) {
const cookie = this.document.cookie
.split('; ')
.find(row => row.startsWith(name))

return cookie ? JSON.parse(cookie.split('=')[1]) : null
}

showConfirmationMessage (userAcceptedCookiePolicy = true) {
this.form.classList.add('app-c-cookie-banner__form--hidden')
this.form.setAttribute('aria-hidden', 'true')
this.acceptButton.removeEventListener('click', this.accept.bind(this))
this.rejectButton.removeEventListener('click', this.reject.bind(this))

if (!userAcceptedCookiePolicy) this.confirmationDecision.textContent = 'rejected'
this.confirmationMessage.classList.remove('app-c-cookie-banner__confirmation--hidden')
this.confirmationMessage.setAttribute('aria-hidden', 'false')
this.confirmationMessage.setAttribute('role', 'status') // Announce status to screen readers
}

hideCookieBanner () {
if (this.banner) {
this.banner.style.display = 'none'
this.banner.setAttribute('aria-hidden', 'true')
}
}
}
7 changes: 7 additions & 0 deletions src/assets/scss/components/_cookie-banner.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.app-c-cookie-banner__form--hidden {
display: none;
}

.app-c-cookie-banner__confirmation--hidden {
display: none;
}
2 changes: 1 addition & 1 deletion src/assets/scss/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ $govuk-global-styles: true;
@import "src/assets/scss/_scrollable-container.scss";
@import "./step-by-step-nav.scss";
@import "./components/dataset-navigation";

@import "./components/cookie-banner";

.app-inset-text---error {
border-left: 5px solid govuk-colour('red');
Expand Down
17 changes: 16 additions & 1 deletion src/routes/cookies.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,23 @@ import nunjucks from 'nunjucks'
const router = express.Router()

router.get('/', (req, res) => {
const cookiesPage = nunjucks.render('cookies.html', {})
const cookiesPage = nunjucks.render('cookies.html', {
cookiePreferences: req.cookies.cookies_preferences_set,
showCookieUpdatedMessage: req.cookies.cookies_preferences_set_updated
})
res.send(cookiesPage)
})

router.post('/update-preference', (req, res) => {
const defaultCookieExpiry = 1000 * 60 * 60 * 24 * 365 // 1 year

res.cookie('cookies_preferences_set', req.body.accept_cookies, { maxAge: defaultCookieExpiry })
res.cookie('cookies_preferences_set_updated', true, { maxAge: 1000 })
res.cookie('cookies_policy', JSON.stringify({ essential: true, settings: true, usage: true, campaigns: true }), {
maxAge: req.body.accept_cookies === 'true' ? defaultCookieExpiry : 0
})

res.redirect('/cookies')
})

export default router
64 changes: 64 additions & 0 deletions src/views/components/cookie-banner.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{% from "govuk/components/cookie-banner/macro.njk" import govukCookieBanner %}

{% if serviceType %}
{% set serviceName = serviceType | getFullServiceName %}
{% endif %}
{% set cookiePagePath = '/cookies' %}
{% set cookieHeader = "Cookies on " + serviceName %}

{% set html %}
<p class="govuk-body">We use some essential cookies to make this service work.</p>
<p class="govuk-body">We’d also like to use analytics cookies so we can understand how you use the service and make improvements.</p>
{% endset %}

{% set acceptHtml %}
<p class="govuk-body">You’ve <span class="js-app-c-cookie-banner__confirmation-decision">accepted</span> additional cookies. You can <a class="govuk-link" href="{{ cookiePagePath }}">change your cookie settings</a> at any time.</p>
{% endset %}

<div class="js-app-c-cookie-banner">
<div class="js-app-c-cookie-banner__form">
{{ govukCookieBanner({
ariaLabel: cookieHeader,
messages: [
{
headingText: cookieHeader,
html: html,
actions: [
{
text: "Accept analytics cookies",
type: "button",
classes: "js-app-c-cookie-banner__accept"
},
{
text: "Reject analytics cookies",
type: "button",
classes: "js-app-c-cookie-banner__reject"
},
{
text: "View cookies",
href: cookiePagePath
}
]
}
]
}) }}
</div>

<div class="js-app-c-cookie-banner__confirmation app-c-cookie-banner__confirmation--hidden" aria-hidden="true">
{{ govukCookieBanner({
ariaLabel: cookieHeader,
messages: [
{
html: acceptHtml,
actions: [
{
text: "Hide cookie message",
type: "button",
classes: "js-app-c-cookie-banner__hide"
}
]
}
]
}) }}
</div>
</div>
57 changes: 54 additions & 3 deletions src/views/cookies.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{% from "govuk/components/radios/macro.njk" import govukRadios %}
{% from "govuk/components/button/macro.njk" import govukButton %}
{% from "govuk/components/notification-banner/macro.njk" import govukNotificationBanner %}

{% extends "layouts/main.html" %}

{% set pageName = 'Accessibility statement for Submit and update planning and housing data for England' %}
{% set pageName = 'Cookie notice for Submit and update planning and housing data for England' %}

{% set markdownContent %}

Expand Down Expand Up @@ -54,7 +58,6 @@
</tbody>
</table>


### Smartlook cookies (optional)

We use Smartlook to measure how you use the Check planning and housing data for England service. This helps us to improve the service experience and make sure it’s meeting your needs. No personal data will be stored in the cookies.
Expand All @@ -67,7 +70,7 @@
* how long you spend on each part
* what you click on the service

The cookies do not store any information you add to the service.
The cookies do not store any information you add to the service.


<table class="govuk-table">
Expand Down Expand Up @@ -114,6 +117,54 @@

{% block content %}

{% if showCookieUpdatedMessage %}
{% set html %}
<h3 class="govuk-notification-banner__heading">
You’ve updated your cookie preferences
</h3>
<p class="govuk-body">You can change your settings at any time by visiting this page.</p>
{% endset %}

{{ govukNotificationBanner({
type: "success",
html: html
}) }}
{% endif %}

{{markdownContent | govukMarkdown(headingsStartWith="xl") | safe}}

<div class="govuk-grid-row govuk-!-margin-top-7">
<div class="govuk-grid-column-two-thirds">
<h2 class="govuk-heading-l">Change your cookie settings</h2>
<form action="/cookies/update-preference" method="post" novalidate>
{{ govukRadios({
name: "accept_cookies",
idPrefix: "cookies-analytics",
fieldset: {
legend: {
text: "Do you want to accept analytics cookies?",
classes: "govuk-fieldset__legend--s"
}
},
items: [
{
value: "true",
text: "Yes",
checked: cookiePreferences == "true"
},
{
value: "false",
text: "No",
checked: cookiePreferences == "false"
}
]
}) }}

{{ govukButton({
text: "Save cookie settings"
}) }}
</form>
</div>
</div>

{% endblock content %}
4 changes: 3 additions & 1 deletion src/views/layouts/main.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
{% endblock %}

{% block header %}
{% include "components/cookie-banner.html" %}

{{ govukHeader({
homepageUrl: homepageUrl,
serviceName: serviceName,
Expand Down Expand Up @@ -98,7 +100,7 @@ <h2 class="govuk-heading-m">Get help</h2>

{% block bodyEnd %}
{{ super()}}
{%block scripts %}
{% block scripts %}
<body>
<script type="module" src="/assets/all.js"></script>
<script type="module">
Expand Down
7 changes: 7 additions & 0 deletions test/PageObjectModels/cookiesPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import BasePage from './BasePage'

export default class CookiesPage extends BasePage {
constructor (page) {
super(page, '/cookies')
}
}
4 changes: 2 additions & 2 deletions test/PageObjectModels/statusPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ export default class StatusPage extends BasePage {
}

async expectCheckStatusButtonToBeVisible () {
await this.page.waitForSelector('button', { text: 'Retrieve Latest Status' })
await this.page.waitForSelector('#js-async-continue-button')
}

async clickCheckStatusButton () {
await this.page.click('button', { text: 'Retrieve Latest Status' })
await this.page.click('#js-async-continue-button')
return await super.verifyAndReturnPage(ResultsPage)
}
}
56 changes: 56 additions & 0 deletions test/acceptance/cookies.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { test, expect } from '@playwright/test'

import CookiesPage from '../PageObjectModels/cookiesPage'
import { beforeEach } from 'node:test'

test.describe('Cookies page', () => {
beforeEach(async ({ page }) => {
await page.context().clearCookies()
})

test('has title', async ({ page }) => {
const cookiesPage = new CookiesPage(page)
await cookiesPage.navigateHere()

await expect(page).toHaveTitle('Cookie notice for Submit and update planning and housing data for England - Submit and update your planning data')
})

test('Can find and select the accept cookies radio button and submit the form', async ({ page, context }) => {
const cookiesPage = new CookiesPage(page)
await cookiesPage.navigateHere()

const acceptCookiesRadio = await page.locator('form input[name="accept_cookies"][value="true"]')
await acceptCookiesRadio.click()
await page.locator('form').evaluate(form => form.submit())

await expect(page).toHaveURL(cookiesPage.url)
await expect(page.locator('.govuk-notification-banner__heading')).toHaveText('You’ve updated your cookie preferences')

const cookies = await context.cookies()
const cookiesPreferencesSet = cookies.find(cookie => cookie.name === 'cookies_preferences_set')
const cookiesPolicy = cookies.find(cookie => cookie.name === 'cookies_policy')

await expect(cookiesPreferencesSet).toBeDefined()
await expect(cookiesPreferencesSet.value).toBe('true')
await expect(cookiesPolicy).toBeDefined()
await expect(cookiesPolicy.value).toBe(encodeURIComponent('{"essential":true,"settings":true,"usage":true,"campaigns":true}'))
})

test('Can reject cookies', async ({ page, context }) => {
const cookiesPage = new CookiesPage(page)
await cookiesPage.navigateHere()

const acceptCookiesRadio = await page.locator('form input[name="accept_cookies"][value="false"]')
await acceptCookiesRadio.click()
await page.locator('form').evaluate(form => form.submit())

await expect(page).toHaveURL(cookiesPage.url)
await expect(page.locator('.govuk-notification-banner__heading')).toHaveText('You’ve updated your cookie preferences')

const cookies = await context.cookies()
const cookiesPreferencesSet = cookies.find(cookie => cookie.name === 'cookies_preferences_set')

await expect(cookiesPreferencesSet).toBeDefined()
await expect(cookiesPreferencesSet.value).toBe('false')
})
})
Loading

0 comments on commit e927f0d

Please sign in to comment.