cypress-plugin-last-failed 1.0.2 โ†’ 1.2.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.
@@ -18,7 +18,7 @@ jobs:
18
18
  - name: Output the file contents ๐Ÿ“
19
19
  if: always()
20
20
  run: |
21
- cat ./test-results/last-run.txt
21
+ cat ./test-results/last-run.json
22
22
  - name: Custom tests ๐Ÿงช
23
23
  if: always()
24
24
  uses: cypress-io/github-action@v6
package/README.md CHANGED
@@ -201,7 +201,7 @@ jobs:
201
201
  - name: Output the file contents ๐Ÿ“
202
202
  if: always()
203
203
  run: |
204
- cat ./test-results/last-run.txt
204
+ cat ./test-results/last-run.json
205
205
  - name: Custom tests ๐Ÿงช
206
206
  if: always()
207
207
  uses: cypress-io/github-action@v6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cypress-plugin-last-failed",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "description": "Cypress plugin to rerun last failed tests in cypress run and open mode",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
package/runFailed.js CHANGED
@@ -10,20 +10,37 @@ async function runLastFailed() {
10
10
  Ensure you are in the directory of your cypress config
11
11
  Try running tests again with cypress run`;
12
12
 
13
- const failedTestFilePath = `${appDir}/test-results/last-run.txt`;
13
+ const failedTestFilePath = `${appDir}/test-results/last-run.json`;
14
14
 
15
15
  if (fs.existsSync(failedTestFilePath)) {
16
16
  // Retrieve the failedTests from the file
17
17
  const failedTests = await fs.promises.readFile(failedTestFilePath, 'utf8');
18
18
 
19
- if (failedTests.length > 0) {
19
+ // Retrieve the parent suite and tests in the results from test-results/last-run
20
+ const parentAndTest = JSON.parse(failedTests).map(({ parent, test }) => ({
21
+ parent,
22
+ test,
23
+ }));
24
+ // Combine parent suite and test together
25
+ const resultSet = new Set(
26
+ Object.values(parentAndTest).flatMap(
27
+ (parent) => parent.parent + ',' + parent.test + ';'
28
+ )
29
+ );
30
+ // Format string for use in grep functionality
31
+ const stringedTests = Array.from(resultSet)
32
+ .toString()
33
+ .replaceAll(',', ' ')
34
+ .slice(0, -1);
35
+
36
+ if (stringedTests.length > 0) {
20
37
  // Allow for additional cli arguments to be passed to the run command
21
38
  const runOptions = await cypress.cli.parseRunArguments(
22
39
  process.argv.slice(2)
23
40
  );
24
41
 
25
42
  // Set cypress environment variables needed for running last failed tests
26
- process.env.CYPRESS_grep = `${failedTests}`;
43
+ process.env.CYPRESS_grep = `${stringedTests}`;
27
44
  process.env.CYPRESS_grepFilterSpecs = true;
28
45
  process.env.CYPRESS_grepOmitFiltered = true;
29
46
 
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
-
3
+ const failedTestToggle = require('./toggle');
4
4
  /**
5
5
  * Collects failed tests from the most recent Cypress test run
6
6
  *
@@ -21,18 +21,23 @@ const collectFailingTests = (on, config) => {
21
21
  for (i in results.runs) {
22
22
  const tests = results.runs[i].tests
23
23
  .filter((test) => test.state === 'failed')
24
- .map((test) => test.title[test.title.length - 1]);
24
+ .map((test) => test.title);
25
+
26
+ const spec = results.runs[i].spec.relative;
25
27
 
26
- // Only store non empty test titles
27
- if (tests != '') {
28
- failedTests.push(tests);
28
+ for (i in tests) {
29
+ let report = {
30
+ spec: spec,
31
+ parent: [...tests[i].slice(0, -1)],
32
+ test: tests[i].pop(),
33
+ };
34
+ // Only store non empty test titles
35
+ if (tests != '') {
36
+ failedTests.push(report);
37
+ }
29
38
  }
30
39
  }
31
40
 
32
- const stringedTests = failedTests.toString();
33
- // Prepare a string that can be read from cy-grep
34
- const greppedTestFormat = stringedTests.replaceAll(',', '; ');
35
-
36
41
  // Use the cypress.config directory for path for storing test-results
37
42
  const failedTestFileDirectory = `${path.dirname(
38
43
  config.configFile
@@ -44,183 +49,12 @@ const collectFailingTests = (on, config) => {
44
49
  });
45
50
  const lastRunReportFile = path.join(
46
51
  `${failedTestFileDirectory}`,
47
- 'last-run.txt'
52
+ 'last-run.json'
48
53
  );
49
- await fs.promises.writeFile(lastRunReportFile, greppedTestFormat);
54
+ await fs.promises.writeFile(lastRunReportFile, JSON.stringify(failedTests));
50
55
  });
