cypress-voice-plugin 1.0.0

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.
@@ -0,0 +1,14 @@
1
+ name: Cypress Tests
2
+
3
+ on: push
4
+
5
+ jobs:
6
+ cypress-run:
7
+ runs-on: ubuntu-22.04
8
+ steps:
9
+ - name: Checkout
10
+ uses: actions/checkout@v4
11
+ # Install npm dependencies, cache them correctly
12
+ # and run all Cypress tests
13
+ - name: Cypress run
14
+ uses: cypress-io/github-action@v6
@@ -0,0 +1,38 @@
1
+ # Contributing
2
+
3
+ Thanks for being willing to contribute!
4
+
5
+ **Working on your first Pull Request?** You can learn more from [Your First Pull Request on GitHub](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork)
6
+
7
+ ## Project setup
8
+
9
+ 1. Fork and clone the repo
10
+ 2. Run `npm install` to install dependencies
11
+ 3. Create a branch for your PR with `git checkout -b pr/your-branch-name`
12
+
13
+ > Tip: Keep your `main` branch pointing at the original repository and make
14
+ > pull requests from branches on your fork. To do this, run:
15
+ >
16
+ > ```
17
+ > git remote add upstream https://github.com/dennisbergevin/cypress-voice-plugin
18
+ > git fetch upstream
19
+ > git branch --set-upstream-to=upstream/main main
20
+ > ```
21
+ >
22
+ > This will add the original repository as a "remote" called "upstream," Then
23
+ > fetch the git information from that remote, then set your local `main`
24
+ > branch to use the upstream main branch whenever you run `git pull`. Then you
25
+ > can make all of your pull request branches based on this `main` branch.
26
+ > Whenever you want to update your version of `main`, do a regular `git pull`.
27
+
28
+ ## Committing and Pushing changes
29
+
30
+ Please make sure to run the tests, located in `cypress/e2e`, before you commit your changes. You can run
31
+ `npx cypress open`. Make sure to include any test changes (if they exist) in your commit.
32
+
33
+ ## Help needed
34
+
35
+ Please checkout the [the open issues](https://github.com/dennisbergevin/cypress-voice-plugin/issues)
36
+
37
+ Also, please watch the repo and respond to questions/bug reports/feature
38
+ requests! Thanks!
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024, Dennis Bergevin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ ![cypress-voice picture](./content/cypress-voice.png)
2
+
3
+ A close companion to other iterations of Text to speech (TTS), this assistive Cypress plugin offers an opt-in auditory tool to hear results of a spec file run to support local development and debugging.
4
+
5
+ > [!IMPORTANT]
6
+ > This plugin is _currently_ only for running `cypress open` locally. See [TODO](#todo) for future plans.
7
+
8
+ Using `cypress open`, this Cypress plugin covers the following auditory feedback at the end of a spec file run:
9
+
10
+ - Whether a spec passed, passed with retries, failed or has all tests skipped
11
+ - The specific number of tests that passed, passed with retries, failed or were skipped
12
+ - The total spec run time
13
+ - Voice rate/pitch/volume adjustment sliders
14
+
15
+ ## 🎬 Demo - Turn on the sound!
16
+
17
+ https://github.com/dennisbergevin/cypress-voice-plugin/assets/65262795/9810ed2e-dce5-4d0d-93a4-35d7cd18e5e8
18
+
19
+ ## 🤝 Benefits
20
+
21
+ There are accessible benefits to auditory feedback, but two general audiences came to mind in developing this plugin:
22
+
23
+ 1. For those who prefer an auditory style of feedback.
24
+ 2. For those who are seeking a tool to help quickly analyze a spec run.
25
+
26
+ ## 📦 Installation
27
+
28
+ Install this package:
29
+
30
+ ```sh
31
+ npm install --save-dev cypress-voice-plugin
32
+ ```
33
+
34
+ In `cypress/support/e2e.js` (For E2E tests) and/or `cypress/support/component.js` (For Component tests),
35
+
36
+ ```js
37
+ import "cypress-voice-plugin";
38
+ ```
39
+
40
+ ## 🦺 Setup
41
+
42
+ Within `cypress open`, the voice plugin is enabled via a [Cypress environment variable](https://docs.cypress.io/guides/guides/environment-variables).
43
+
44
+ > [!NOTE]
45
+ > You can only enable a single value for `voiceResultType` at a time. `voiceResultType` and/or `voiceTime` can be enabled together or independently.
46
+
47
+ | Environment variable | Value | Can this variable alone enable plugin? | Purpose | Sample Spoken Result |
48
+ | -------------------- | ------------ | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
49
+ | `voiceResultType` | `"simple"` | ✅ Yes | High-level result of entire spec file run. | "Spec passed." |
50
+ | `voiceResultType` | `"detailed"` | ✅ Yes | In addition to what is provided by `"simple"`, the counts of tests passed, passed with retries, failed and skipped. | "Spec failed: 1 test passed, 2 tests failed, 1 test skipped." |
51
+ | `voiceTime` | `true` | ✅ Yes | The time of entire spec file run. | "Total time: 34 seconds" |
52
+
53
+ ## 🏃‍♀️ Voice adjustments
54
+
55
+ To adjust the rate, pitch, and/or volume of spoken results within the browser when using this plugin, utilize the slider once inside a spec file:
56
+
57
+ ![cypress-voice-slider](./content/cypress-voice-slider.gif)
58
+
59
+ ## 🌏 Language support
60
+
61
+ Under the hood, the plugin utilizes in-browser text-to-speech capabilities via the [`speechSynthesis` Web Speech API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API/Using_the_Web_Speech_API#speech_synthesis).
62
+
63
+ As the plugin does not set a language itself, [`speechSynthesis` is able to detect the language to use](https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesisUtterance/lang):
64
+
65
+ > If unset, the app's (i.e. the <html> lang value) lang will be used, or the user-agent default if that is unset too.
66
+
67
+ ## 📕 Example Environment Variable Setups
68
+
69
+ The following options are suggestions of how to set the environment variable(s). A more comprehensive [guide on environment variable setting](https://docs.cypress.io/guides/guides/environment-variables#Setting) can be found within official Cypress documentation.
70
+
71
+ ### Setup using `cypress.env.json`
72
+
73
+ Add environment variable(s) to a created `cypress.env.json` file.
74
+
75
+ Example:
76
+
77
+ ```js
78
+ {
79
+ "voiceTime": true,
80
+ "voiceResultType": "detailed",
81
+ }
82
+ ```
83
+
84
+ This is a useful method for handling local use of this plugin, particularly if you add `cypress.env.json` to your `.gitignore` file. This way, enabling the plugin functionality via environment variable can be different for each developer machine rather than committed to the remote repository.
85
+
86
+ From official Cypress docs, more information on the [`cypress.env.json` method](https://docs.cypress.io/guides/guides/environment-variables#Option-2-cypressenvjson).
87
+
88
+ ### Setup using `--env`
89
+
90
+ Alternatively, append the environment variable to the end of your `cypress open` cli command:
91
+
92
+ ```shell
93
+ npx cypress open --env voiceResultType=simple
94
+ ```
95
+
96
+ Or, combine multiple variables in one command to hear both result and total run time:
97
+
98
+ ```shell
99
+ npx cypress open --env voiceResultType=detailed,voiceTime=true
100
+ ```
101
+
102
+ From official Cypress docs, more information on the [`--env` method](https://docs.cypress.io/guides/guides/environment-variables#Option-4---env).
103
+
104
+ ## TODO
105
+
106
+ - [ ] Look into additional ways to modify the voice and language for spoken content within the plugin
107
+ - [ ] Add functionality for `cypress run`
108
+
109
+ ## Contributions
110
+
111
+ Feel free to open a pull request or drop any feature request or bug in the [issues](https://github.com/dennisbergevin/cypress-voice-plugin/issues).
112
+
113
+ Please see more details in the [contributing doc](./CONTRIBUTING.md).
package/addStyles.js ADDED
@@ -0,0 +1,147 @@
1
+ export const addStyles = () => {
2
+ const hasStyles = top?.document.querySelector("#rateStyle");
3
+ const hasRate = top?.document.querySelector("#rate");
4
+ const hasPitch = top?.document.querySelector("#pitch");
5
+ const hasVolume = top?.document.querySelector("#volume");
6
+
7
+ const styles = `
8
+ .reporter header {
9
+ overflow: visible;
10
+ z-index: 2;
11
+ }
12
+ .reporter label {
13
+ background-color: inherit;
14
+ color: inherit;
15
+ font-weight: bold;
16
+ font-size: 11px
17
+ }
18
+ #rate-control {
19
+ display: flex;
20
+ align-items: center;
21
+ }
22
+ #rate {
23
+ position: relative;
24
+ display: inline-block;
25
+ overflow-block: auto;
26
+ cursor: grab;
27
+ }
28
+ /* Apply a "closed-hand" cursor during drag operation. */
29
+ #rate:active {
30
+ cursor: grabbing;
31
+ cursor: -moz-grabbing;
32
+ cursor: -webkit-grabbing;
33
+ }
34
+ #pitch-control {
35
+ display: flex;
36
+ align-items: center;
37
+ }
38
+ #pitch {
39
+ position: relative;
40
+ display: inline-block;
41
+ overflow-block: auto;
42
+ cursor: grab;
43
+ }
44
+ /* Apply a "closed-hand" cursor during drag operation. */
45
+ #pitch:active {
46
+ cursor: grabbing;
47
+ cursor: -moz-grabbing;
48
+ cursor: -webkit-grabbing;
49
+ }
50
+ #volume-control {
51
+ display: flex;
52
+ align-items: center;
53
+ }
54
+ #volume {
55
+ position: relative;
56
+ display: inline-block;
57
+ overflow-block: auto;
58
+ cursor: grab;
59
+ }
60
+ /* Apply a "closed-hand" cursor during drag operation. */
61
+ #volume:active {
62
+ cursor: grabbing;
63
+ cursor: -moz-grabbing;
64
+ cursor: -webkit-grabbing;
65
+ }
66
+ `;
67
+ // Add styles
68
+ if (!hasStyles) {
69
+ const reporter = top?.document.querySelector("#unified-reporter");
70
+ const reporterStyle = document.createElement("style");
71
+ reporterStyle.setAttribute("id", "rateStyle");
72
+ reporterStyle.innerHTML = styles;
73
+ reporter?.appendChild(reporterStyle);
74
+ }
75
+
76
+ if (!hasRate && !hasPitch && !hasVolume) {
77
+ const header = top?.document.querySelector("#unified-reporter header");
78
+ const headerRateSliderDiv = document.createElement("div");
79
+ const headerRateSliderLabel = document.createElement("label");
80
+ const headerRateSliderInput = document.createElement("input");
81
+
82
+ headerRateSliderLabel.setAttribute("for", "rate");
83
+ headerRateSliderLabel.innerText = "Voice rate: ";
84
+ headerRateSliderDiv.setAttribute("id", "rate-control");
85
+
86
+ headerRateSliderInput.setAttribute("id", "rate");
87
+ headerRateSliderInput.setAttribute("step", "0.1");
88
+ headerRateSliderInput.setAttribute("value", "1");
89
+ headerRateSliderInput.setAttribute("max", "1.3");
90
+ headerRateSliderInput.setAttribute("min", "0.3");
91
+ headerRateSliderInput.setAttribute("type", "range");
92
+ headerRateSliderInput.setAttribute("aria-valuenow", "1");
93
+ headerRateSliderInput.setAttribute("aria-label", "Voice rate of speed");
94
+ headerRateSliderInput.setAttribute("aria-valuetext", "Normal voice rate");
95
+
96
+ header?.appendChild(headerRateSliderDiv);
97
+ headerRateSliderDiv?.appendChild(headerRateSliderLabel);
98
+ headerRateSliderDiv?.appendChild(headerRateSliderInput);
99
+
100
+ const headerPitchSliderDiv = document.createElement("div");
101
+ const headerPitchSliderLabel = document.createElement("label");
102
+ const headerPitchSliderInput = document.createElement("input");
103
+
104
+ headerPitchSliderLabel.setAttribute("for", "pitch");
105
+ headerPitchSliderLabel.innerText = "Voice pitch: ";
106
+ headerPitchSliderDiv.setAttribute("id", "pitch-control");
107
+
108
+ headerPitchSliderInput.setAttribute("id", "pitch");
109
+ headerPitchSliderInput.setAttribute("step", "0.1");
110
+ headerPitchSliderInput.setAttribute("value", "1");
111
+ headerPitchSliderInput.setAttribute("max", "1.3");
112
+ headerPitchSliderInput.setAttribute("min", "0.3");
113
+ headerPitchSliderInput.setAttribute("type", "range");
114
+ headerPitchSliderInput.setAttribute("aria-valuenow", "1");
115
+ headerPitchSliderInput.setAttribute("aria-label", "Voice pitch");
116
+ headerPitchSliderInput.setAttribute("aria-valuetext", "Normal voice pitch");
117
+
118
+ header?.appendChild(headerPitchSliderDiv);
119
+ headerPitchSliderDiv?.appendChild(headerPitchSliderLabel);
120
+ headerPitchSliderDiv?.appendChild(headerPitchSliderInput);
121
+
122
+ const headerVolumeSliderDiv = document.createElement("div");
123
+ const headerVolumeSliderLabel = document.createElement("label");
124
+ const headerVolumeSliderInput = document.createElement("input");
125
+
126
+ headerVolumeSliderLabel.setAttribute("for", "volume");
127
+ headerVolumeSliderLabel.innerText = "Voice volume: ";
128
+ headerVolumeSliderDiv.setAttribute("id", "volume-control");
129
+
130
+ headerVolumeSliderInput.setAttribute("id", "volume");
131
+ headerVolumeSliderInput.setAttribute("step", "0.1");
132
+ headerVolumeSliderInput.setAttribute("value", "1");
133
+ headerVolumeSliderInput.setAttribute("max", "1");
134
+ headerVolumeSliderInput.setAttribute("min", "0");
135
+ headerVolumeSliderInput.setAttribute("type", "range");
136
+ headerVolumeSliderInput.setAttribute("aria-valuenow", "1");
137
+ headerVolumeSliderInput.setAttribute("aria-label", "Voice volume");
138
+ headerVolumeSliderInput.setAttribute(
139
+ "aria-valuetext",
140
+ "Normal voice volume"
141
+ );
142
+
143
+ header?.appendChild(headerVolumeSliderDiv);
144
+ headerVolumeSliderDiv?.appendChild(headerVolumeSliderLabel);
145
+ headerVolumeSliderDiv?.appendChild(headerVolumeSliderInput);
146
+ }
147
+ };
Binary file
Binary file
@@ -0,0 +1,18 @@
1
+ // Running the test in `cypress open` will allow you to check the assertion pass for the expected message
2
+ // The browser will not speak the message
3
+
4
+ describe("Voice result and time", () => {
5
+ it(
6
+ "Announces short passing test with result and time",
7
+ { env: { voiceResultType: "detailed", voiceTime: true } },
8
+ () => {
9
+ const synth = window.speechSynthesis;
10
+ synth.speak = (event) => {
11
+ const text = event.text;
12
+ expect(text).to.eq(
13
+ "Spec passed: 1 test passed. Total time: Less than 1 second."
14
+ );
15
+ };
16
+ }
17
+ );
18
+ });
@@ -0,0 +1,30 @@
1
+ // Running the test in `cypress open` will allow you to check the assertion pass for the expected message
2
+ // The browser will not speak the message
3
+
4
+ describe("Failing voice", () => {
5
+ it(
6
+ "Announces failed test without time",
7
+ {
8
+ env: {
9
+ voiceResultType: "simple",
10
+ },
11
+ },
12
+ () => {
13
+ // Trigger a "Spec failed" TTS on failure
14
+ cy.on("fail", () => {
15
+ const message = new SpeechSynthesisUtterance("Spec failed.");
16
+ speechSynthesis.speak(message);
17
+ });
18
+
19
+ cy.get("body", { timeout: 25 }).should("not.exist");
20
+
21
+ const synth = window.speechSynthesis;
22
+
23
+ synth.speak = (event) => {
24
+ const char = event.text;
25
+
26
+ expect(char).to.eq("Spec failed.");
27
+ };
28
+ }
29
+ );
30
+ });
@@ -0,0 +1,5 @@
1
+ describe("No plugin enabled", () => {
2
+ it("Passes without plugin enabled", () => {
3
+ expect(1).to.eq(1);
4
+ });
5
+ });
@@ -0,0 +1,17 @@
1
+ // Running the test in `cypress open` will allow you to check the assertion pass for the expected message
2
+ // The browser will not speak the message
3
+
4
+ describe("Passing voice", () => {
5
+ it(
6
+ "Announces passed test",
7
+ { env: { voiceResultType: "simple", voiceTime: false } },
8
+ () => {
9
+ const synth = window.speechSynthesis;
10
+ synth.speak = (event) => {
11
+ const char = event.text;
12
+
13
+ expect(char).to.eq("Spec passed.");
14
+ };
15
+ }
16
+ );
17
+ });
@@ -0,0 +1,27 @@
1
+ // Running the test in `cypress open` will allow you to check the assertion pass for the expected message
2
+ // The browser will not speak the message
3
+
4
+ describe("Voice on retries", () => {
5
+ it(
6
+ "Announces short retried test",
7
+ {
8
+ retries: 1,
9
+ env: { voiceResultType: "detailed", voiceTime: false },
10
+ },
11
+ () => {
12
+ // this test will fail on first try, but pass on second
13
+
14
+ if (Cypress.currentRetry === 0) {
15
+ expect(1).to.eq(2);
16
+ } else {
17
+ const synth = window.speechSynthesis;
18
+ synth.speak = (event) => {
19
+ const text = event.text;
20
+ expect(text).to.eq(
21
+ "Spec passed with retries: 1 test passed with retries."
22
+ );
23
+ };
24
+ }
25
+ }
26
+ );
27
+ });
@@ -0,0 +1,16 @@
1
+ // Running the test in `cypress open` will allow you to check the assertion pass for the expected message
2
+ // The browser will not speak the message
3
+
4
+ describe("Test results only", () => {
5
+ it(
6
+ "Announces short passing test with only test results",
7
+ { env: { voiceResultType: "detailed", voiceTime: false } },
8
+ () => {
9
+ const synth = window.speechSynthesis;
10
+ synth.speak = (event) => {
11
+ const text = event.text;
12
+ expect(text).to.eq("Spec passed: 1 test passed.");
13
+ };
14
+ }
15
+ );
16
+ });
@@ -0,0 +1,16 @@
1
+ // Running the test in `cypress open` will allow you to check the assertion pass for the expected message
2
+ // The browser will not speak the message
3
+
4
+ describe("Voice time", () => {
5
+ it(
6
+ "Announces short passing test with only time",
7
+ { env: { voiceTime: true, voiceResultType: false } },
8
+ () => {
9
+ const synth = window.speechSynthesis;
10
+ synth.speak = (event) => {
11
+ const text = event.text;
12
+ expect(text).to.eq("Total time: Less than 1 second.");
13
+ };
14
+ }
15
+ );
16
+ });
@@ -0,0 +1,37 @@
1
+ /// <reference types="cypress" />
2
+ // ***********************************************
3
+ // This example commands.ts shows you how to
4
+ // create various custom commands and overwrite
5
+ // existing commands.
6
+ //
7
+ // For more comprehensive examples of custom
8
+ // commands please read more here:
9
+ // https://on.cypress.io/custom-commands
10
+ // ***********************************************
11
+ //
12
+ //
13
+ // -- This is a parent command --
14
+ // Cypress.Commands.add('login', (email, password) => { ... })
15
+ //
16
+ //
17
+ // -- This is a child command --
18
+ // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
19
+ //
20
+ //
21
+ // -- This is a dual command --
22
+ // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
23
+ //
24
+ //
25
+ // -- This will overwrite an existing command --
26
+ // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
27
+ //
28
+ // declare global {
29
+ // namespace Cypress {
30
+ // interface Chainable {
31
+ // login(email: string, password: string): Chainable<void>
32
+ // drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
33
+ // dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
34
+ // visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
35
+ // }
36
+ // }
37
+ // }
@@ -0,0 +1,21 @@
1
+ // ***********************************************************
2
+ // This example support/e2e.ts is processed and
3
+ // loaded automatically before your test files.
4
+ //
5
+ // This is a great place to put global configuration and
6
+ // behavior that modifies Cypress.
7
+ //
8
+ // You can change the location of this file or turn off
9
+ // automatically serving support files with the
10
+ // 'supportFile' configuration option.
11
+ //
12
+ // You can read more here:
13
+ // https://on.cypress.io/configuration
14
+ // ***********************************************************
15
+
16
+ // Import commands.js using ES2015 syntax:
17
+ import "./commands.js";
18
+ import "../../index.js";
19
+
20
+ // Alternatively you can use CommonJS syntax:
21
+ // require('./commands')
@@ -0,0 +1,9 @@
1
+ const { defineConfig } = require("cypress");
2
+
3
+ module.exports = defineConfig({
4
+ e2e: {
5
+ setupNodeEvents(on, config) {
6
+ // implement node event listeners here
7
+ },
8
+ },
9
+ });
package/index.js ADDED
@@ -0,0 +1,284 @@
1
+ import { addStyles } from "./addStyles";
2
+
3
+ // Voice plugin to announce spec result when Cypress runner UI is open (cypress open)
4
+ if (
5
+ (Cypress.env("voiceResultType") === "simple" ||
6
+ Cypress.env("voiceResultType") === "detailed" ||
7
+ Cypress.env("voiceTime")) &&
8
+ Cypress.config("isInteractive")
9
+ ) {
10
+ // Cancel any ongoing spoken results when a new spec is selected mid-speech
11
+ // Apply styles for voice rate slider bar
12
+ Cypress.on("test:before:run", () => {
13
+ const voice = window.speechSynthesis;
14
+ voice.cancel();
15
+ addStyles();
16
+ });
17
+
18
+ Cypress.on("test:after:run", (config, test) => {
19
+ // Additional context of test index
20
+ // To account for loops where there is one test per describe and suite names may stay
21
+ // And to account for both specs with multiple suites or only one suite
22
+ const lastTestIndex = test.order;
23
+
24
+ // Querying the headed Cypress browser to find number of total tests in spec
25
+ const testNumber = window.top?.document.querySelectorAll(".test").length;
26
+
27
+ // Checking the current test index is either equal to the last index of the suite or the test context
28
+ const currentTestIsLast = lastTestIndex === testNumber;
29
+
30
+ // Checks within the document for a failure state
31
+ const failed = window.top?.document.querySelector(".runnable-failed");
32
+ // Checks within the document for a retried state to determine if retry occurred
33
+ const retried = window.top?.document.querySelector(".runnable-retried");
34
+ // Checks within the document for a passed state
35
+ const passed = window.top?.document.querySelector(".runnable-passed");
36
+ // Checks within the document for a skipped state
37
+ const skipped = window.top?.document.querySelector(".runnable-pending");
38
+
39
+ // Rate toggle for speed of spoken text
40
+ const rate = window.top?.document.querySelector("#rate");
41
+
42
+ // Pitch toggle for pitch of spoken text
43
+ const pitch = window.top?.document.querySelector("#pitch");
44
+
45
+ // Volume toggle for volume of spoken text
46
+ const volume = window.top?.document.querySelector("#volume");
47
+
48
+ const failedTests = window.top?.document.querySelectorAll(
49
+ ".test.runnable-failed"
50
+ ).length;
51
+ const retriedTests = window.top?.document.querySelectorAll(
52
+ ".test.runnable-passed.runnable-retried"
53
+ ).length;
54
+ const passedTests = window.top?.document.querySelectorAll(
55
+ ".test.runnable-passed"
56
+ ).length;
57
+ const skippedTests = window.top?.document.querySelectorAll(
58
+ ".test.runnable-pending"
59
+ ).length;
60
+
61
+ // Check test result count for passed, failed, skipped, retried
62
+ // If count is 1, use singular
63
+ function pluralizeWord(singularWord, pluralWord, count) {
64
+ return count > 1 ? pluralWord : singularWord;
65
+ }
66
+
67
+ // Calculate total spec time in mm:ss from milliseconds
68
+ // Taking total spec time from browser
69
+ // Only return seconds if minutes are less than or equal to 0
70
+ function specTime() {
71
+ const specTime = window.top?.document.querySelector(
72
+ '[data-cy="spec-duration"]'
73
+ )?.textContent;
74
+ const specSec = specTime?.slice(-2);
75
+ const specMin = specTime?.slice(0, 2);
76
+
77
+ // House single digit options
78
+ // Check against captured minute and second so browser speechSynthesis does not announce the zero
79
+ const singleDigit = [
80
+ "01",
81
+ "02",
82
+ "03",
83
+ "04",
84
+ "05",
85
+ "06",
86
+ "07",
87
+ "08",
88
+ "09",
89
+ ];
90
+ // If the captured minute or second matches one of the singleDigit values, remove the zero
91
+ const singleMin = specTime?.slice(1, 2);
92
+ const singleSec = specTime?.slice(-1);
93
+
94
+ // Checking for undefined time if an immediate spec error occurs
95
+ if (
96
+ specTime?.includes("ms") ||
97
+ specMin === undefined ||
98
+ specSec === undefined
99
+ ) {
100
+ return "Less than 1 second.";
101
+ } else if (specMin === "00") {
102
+ if (singleDigit.includes(specSec)) {
103
+ return (
104
+ `${singleSec}` + pluralizeWord("second", "seconds", `${singleSec}`)
105
+ );
106
+ } else {
107
+ return `${specSec} seconds`;
108
+ }
109
+ } else {
110
+ if (singleDigit.includes(specMin) && singleDigit.includes(specSec)) {
111
+ return (
112
+ `${singleMin}` +
113
+ pluralizeWord("minute", "minutes", `${singleMin}`) +
114
+ " and " +
115
+ `${singleSec} ` +
116
+ pluralizeWord("second", "seconds", `${singleSec}`)
117
+ );
118
+ } else if (
119
+ singleDigit.includes(specMin) &&
120
+ !singleDigit.includes(specSec)
121
+ ) {
122
+ return (
123
+ `${singleMin} ` +
124
+ pluralizeWord("minute", "minutes", `${singleMin}`) +
125
+ " and " +
126
+ `${specSec} seconds.`
127
+ );
128
+ } else if (
129
+ !singleDigit.includes(specMin) &&
130
+ singleDigit.includes(specSec)
131
+ ) {
132
+ return (
133
+ `${specMin} minutes and ${singleSec} ` +
134
+ pluralizeWord("second", "seconds", `${singleSec}`)
135
+ );
136
+ } else return `${specMin} minutes and ${specSec} seconds.`;
137
+ }
138
+ }
139
+
140
+ // Checking if retries are set in config
141
+ // If so, check the config object for the current retry number, the configured number of retries, and if current test failed
142
+ // If return true, do not allow the voice to activate
143
+ function checkRetry() {
144
+ if (
145
+ config.retries > 0 &&
146
+ config.retries !== config.currentRetry &&
147
+ config.state === "failed"
148
+ ) {
149
+ return true;
150
+ } else {
151
+ return false;
152
+ }
153
+ }
154
+
155
+ // Checking if all tests are skipped in spec file
156
+ // If so, announce that all spec tests are skipped
157
+ function checkSkipped() {
158
+ if (skipped && !failed && !passed) {
159
+ return true;
160
+ } else {
161
+ return false;
162
+ }
163
+ }
164
+
165
+ // Retrieve count of passed, failed, retried, and skipped tests
166
+ function retrieveTestStats() {
167
+ const stats = [
168
+ `${passedTests - retriedTests}` +
169
+ pluralizeWord(" test", " tests", `${passedTests - retriedTests}`) +
170
+ " passed.",
171
+ `${retriedTests}` +
172
+ pluralizeWord(" test", " tests", `${retriedTests}`) +
173
+ " passed with retries.",
174
+ `${failedTests}` +
175
+ pluralizeWord(" test", " tests", `${failedTests}`) +
176
+ " failed.",
177
+ `${skippedTests}` +
178
+ pluralizeWord(" test", " tests", `${skippedTests}`) +
179
+ " skipped.",
180
+ ];
181
+ // Filter out any zero counts and return a string for use in speechSynthesis
182
+ const filteredStats = stats.filter((stat) => !stat.startsWith("0"));
183
+ return `${filteredStats.toString()}`;
184
+ }
185
+
186
+ // Checking if the index of current test matches the total length of tests in the spec
187
+ // Checking if we are awaiting retries on final spec
188
+ if (currentTestIsLast && checkSkipped() === true) {
189
+ const message = new SpeechSynthesisUtterance(
190
+ "All Spec tests are skipped."
191
+ );
192
+ message.rate = rate.value;
193
+ message.pitch = pitch.value;
194
+ message.volume = volume.value;
195
+ speechSynthesis.speak(message);
196
+ }
197
+
198
+ // Before executing speechSynthesis, ensure we don't have any remaining tests to run
199
+ // Determine which environment variables were set and speak appropriate message
200
+ if (checkRetry() === false && checkSkipped() === false) {
201
+ // Announce spec run result and/or total time based on provided environment variable(s)
202
+ if (
203
+ Cypress.env("voiceResultType") === "simple" &&
204
+ !Cypress.env("voiceTime")
205
+ ) {
206
+ const message = new SpeechSynthesisUtterance(
207
+ `Spec ${
208
+ failed ? "failed." : retried ? "passed with retries." : "passed."
209
+ }`
210
+ );
211
+ message.rate = rate.value;
212
+ message.pitch = pitch.value;
213
+ message.volume = volume.value;
214
+ speechSynthesis.speak(message);
215
+ } else if (
216
+ Cypress.env("voiceTime") &&
217
+ !(Cypress.env("voiceResultType") === "simple") &&
218
+ !(Cypress.env("voiceResultType") === "detailed")
219
+ ) {
220
+ const message = new SpeechSynthesisUtterance(
221
+ "Total time: " + specTime()
222
+ );
223
+ message.rate = rate.value;
224
+ message.pitch = pitch.value;
225
+ message.volume = volume.value;
226
+ speechSynthesis.speak(message);
227
+ } else if (
228
+ Cypress.env("voiceResultType") === "detailed" &&
229
+ !Cypress.env("voiceTime")
230
+ ) {
231
+ const message = new SpeechSynthesisUtterance(
232
+ `Spec ${
233
+ failed
234
+ ? "failed: " + retrieveTestStats()
235
+ : retried
236
+ ? "passed with retries: " + retrieveTestStats()
237
+ : "passed: " + retrieveTestStats()
238
+ }`
239
+ );
240
+ message.rate = rate.value;
241
+ message.pitch = pitch.value;
242
+ message.volume = volume.value;
243
+ speechSynthesis.speak(message);
244
+ } else if (
245
+ Cypress.env("voiceResultType") === "simple" &&
246
+ Cypress.env("voiceTime")
247
+ ) {
248
+ const message = new SpeechSynthesisUtterance(
249
+ `Spec ${
250
+ failed
251
+ ? "failed. Total time: " + specTime()
252
+ : retried
253
+ ? "passed with retries. Total time: " + specTime()
254
+ : "passed. Total time: " + specTime()
255
+ }`
256
+ );
257
+ message.rate = rate.value;
258
+ message.pitch = pitch.value;
259
+ message.volume = volume.value;
260
+ speechSynthesis.speak(message);
261
+ } else if (
262
+ Cypress.env("voiceResultType") === "detailed" &&
263
+ Cypress.env("voiceTime")
264
+ ) {
265
+ const message = new SpeechSynthesisUtterance(
266
+ `Spec ${
267
+ failed
268
+ ? "failed: " + retrieveTestStats() + " Total time: " + specTime()
269
+ : retried
270
+ ? "passed with retries: " +
271
+ retrieveTestStats() +
272
+ ". Total time: " +
273
+ specTime()
274
+ : "passed: " + retrieveTestStats() + " Total time: " + specTime()
275
+ }`
276
+ );
277
+ message.rate = rate.value;
278
+ message.pitch = pitch.value;
279
+ message.volume = volume.value;
280
+ speechSynthesis.speak(message);
281
+ }
282
+ }
283
+ });
284
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "cypress-voice-plugin",
3
+ "version": "1.0.0",
4
+ "description": "Voice plugin for the Cypress Test Runner",
5
+ "main": "./index.js",
6
+ "scripts": {
7
+ "open": "cypress open --e2e --browser electron"
8
+ },
9
+ "keywords": [
10
+ "cypress",
11
+ "cypress.io",
12
+ "cypress-plugin",
13
+ "voice"
14
+ ],
15
+ "devDependencies": {
16
+ "cypress": "^13.7.1"
17
+ },
18
+ "publishConfig": {
19
+ "registry": "https://registry.npmjs.org/"
20
+ },
21
+ "author": "Dennis Bergevin",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/dennisbergevin/cypress-voice-plugin.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/dennisbergevin/cypress-voice-plugin/issues"
29
+ },
30
+ "homepage": "https://github.com/dennisbergevin/cypress-voice-plugin#readme"
31
+ }