From 731741eec2f059f08d51075ca0967b8fff6ed60c Mon Sep 17 00:00:00 2001 From: George Goodall Date: Tue, 21 Nov 2023 12:18:43 +0000 Subject: [PATCH 01/11] fixed mock api --- test/mock-api/index.js | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/test/mock-api/index.js b/test/mock-api/index.js index 3c676b50..942bffb5 100644 --- a/test/mock-api/index.js +++ b/test/mock-api/index.js @@ -1,33 +1,31 @@ -// a basic json express server with one endpoint that returns a json object +'use strict' +// this script runs a mock server that mimics the pipeline runner api +// it doesn't perform any validations but instead looks at the filename to determine if it should return errors or not import express from 'express' import config from '../../config/index.js' +import multer from 'multer' + +import { readFileSync } from 'fs' +const upload = multer({ dest: 'uploads/' }) + +const APIResponse = JSON.parse(readFileSync('../testData/API_RUN_PIPELINE_RESPONSE.json')) + const app = express() +app.use(config.api.validationEndpoint, upload.single('upload_file')) + app.post(config.api.validationEndpoint, (req, res) => { const filename = req.file.originalname - if (filename === 'conservation-area-errors.csv') { - res.json({ - issueLog: [ - { - row: 2, - column: 'name', - issue: 'Name is required' - }, - { - row: 3, - column: 'location', - issue: 'Location is required' - } - ] - }) - } else { - res.json({ - issueLog: [] - }) + + const _toSend = { ...APIResponse } + + if (filename !== 'conservation-area-errors.csv') { + _toSend['issue-log'] = [] } + res.json(_toSend) }) app.listen(config.api.port, () => { From a776a32c237ec9613d3415fff9d365e49c1694e4 Mon Sep 17 00:00:00 2001 From: George Goodall Date: Tue, 21 Nov 2023 12:22:11 +0000 Subject: [PATCH 02/11] fix import for mock:api --- test/mock-api/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/mock-api/index.js b/test/mock-api/index.js index 942bffb5..ce2a767a 100644 --- a/test/mock-api/index.js +++ b/test/mock-api/index.js @@ -11,7 +11,7 @@ import multer from 'multer' import { readFileSync } from 'fs' const upload = multer({ dest: 'uploads/' }) -const APIResponse = JSON.parse(readFileSync('../testData/API_RUN_PIPELINE_RESPONSE.json')) +const APIResponse = JSON.parse(readFileSync('./test/testData/API_RUN_PIPELINE_RESPONSE.json')) const app = express() From 8af9e2a993b466bdc340c2d5b5015e8074b73440 Mon Sep 17 00:00:00 2001 From: George Goodall Date: Tue, 21 Nov 2023 12:22:26 +0000 Subject: [PATCH 03/11] enable test for uploading data --- playwright.config.js | 2 +- test/acceptance/upload_data.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/playwright.config.js b/playwright.config.js index dcfdb755..07f02b04 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -71,7 +71,7 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { - command: 'NODE_ENV=test npm run start', // 'concurrently "NODE_ENV=test npm run start" "NODE_ENV=test npm run mock:api"', + command: 'concurrently "NODE_ENV=test npm run start" "NODE_ENV=test npm run mock:api"', url: 'http://127.0.0.1:3000', reuseExistingServer: !process.env.CI } diff --git a/test/acceptance/upload_data.test.js b/test/acceptance/upload_data.test.js index 619eb269..475a565a 100644 --- a/test/acceptance/upload_data.test.js +++ b/test/acceptance/upload_data.test.js @@ -23,7 +23,7 @@ test('Enter form information', async ({ page }) => { }) // currently skipping this test as im not sure how to go about providing the pipeline runner api -test.skip('Enter form information and upload a file with errors and without errors', async ({ page }) => { +test('Enter form information and upload a file with errors and without errors', async ({ page }) => { await page.goto('/') await page.getByRole('button', { name: 'Start now' }).click() From 00c7da96ca7a3ea0c763c7ba9ecb6ad2a21bc766 Mon Sep 17 00:00:00 2001 From: George Goodall Date: Tue, 21 Nov 2023 12:28:12 +0000 Subject: [PATCH 04/11] extended test to cover the email page --- test/acceptance/upload_data.test.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/acceptance/upload_data.test.js b/test/acceptance/upload_data.test.js index 475a565a..156713e5 100644 --- a/test/acceptance/upload_data.test.js +++ b/test/acceptance/upload_data.test.js @@ -60,4 +60,11 @@ test('Enter form information and upload a file with errors and without errors', await page.getByRole('button', { name: 'Continue' }).click() await page.waitForURL('**/no-errors') -}) + await page.getByRole('button', { name: 'Continue' }).click() + + await page.waitForURL('**/email-address') + await page.getByLabel('Your email address').fill('dataOfficer@fakeLPA.com'); + await page.getByRole('button', { name: 'Continue' }).click() + + await page.waitForURL('**/name') +}); From 216d548977d6a15f2b67ca6721c34ccc5169fcfd Mon Sep 17 00:00:00 2001 From: George Goodall Date: Tue, 21 Nov 2023 12:28:37 +0000 Subject: [PATCH 05/11] linting fixes --- test/acceptance/upload_data.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/acceptance/upload_data.test.js b/test/acceptance/upload_data.test.js index 156713e5..67aa78be 100644 --- a/test/acceptance/upload_data.test.js +++ b/test/acceptance/upload_data.test.js @@ -63,8 +63,8 @@ test('Enter form information and upload a file with errors and without errors', await page.getByRole('button', { name: 'Continue' }).click() await page.waitForURL('**/email-address') - await page.getByLabel('Your email address').fill('dataOfficer@fakeLPA.com'); + await page.getByLabel('Your email address').fill('dataOfficer@fakeLPA.com') await page.getByRole('button', { name: 'Continue' }).click() await page.waitForURL('**/name') -}); +}) From 9435868be982e66feeabd97815c5b52fb7a04265 Mon Sep 17 00:00:00 2001 From: George Goodall Date: Tue, 21 Nov 2023 12:48:10 +0000 Subject: [PATCH 06/11] added some validation error message testing --- test/acceptance/validation_errors.test.js | 80 +++++++++++++++-------- 1 file changed, 52 insertions(+), 28 deletions(-) diff --git a/test/acceptance/validation_errors.test.js b/test/acceptance/validation_errors.test.js index c283ac5a..733683bc 100644 --- a/test/acceptance/validation_errors.test.js +++ b/test/acceptance/validation_errors.test.js @@ -7,18 +7,7 @@ test('when the user clicks continue on the data subject page without entering a await page.waitForSelector('input#data-subject.govuk-radios__input') - const errorLink = await page.getByRole('link', { name: 'Please select a data subject' }) - const fieldError = await page.getByText('Error: Please select a data subject') - const errorSummary = await page.getByText('There is a problem') - - expect(await errorSummary.isVisible(), 'Page should show the error summary').toBeTruthy() - expect(await errorLink.isVisible(), 'Page should the error message that is a link to the problem field').toBeTruthy() - expect(await fieldError.isVisible(), 'Page should show the error message next to the problem field').toBeTruthy() - await errorLink.click() - const problemFieldIsFocused = await page.$eval('input#data-subject.govuk-radios__input', (el) => el === document.activeElement) - expect(problemFieldIsFocused, 'The focus should be on the problem field').toBeTruthy() - - expect(await page.title(), 'Page title should indicate there\'s an error').toMatch(/Error: .*/) + await testErrorMessage(page, 'input#data-subject.govuk-radios__input', 'Please select a data subject') }) test('when the user clicks continue on the dataset page without entering a dataset, the page correctly indicates there\'s an error', async ({ page }) => { @@ -35,21 +24,30 @@ test('when the user clicks continue on the dataset page without entering a datas await page.waitForSelector('input#dataset.govuk-radios__input') - const errorLink = await page.getByRole('link', { name: 'Please select a dataset' }) - const fieldError = await page.getByText('Error: Please select a dataset') - const errorSummary = await page.getByText('There is a problem') + await testErrorMessage(page, 'input#dataset.govuk-radios__input', 'Please select a dataset') +}) - expect(await errorSummary.isVisible(), 'Page should show the error summary').toBeTruthy() - expect(await errorLink.isVisible(), 'Page should the error message that is a link to the problem field').toBeTruthy() - expect(await fieldError.isVisible(), 'Page should show the error message next to the problem field').toBeTruthy() - await errorLink.click() - const problemFieldIsFocused = await page.$eval('input#dataset.govuk-radios__input', (el) => el === document.activeElement) - expect(problemFieldIsFocused, 'The focus should be on the problem field').toBeTruthy() +test('when the user clicks continue on the file upload page without selecting a file, the page correctly indicates there\'s an error', async ({ page }) => { + await page.goto('/') + // start page + await page.getByRole('button', { name: 'Start now' }).click() - expect(await page.title(), 'Page title should indicate there\'s an error').toMatch(/Error: .*/) + // data subject page + await page.getByLabel('Conservation area').check() + await page.getByRole('button', { name: 'Continue' }).click() + + // dataset page + await page.getByLabel('Conservation area dataset').check() + await page.getByRole('button', { name: 'Continue' }).click() + + // file upload page + await page.getByRole('button', { name: 'Continue' }).click() + await page.waitForSelector('input#datafile.govuk-file-upload') + + await testErrorMessage(page, 'input#datafile.govuk-file-upload', 'Please select a file to upload') }) -test('when the user clicks continue on the file upload page without selecting a file, the page correctly indicates there\'s an error', async ({ page }) => { +test('when the user clicks continue on the email page without entering a valid email, the page correctly indicates there\'s an error', async ({ page }) => { await page.goto('/') // start page await page.getByRole('button', { name: 'Start now' }).click() @@ -66,17 +64,43 @@ test('when the user clicks continue on the file upload page without selecting a await page.getByRole('button', { name: 'Continue' }).click() await page.waitForSelector('input#datafile.govuk-file-upload') - const errorLink = await page.getByRole('link', { name: 'Please select a file' }) - const fieldError = await page.getByText('Error: Please select a file') + + let fileChooserPromise = page.waitForEvent('filechooser') + await page.getByText('Upload data').click() + let fileChooser = await fileChooserPromise + await fileChooser.setFiles('test/testData/conservation-area-ok.csv') + + await page.getByRole('button', { name: 'Continue' }).click() + + await page.waitForURL('**/no-errors') + await page.getByRole('button', { name: 'Continue' }).click() + + await page.waitForURL('**/email-address') + + await page.getByRole('button', { name: 'Continue' }).click() + + await testErrorMessage(page, 'input#email-address.govuk-input', 'Please enter a valid email address') + + await page.getByLabel('Your email address').fill('invalidEmail1') + await page.getByRole('button', { name: 'Continue' }).click() + + await testErrorMessage(page, 'input#email-address.govuk-input', 'Please enter a valid email address') +}) + + + + +const testErrorMessage = async (page, fieldName, expectedErrorMessage) => { + const errorLink = await page.getByRole('link', { name: expectedErrorMessage }) + const fieldError = await page.getByText(`Error: ${expectedErrorMessage}`) const errorSummary = await page.getByText('There is a problem') expect(await errorSummary.isVisible(), 'Page should show the error summary').toBeTruthy() expect(await errorLink.isVisible(), 'Page should the error message that is a link to the problem field').toBeTruthy() expect(await fieldError.isVisible(), 'Page should show the error message next to the problem field').toBeTruthy() await errorLink.click() - - const problemFieldIsFocused = await page.$eval('input#datafile.govuk-file-upload', (el) => el === document.activeElement) + const problemFieldIsFocused = await page.$eval(fieldName, (el) => el === document.activeElement) expect(problemFieldIsFocused, 'The focus should be on the problem field').toBeTruthy() expect(await page.title(), 'Page title should indicate there\'s an error').toMatch(/Error: .*/) -}) +} \ No newline at end of file From af2cbe52a97588a49a90cee73b8acd37e759c9d5 Mon Sep 17 00:00:00 2001 From: George Goodall Date: Tue, 21 Nov 2023 12:48:24 +0000 Subject: [PATCH 07/11] linting fix --- test/acceptance/validation_errors.test.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/test/acceptance/validation_errors.test.js b/test/acceptance/validation_errors.test.js index 733683bc..01422b00 100644 --- a/test/acceptance/validation_errors.test.js +++ b/test/acceptance/validation_errors.test.js @@ -64,19 +64,18 @@ test('when the user clicks continue on the email page without entering a valid e await page.getByRole('button', { name: 'Continue' }).click() await page.waitForSelector('input#datafile.govuk-file-upload') - - let fileChooserPromise = page.waitForEvent('filechooser') + const fileChooserPromise = page.waitForEvent('filechooser') await page.getByText('Upload data').click() - let fileChooser = await fileChooserPromise + const fileChooser = await fileChooserPromise await fileChooser.setFiles('test/testData/conservation-area-ok.csv') await page.getByRole('button', { name: 'Continue' }).click() await page.waitForURL('**/no-errors') await page.getByRole('button', { name: 'Continue' }).click() - + await page.waitForURL('**/email-address') - + await page.getByRole('button', { name: 'Continue' }).click() await testErrorMessage(page, 'input#email-address.govuk-input', 'Please enter a valid email address') @@ -87,9 +86,6 @@ test('when the user clicks continue on the email page without entering a valid e await testErrorMessage(page, 'input#email-address.govuk-input', 'Please enter a valid email address') }) - - - const testErrorMessage = async (page, fieldName, expectedErrorMessage) => { const errorLink = await page.getByRole('link', { name: expectedErrorMessage }) const fieldError = await page.getByText(`Error: ${expectedErrorMessage}`) @@ -103,4 +99,4 @@ const testErrorMessage = async (page, fieldName, expectedErrorMessage) => { expect(problemFieldIsFocused, 'The focus should be on the problem field').toBeTruthy() expect(await page.title(), 'Page title should indicate there\'s an error').toMatch(/Error: .*/) -} \ No newline at end of file +} From 0d546a77249b6e2419e764a72c6c8fbd5e92860a Mon Sep 17 00:00:00 2001 From: George Goodall Date: Tue, 21 Nov 2023 13:33:48 +0000 Subject: [PATCH 08/11] improved validation testing checks --- test/acceptance/validation_errors.test.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test/acceptance/validation_errors.test.js b/test/acceptance/validation_errors.test.js index 01422b00..673d4e3f 100644 --- a/test/acceptance/validation_errors.test.js +++ b/test/acceptance/validation_errors.test.js @@ -5,8 +5,6 @@ test('when the user clicks continue on the data subject page without entering a await page.getByRole('button', { name: 'Start now' }).click() await page.getByRole('button', { name: 'Continue' }).click() - await page.waitForSelector('input#data-subject.govuk-radios__input') - await testErrorMessage(page, 'input#data-subject.govuk-radios__input', 'Please select a data subject') }) @@ -22,8 +20,6 @@ test('when the user clicks continue on the dataset page without entering a datas // dataset page await page.getByRole('button', { name: 'Continue' }).click() - await page.waitForSelector('input#dataset.govuk-radios__input') - await testErrorMessage(page, 'input#dataset.govuk-radios__input', 'Please select a dataset') }) @@ -42,9 +38,8 @@ test('when the user clicks continue on the file upload page without selecting a // file upload page await page.getByRole('button', { name: 'Continue' }).click() - await page.waitForSelector('input#datafile.govuk-file-upload') - await testErrorMessage(page, 'input#datafile.govuk-file-upload', 'Please select a file to upload') + await testErrorMessage(page, 'input#datafile.govuk-file-upload', 'Please select a file') }) test('when the user clicks continue on the email page without entering a valid email, the page correctly indicates there\'s an error', async ({ page }) => { @@ -83,16 +78,20 @@ test('when the user clicks continue on the email page without entering a valid e await page.getByLabel('Your email address').fill('invalidEmail1') await page.getByRole('button', { name: 'Continue' }).click() + await page.waitForSelector('input#email-address.govuk-input') + await testErrorMessage(page, 'input#email-address.govuk-input', 'Please enter a valid email address') }) const testErrorMessage = async (page, fieldName, expectedErrorMessage) => { + await page.waitForSelector(fieldName) + const errorLink = await page.getByRole('link', { name: expectedErrorMessage }) const fieldError = await page.getByText(`Error: ${expectedErrorMessage}`) const errorSummary = await page.getByText('There is a problem') expect(await errorSummary.isVisible(), 'Page should show the error summary').toBeTruthy() - expect(await errorLink.isVisible(), 'Page should the error message that is a link to the problem field').toBeTruthy() + expect(await errorLink.isVisible(), 'Page should show an error summary that is a link to the problem field').toBeTruthy() expect(await fieldError.isVisible(), 'Page should show the error message next to the problem field').toBeTruthy() await errorLink.click() const problemFieldIsFocused = await page.$eval(fieldName, (el) => el === document.activeElement) From e2ecc6698ab7db39fb255e505032e33b55a4870d Mon Sep 17 00:00:00 2001 From: George Goodall Date: Tue, 21 Nov 2023 13:34:07 +0000 Subject: [PATCH 09/11] added email validator package --- package-lock.json | 9 +++++++++ package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index aba71f3c..d39834a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@x-govuk/govuk-prototype-components": "^2.0.3", "@x-govuk/govuk-prototype-filters": "^1.2.0", "axios": "^1.6.2", + "email-validator": "^2.0.4", "express": "^4.18.2", "govuk-frontend": "^4.7.0", "hmpo-config": "^3.0.0", @@ -2286,6 +2287,14 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/email-validator": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/email-validator/-/email-validator-2.0.4.tgz", + "integrity": "sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ==", + "engines": { + "node": ">4.0" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", diff --git a/package.json b/package.json index e03dd462..c1adc4f0 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,6 @@ "mock:api": "node ./test/mock-api/index.js", "scss": "sass --quiet-deps --load-path=./ src/assets/scss:public/stylesheets", "scss:watch": "sass --quiet-deps --load-path=./ --watch src/assets/scss:public/stylesheets", - "mock:api": "node ./test/mock-api/index.js", "test": "npm run test:unit && npm run test:integration && npm run test:contract && npm run test:acceptance", "test:unit": "vitest run test/unit", "test:integration": "vitest run test/integration", @@ -43,6 +42,7 @@ "@x-govuk/govuk-prototype-components": "^2.0.3", "@x-govuk/govuk-prototype-filters": "^1.2.0", "axios": "^1.6.2", + "email-validator": "^2.0.4", "express": "^4.18.2", "govuk-frontend": "^4.7.0", "hmpo-config": "^3.0.0", From 6241ffeaea48ef9199a6c04387d8f982eff45701 Mon Sep 17 00:00:00 2001 From: George Goodall Date: Tue, 21 Nov 2023 13:34:42 +0000 Subject: [PATCH 10/11] add email address page and controller --- src/controllers/emailAddressController.js | 20 +++++++ src/routes/form-wizard/steps.js | 8 ++- src/views/email-address.html | 71 +++++++++++++++++++++++ src/views/no-errors.html | 5 +- 4 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 src/controllers/emailAddressController.js create mode 100644 src/views/email-address.html diff --git a/src/controllers/emailAddressController.js b/src/controllers/emailAddressController.js new file mode 100644 index 00000000..ab2aca23 --- /dev/null +++ b/src/controllers/emailAddressController.js @@ -0,0 +1,20 @@ +'use strict' + +import MyController from './MyController.js' + +import { validate } from 'email-validator' + +class EmailAddressController extends MyController { + // perform some additional validation on the email address + validate (req, res, next) { + if (!validate(req.form.values['email-address'])) { + const errors = {} + errors['email-address'] = new this.Error('email-address', {}, req.form.values['email-address'], 'Email address is not valid') + next(errors) + } else { + super.validate(req, res, next) + } + } +} + +export default EmailAddressController diff --git a/src/routes/form-wizard/steps.js b/src/routes/form-wizard/steps.js index a02175a1..4c86b7d0 100644 --- a/src/routes/form-wizard/steps.js +++ b/src/routes/form-wizard/steps.js @@ -3,6 +3,7 @@ import datasetController from '../../controllers/datasetController.js' import uploadController from '../../controllers/uploadController.js' import errorsController from '../../controllers/errorsController.js' import MyController from '../../controllers/MyController.js' +import emailAddressController from '../../controllers/emailAddressController.js' export default { '/': { @@ -35,6 +36,11 @@ export default { }, '/no-errors': { controller: MyController, - next: 'transformations' + next: 'email-address' + }, + '/email-address': { + controller: emailAddressController, + fields: ['email-address'], + next: 'name' } } diff --git a/src/views/email-address.html b/src/views/email-address.html new file mode 100644 index 00000000..cca4d776 --- /dev/null +++ b/src/views/email-address.html @@ -0,0 +1,71 @@ +{% extends "layouts/main.html" %} + +{% from 'govuk/components/button/macro.njk' import govukButton %} +{% from 'govuk/components/input/macro.njk' import govukInput %} +{% from 'govuk/components/error-message/macro.njk' import govukErrorMessage %} +{% from 'govuk/components/error-summary/macro.njk' import govukErrorSummary %} + +{% set pageName = 'Your email address' %} + +{% set errorMessage = 'Please enter a valid email address' %} + + +{% if 'email-address' in errors %} + {% set datasetError = true %} +{% endif %} + +{% block pageTitle %} + {% if datasetError %} + Error: {{super()}} + {% else %} + {{super()}} + {% endif %} +{% endblock %} + +{% block content %} + {{errors | dump}} +
+
+ {% if datasetError %} + {{ govukErrorSummary({ + titleText: "There is a problem", + errorList: [ + { + text: errorMessage, + href: "#email-address" + } + ] + }) }} + {% endif %} + +
+ + {{ govukInput({ + id: "email-address", + name: "email-address", + label: { + text: pageName, + isPageHeading: true, + classes: 'govuk-label--l' + }, + type: "email", + autocomplete: "email", + spellcheck: false, + value: data.check.emailAddress, + errorMessage: { + text: errorMessage + } if datasetError else undefined + }) }} + + + + + + + {{ govukButton({ + text: "Continue" + }) }} +
+
+
+{% endblock %} diff --git a/src/views/no-errors.html b/src/views/no-errors.html index 82b2c776..fe0c4f46 100644 --- a/src/views/no-errors.html +++ b/src/views/no-errors.html @@ -23,10 +23,9 @@

-
+ {{ govukButton({ - text: "Continue", - href: '/email-address' + text: "Continue" }) }}
From bfe1e35115d8fd16778ad2f3e96443b3ce4091e1 Mon Sep 17 00:00:00 2001 From: George Goodall Date: Tue, 21 Nov 2023 16:01:47 +0000 Subject: [PATCH 11/11] remove info dump --- src/views/email-address.html | 1 - 1 file changed, 1 deletion(-) diff --git a/src/views/email-address.html b/src/views/email-address.html index cca4d776..5167f6e4 100644 --- a/src/views/email-address.html +++ b/src/views/email-address.html @@ -23,7 +23,6 @@ {% endblock %} {% block content %} - {{errors | dump}}
{% if datasetError %}