cypress-plugin-last-failed 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,28 @@
1
+ name: test-last-failed-node-script
2
+ on:
3
+ push:
4
+ branches:
5
+ - 'main'
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ node-script:
11
+ runs-on: ubuntu-22.04
12
+ steps:
13
+ - name: Checkout ๐Ÿ“ฆ
14
+ uses: actions/checkout@v4
15
+ - name: Cypress run ๐Ÿ‘Ÿ
16
+ uses: cypress-io/github-action@v6
17
+ continue-on-error: true
18
+ - name: Output the file contents ๐Ÿ“
19
+ if: always()
20
+ run: |
21
+ cat ./test-results/last-run.txt
22
+ - name: Custom tests ๐Ÿงช
23
+ if: always()
24
+ uses: cypress-io/github-action@v6
25
+ with:
26
+ # environment variable used for CI/CD tests in this repo
27
+ command: npx cypress-last-failed run --env shouldPass=true
28
+ working-directory: ${{ github.workspace }}
@@ -0,0 +1,43 @@
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-plugin-last-failed
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 node script before you commit your changes:
31
+
32
+ ```bash
33
+ npx cypress-last-failed run
34
+ ```
35
+
36
+ For changes related to the `cypress open` toggle, you can run `npx cypress open` to test functionality in the Cypress Test Runner UI. Make sure to include any test changes (if they exist) in your commit.
37
+
38
+ ## Help needed
39
+
40
+ Please checkout the [the open issues](https://github.com/dennisbergevin/cypress-plugin-last-failed/issues)
41
+
42
+ Also, please watch the repo and respond to questions/bug reports/feature
43
+ 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,213 @@
1
+ <h2 align=center>Cypress plugin last-failed</h2>
2
+ <p align="center">
3
+ </p>
4
+
5
+ <p align="center">
6
+ A companion Cypress plugin for <code>cy-grep</code> that re-runs the last failed test(s).
7
+ </p>
8
+
9
+ ![Cypress-last-failed-toggle](./assets/cypress-demo.gif)
10
+
11
+ ![Cypress run example image](./assets/cypress-last-failed.png)
12
+
13
+ ## Features
14
+
15
+ - ๐Ÿ‘Ÿ A new `cypress run` command to **only** run the previous run's failed tests
16
+ - โณ A new UI toggle within `cypress open` to filter and only run any failed tests in a given spec
17
+ - ๐Ÿค– CI/CD support
18
+
19
+ #### Table of Contents
20
+
21
+ - [Installation](#-installation)
22
+ - [Run mode](#-run-mode)
23
+ - [Optional custom `failedTestDirectory`](#optional-custom-failedtestdirectory)
24
+ - [Add rule to gitignore](#add-rule-to-gitignore)
25
+ - [Setting up a `npm` script](#-setting-up-a-npm-script)
26
+ - [Open mode](#-open-mode)
27
+ - [Recommended open mode env variables](#recommended-open-mode-env-variables)
28
+ - [CI support](#continuous-integration-support)
29
+ - [Typescript support](#typescript-support)
30
+ - [Contributions](#contributions)
31
+
32
+ ---
33
+
34
+ ## ๐Ÿ“ฆ Installation
35
+
36
+ 1. Install the following packages:
37
+
38
+ ```sh
39
+ npm install --save-dev @bahmutov/cy-grep # Dependent package for the plugin
40
+ npm install --save-dev cypress-plugin-last-failed
41
+ ```
42
+
43
+ 2. In `cypress/support/e2e.js` (For E2E tests) and/or `cypress/support/component.js` (For Component tests),
44
+
45
+ ```js
46
+ import { failedTestToggle } from 'cypress-plugin-last-failed';
47
+
48
+ const registerCypressGrep = require('@bahmutov/cy-grep');
49
+ registerCypressGrep();
50
+
51
+ failedTestToggle();
52
+ ```
53
+
54
+ 3. In `cypress.config`, include the following within `setupNodeEvents` for `e2e` and/or `component` testing:
55
+
56
+ ```js
57
+ module.exports = defineConfig({
58
+ screenshotOnRunFailure: false,
59
+ env: {
60
+ failedTestDirectory: './',
61
+ grepOmitFiltered: true,
62
+ grepFilterSpecs: true,
63
+ },
64
+ e2e: {
65
+ setupNodeEvents(on, config) {
66
+ collectFailingTests(on, config);
67
+
68
+ require('@bahmutov/cy-grep/src/plugin')(config);
69
+ return config;
70
+ },
71
+ },
72
+ component: {
73
+ setupNodeEvents(on, config) {
74
+ collectFailingTests(on, config);
75
+
76
+ require('@bahmutov/cy-grep/src/plugin')(config);
77
+ return config;
78
+ },
79
+ },
80
+ });
81
+ ```
82
+
83
+ ---
84
+
85
+ ## ๐Ÿ‘Ÿ Run mode
86
+
87
+ 1. Run tests using `cypress run`:
88
+
89
+ ```bash
90
+ # Example
91
+ npx cypress run
92
+ ```
93
+
94
+ 2. If there are failed tests, run the following command from the **directory of the project's `cypress.config`**:
95
+
96
+ ```bash
97
+ npx cypress-last-failed run
98
+ ```
99
+
100
+ You can also include more cli arguments similar to `cypress run`, as the command harnesses the power of [Cypress module API](https://docs.cypress.io/guides/guides/module-api):
101
+
102
+ ```bash
103
+ # Example
104
+ npx cypress-last-failed run --e2e --browser chrome
105
+ ```
106
+
107
+ ### Optional custom `failedTestDirectory`
108
+
109
+ By default, there will be a folder called `test-results` created in the directory of the `cypress.config`.
110
+
111
+ - To customize where the `test-results` folder should be stored, add the `failedTestDirectory` environment variable:
112
+
113
+ ```js
114
+ // Example using a fixtures folder path relative to the cypress.config
115
+
116
+ module.exports = defineConfig({
117
+ env: {
118
+ failedTestDirectory: './cypress/fixtures',
119
+ },
120
+ e2e: {
121
+ setupNodeEvents(on, config) {},
122
+ },
123
+ });
124
+ ```
125
+
126
+ ### Add rule to gitignore
127
+
128
+ - **Optional**: If you do not want to commit the file storing last failed tests to your remote repository, include a rule within your project's `.gitignore` file:
129
+
130
+ ```
131
+
132
+ # Example
133
+
134
+ **/test-results
135
+
136
+ ```
137
+
138
+ ### ๐Ÿ“ƒ Setting up a `npm` script
139
+
140
+ For convenience, you may desire to house the `npx` command within an npm script in your project's `package.json`, including any desired cli arguments:
141
+
142
+ ```json
143
+ "scripts": {
144
+ "last-failed": "npx cypress-run-last-failed run --e2e --browser electron"
145
+ }
146
+ ```
147
+
148
+ ## โŒ› Open mode
149
+
150
+ Toggling the filter will run any previously failed tests on the particular spec file.
151
+
152
+ ![Failed test toggle](./assets/failed-test-toggle.png)
153
+
154
+ ### Recommended open mode env variables
155
+
156
+ - **Recommended**: Set two common environment variables tied to the `@bahmutov/cy-grep` package to enhance the experience utilizing the grep logic within the Cypress Test Runner UI using `cypress open`:
157
+
158
+ ```json
159
+ {
160
+ "env": {
161
+ "grepOmitFiltered": true,
162
+ "grepFilterSpecs": true
163
+ }
164
+ }
165
+ ```
166
+
167
+ > [!NOTE]
168
+ > More information on `grepOmitFiltered` and `grepFilterSpecs` can be read within the [README for `@bahmutov/cy-grep`](https://github.com/bahmutov/cy-grep?tab=readme-ov-file#pre-filter-specs-grepfilterspecs).
169
+
170
+ ---
171
+
172
+ ## Continuous integration support
173
+
174
+ An example of utilizing the plugin to re-run only the failed tests from a previous step in CI:
175
+
176
+ ```yaml
177
+ name: test-last-failed-node-script
178
+ on:
179
+ push:
180
+ branches:
181
+ - 'main'
182
+ pull_request:
183
+ workflow_dispatch:
184
+
185
+ jobs:
186
+ node-script:
187
+ runs-on: ubuntu-22.04
188
+ steps:
189
+ - name: Checkout ๐Ÿ“ฆ
190
+ uses: actions/checkout@v4
191
+ - name: Cypress run ๐Ÿ‘Ÿ
192
+ uses: cypress-io/github-action@v6
193
+ - name: Output the file contents ๐Ÿ“
194
+ if: always()
195
+ run: |
196
+ cat ./test-results/last-run.txt
197
+ - name: Custom tests ๐Ÿงช
198
+ if: always()
199
+ uses: cypress-io/github-action@v6
200
+ with:
201
+ command: npx cypress-last-failed run
202
+ working-directory: ${{ github.workspace }}
203
+ ```
204
+
205
+ ## Typescript support
206
+
207
+ For more information on Typescript support involved with `@bahmutov/cy-grep` package, refer to it's [README](https://github.com/bahmutov/cy-grep?tab=readme-ov-file#typescript-support).
208
+
209
+ ## Contributions
210
+
211
+ Feel free to open a pull request or drop any feature request or bug in the [issues](https://github.com/dennisbergevin/cypress-plugin-last-failed/issues).
212
+
213
+ Please see more details in the [contributing doc](./CONTRIBUTING.md).
Binary file
Binary file
Binary file
@@ -0,0 +1,26 @@
1
+ describe('Should run expected tests', () => {
2
+ it('should run', () => {
3
+ if (Cypress.env('shouldPass')) {
4
+ expect(true).to.eq(true);
5
+ } else {
6
+ expect(true).to.eq(false);
7
+ }
8
+ });
9
+ it('should not run', () => {
10
+ expect(true).to.eq(true);
11
+ });
12
+ it('needs to run', () => {
13
+ if (Cypress.env('shouldPass')) {
14
+ expect(1).to.eq(1);
15
+ } else {
16
+ expect(1).to.eq(2);
17
+ }
18
+ });
19
+ it('will be included in failed tests', () => {
20
+ if (Cypress.env('shouldPass')) {
21
+ expect(10).to.eq(10);
22
+ } else {
23
+ expect(10).to.eq(2);
24
+ }
25
+ });
26
+ });
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "Using fixtures to represent data",
3
+ "email": "hello@cypress.io",
4
+ "body": "Fixtures are a great way to mock data for responses to routes"
5
+ }
@@ -0,0 +1,25 @@
1
+ // ***********************************************
2
+ // This example commands.js shows you how to
3
+ // create various custom commands and overwrite
4
+ // existing commands.
5
+ //
6
+ // For more comprehensive examples of custom
7
+ // commands please read more here:
8
+ // https://on.cypress.io/custom-commands
9
+ // ***********************************************
10
+ //
11
+ //
12
+ // -- This is a parent command --
13
+ // Cypress.Commands.add('login', (email, password) => { ... })
14
+ //
15
+ //
16
+ // -- This is a child command --
17
+ // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
18
+ //
19
+ //
20
+ // -- This is a dual command --
21
+ // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
22
+ //
23
+ //
24
+ // -- This will overwrite an existing command --
25
+ // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
@@ -0,0 +1,28 @@
1
+ // ***********************************************************
2
+ // This example support/e2e.js 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 { failedTestToggle } from '../../src';
18
+ import './commands';
19
+
20
+ failedTestToggle();
21
+
22
+ // load and register the grep feature using "require" function
23
+ // https://github.com/cypress-io/cypress/tree/develop/npm/grep
24
+ const registerCypressGrep = require('@bahmutov/cy-grep');
25
+ registerCypressGrep();
26
+
27
+ // Alternatively you can use CommonJS syntax:
28
+ // require('./commands')
@@ -0,0 +1,27 @@
1
+ const { defineConfig } = require('cypress');
2
+ const { collectFailingTests } = require('./src/index');
3
+
4
+ module.exports = defineConfig({
5
+ env: {
6
+ failedTestDirectory: './',
7
+ grepOmitFiltered: true,
8
+ grepFilterSpecs: true,
9
+ },
10
+ screenshotOnRunFailure: false,
11
+ e2e: {
12
+ setupNodeEvents(on, config) {
13
+ collectFailingTests(on, config);
14
+
15
+ require('@bahmutov/cy-grep/src/plugin')(config);
16
+ return config;
17
+ },
18
+ },
19
+ component: {
20
+ setupNodeEvents(on, config) {
21
+ collectFailingTests(on, config);
22
+
23
+ require('@bahmutov/cy-grep/src/plugin')(config);
24
+ return config;
25
+ },
26
+ },
27
+ });
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "cypress-plugin-last-failed",
3
+ "version": "1.0.0",
4
+ "description": "Cypress plugin to rerun last failed tests in cypress run and open mode",
5
+ "main": "./src/index.js",
6
+ "scripts": {
7
+ "last-failed": "node runFailed.js run --e2e --browser electron"
8
+ },
9
+ "bin": {
10
+ "cypress-last-failed": "runFailed.js"
11
+ },
12
+ "keywords": [
13
+ "cypress",
14
+ "testing",
15
+ "cypress-plugin"
16
+ ],
17
+ "devDependencies": {
18
+ "@bahmutov/cy-grep": "^1.9.17",
19
+ "cypress": "^13.9.0"
20
+ },
21
+ "publishConfig": {
22
+ "registry": "https://registry.npmjs.org/"
23
+ },
24
+ "author": "Dennis Bergevin",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/dennisbergevin/cypress-plugin-last-failed.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/dennisbergevin/cypress-plugin-last-failed/issues"
32
+ },
33
+ "homepage": "https://github.com/dennisbergevin/cypress-plugin-last-failed#readme"
34
+ }
package/runFailed.js ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+
3
+ const cypress = require('cypress');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const appDir = process.env.INIT_CWD ? process.env.INIT_CWD : path.resolve('.');
7
+ const cypressConfig = require(`${appDir}/cypress.config`);
8
+
9
+ async function runLastFailed() {
10
+ const noFailedTestsMessage =
11
+ 'No previous failed tests detected\nTry running tests again with cypress run';
12
+
13
+ // Use the cypress environment variable failedTestDirectory for where to store failed tests
14
+ // If not set then use the directory of the project's cypress.config where the test-results defaults to
15
+ const failedTestFilePath =
16
+ cypressConfig.env?.failedTestDirectory === undefined
17
+ ? `${appDir}/test-results/last-run.txt`
18
+ : `${cypressConfig.env.failedTestDirectory}/test-results/last-run.txt`;
19
+
20
+ if (fs.existsSync(failedTestFilePath)) {
21
+ // Retrieve the failedTests from the file
22
+ const failedTests = await fs.promises.readFile(failedTestFilePath, 'utf8');
23
+
24
+ if (failedTests.length > 0) {
25
+ // Allow for additional cli arguments to be passed to the run command
26
+ const runOptions = await cypress.cli.parseRunArguments(
27
+ process.argv.slice(2)
28
+ );
29
+
30
+ // Set cypress environment variables needed for running last failed tests
31
+ process.env.CYPRESS_grep = `${failedTests}`;
32
+ process.env.CYPRESS_grepFilterSpecs = true;
33
+ process.env.CYPRESS_grepOmitFiltered = true;
34
+
35
+ await cypress.run(runOptions);
36
+ } else {
37
+ console.log(noFailedTestsMessage);
38
+ }
39
+ } else {
40
+ console.log(noFailedTestsMessage);
41
+ }
42
+ }
43
+
44
+ runLastFailed();
package/src/index.js ADDED
@@ -0,0 +1,198 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Collects failed tests from the most recent Cypress test run
6
+ *
7
+ * After each run, a file will store failed test titles within a test-results directory
8
+ *
9
+ * Environment variable `failedTestDirectory` can be used to set a specific directory to store test-results
10
+ *
11
+ * If failedTestDirectory env var is unset, test-results will be stored in cypress.config directory
12
+ *
13
+ * Subsequent test runs containing failed tests will overwrite this file
14
+ * @param {*} on
15
+ * @param {*} config
16
+ * @returns
17
+ */
18
+
19
+ const collectFailingTests = (on, config) => {
20
+ // Check for environment variable `collectFailingTests` to be true
21
+ on('after:run', async (results) => {
22
+ let failedTests = [];
23
+ // Grab every failed test's title
24
+ for (i in results.runs) {
25
+ const tests = results.runs[i].tests
26
+ .filter((test) => test.state === 'failed')
27
+ .map((test) => test.title[test.title.length - 1]);
28
+
29
+ // Only store non empty test titles
30
+ if (tests != '') {
31
+ failedTests.push(tests);
32
+ }
33
+ }
34
+
35
+ const stringedTests = failedTests.toString();
36
+ // Prepare a string that can be read from cy-grep
37
+ const greppedTestFormat = stringedTests.replaceAll(',', '; ');
38
+
39
+ // Use the cypress.config environment variable for failedTestDirectory
40
+ // If not set then use the cypress.config directory
41
+ const failedTestFileDirectory =
42
+ config.env.failedTestDirectory === undefined
43
+ ? `${path.dirname(config.configFile)}/test-results/`
44
+ : `${config.env.failedTestDirectory}/test-results/`;
45
+
46
+ await fs.promises.mkdir(`${failedTestFileDirectory}`, {
47
+ recursive: true,
48
+ });
49
+ const lastRunReportFile = path.join(
50
+ `${failedTestFileDirectory}`,
51
+ 'last-run.txt'
52
+ );
53
+ await fs.promises.writeFile(lastRunReportFile, greppedTestFormat);
54
+ });
55
+
56
+ return collectFailingTests;
57
+ };
58
+
59
+ /**
60
+ * Toggle for use within a spec file during `cypress open`
61
+ */
62
+
63
+ const failedTestToggle = () => {
64
+ const hasStyles = top?.document.querySelector('#runFailedStyle');
65
+ const hasToggleButton = top?.document.querySelector('#runFailedToggle');
66
+ const defaultStyles = `
67
+ .reporter header {
68
+ overflow: visible;
69
+ z-index: 2;
70
+ }
71
+ #runFailedControls {
72
+ position: relative;
73
+ display: inline-block;
74
+ }
75
+ #runFailedToggle {
76
+ display: none;
77
+ }
78
+ #runFailedControls label {
79
+ background-color: transparent;
80
+ padding-top: 5px;
81
+ }
82
+ #runFailedControls #runFailedTooltip {
83
+ visibility: hidden;
84
+ width: 134px;
85
+ background-color: #f3f4fa;
86
+ color: #1b1e2e;
87
+ text-align: center;
88
+ padding: 5px;
89
+ border-radius: 3px;
90
+ position: absolute;
91
+ z-index: 1;
92
+ top: 27px;
93
+ left: 0px;
94
+ height: 28px;
95
+ }
96
+ #runFailedControls:hover #runFailedTooltip {
97
+ visibility: visible;
98
+ }
99
+ #runFailedButton #runFailedLabel {
100
+ cursor: pointer;
101
+ }
102
+ #runFailedTooltip::after {
103
+ content: " ";
104
+ position: absolute;
105
+ bottom: 100%; /* At the top of the tooltip */
106
+ right: 85%;
107
+ margin-left: -5px;
108
+ border-width: 5px;
109
+ border-style: solid;
110
+ border-color: transparent transparent #f3f4fa transparent;
111
+ }
112
+ .reporter:has(#runFailed:checked) .command.command-name-request:has(.command-is-event) {
113
+ display:none
114
+ }
115
+ `;
116
+ const turnOffRunFailedIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#f59aa9" class="bi bi-filter-circle" viewBox="0 0 16 16">
117
+ <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
118
+ <path d="M7 11.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5m-2-3a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m-2-3a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5"/>
119
+ </svg>`;
120
+
121
+ const turnOnRunFailedIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#f59aa9" class="bi bi-filter-circle-fill" viewBox="0 0 16 16">
122
+ <path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16M3.5 5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1 0-1M5 8.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m2 3a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5"/>
123
+ </svg>`;
124
+
125
+ const turnOffRunFailedDescription = 'Filter failed tests';
126
+ const turnOnRunFailedDescription = 'Unfilter failed tests';
127
+
128
+ // append styles
129
+ if (!hasStyles) {
130
+ const reporterEl = top?.document.querySelector('#unified-reporter');
131
+ const reporterStyleEl = document.createElement('style');
132
+ reporterStyleEl.setAttribute('id', 'runFailedStyle');
133
+ reporterStyleEl.innerHTML = defaultStyles;
134
+ reporterEl?.appendChild(reporterStyleEl);
135
+ }
136
+
137
+ if (!hasToggleButton) {
138
+ const header = top?.document.querySelector('#unified-reporter header');
139
+ const headerToggleDiv = document.createElement('div');
140
+ const headerToggleSpan = document.createElement('span');
141
+ const headerToggleTooltip = document.createElement('span');
142
+ const headerToggleButton = document.createElement('button');
143
+ const headerToggleInput = document.createElement('input');
144
+ const headerToggleLabel = document.createElement('label');
145
+
146
+ headerToggleInput.setAttribute('type', 'checkbox');
147
+
148
+ headerToggleInput.setAttribute('id', 'runFailedToggle');
149
+ headerToggleLabel.setAttribute('for', 'runFailedToggle');
150
+ headerToggleLabel.setAttribute('id', 'runFailedLabel');
151
+ headerToggleLabel.innerHTML = turnOffRunFailedIcon;
152
+
153
+ headerToggleDiv.setAttribute('class', 'controls');
154
+ headerToggleDiv.setAttribute('id', 'runFailedControls');
155
+ headerToggleTooltip.setAttribute('id', 'runFailedTooltip');
156
+ headerToggleTooltip.innerText = turnOffRunFailedDescription;
157
+ headerToggleButton.setAttribute('aria-label', turnOffRunFailedDescription);
158
+ headerToggleButton.setAttribute('id', 'runFailedButton');
159
+
160
+ header?.appendChild(headerToggleDiv);
161
+ headerToggleDiv?.appendChild(headerToggleSpan);
162
+ headerToggleDiv?.appendChild(headerToggleTooltip);
163
+ headerToggleSpan?.appendChild(headerToggleButton);
164
+ headerToggleButton?.appendChild(headerToggleInput);
165
+ headerToggleButton?.appendChild(headerToggleLabel);
166
+ }
167
+
168
+ const runFailedElement = top.document.querySelector('#runFailedToggle');
169
+ const runFailedLabelElement = top.document.querySelector(
170
+ '[for=runFailedToggle]'
171
+ );
172
+ const runFailedTooltipElement =
173
+ top.document.querySelector('#runFailedTooltip');
174
+
175
+ runFailedElement?.addEventListener('change', (e) => {
176
+ const stopBtn = window.top.document.querySelector('.reporter .stop');
177
+
178
+ if (e.target.checked) {
179
+ if (stopBtn) {
180
+ stopBtn.click();
181
+ }
182
+ // when checked, grep only failed tests in spec
183
+ Cypress.grepFailed();
184
+ runFailedLabelElement.innerHTML = turnOnRunFailedIcon;
185
+ runFailedTooltipElement.innerHTML = turnOnRunFailedDescription;
186
+ } else {
187
+ if (stopBtn) {
188
+ stopBtn.click();
189
+ }
190
+ // when unchecked, ungrep and show all tests in spec
191
+ Cypress.grep();
192
+ runFailedLabelElement.innerHTML = turnOffRunFailedIcon;
193
+ runFailedTooltipElement.innerHTML = turnOffRunFailedDescription;
194
+ }
195
+ });
196
+ };
197
+
198
+ module.exports = { collectFailingTests, failedTestToggle };