Merge pull request #1950 from GSA/1927-user-story-brief-description-of-the-user-story

Backstop is implemented locally
This commit is contained in:
Alex Janousek
2024-10-04 14:47:06 -04:00
committed by GitHub
25 changed files with 2173 additions and 72 deletions

5
.gitignore vendored
View File

@@ -134,3 +134,8 @@ playwright/
# Nodenv
.node-version
# BackstopJS
backstop_data/bitmaps_test/
backstop_data/html_report/
backstop_data/engine_scripts/playwright/

3
.prettierrc Normal file
View File

@@ -0,0 +1,3 @@
{
"singleQuote": true
}

View File

@@ -64,11 +64,11 @@ def _get_access_token(code, state): # pragma: no cover
url = f"{base_url}{cli_assert}&{cli_assert_type}&{code_param}&grant_type=authorization_code"
headers = {"Authorization": "Bearer %s" % token}
response = requests.post(url, headers=headers)
response_json = response.json()
try:
encoded_id_token = response_json["id_token"]
except KeyError as e:
# Capture the response json here so it hopefully shows up in error reports
current_app.logger.exception(f"Error when getting id token {response_json}")
raise KeyError(f"'access_token' {response.json()}") from e
@@ -97,7 +97,6 @@ def _get_access_token(code, state): # pragma: no cover
try:
access_token = response_json["access_token"]
except KeyError as e:
# Capture the response json here so it hopefully shows up in error reports
current_app.logger.exception(
f"Error when getting access token {response.json()} #notify-admin-1505"
)
@@ -157,8 +156,9 @@ def _do_login_dot_gov(): # $ pragma: no cover
current_app.logger.info(f"activating user {usr.id} #notify-admin-1505")
activate_user(usr.id)
except BaseException as be: # noqa B036
current_app.logger.exception(f"Error signing in: {be} #notify-admin-1505 ")
current_app.logger.error(f"Error signing in: {be} #notify-admin-1505 ")
error(401)
return redirect(url_for("main.show_accounts_or_dashboard", next=redirect_url))
# end login.gov
@@ -181,16 +181,22 @@ def _handle_e2e_tests(redirect_url): # pragma: no cover
)
user = user_api_client.get_user_by_email(os.getenv("NOTIFY_E2E_TEST_EMAIL"))
activate_user(user["id"])
# Check if the redirect URL is present and safe before proceeding further
if redirect_url and is_safe_redirect_url(redirect_url):
return redirect(redirect_url)
return redirect(
url_for(
"main.show_accounts_or_dashboard",
next="EMAIL_IS_OK",
)
)
except Exception as e:
stre = str(e)
stre = stre.replace(" ", "_")
# Trying to get a message back to playwright somehow since we can't see the admin logs
# Trying to get a message back to playwright somehow since we can't raise an error
return redirect(url_for(f"https://{stre}"))
@@ -200,7 +206,7 @@ def sign_in(): # pragma: no cover
redirect_url = request.args.get("next")
if os.getenv("NOTIFY_E2E_TEST_EMAIL"):
return _handle_e2e_tests(None)
return _handle_e2e_tests(redirect_url)
# If we have to revalidated the email, send the message
# via email and redirect to the "verify your email page"

61
backstop.config.js Normal file
View File