51
56
 
52
57
  return collectFailingTests;
53
58
  };
54
59
 
55
- /**
56
- * Find and grep all the failed test titles designated within the Cypress Test Runner UI.
57
- *
58
- * Any retried tests that failed but ultimately passed will not be included.
59
- *
60
- * See README for recommendation on handling skipped tests ordinarily seen within the Cypress Test Runner UI.
61
- */
62
-
63
- const grepFailed = () => {
64
- // @ts-ignore
65
- const failedTestTitles = [];
66
-
67
- const failedTests = window.top?.document.querySelectorAll(
68
- '.test.runnable.runnable-failed'
69
- );
70
-
71
- [...failedTests].forEach((test) => {
72
- failedTestTitles.push(test.innerText.split('\n')[0]);
73
- });
74
-
75
- if (!failedTestTitles.length) {
76
- console.log('No failed tests found');
77
- } else {
78
- console.log('running only the failed tests');
79
- const grepTitles = failedTestTitles.join('; ');
80
- console.log(grepTitles);
81
- // @ts-ignore
82
- Cypress.grep(grepTitles);
83
- }
84
- };
85
-
86
- /**
87
- * Toggle for use within a spec file during `cypress open`
88
- */
89
-
90
- const failedTestToggle = () => {
91
- const hasStyles = top?.document.querySelector('#runFailedStyle');
92
- const hasToggleButton = top?.document.querySelector('#runFailedToggle');
93
- const defaultStyles = `
94
- .reporter header {
95
- overflow: visible;
96
- z-index: 2;
97
- }
98
- #runFailedControls {
99
- position: relative;
100
- display: inline-block;
101
- }
102
- #runFailedToggle {
103
- display: none;
104
- }
105
- #runFailedControls label {
106
- background-color: transparent;
107
- padding-top: 5px;
108
- }
109
- #runFailedControls #runFailedTooltip {
110
- visibility: hidden;
111
- width: 134px;
112
- background-color: #f3f4fa;
113
- color: #1b1e2e;
114
- text-align: center;
115
- padding: 5px;
116
- border-radius: 3px;
117
- position: absolute;
118
- z-index: 1;
119
- top: 27px;
120
- left: 0px;
121
- height: 28px;
122
- }
123
- #runFailedControls:hover #runFailedTooltip {
124
- visibility: visible;
125
- }
126
- #runFailedButton #runFailedLabel {
127
- cursor: pointer;
128
- }
129
- #runFailedTooltip::after {
130
- content: " ";
131
- position: absolute;
132
- bottom: 100%; /* At the top of the tooltip */
133
- right: 85%;
134
- margin-left: -5px;
135
- border-width: 5px;
136
- border-style: solid;
137
- border-color: transparent transparent #f3f4fa transparent;
138
- }
139
- .reporter:has(#runFailed:checked) .command.command-name-request:has(.command-is-event) {
140
- display:none
141
- }
142
- `;
143
- 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">
144
- <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"/>
145
- <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"/>
146
- </svg>`;
147
-
148
- 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">
149
- <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"/>
150
- </svg>`;
151
-
152
- const turnOffRunFailedDescription = 'Filter failed tests';
153
- const turnOnRunFailedDescription = 'Unfilter failed tests';
154
-
155
- // append styles
156
- if (!hasStyles) {
157
- const reporterEl = top?.document.querySelector('#unified-reporter');
158
- const reporterStyleEl = document.createElement('style');
159
- reporterStyleEl.setAttribute('id', 'runFailedStyle');
160
- reporterStyleEl.innerHTML = defaultStyles;
161
- reporterEl?.appendChild(reporterStyleEl);
162
- }
163
-
164
- if (!hasToggleButton) {
165
- const header = top?.document.querySelector('#unified-reporter header');
166
- const headerToggleDiv = document.createElement('div');
167
- const headerToggleSpan = document.createElement('span');
168
- const headerToggleTooltip = document.createElement('span');
169
- const headerToggleButton = document.createElement('button');
170
- const headerToggleInput = document.createElement('input');
171
- const headerToggleLabel = document.createElement('label');
172
-
173
- headerToggleInput.setAttribute('type', 'checkbox');
174
-
175
- headerToggleInput.setAttribute('id', 'runFailedToggle');
176
- headerToggleLabel.setAttribute('for', 'runFailedToggle');
177
- headerToggleLabel.setAttribute('id', 'runFailedLabel');
178
- headerToggleLabel.innerHTML = turnOffRunFailedIcon;
179
-
180
- headerToggleDiv.setAttribute('class', 'controls');
181
- headerToggleDiv.setAttribute('id', 'runFailedControls');
182
- headerToggleTooltip.setAttribute('id', 'runFailedTooltip');
183
- headerToggleTooltip.innerText = turnOffRunFailedDescription;
184
- headerToggleButton.setAttribute('aria-label', turnOffRunFailedDescription);
185
- headerToggleButton.setAttribute('id', 'runFailedButton');
186
-
187
- header?.appendChild(headerToggleDiv);
188
- headerToggleDiv?.appendChild(headerToggleSpan);
189
- headerToggleDiv?.appendChild(headerToggleTooltip);
190
- headerToggleSpan?.appendChild(headerToggleButton);
191
- headerToggleButton?.appendChild(headerToggleInput);
192
- headerToggleButton?.appendChild(headerToggleLabel);
193
- }
194
-
195
- const runFailedElement = top.document.querySelector('#runFailedToggle');
196
- const runFailedLabelElement = top.document.querySelector(
197
- '[for=runFailedToggle]'
198
- );
199
- const runFailedTooltipElement =
200
- top.document.querySelector('#runFailedTooltip');
201
-
202
- runFailedElement?.addEventListener('change', (e) => {
203
- const stopBtn = window.top.document.querySelector('.reporter .stop');
204
-
205
- if (e.target.checked) {
206
- if (stopBtn) {
207
- stopBtn.click();
208
- }
209
- // when checked, grep only failed tests in spec
210
- grepFailed();
211
-
212
- runFailedLabelElement.innerHTML = turnOnRunFailedIcon;
213
- runFailedTooltipElement.innerHTML = turnOnRunFailedDescription;
214
- } else {
215
- if (stopBtn) {
216
- stopBtn.click();
217
- }
218
- // when unchecked, ungrep and show all tests in spec
219
- Cypress.grep();
220
- runFailedLabelElement.innerHTML = turnOffRunFailedIcon;
221
- runFailedTooltipElement.innerHTML = turnOffRunFailedDescription;
222
- }
223
- });
224
- };
225
-
226
60
  module.exports = { collectFailingTests, failedTestToggle };
