@govtechsg/oobee 0.10.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +22 -0
- package/.github/pull_request_template.md +11 -0
- package/.github/workflows/docker-test.yml +54 -0
- package/.github/workflows/image.yml +107 -0
- package/.github/workflows/publish.yml +18 -0
- package/.idea/modules.xml +8 -0
- package/.idea/purple-a11y.iml +9 -0
- package/.idea/vcs.xml +6 -0
- package/.prettierrc.json +12 -0
- package/.vscode/extensions.json +5 -0
- package/.vscode/settings.json +10 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/DETAILS.md +163 -0
- package/Dockerfile +60 -0
- package/INSTALLATION.md +146 -0
- package/INTEGRATION.md +785 -0
- package/LICENSE +22 -0
- package/README.md +587 -0
- package/SECURITY.md +5 -0
- package/__mocks__/mock-report.html +1431 -0
- package/__mocks__/mockFunctions.ts +32 -0
- package/__mocks__/mockIssues.ts +64 -0
- package/__mocks__/mock_all_issues/000000001.json +64 -0
- package/__mocks__/mock_all_issues/000000002.json +53 -0
- package/__mocks__/mock_all_issues/fake-file.txt +0 -0
- package/__tests__/logs.test.ts +25 -0
- package/__tests__/mergeAxeResults.test.ts +278 -0
- package/__tests__/utils.test.ts +118 -0
- package/a11y-scan-results.zip +0 -0
- package/eslint.config.js +53 -0
- package/exclusions.txt +2 -0
- package/gitlab-pipeline-template.yml +54 -0
- package/jest.config.js +1 -0
- package/package.json +96 -0
- package/scripts/copyFiles.js +44 -0
- package/scripts/install_oobee_dependencies.cmd +13 -0
- package/scripts/install_oobee_dependencies.command +101 -0
- package/scripts/install_oobee_dependencies.ps1 +110 -0
- package/scripts/oobee_shell.cmd +13 -0
- package/scripts/oobee_shell.command +11 -0
- package/scripts/oobee_shell.sh +55 -0
- package/scripts/oobee_shell_ps.ps1 +54 -0
- package/src/cli.ts +401 -0
- package/src/combine.ts +240 -0
- package/src/constants/__tests__/common.test.ts +44 -0
- package/src/constants/cliFunctions.ts +305 -0
- package/src/constants/common.ts +1840 -0
- package/src/constants/constants.ts +443 -0
- package/src/constants/errorMeta.json +319 -0
- package/src/constants/itemTypeDescription.ts +11 -0
- package/src/constants/oobeeAi.ts +141 -0
- package/src/constants/questions.ts +181 -0
- package/src/constants/sampleData.ts +187 -0
- package/src/crawlers/__tests__/commonCrawlerFunc.test.ts +51 -0
- package/src/crawlers/commonCrawlerFunc.ts +656 -0
- package/src/crawlers/crawlDomain.ts +877 -0
- package/src/crawlers/crawlIntelligentSitemap.ts +156 -0
- package/src/crawlers/crawlLocalFile.ts +193 -0
- package/src/crawlers/crawlSitemap.ts +356 -0
- package/src/crawlers/custom/extractAndGradeText.ts +57 -0
- package/src/crawlers/custom/flagUnlabelledClickableElements.ts +964 -0
- package/src/crawlers/custom/utils.ts +486 -0
- package/src/crawlers/customAxeFunctions.ts +82 -0
- package/src/crawlers/pdfScanFunc.ts +468 -0
- package/src/crawlers/runCustom.ts +117 -0
- package/src/index.ts +173 -0
- package/src/logs.ts +66 -0
- package/src/mergeAxeResults.ts +964 -0
- package/src/npmIndex.ts +284 -0
- package/src/screenshotFunc/htmlScreenshotFunc.ts +411 -0
- package/src/screenshotFunc/pdfScreenshotFunc.ts +762 -0
- package/src/static/ejs/partials/components/categorySelector.ejs +4 -0
- package/src/static/ejs/partials/components/categorySelectorDropdown.ejs +57 -0
- package/src/static/ejs/partials/components/pagesScannedModal.ejs +70 -0
- package/src/static/ejs/partials/components/reportSearch.ejs +47 -0
- package/src/static/ejs/partials/components/ruleOffcanvas.ejs +105 -0
- package/src/static/ejs/partials/components/scanAbout.ejs +263 -0
- package/src/static/ejs/partials/components/screenshotLightbox.ejs +13 -0
- package/src/static/ejs/partials/components/summaryScanAbout.ejs +141 -0
- package/src/static/ejs/partials/components/summaryScanResults.ejs +16 -0
- package/src/static/ejs/partials/components/summaryTable.ejs +20 -0
- package/src/static/ejs/partials/components/summaryWcagCompliance.ejs +94 -0
- package/src/static/ejs/partials/components/topFive.ejs +6 -0
- package/src/static/ejs/partials/components/wcagCompliance.ejs +70 -0
- package/src/static/ejs/partials/footer.ejs +21 -0
- package/src/static/ejs/partials/header.ejs +230 -0
- package/src/static/ejs/partials/main.ejs +40 -0
- package/src/static/ejs/partials/scripts/bootstrap.ejs +8 -0
- package/src/static/ejs/partials/scripts/categorySelectorDropdownScript.ejs +190 -0
- package/src/static/ejs/partials/scripts/categorySummary.ejs +141 -0
- package/src/static/ejs/partials/scripts/highlightjs.ejs +335 -0
- package/src/static/ejs/partials/scripts/popper.ejs +7 -0
- package/src/static/ejs/partials/scripts/reportSearch.ejs +248 -0
- package/src/static/ejs/partials/scripts/ruleOffcanvas.ejs +801 -0
- package/src/static/ejs/partials/scripts/screenshotLightbox.ejs +71 -0
- package/src/static/ejs/partials/scripts/summaryScanResults.ejs +14 -0
- package/src/static/ejs/partials/scripts/summaryTable.ejs +78 -0
- package/src/static/ejs/partials/scripts/utils.ejs +441 -0
- package/src/static/ejs/partials/styles/bootstrap.ejs +12375 -0
- package/src/static/ejs/partials/styles/highlightjs.ejs +54 -0
- package/src/static/ejs/partials/styles/styles.ejs +1843 -0
- package/src/static/ejs/partials/styles/summaryBootstrap.ejs +12458 -0
- package/src/static/ejs/partials/summaryHeader.ejs +70 -0
- package/src/static/ejs/partials/summaryMain.ejs +75 -0
- package/src/static/ejs/report.ejs +420 -0
- package/src/static/ejs/summary.ejs +47 -0
- package/src/static/mustache/.prettierrc +4 -0
- package/src/static/mustache/Attention Deficit.mustache +11 -0
- package/src/static/mustache/Blind.mustache +11 -0
- package/src/static/mustache/Cognitive.mustache +7 -0
- package/src/static/mustache/Colorblindness.mustache +20 -0
- package/src/static/mustache/Deaf.mustache +12 -0
- package/src/static/mustache/Deafblind.mustache +7 -0
- package/src/static/mustache/Dyslexia.mustache +14 -0
- package/src/static/mustache/Low Vision.mustache +7 -0
- package/src/static/mustache/Mobility.mustache +15 -0
- package/src/static/mustache/Sighted Keyboard Users.mustache +42 -0
- package/src/static/mustache/report.mustache +1709 -0
- package/src/types/print-message.d.ts +28 -0
- package/src/types/types.ts +46 -0
- package/src/types/xpath-to-css.d.ts +3 -0
- package/src/utils.ts +332 -0
- package/tsconfig.json +15 -0
package/INTEGRATION.md
ADDED
@@ -0,0 +1,785 @@
|
|
1
|
+
## Integrating Oobee with end-to-end testing frameworks
|
2
|
+
|
3
|
+
Oobee provides functionalities that makes it possible to be integrated with end-to-end testing frameworks such as [Cypress](https://www.cypress.io/) and [Playwright](https://playwright.dev/).
|
4
|
+
|
5
|
+
### Prerequisites
|
6
|
+
|
7
|
+
In order to use this functionality, the testing framework must support:
|
8
|
+
|
9
|
+
- Execution of scripts in a NodeJS environment.
|
10
|
+
- Injection of JavaScript into the document that is being tested.
|
11
|
+
- Execution of JavaScript in the context of the document and retrieval of results back into the NodeJS environment after execution.
|
12
|
+
|
13
|
+
### How to include Oobee in your project
|
14
|
+
|
15
|
+
1. Add Oobee to your project by running the following command:
|
16
|
+
|
17
|
+
`npm install --save-dev @govtechsg/oobee`
|
18
|
+
|
19
|
+
2. In the file of choice, import Oobee using:
|
20
|
+
|
21
|
+
`import oobeeA11yInit from '@govtechsg/oobee'`
|
22
|
+
|
23
|
+
Note that Oobee should be imported in a script that runs in a NodeJS environment.
|
24
|
+
|
25
|
+
3. Create an instance of Oobee with:
|
26
|
+
|
27
|
+
`const oobeeA11y = await oobeeA11yInit(entryUrl)`
|
28
|
+
|
29
|
+
`entryUrl` should be a valid URL referring to the domain of the website to be scanned with Oobee.
|
30
|
+
|
31
|
+
### API Reference
|
32
|
+
|
33
|
+
#### `async oobeeA11yInit(entryUrl, testLabel, name, email, includeScreenshots, viewportSettings, thresholds, scanAboutMetadata)`
|
34
|
+
|
35
|
+
Returns an instance of Oobee
|
36
|
+
|
37
|
+
##### Parameters
|
38
|
+
|
39
|
+
- `entryUrl`
|
40
|
+
- Initial URL to start the oobee oobee scan
|
41
|
+
- `testLabel`
|
42
|
+
- Label for test in report
|
43
|
+
- `name`
|
44
|
+
- For Oobee data collection purposes
|
45
|
+
- `email`
|
46
|
+
- For Oobee data collection purposes
|
47
|
+
- `includeScreenshots` (optional)
|
48
|
+
- Include screenshots of affected elements in the report. Defaults to false.
|
49
|
+
- `viewportSettings` (optional)
|
50
|
+
- Viewport settings used in cypress tests needed to optimize screenshot function. Defaults to cypress’ default viewport settings. Example: `{ width: 1000, height: 600 }`
|
51
|
+
- `thresholds` (optional)
|
52
|
+
- Object containing the max number of mustFix or goodToFix issue occurrences before an error is thrown for test failure. Does not fail tests by default. Example: `{ mustFix: 1, goodToFix: 3 }`
|
53
|
+
- `scanAboutMetadata` (optional)
|
54
|
+
- Include additional information in the Scan About section of the report by passing in a JSON object.
|
55
|
+
- `zip` (optional)
|
56
|
+
- Name of the generated zip of Oobee results at the end of scan. Defaults to "oobee-scan-results".
|
57
|
+
|
58
|
+
#### Oobee Instance
|
59
|
+
|
60
|
+
##### Properties
|
61
|
+
|
62
|
+
`scanDetails`
|
63
|
+
|
64
|
+
Object containing details about the scan
|
65
|
+
|
66
|
+
- E.g. `{
|
67
|
+
startTime: 1700104789495,
|
68
|
+
crawlType: 'Customized',
|
69
|
+
requestUrl: 'https://govtechsg.github.io',
|
70
|
+
urlsCrawled: { }
|
71
|
+
}`
|
72
|
+
|
73
|
+
`randomToken`
|
74
|
+
|
75
|
+
Unique identifier for the scan instance
|
76
|
+
|
77
|
+
##### Methods
|
78
|
+
|
79
|
+
`getScripts()`
|
80
|
+
|
81
|
+
Get the axe-core script to be injected into the browser
|
82
|
+
|
83
|
+
- `runA11yScan(elementsToScan)`
|
84
|
+
Runs axe scan on the current page.
|
85
|
+
|
86
|
+
Parameter(s):
|
87
|
+
|
88
|
+
- `elementsToScan`: Specifies which element should and which should not be tested
|
89
|
+
|
90
|
+
Returns:
|
91
|
+
|
92
|
+
- Object consisting of the current page url, current page title and axe scan result. `{ pageUrl, pageTitle, axeScanResults }`
|
93
|
+
|
94
|
+
`async pushScanResults(res, metadata, elementsToClick)`
|
95
|
+
|
96
|
+
Process scan results to be included in the report.
|
97
|
+
|
98
|
+
Parameter(s):
|
99
|
+
|
100
|
+
- `res`: Object consisting of the current page url, current page title and axe scan result. ` {pageUrl, pageTitle, axeScanResults}`
|
101
|
+
- `metadata` (optional): Additional information to be displayed for each page scanned in the report
|
102
|
+
- `elementsToClick` (optional): Elements clicked during the test to reveal hidden elements. Required to be able identify hidden elements if they were scanned for screenshot purposes. Ensure selectors resolve to a single element.
|
103
|
+
|
104
|
+
Returns:
|
105
|
+
|
106
|
+
- Object containing the number of mustFix and goodToFix issue occurrences for this scan run e.g. `{ mustFix: 1, goodToFix: 5 }`
|
107
|
+
|
108
|
+
`testThresholds()`
|
109
|
+
|
110
|
+
Checks the accumulated issue occurrences count against the specified threshold.
|
111
|
+
|
112
|
+
- Terminates oobeeA11y instance and throws an error if the number of accumulated mustFix or goodToFix issue occurrences exceeds either of the specified thresholds.
|
113
|
+
|
114
|
+
`async terminate()`
|
115
|
+
|
116
|
+
Stops the Oobee instance and generates the scan report and other scan result artifacts. Returns the name of the generated folder containing the results.
|
117
|
+
|
118
|
+
### How to use
|
119
|
+
|
120
|
+
Example usages for Cypress and Playwright can be found in [this section](#example-usages).
|
121
|
+
|
122
|
+
With reference to an instance of Oobee as `oobeeA11y`:
|
123
|
+
|
124
|
+
1. Fetch the necessary scripts needed to be injected to document to be scanned by executing `oobeeA11y.getScripts()`. The scripts will be returned as a string.
|
125
|
+
2. Inject the scripts into the document to be scanned. The easiest way that this can be done is by using `eval()` in the document's environment.
|
126
|
+
- Note that this step needs to be done for every page visited.
|
127
|
+
3. Run a scan by executing `runA11yScan()` in the document's environment.
|
128
|
+
- By default, the scan will be run for the entire page.
|
129
|
+
- It is possible to run the scan for specific sections or elements in the page. One way to do this is to pass an array of CSS selectors of the elements to be scanned into `runA11yScan`. For example, `runA11yScan(['#my-component', 'button'])`. Other acceptable forms of argument can be found [here](https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#context-parameter).
|
130
|
+
4. Pass the scan results back into the NodeJS environment where `oobeeA11y` is in.
|
131
|
+
5. Push the results using `await oobeeA11y.pushScanResults(scanResults)`.
|
132
|
+
6. Repeat steps 2-5 as many times as desired.
|
133
|
+
7. Terminate Oobee by using `await oobeeA11y.terminate()`. A folder containing the details and report of your scan will be created, under the directory `results` which can be found in your project's root directory.
|
134
|
+
|
135
|
+
### Example usages
|
136
|
+
|
137
|
+
#### Cypress
|
138
|
+
|
139
|
+
<details>
|
140
|
+
<summary>Click here to see an example usage in an E2E Cypress test (javascript)</summary>
|
141
|
+
|
142
|
+
We will be creating the following files in a demo Cypress project:
|
143
|
+
|
144
|
+
├── cypress
|
145
|
+
│ ├── e2e
|
146
|
+
│ │ └── spec.cy.js
|
147
|
+
│ └── support
|
148
|
+
│ └── e2e.js
|
149
|
+
├── cypress.config.js
|
150
|
+
└── package.json
|
151
|
+
|
152
|
+
Create a <code>package.json</code> by running <code>npm init</code> . Accept the default options or customise it as needed.
|
153
|
+
|
154
|
+
Change the type of npm package to module by running <code>npm pkg set type="module"</code>
|
155
|
+
|
156
|
+
Install the following node dependencies by running <code>npm install cypress @govtechsg/oobee --save-dev </code>
|
157
|
+
|
158
|
+
Navigate to <code>node_modules/@govtechsg/oobee</code> and run <code>npm install</code> and <code>npm run build</code> within the folder to install remaining Oobee dependencies:
|
159
|
+
|
160
|
+
cd node_modules/@govtechsg/oobee
|
161
|
+
npm install
|
162
|
+
npm run build
|
163
|
+
cd ../../..
|
164
|
+
|
165
|
+
Create <code>cypress.config.js</code> with the following contents, and change your Name, E-mail address, and boolean value for whether rule items requiring manual review in the report should be displayed below:
|
166
|
+
|
167
|
+
import { defineConfig } from "cypress";
|
168
|
+
import oobeeA11yInit from "@govtechsg/oobee";
|
169
|
+
|
170
|
+
// viewport used in tests to optimise screenshots
|
171
|
+
const viewportSettings = { width: 1920, height: 1040 };
|
172
|
+
// specifies the number of occurrences before error is thrown for test failure
|
173
|
+
const thresholds = { mustFix: 20, goodToFix: 25 };
|
174
|
+
// additional information to include in the "Scan About" section of the report
|
175
|
+
const scanAboutMetadata = { browser: 'Chrome (Desktop)' };
|
176
|
+
// name of the generated zip of the results at the end of scan
|
177
|
+
const resultsZipName = "oobee-scan-results"
|
178
|
+
|
179
|
+
const oobeeA11y = await oobeeA11yInit(
|
180
|
+
"https://govtechsg.github.io", // initial url to start scan
|
181
|
+
"Demo Cypress Scan", // label for test
|
182
|
+
"Your Name",
|
183
|
+
"email@domain.com",
|
184
|
+
true, // include screenshots of affected elements in the report
|
185
|
+
viewportSettings,
|
186
|
+
thresholds,
|
187
|
+
scanAboutMetadata,
|
188
|
+
resultsZipName
|
189
|
+
);
|
190
|
+
|
191
|
+
export default defineConfig({
|
192
|
+
taskTimeout: 120000, // need to extend as screenshot function requires some time
|
193
|
+
viewportHeight: viewportSettings.height,
|
194
|
+
viewportWidth: viewportSettings.width,
|
195
|
+
e2e: {
|
196
|
+
setupNodeEvents(on, _config) {
|
197
|
+
on("task", {
|
198
|
+
getPurpleA11yScripts() {
|
199
|
+
return oobeeA11y.getScripts();
|
200
|
+
},
|
201
|
+
async pushPurpleA11yScanResults({res, metadata, elementsToClick}) {
|
202
|
+
return await oobeeA11y.pushScanResults(res, metadata, elementsToClick);
|
203
|
+
},
|
204
|
+
returnResultsDir() {
|
205
|
+
return `results/${oobeeA11y.randomToken}_${oobeeA11y.scanDetails.urlsCrawled.scanned.length}pages/report.html`;
|
206
|
+
},
|
207
|
+
finishPurpleA11yTestCase() {
|
208
|
+
oobeeA11y.testThresholds();
|
209
|
+
return null;
|
210
|
+
},
|
211
|
+
async terminatePurpleA11y() {
|
212
|
+
return await oobeeA11y.terminate();
|
213
|
+
},
|
214
|
+
});
|
215
|
+
},
|
216
|
+
},
|
217
|
+
});
|
218
|
+
|
219
|
+
Create a sub-folder and file <code>cypress/support/e2e.js</code> with the following contents:
|
220
|
+
|
221
|
+
Cypress.Commands.add("injectPurpleA11yScripts", () => {
|
222
|
+
cy.task("getPurpleA11yScripts").then((s) => {
|
223
|
+
cy.window().then((win) => {
|
224
|
+
win.eval(s);
|
225
|
+
});
|
226
|
+
});
|
227
|
+
});
|
228
|
+
|
229
|
+
Cypress.Commands.add("runPurpleA11yScan", (items={}) => {
|
230
|
+
cy.window().then(async (win) => {
|
231
|
+
const { elementsToScan, elementsToClick, metadata } = items;
|
232
|
+
const res = await win.runA11yScan(elementsToScan);
|
233
|
+
cy.task("pushPurpleA11yScanResults", {res, metadata, elementsToClick}).then((count) => { return count });
|
234
|
+
cy.task("finishPurpleA11yTestCase"); // test the accumulated number of issue occurrences against specified thresholds. If exceed, terminate oobeeA11y instance.
|
235
|
+
});
|
236
|
+
});
|
237
|
+
|
238
|
+
Cypress.Commands.add("terminatePurpleA11y", () => {
|
239
|
+
cy.task("terminatePurpleA11y");
|
240
|
+
});
|
241
|
+
|
242
|
+
Create <code>cypress/e2e/spec.cy.js</code> with the following contents:
|
243
|
+
|
244
|
+
describe("template spec", () => {
|
245
|
+
it("should run oobee A11y", () => {
|
246
|
+
cy.visit(
|
247
|
+
"https://govtechsg.github.io/purple-banner-embeds/oobee-integrated-scan-example.htm"
|
248
|
+
);
|
249
|
+
cy.injectPurpleA11yScripts();
|
250
|
+
cy.runPurpleA11yScan();
|
251
|
+
cy.get("button[onclick=\"toggleSecondSection()\"]").click();
|
252
|
+
// Run a scan on <input> and <button> elements
|
253
|
+
cy.runPurpleA11yScan({
|
254
|
+
elementsToScan: ["input", "button"],
|
255
|
+
elementsToClick: ["button[onclick=\"toggleSecondSection()\"]"],
|
256
|
+
metadata: "Clicked button"
|
257
|
+
});
|
258
|
+
|
259
|
+
cy.terminatePurpleA11y();
|
260
|
+
});
|
261
|
+
});
|
262
|
+
|
263
|
+
Run your test with <code>npx cypress run</code>.
|
264
|
+
You will see Oobee results generated in <code>results</code> folder.
|
265
|
+
|
266
|
+
</details>
|
267
|
+
<details>
|
268
|
+
<summary>Click here to see an example usage in an E2E Cypress test (typescript)</summary>
|
269
|
+
|
270
|
+
We will be creating the following files in a demo Cypress project:
|
271
|
+
|
272
|
+
├── cypress.config.ts
|
273
|
+
├── cypress.d.ts
|
274
|
+
├── package.json
|
275
|
+
├── src
|
276
|
+
│ └── cypress
|
277
|
+
│ ├── e2e
|
278
|
+
│ │ └── spec.cy.ts
|
279
|
+
│ └── support
|
280
|
+
│ └── e2e.ts
|
281
|
+
└── tsconfig.json
|
282
|
+
|
283
|
+
Create a <code>package.json</code> by running <code>npm init</code> . Accept the default options or customise it as needed.
|
284
|
+
|
285
|
+
Change the type of npm package to module by running <code>npm pkg set type="module"</code>
|
286
|
+
|
287
|
+
Install the following node dependencies by running <code>npm install cypress @types/cypress @govtechsg/oobee typescript --save-dev </code>
|
288
|
+
|
289
|
+
Create a <code>tsconfig.json</code> in the root directory and add the following:
|
290
|
+
|
291
|
+
```
|
292
|
+
{
|
293
|
+
"compilerOptions": {
|
294
|
+
"outDir": "./dist",
|
295
|
+
"allowJs": true,
|
296
|
+
"target": "es2021",
|
297
|
+
"module": "nodenext",
|
298
|
+
"rootDir": "./src",
|
299
|
+
"skipLibCheck": true,
|
300
|
+
"types": ["cypress"]
|
301
|
+
},
|
302
|
+
"include": ["./src/**/*", "cypress.d.ts"]
|
303
|
+
}
|
304
|
+
```
|
305
|
+
|
306
|
+
Navigate to <code>node_modules/@govtechsg/oobee</code> and run <code>npm install</code> and <code>npm run build</code> within the folder to install remaining Oobee dependencies:
|
307
|
+
|
308
|
+
cd node_modules/@govtechsg/oobee
|
309
|
+
npm install
|
310
|
+
npm run build
|
311
|
+
cd ../../..
|
312
|
+
|
313
|
+
Create <code>cypress.config.ts</code> with the following contents, and change your Name, E-mail address, and boolean value for whether rule items requiring manual review in the report should be displayed below:
|
314
|
+
|
315
|
+
import { defineConfig } from "cypress";
|
316
|
+
import oobeeA11yInit from "@govtechsg/oobee";
|
317
|
+
|
318
|
+
interface ViewportSettings {
|
319
|
+
width: number;
|
320
|
+
height: number;
|
321
|
+
}
|
322
|
+
|
323
|
+
interface Thresholds {
|
324
|
+
mustFix: number;
|
325
|
+
goodToFix: number;
|
326
|
+
}
|
327
|
+
|
328
|
+
interface ScanAboutMetadata {
|
329
|
+
browser: string;
|
330
|
+
}
|
331
|
+
|
332
|
+
// viewport used in tests to optimise screenshots
|
333
|
+
const viewportSettings: ViewportSettings = { width: 1920, height: 1040 };
|
334
|
+
// specifies the number of occurrences before error is thrown for test failure
|
335
|
+
const thresholds: Thresholds = { mustFix: 20, goodToFix: 20 };
|
336
|
+
// additional information to include in the "Scan About" section of the report
|
337
|
+
const scanAboutMetadata: ScanAboutMetadata = { browser: 'Chrome (Desktop)' };
|
338
|
+
// name of the generated zip of the results at the end of scan
|
339
|
+
const resultsZipName: string = "oobee-scan-results"
|
340
|
+
|
341
|
+
const oobeeA11y = await oobeeA11yInit(
|
342
|
+
"https://govtechsg.github.io", // initial url to start scan
|
343
|
+
"Demo Cypress Scan", // label for test
|
344
|
+
"Your Name",
|
345
|
+
"email@domain.com",
|
346
|
+
true, // include screenshots of affected elements in the report
|
347
|
+
viewportSettings,
|
348
|
+
thresholds,
|
349
|
+
scanAboutMetadata,
|
350
|
+
resultsZipName
|
351
|
+
);
|
352
|
+
|
353
|
+
export default defineConfig({
|
354
|
+
taskTimeout: 120000, // need to extend as screenshot function requires some time
|
355
|
+
viewportHeight: viewportSettings.height,
|
356
|
+
viewportWidth: viewportSettings.width,
|
357
|
+
e2e: {
|
358
|
+
setupNodeEvents(on, _config) {
|
359
|
+
on("task", {
|
360
|
+
getPurpleA11yScripts(): string {
|
361
|
+
return oobeeA11y.getScripts();
|
362
|
+
},
|
363
|
+
async pushPurpleA11yScanResults({res, metadata, elementsToClick}: { res: any, metadata: any, elementsToClick: any[] }): Promise<{ mustFix: number, goodToFix: number }> {
|
364
|
+
return await oobeeA11y.pushScanResults(res, metadata, elementsToClick);
|
365
|
+
},
|
366
|
+
returnResultsDir(): string {
|
367
|
+
return `results/${oobeeA11y.randomToken}_${oobeeA11y.scanDetails.urlsCrawled.scanned.length}pages/reports/report.html`;
|
368
|
+
},
|
369
|
+
finishPurpleA11yTestCase(): null {
|
370
|
+
oobeeA11y.testThresholds();
|
371
|
+
return null;
|
372
|
+
},
|
373
|
+
async terminatePurpleA11y(): Promise<string> {
|
374
|
+
return await oobeeA11y.terminate();
|
375
|
+
},
|
376
|
+
});
|
377
|
+
},
|
378
|
+
supportFile: 'dist/cypress/support/e2e.js',
|
379
|
+
specPattern: 'dist/cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
|
380
|
+
},
|
381
|
+
});
|
382
|
+
|
383
|
+
Create a sub-folder and file <code>src/cypress/support/e2e.ts</code> with the following contents:
|
384
|
+
|
385
|
+
Cypress.Commands.add("injectPurpleA11yScripts", () => {
|
386
|
+
cy.task("getPurpleA11yScripts").then((s: string) => {
|
387
|
+
cy.window().then((win) => {
|
388
|
+
win.eval(s);
|
389
|
+
});
|
390
|
+
});
|
391
|
+
});
|
392
|
+
|
393
|
+
Cypress.Commands.add("runPurpleA11yScan", (items={}) => {
|
394
|
+
cy.window().then(async (win) => {
|
395
|
+
const { elementsToScan, elementsToClick, metadata } = items;
|
396
|
+
const res = await win.runA11yScan(elementsToScan);
|
397
|
+
cy.task("pushPurpleA11yScanResults", {res, metadata, elementsToClick}).then((count) => { return count });
|
398
|
+
cy.task("finishPurpleA11yTestCase"); // test the accumulated number of issue occurrences against specified thresholds. If exceed, terminate oobeeA11y instance.
|
399
|
+
});
|
400
|
+
});
|
401
|
+
|
402
|
+
Cypress.Commands.add("terminatePurpleA11y", () => {
|
403
|
+
cy.task("terminatePurpleA11y");
|
404
|
+
});
|
405
|
+
|
406
|
+
Create <code>src/cypress/e2e/spec.cy.ts</code> with the following contents:
|
407
|
+
|
408
|
+
describe("template spec", () => {
|
409
|
+
it("should run oobee A11y", () => {
|
410
|
+
cy.visit(
|
411
|
+
"https://govtechsg.github.io/purple-banner-embeds/oobee-integrated-scan-example.htm"
|
412
|
+
);
|
413
|
+
cy.injectPurpleA11yScripts();
|
414
|
+
cy.runPurpleA11yScan();
|
415
|
+
cy.get("button[onclick=\"toggleSecondSection()\"]").click();
|
416
|
+
// Run a scan on <input> and <button> elements
|
417
|
+
cy.runPurpleA11yScan({
|
418
|
+
elementsToScan: ["input", "button"],
|
419
|
+
elementsToClick: ["button[onclick=\"toggleSecondSection()\"]"],
|
420
|
+
metadata: "Clicked button"
|
421
|
+
});
|
422
|
+
|
423
|
+
cy.terminatePurpleA11y();
|
424
|
+
});
|
425
|
+
});
|
426
|
+
|
427
|
+
Create <code>cypress.d.ts</code> in the root directory with the following contents:
|
428
|
+
|
429
|
+
```
|
430
|
+
declare namespace Cypress {
|
431
|
+
interface Chainable<Subject> {
|
432
|
+
injectPurpleA11yScripts(): Chainable<void>;
|
433
|
+
runPurpleA11yScan(options?: PurpleA11yScanOptions): Chainable<void>;
|
434
|
+
terminatePurpleA11y(): Chainable<any>;
|
435
|
+
}
|
436
|
+
|
437
|
+
interface PurpleA11yScanOptions {
|
438
|
+
elementsToScan?: string[];
|
439
|
+
elementsToClick?: string[];
|
440
|
+
metadata?: string;
|
441
|
+
}
|
442
|
+
}
|
443
|
+
|
444
|
+
interface Window {
|
445
|
+
runA11yScan: (elementsToScan?: string[]) => Promise<any>;
|
446
|
+
}
|
447
|
+
```
|
448
|
+
|
449
|
+
Compile your typescript code with <code>npx tsc</code>.
|
450
|
+
Run your test with <code>npx cypress run</code>.
|
451
|
+
|
452
|
+
You will see Oobee results generated in <code>results</code> folder.
|
453
|
+
|
454
|
+
</details>
|
455
|
+
|
456
|
+
#### Playwright
|
457
|
+
|
458
|
+
<details>
|
459
|
+
<summary>Click here to see an example usage in Playwright (javascript)</summary>
|
460
|
+
|
461
|
+
Create a <code>package.json</code> by running <code>npm init</code> . Accept the default options or customise it as needed.
|
462
|
+
|
463
|
+
Change the type of npm package to module by running <code>npm pkg set type="module"</code>
|
464
|
+
|
465
|
+
Install the following node dependencies by running <code>npm install playwright @govtechsg/oobee --save-dev</code> and <code>npx playwright install</code>
|
466
|
+
|
467
|
+
Navigate to <code>node_modules/@govtechsg/oobee</code> and run <code>npm install</code> and <code>npm run build</code> within the folder to install remaining Oobee dependencies:
|
468
|
+
|
469
|
+
cd node_modules/@govtechsg/oobee
|
470
|
+
npm install
|
471
|
+
npm run build
|
472
|
+
cd ../../..
|
473
|
+
|
474
|
+
On your project's root folder, create a Playwright test file <code>oobeeA11y-playwright-demo.js</code>:
|
475
|
+
|
476
|
+
import { chromium } from "playwright";
|
477
|
+
import oobeeA11yInit from "@govtechsg/oobee";
|
478
|
+
|
479
|
+
// viewport used in tests to optimise screenshots
|
480
|
+
const viewportSettings = { width: 1920, height: 1040 };
|
481
|
+
// specifies the number of occurrences before error is thrown for test failure
|
482
|
+
const thresholds = { mustFix: 20, goodToFix: 25 };
|
483
|
+
// additional information to include in the "Scan About" section of the report
|
484
|
+
const scanAboutMetadata = { browser: 'Chrome (Desktop)' };
|
485
|
+
|
486
|
+
const oobeeA11y = await oobeeA11yInit(
|
487
|
+
"https://govtechsg.github.io", // initial url to start scan
|
488
|
+
"Demo Playwright Scan", // label for test
|
489
|
+
"Your Name",
|
490
|
+
"email@domain.com",
|
491
|
+
true, // include screenshots of affected elements in the report
|
492
|
+
viewportSettings,
|
493
|
+
thresholds,
|
494
|
+
scanAboutMetadata,
|
495
|
+
);
|
496
|
+
|
497
|
+
(async () => {
|
498
|
+
const browser = await chromium.launch({
|
499
|
+
headless: false,
|
500
|
+
});
|
501
|
+
const context = await browser.newContext();
|
502
|
+
const page = await context.newPage();
|
503
|
+
|
504
|
+
const runPurpleA11yScan = async (elementsToScan) => {
|
505
|
+
const scanRes = await page.evaluate(
|
506
|
+
async elementsToScan => await runA11yScan(elementsToScan),
|
507
|
+
elementsToScan,
|
508
|
+
);
|
509
|
+
await oobeeA11y.pushScanResults(scanRes);
|
510
|
+
oobeeA11y.testThresholds(); // test the accumulated number of issue occurrences against specified thresholds. If exceed, terminate oobeeA11y instance.
|
511
|
+
};
|
512
|
+
|
513
|
+
await page.goto('https://govtechsg.github.io/purple-banner-embeds/oobee-integrated-scan-example.htm');
|
514
|
+
await page.evaluate(oobeeA11y.getScripts());
|
515
|
+
await runPurpleA11yScan();
|
516
|
+
|
517
|
+
await page.getByRole('button', { name: 'Click Me' }).click();
|
518
|
+
// Run a scan on <input> and <button> elements
|
519
|
+
await runPurpleA11yScan(['input', 'button'])
|
520
|
+
|
521
|
+
|
522
|
+
// ---------------------
|
523
|
+
await context.close();
|
524
|
+
await browser.close();
|
525
|
+
await oobeeA11y.terminate();
|
526
|
+
})();
|
527
|
+
|
528
|
+
Run your test with <code>node oobeeA11y-playwright-demo.js</code> .
|
529
|
+
|
530
|
+
You will see Oobee results generated in <code>results</code> folder.
|
531
|
+
|
532
|
+
</details>
|
533
|
+
<details>
|
534
|
+
<summary>Click here to see an example usage in Playwright (typescript)</summary>
|
535
|
+
|
536
|
+
Create a <code>package.json</code> by running <code>npm init</code> . Accept the default options or customise it as needed.
|
537
|
+
|
538
|
+
Change the type of npm package to module by running <code>npm pkg set type="module"</code>
|
539
|
+
|
540
|
+
Install the following node dependencies by running <code>npm install playwright @govtechsg/oobee typescript --save-dev</code> and <code>npx playwright install</code>
|
541
|
+
|
542
|
+
Create a <code>tsconfig.json</code> in the root directory and add the following:
|
543
|
+
|
544
|
+
```
|
545
|
+
{
|
546
|
+
"compilerOptions": {
|
547
|
+
"outDir": "./dist",
|
548
|
+
"allowJs": true,
|
549
|
+
"target": "es2021",
|
550
|
+
"module": "nodenext",
|
551
|
+
"rootDir": "./src",
|
552
|
+
"skipLibCheck": true
|
553
|
+
},
|
554
|
+
"include": ["./src/**/*"]
|
555
|
+
}
|
556
|
+
```
|
557
|
+
|
558
|
+
Navigate to <code>node_modules/@govtechsg/oobee</code> and run <code>npm install</code> and <code>npm run build</code> within the folder to install remaining Oobee dependencies:
|
559
|
+
|
560
|
+
cd node_modules/@govtechsg/oobee
|
561
|
+
npm install
|
562
|
+
npm run build
|
563
|
+
cd ../../..
|
564
|
+
|
565
|
+
Create a sub-folder and Playwright test file <code>src/oobeeA11y-playwright-demo.ts</code> with the following contents:
|
566
|
+
|
567
|
+
import { Browser, BrowserContext, Page, chromium } from "playwright";
|
568
|
+
import oobeeA11yInit from "@govtechsg/oobee";
|
569
|
+
|
570
|
+
declare const runA11yScan: (elementsToScan?: string[]) => Promise<any>;
|
571
|
+
|
572
|
+
interface ViewportSettings {
|
573
|
+
width: number;
|
574
|
+
height: number;
|
575
|
+
}
|
576
|
+
|
577
|
+
interface Thresholds {
|
578
|
+
mustFix: number;
|
579
|
+
goodToFix: number;
|
580
|
+
}
|
581
|
+
|
582
|
+
interface ScanAboutMetadata {
|
583
|
+
browser: string;
|
584
|
+
}
|
585
|
+
|
586
|
+
// viewport used in tests to optimise screenshots
|
587
|
+
const viewportSettings: ViewportSettings = { width: 1920, height: 1040 };
|
588
|
+
// specifies the number of occurrences before error is thrown for test failure
|
589
|
+
const thresholds: Thresholds = { mustFix: 20, goodToFix: 25 };
|
590
|
+
// additional information to include in the "Scan About" section of the report
|
591
|
+
const scanAboutMetadata: ScanAboutMetadata = { browser: 'Chrome (Desktop)' };
|
592
|
+
|
593
|
+
const oobeeA11y = await oobeeA11yInit(
|
594
|
+
"https://govtechsg.github.io", // initial url to start scan
|
595
|
+
"Demo Playwright Scan", // label for test
|
596
|
+
"Your Name",
|
597
|
+
"email@domain.com",
|
598
|
+
true, // include screenshots of affected elements in the report
|
599
|
+
viewportSettings,
|
600
|
+
thresholds,
|
601
|
+
scanAboutMetadata,
|
602
|
+
);
|
603
|
+
|
604
|
+
(async () => {
|
605
|
+
const browser: Browser = await chromium.launch({
|
606
|
+
headless: false,
|
607
|
+
});
|
608
|
+
const context: BrowserContext = await browser.newContext();
|
609
|
+
const page: Page = await context.newPage();
|
610
|
+
|
611
|
+
const runPurpleA11yScan = async (elementsToScan?: string[]) => {
|
612
|
+
const scanRes = await page.evaluate(
|
613
|
+
async elementsToScan => await runA11yScan(elementsToScan),
|
614
|
+
elementsToScan,
|
615
|
+
);
|
616
|
+
await oobeeA11y.pushScanResults(scanRes);
|
617
|
+
oobeeA11y.testThresholds(); // test the accumulated number of issue occurrences against specified thresholds. If exceed, terminate oobeeA11y instance.
|
618
|
+
};
|
619
|
+
|
620
|
+
await page.goto('https://govtechsg.github.io/purple-banner-embeds/oobee-integrated-scan-example.htm');
|
621
|
+
await page.evaluate(oobeeA11y.getScripts());
|
622
|
+
await runPurpleA11yScan();
|
623
|
+
|
624
|
+
await page.getByRole('button', { name: 'Click Me' }).click();
|
625
|
+
// Run a scan on <input> and <button> elements
|
626
|
+
await runPurpleA11yScan(['input', 'button'])
|
627
|
+
|
628
|
+
|
629
|
+
// ---------------------
|
630
|
+
await context.close();
|
631
|
+
await browser.close();
|
632
|
+
await oobeeA11y.terminate();
|
633
|
+
})();
|
634
|
+
|
635
|
+
Compile your typescript code with <code>npx tsc</code>.
|
636
|
+
Run your test with <code>node dist/oobeeA11y-playwright-demo.js</code>.
|
637
|
+
|
638
|
+
You will see Oobee results generated in <code>results</code> folder.
|
639
|
+
|
640
|
+
</details>
|
641
|
+
|
642
|
+
#### Automating Web Crawler Login
|
643
|
+
|
644
|
+
<details>
|
645
|
+
<summary>Click here to see an example automated web crawler login (javascript)</summary>
|
646
|
+
<code>automated-web-crawler-login.js</code>:
|
647
|
+
|
648
|
+
import { chromium } from 'playwright';
|
649
|
+
import { exec } from 'child_process';
|
650
|
+
|
651
|
+
const loginAndCaptureHeaders = async (url, email, password) => {
|
652
|
+
const browser = await chromium.launch({ headless: true });
|
653
|
+
const page = await browser.newPage();
|
654
|
+
|
655
|
+
await page.goto(url);
|
656
|
+
await page.fill('input[name="email"]', email);
|
657
|
+
await page.fill('input[name="password"]', password);
|
658
|
+
|
659
|
+
const [response] = await Promise.all([
|
660
|
+
page.waitForNavigation(),
|
661
|
+
page.click('input[type="submit"]'),
|
662
|
+
]);
|
663
|
+
|
664
|
+
// Format cookie retrieved from page
|
665
|
+
const formatCookies = cookies => {
|
666
|
+
return cookies.map(cookie => `cookie ${cookie.name}=${cookie.value}`).join('; ');
|
667
|
+
};
|
668
|
+
|
669
|
+
// Retrieve cookies after login
|
670
|
+
let cookies = await page.context().cookies();
|
671
|
+
const formattedCookies = formatCookies(cookies);
|
672
|
+
|
673
|
+
// Close browser
|
674
|
+
await browser.close();
|
675
|
+
|
676
|
+
return formattedCookies;
|
677
|
+
};
|
678
|
+
|
679
|
+
const runPurpleA11yScan = command => {
|
680
|
+
exec(command, (error, stdout, stderr) => {
|
681
|
+
if (error) {
|
682
|
+
console.error(`Error: ${error.message}`);
|
683
|
+
return;
|
684
|
+
}
|
685
|
+
if (stderr) {
|
686
|
+
console.error(stderr);
|
687
|
+
}
|
688
|
+
console.log(stdout);
|
689
|
+
});
|
690
|
+
};
|
691
|
+
|
692
|
+
const runScript = () => {
|
693
|
+
loginAndCaptureHeaders(
|
694
|
+
// Test example with authenticationtest.com
|
695
|
+
'https://authenticationtest.com/simpleFormAuth/',
|
696
|
+
'simpleForm@authenticationtest.com',
|
697
|
+
'pa$$w0rd',
|
698
|
+
)
|
699
|
+
.then(formattedCookies => {
|
700
|
+
console.log('Cookies retrieved.\n');
|
701
|
+
// where -m "..." are the headers needed in the format "header1 value1, header2 value2" etc
|
702
|
+
// where -u ".../loginSuccess/" is the destination page after login
|
703
|
+
const command = `npm run cli -- -c website -u "https://authenticationtest.com/loginSuccess/" -p 1 -k "Your Name:email@domain.com" -m "${formattedCookies}"`;
|
704
|
+
console.log(`Executing PurpleA11y scan command:\n> ${command}\n`);
|
705
|
+
runPurpleA11yScan(command);
|
706
|
+
})
|
707
|
+
.catch(err => {
|
708
|
+
console.error('Error:', err);
|
709
|
+
});
|
710
|
+
};
|
711
|
+
|
712
|
+
runScript();
|
713
|
+
|
714
|
+
</details>
|
715
|
+
<details>
|
716
|
+
<summary>Click here to see an example automated web crawler login (typescript)</summary>
|
717
|
+
<code>automated-web-crawler-login.ts</code>:
|
718
|
+
|
719
|
+
import { chromium, Browser, Page, Cookie } from 'playwright';
|
720
|
+
import { exec } from 'child_process';
|
721
|
+
|
722
|
+
const loginAndCaptureHeaders = async (url: string, email: string, password: string): Promise<string> => {
|
723
|
+
const browser: Browser = await chromium.launch({ headless: true });
|
724
|
+
const page: Page = await browser.newPage();
|
725
|
+
|
726
|
+
await page.goto(url);
|
727
|
+
await page.fill('input[name="email"]', email);
|
728
|
+
await page.fill('input[name="password"]', password);
|
729
|
+
|
730
|
+
const [response] = await Promise.all([
|
731
|
+
page.waitForNavigation(),
|
732
|
+
page.click('input[type="submit"]'),
|
733
|
+
]);
|
734
|
+
|
735
|
+
// Format cookie retrieved from page
|
736
|
+
const formatCookies = (cookies: Cookie[]): string => {
|
737
|
+
return cookies.map(cookie => `cookie ${cookie.name}=${cookie.value}`).join('; ');
|
738
|
+
};
|
739
|
+
|
740
|
+
// Retrieve cookies after login
|
741
|
+
let cookies: Cookie[] = await page.context().cookies();
|
742
|
+
const formattedCookies: string = formatCookies(cookies);
|
743
|
+
|
744
|
+
// Close browser
|
745
|
+
await browser.close();
|
746
|
+
|
747
|
+
return formattedCookies;
|
748
|
+
};
|
749
|
+
|
750
|
+
const runPurpleA11yScan = (command: string): void => {
|
751
|
+
exec(command, (error, stdout, stderr) => {
|
752
|
+
if (error) {
|
753
|
+
console.error(`Error: ${error.message}`);
|
754
|
+
return;
|
755
|
+
}
|
756
|
+
if (stderr) {
|
757
|
+
console.error(stderr);
|
758
|
+
}
|
759
|
+
console.log(stdout);
|
760
|
+
});
|
761
|
+
};
|
762
|
+
|
763
|
+
const runScript = (): void => {
|
764
|
+
loginAndCaptureHeaders(
|
765
|
+
// Test example with authenticationtest.com
|
766
|
+
'https://authenticationtest.com/simpleFormAuth/',
|
767
|
+
'simpleForm@authenticationtest.com',
|
768
|
+
'pa$$w0rd',
|
769
|
+
)
|
770
|
+
.then((formattedCookies: string) => {
|
771
|
+
console.log('Cookies retrieved.\n');
|
772
|
+
// where -m "..." are the headers needed in the format "header1 value1, header2 value2" etc
|
773
|
+
// where -u ".../loginSuccess/" is the destination page after login
|
774
|
+
const command: string = `npm run cli -- -c website -u "https://authenticationtest.com/loginSuccess/" -p 1 -k "Your Name:email@domain.com" -m "${formattedCookies}"`;
|
775
|
+
console.log(`Executing PurpleA11y scan command:\n> ${command}\n`);
|
776
|
+
runPurpleA11yScan(command);
|
777
|
+
})
|
778
|
+
.catch((err: Error) => {
|
779
|
+
console.error('Error:', err);
|
780
|
+
});
|
781
|
+
};
|
782
|
+
|
783
|
+
runScript();
|
784
|
+
|
785
|
+
</details>
|