@@ -0,0 +1,61 @@
const { urls, baseUrl } = require('./urls');
const MISMATCH_THRESHOLD = 0.2;
const SCREENSHOT_DELAY = 2000;
const createScenariosFromUrls = (urls, delay = SCREENSHOT_DELAY) => {
return Object.keys(urls).map((label) => ({
label,
url: urls[label],
selectors: ['document'],
misMatchThreshold: MISMATCH_THRESHOLD,
requireSameDimensions: true,
delay,
}));
};
module.exports = {
id: 'backstop_test',
viewports: [
{
label: 'desktop',
width: 1024,
height: 768,
},
],
scenarios: [
...createScenariosFromUrls(urls),
{
label: 'Choose Service - Accounts',
url: `${baseUrl}/accounts`,
selectors: ['h1.heading-large', 'a.usa-button[href="/add-service"]'],
misMatchThreshold: MISMATCH_THRESHOLD,
requireSameDimensions: true,
delay: SCREENSHOT_DELAY,
},
// example page with script
{
label: 'Get Started Page - Highlight Trial Mode',
url: `${baseUrl}/using-notify/get-started`,
selectors: ['document'],
misMatchThreshold: MISMATCH_THRESHOLD,
requireSameDimensions: true,
onBeforeScript: 'puppeteer/countFeatureLinks.js',
delay: SCREENSHOT_DELAY,
},
],
paths: {
bitmaps_reference: 'backstop_data/bitmaps_reference',
bitmaps_test: 'backstop_data/bitmaps_test',
engine_scripts: 'backstop_data/engine_scripts',
html_report: 'backstop_data/html_report',
ci_report: 'backstop_data/ci_report',
},
engine: 'puppeteer',
engineOptions: {
browser: 'chromium',
timeout: 30000,
},
report: ['browser'],
debug: false,
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 908 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

View File

@@ -0,0 +1,18 @@
module.exports = async (page, scenario) => {
await page.goto(scenario.url, { waitUntil: 'networkidle2' });
console.log('Page loaded.');
// Count the number of items in the side navigation and log their text
const navItems = await page.$$eval('nav ul li.usa-sidenav__item a', (items) =>
items.map((item) => item.textContent.trim())
);
console.log(`Found ${navItems.length} navigation items:`);
navItems.forEach((itemText, index) => {
console.log(`${index + 1}: ${itemText}`);
});
// Wait a moment for the logging to complete
await new Promise((resolve) => setTimeout(resolve, 1000));
};

65
docs/backstop.md Normal file
View File

@@ -0,0 +1,65 @@
# BackstopJS Getting Started Guide
## Overview
BackstopJS is currently optional in this project and is used for visual regression testing to ensure consistency in the UI. To get started with BackstopJS, follow the steps below. The official guide can be found at the [BackstopJS GitHub Repository](https://github.com/garris/BackstopJS).
## Goal
Eventually BackstopJS will be implemented in the pipeline process to help catch errors before they make their way to higher environments.
## Prerequisites
Before running BackstopJS, ensure the following:
- Both the API and Admin projects must be running.
- In the .env file of both the API and Admin projects, make sure the following environment variables are uncommented and set to matching values:
- `NOTIFY_E2E_TEST_EMAIL`
- `NOTIFY_E2E_TEST_PASSWORD`
Example:
```bash
NOTIFY_E2E_TEST_EMAIL=your-email@example.com
NOTIFY_E2E_TEST_PASSWORD=your-password
```
### How to Run BackstopJS
#### Step 1: Install Gulp Globally
First, make sure all dependencies are installed and updated:
```
make bootstrap
```
#### Step 2: Run the Gulp Test Task
To run the visual regression tests, use the following command:
```
gulp backstopTest
```
If there are new expected changes in the UI, one will need to update the reference images before testing. To do this, run the following command:
```
gulp backstopReference
```
**_Note: After running the `gulp backstopReference` command, immediately running the test (`gulp backstopTest`) should pass all tests, assuming the changes are expected._**
### Adding New Tests
The entry point for BackstopJS configuration can be found in the `backstop.config.js` file.
To add a new feature or page to be tested, modify the `scenarios` array. Refer to the [official documentation](https://github.com/garris/BackstopJS) for details on the structure of the object.
In most cases, one will be testing entire pages instead of individual components. To reduce redundancy, helper functions were created with our default settings that will be applied to every URL in the `urls.js` file. To add a new URL, one would add an object like such:
```
{ label: 'Support', path: '/support' }
```
For unique cases that involve UI interaction, scripts linked to the `engine_scripts/puppeteer` directory can be used to simulate user interactions during tests. An example can be found with `countFeatureLinks.js`

View File

@@ -5,8 +5,8 @@ const rollupPluginNodeResolve = require('@rollup/plugin-node-resolve');
const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer');
const gulpMerge = require('gulp-merge');
const uswds = require("@uswds/compile");
const uswds = require('@uswds/compile');
const { exec } = require('child_process');
const plugins = {};
plugins.addSrc = require('gulp-add-src');
plugins.babel = require('gulp-babel');
@@ -21,7 +21,7 @@ const paths = {
dist: 'app/static/',
npm: 'node_modules/',
toolkit: 'node_modules/govuk_frontend_toolkit/',
govuk_frontend: 'node_modules/govuk-frontend/'
govuk_frontend: 'node_modules/govuk-frontend/',
};
const javascripts = () => {
@@ -29,28 +29,30 @@ const javascripts = () => {
input: paths.src + 'javascripts/modules/all.mjs',
plugins: [
rollupPluginNodeResolve({
mainFields: ['module', 'main']
mainFields: ['module', 'main'],
}),
rollupPluginCommonjs({
include: 'node_modules/**'
})
include: 'node_modules/**',
}),
],
output: {
format: 'iife',
name: 'GOVUK'
}
name: 'GOVUK',
},
})
.pipe(source('all.mjs'))
.pipe(buffer())
.pipe(plugins.addSrc.prepend([
paths.npm + 'hogan.js/dist/hogan-3.0.2.js',
paths.npm + 'jquery/dist/jquery.min.js',
paths.npm + 'query-command-supported/dist/queryCommandSupported.min.js',
paths.npm + 'timeago/jquery.timeago.js',
paths.npm + 'textarea-caret/index.js',
paths.npm + 'cbor-js/cbor.js',
paths.npm + 'd3/dist/d3.min.js'
]));
.pipe(
plugins.addSrc.prepend([
paths.npm + 'hogan.js/dist/hogan-3.0.2.js',
paths.npm + 'jquery/dist/jquery.min.js',
paths.npm + 'query-command-supported/dist/queryCommandSupported.min.js',
paths.npm + 'timeago/jquery.timeago.js',
paths.npm + 'textarea-caret/index.js',
paths.npm + 'cbor-js/cbor.js',
paths.npm + 'd3/dist/d3.min.js',
])
);
const local = src([
paths.toolkit + 'javascripts/govuk/modules.js',
@@ -81,9 +83,11 @@ const javascripts = () => {
paths.src + 'javascripts/activityChart.js',
])
.pipe(plugins.prettyerror())
.pipe(plugins.babel({
presets: ['@babel/preset-env']
}));
.pipe(
plugins.babel({
presets: ['@babel/preset-env'],
})
);
return gulpMerge(vendored, local)
.pipe(plugins.uglify())
@@ -93,24 +97,21 @@ const javascripts = () => {
// Task to copy `gtm_head.js`
const copyGtmHead = () => {
return src(paths.src + 'js/gtm_head.js')
.pipe(dest(paths.dist + 'js/'));
return src(paths.src + 'js/gtm_head.js').pipe(dest(paths.dist + 'js/'));
};
// Task to copy `setTimezone.js`
const copySetTimezone = () => {
return src(paths.src + 'js/setTimezone.js')
.pipe(dest(paths.dist + 'js/'));
return src(paths.src + 'js/setTimezone.js').pipe(dest(paths.dist + 'js/'));
};
// Task to copy images
const copyImages = () => {
return src(paths.src + 'images/**/*', { encoding: false })
.pipe(dest(paths.dist + 'images/'));
return src(paths.src + 'images/**/*', { encoding: false }).pipe(
dest(paths.dist + 'images/')
);
};
// Configure USWDS paths
uswds.settings.version = 3;
uswds.paths.dist.css = paths.dist + 'css';
@@ -129,4 +130,40 @@ const copyAssets = async () => {
await uswds.copyAssets();
};
exports.default = series(styles, javascripts, copyGtmHead, copySetTimezone, copyImages, copyAssets);
// Optional backstopJS task
// Install gulp globally and run `gulp backstopTest`
const backstopTest = (done) => {
exec(
'npx backstop test --configPath=backstop.config.js',
(err, stdout, stderr) => {
console.log(stdout);
console.error(stderr);
done(err);
}
);
};
// Optional backstopJS reference task
// Install gulp globally and run `gulp backstopReference`
const backstopReference = (done) => {
exec(
'npx backstop reference --configPath=backstop.config.js',
(err, stdout, stderr) => {
console.log(stdout);
console.error(stderr);
done(err);
}
);
};
// Export tasks
exports.default = series(
styles,
javascripts,
copyGtmHead,
copySetTimezone,
copyImages,
copyAssets
);
exports.backstopTest = backstopTest;
exports.backstopReference = backstopReference;

1941
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -25,8 +25,8 @@
"graceful-fs": "^4.2.11"
},
"dependencies": {
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-commonjs": "^26.0.3",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/stream": "^3.0.1",
"@uswds/uswds": "^3.8.2",
"cbor-js": "0.1.0",
@@ -37,6 +37,7 @@
"hogan": "1.0.2",
"jquery": "3.7.1",
"morphdom": "^2.7.4",
"playwright": "^1.47.2",
"python": "^0.0.4",
"query-command-supported": "1.0.0",
"sass-embedded": "^1.79.3",
@@ -49,6 +50,7 @@
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.4",
"@uswds/compile": "^1.2.0",
"backstopjs": "^6.3.25",
"better-npm-audit": "^3.11.0",
"gulp": "^5.0.0",
"gulp-add-src": "^1.0.0",

5
poetry.lock generated
View File

@@ -1326,9 +1326,13 @@ files = [
{file = "lxml-5.2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:edcfa83e03370032a489430215c1e7783128808fd3e2e0a3225deee278585196"},
{file = "lxml-5.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:28bf95177400066596cdbcfc933312493799382879da504633d16cf60bba735b"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a745cc98d504d5bd2c19b10c79c61c7c3df9222629f1b6210c0368177589fb8"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b590b39ef90c6b22ec0be925b211298e810b4856909c8ca60d27ffbca6c12e6"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b336b0416828022bfd5a2e3083e7f5ba54b96242159f83c7e3eebaec752f1716"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:c2faf60c583af0d135e853c86ac2735ce178f0e338a3c7f9ae8f622fd2eb788c"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:4bc6cb140a7a0ad1f7bc37e018d0ed690b7b6520ade518285dc3171f7a117905"},
{file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7ff762670cada8e05b32bf1e4dc50b140790909caa8303cfddc4d702b71ea184"},
{file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:57f0a0bbc9868e10ebe874e9f129d2917750adf008fe7b9c1598c0fbbfdde6a6"},
{file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:a6d2092797b388342c1bc932077ad232f914351932353e2e8706851c870bca1f"},
{file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:60499fe961b21264e17a471ec296dcbf4365fbea611bf9e303ab69db7159ce61"},
{file = "lxml-5.2.2-cp37-cp37m-win32.whl", hash = "sha256:d9b342c76003c6b9336a80efcc766748a333573abf9350f4094ee46b006ec18f"},
{file = "lxml-5.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b16db2770517b8799c79aa80f4053cd6f8b716f21f8aca962725a9565ce3ee40"},
@@ -2481,6 +2485,7 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},

30
urls.js Normal file
View File

@@ -0,0 +1,30 @@
const baseUrl = 'http://localhost:6012';
// List of routes with paths and labels
const sublinks = [
{ label: 'Homepage', path: '/' },
{ label: 'Add Service', path: '/add-service' },
{ label: 'Get Started', path: '/using-notify/get-started' },
{ label: 'Trial Mode', path: '/using-notify/trial-mode' },
{ label: 'Pricing', path: '/using-notify/pricing' },
{ label: 'Delivery Status', path: '/using-notify/delivery-status' },
{ label: 'Guidance', path: '/using-notify/guidance' },
{ label: 'Features', path: '/features' },
{ label: 'Roadmap', path: '/features/roadmap' },
{ label: 'Security', path: '/features/security' },
{ label: 'Support', path: '/support' },
// Add more links here as needed
];
const createFullUrl = (base, path) => `${base}${path}`;
// Build url using base and path
const constructUrls = (base, sublinks) =>
sublinks.reduce((acc, { label, path }) => {
return { ...acc, [label]: createFullUrl(base, path) };
}, {});
module.exports = {
baseUrl,
urls: constructUrls(baseUrl, sublinks),
};