package/src/toggle.js ADDED
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Find and grep all the failed test titles designated within the Cypress Test Runner UI.
3
+ *
4
+ * Any retried tests that failed but ultimately passed will not be included.
5
+ *
6
+ * See README for recommendation on handling skipped tests ordinarily seen within the Cypress Test Runner UI.
7
+ */
8
+
9
+ const grepFailed = () => {
10
+ // @ts-ignore
11
+ const failedTestTitles = [];
12
+
13
+ const failedTests = window.top?.document.querySelectorAll(
14
+ '.test.runnable.runnable-failed'
15
+ );
16
+
17
+ [...failedTests].forEach((test) => {
18
+ failedTestTitles.push(test.innerText.split('\n')[0]);
19
+ });
20
+
21
+ if (!failedTestTitles.length) {
22
+ console.log('No failed tests found');
23
+ } else {
24
+ console.log('running only the failed tests');
25
+ const grepTitles = failedTestTitles.join('; ');
26
+ // @ts-ignore
27
+ Cypress.grep(grepTitles);
28
+ }
29
+ };
30
+
31
+ /**
32
+ * Toggle for use within a spec file during `cypress open`
33
+ */
34
+
35
+ const failedTestToggle = () => {
36
+ const hasStyles = top?.document.querySelector('#runFailedStyle');
37
+ const hasToggleButton = top?.document.querySelector('#runFailedToggle');
38
+ const defaultStyles = `
39
+ .reporter header {
40
+ overflow: visible;
41
+ z-index: 2;
42
+ }
43
+ #runFailedControls {
44
+ position: relative;
45
+ display: inline-block;
46
+ }
47
+ #runFailedToggle {
48
+ display: none;
49
+ }
50
+ #runFailedControls label {
51
+ background-color: transparent;
52
+ padding-top: 5px;
53
+ }
54
+ #runFailedControls #runFailedTooltip {
55
+ visibility: hidden;
56
+ width: 134px;
57
+ background-color: #f3f4fa;
58
+ color: #1b1e2e;
59
+ text-align: center;
60
+ padding: 5px;
61
+ border-radius: 3px;
62
+ position: absolute;
63
+ z-index: 1;
64
+ top: 27px;
65
+ left: 0px;
66
+ height: 28px;
67
+ }
68
+ #runFailedControls:hover #runFailedTooltip {
69
+ visibility: visible;
70
+ }
71
+ #runFailedButton #runFailedLabel {
72
+ cursor: pointer;
73
+ }
74
+ #runFailedTooltip::after {
75
+ content: " ";
76
+ position: absolute;
77
+ bottom: 100%; /* At the top of the tooltip */
78
+ right: 85%;
79
+ margin-left: -5px;
80
+ border-width: 5px;
81
+ border-style: solid;
82
+ border-color: transparent transparent #f3f4fa transparent;
83
+ }
84
+ .reporter:has(#runFailed:checked) .command.command-name-request:has(.command-is-event) {
85
+ display:none
86
+ }
87
+ `;
88
+ 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">
89
+ <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"/>
90
+ <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"/>
91
+ </svg>`;
92
+
93
+ 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">
94
+ <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"/>
95
+ </svg>`;
96
+
97
+ const turnOffRunFailedDescription = 'Filter failed tests';
98
+ const turnOnRunFailedDescription = 'Unfilter failed tests';
99
+
100
+ // append styles
101
+ if (!hasStyles) {
102
+ const reporterEl = top?.document.querySelector('#unified-reporter');
103
+ const reporterStyleEl = document.createElement('style');
104
+ reporterStyleEl.setAttribute('id', 'runFailedStyle');
105
+ reporterStyleEl.innerHTML = defaultStyles;
106
+ reporterEl?.appendChild(reporterStyleEl);
107
+ }
108
+
109
+ if (!hasToggleButton) {
110
+ const header = top?.document.querySelector('#unified-reporter header');
111
+ const headerToggleDiv = document.createElement('div');
112
+ const headerToggleSpan = document.createElement('span');
113
+ const headerToggleTooltip = document.createElement('span');
114
+ const headerToggleButton = document.createElement('button');
115
+ const headerToggleInput = document.createElement('input');
116
+ const headerToggleLabel = document.createElement('label');
117
+
118
+ headerToggleInput.setAttribute('type', 'checkbox');
119
+
120
+ headerToggleInput.setAttribute('id', 'runFailedToggle');
121
+ headerToggleLabel.setAttribute('for', 'runFailedToggle');
122
+ headerToggleLabel.setAttribute('id', 'runFailedLabel');
123
+ headerToggleLabel.innerHTML = turnOffRunFailedIcon;
124
+
125
+ headerToggleDiv.setAttribute('class', 'controls');
126
+ headerToggleDiv.setAttribute('id', 'runFailedControls');
127
+ headerToggleTooltip.setAttribute('id', 'runFailedTooltip');
128
+ headerToggleTooltip.innerText = turnOffRunFailedDescription;
129
+ headerToggleButton.setAttribute('aria-label', turnOffRunFailedDescription);
130
+ headerToggleButton.setAttribute('id', 'runFailedButton');
131
+
132
+ header?.appendChild(headerToggleDiv);
133
+ headerToggleDiv?.appendChild(headerToggleSpan);
134
+ headerToggleDiv?.appendChild(headerToggleTooltip);
135
+ headerToggleSpan?.appendChild(headerToggleButton);
136
+ headerToggleButton?.appendChild(headerToggleInput);
137
+ headerToggleButton?.appendChild(headerToggleLabel);
138
+ }
139
+
140
+ const runFailedElement = top.document.querySelector('#runFailedToggle');
141
+ const runFailedLabelElement = top.document.querySelector(
142
+ '[for=runFailedToggle]'
143
+ );
144
+ const runFailedTooltipElement =
145
+ top.document.querySelector('#runFailedTooltip');
146
+
147
+ runFailedElement?.addEventListener('change', (e) => {
148
+ const stopBtn = window.top.document.querySelector('.reporter .stop');
149
+
150
+ if (e.target.checked) {
151
+ if (stopBtn) {
152
+ stopBtn.click();
153
+ }
154
+ // when checked, grep only failed tests in spec
155
+ grepFailed();
156
+
157
+ runFailedLabelElement.innerHTML = turnOnRunFailedIcon;
158
+ runFailedTooltipElement.innerHTML = turnOnRunFailedDescription;
159
+ } else {
160
+ if (stopBtn) {
161
+ stopBtn.click();
162
+ }
163
+ // when unchecked, ungrep and show all tests in spec
164
+ Cypress.grep();
165
+ runFailedLabelElement.innerHTML = turnOffRunFailedIcon;
166
+ runFailedTooltipElement.innerHTML = turnOffRunFailedDescription;
167
+ }
168
+ });
169
+ // Wrapping logic within isInteractive check
170
+ // This targets cypress open mode where user can switch specs
171
+ if (Cypress.config('isInteractive')) {
172
+ Cypress.on('window:unload', () => {
173
+ // Store the current Cypress test runner url
174
+ // This is to check against any spec change in test runner while the grep filter is activated
175
+ // If a user does switch spec while filter is active, the filter will be reset
176
+ const sidebarRunsLinkPage = window.top?.document.querySelector(
177
+ '[data-cy="sidebar-link-runs-page"]'
178
+ );
179
+ const runFailedToggleElement =
180
+ window.top?.document.querySelector('#runFailedToggle');
181
+
182
+ if (
183
+ window.top?.document.URL !=
184
+ sidebarRunsLinkPage.getAttribute('data-url') &&
185
+ runFailedToggleElement.checked
186
+ ) {
187
+ runFailedToggleElement.click();
188
+ }
189
+
190
+ sidebarRunsLinkPage.setAttribute('data-url', window.top?.document.URL);
191
+ });
192
+ }
193
+ };
194
+
195
+ module.exports = failedTestToggle;