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.
- package/.github/workflows/main.yml +14 -0
- package/CONTRIBUTING.md +38 -0
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/addStyles.js +147 -0
- package/content/cypress-voice-slider.gif +0 -0
- package/content/cypress-voice.png +0 -0
- package/cypress/e2e/combinedResultTime.cy.js +18 -0
- package/cypress/e2e/failedVoiceResult.cy.js +30 -0
- package/cypress/e2e/noPlugin.cy.js +5 -0
- package/cypress/e2e/passedVoiceResult.cy.js +17 -0
- package/cypress/e2e/retriedVoiceResult.cy.js +27 -0
- package/cypress/e2e/testResultsOnly.cy.js +16 -0
- package/cypress/e2e/voiceTime.cy.js +16 -0
- package/cypress/support/commands.js +37 -0
- package/cypress/support/e2e.js +21 -0
- package/cypress.config.js +9 -0
- package/index.js +284 -0
- package/package.json +31 -0
|
@@ -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
|
package/CONTRIBUTING.md
ADDED
|
@@ -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
|
+

|
|
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
|
+

|
|
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,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')
|
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
|
+
}
|