Skip to content

Commit eab1730

Browse files
fix: handle download from element missing download attribute (#28222)
1 parent db8609e commit eab1730

15 files changed

Lines changed: 188 additions & 18 deletions

File tree

cli/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ _Released 11/7/2023 (PENDING)_
55

66
**Bugfixes:**
77

8+
- Fixed an issue where clicking a link to download a file could cause a page load timeout when the download attribute was missing. Note: download behaviors in experimental Webkit are still an issue. Fixes [#14857](https://github.com/cypress-io/cypress/issues/14857).
9+
- Fixed an issue to account for canceled and failed downloads to correctly reflect these status in Command log as a download failure where previously it would be pending. Fixed in [#28222](https://github.com/cypress-io/cypress/pull/28222).
810
- Fixed an issue determining visibility when an element is hidden by an ancestor with a shared edge. Fixes [#27514](https://github.com/cypress-io/cypress/issues/27514).
911
- We now pass a flag to Chromium browsers to disable Chrome translation, both the manual option and the popup prompt, when a page with a differing language is detected. Fixes [#28225](https://github.com/cypress-io/cypress/issues/28225).
1012
- Fixed an issue where in chromium based browsers, global style updates can trigger flooding of font face requests in DevTools and Test Replay. This can affect performance due to the flooding of messages in CDP. Fixes [#28150](https://github.com/cypress-io/cypress/issues/28150) and [#28215](https://github.com/cypress-io/cypress/issues/28215).

packages/app/src/runner/event-manager.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,9 @@ export class EventManager {
148148
case 'complete:download':
149149
Cypress.downloads.end(data)
150150
break
151+
case 'canceled:download':
152+
Cypress.downloads.end(data, true)
153+
break
151154
default:
152155
break
153156
}

packages/driver/cypress/e2e/cypress/downloads.cy.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,24 @@ describe('src/cypress/downloads', () => {
44
let log
55
let snapshot
66
let end
7+
let error
78
let downloads
89
let downloadItem = {
910
id: '1',
1011
filePath: '/path/to/save/location.csv',
1112
url: 'http://localhost:1234/location.csv',
1213
mime: 'text/csv',
1314
}
15+
let action
1416

1517
beforeEach(() => {
1618
end = cy.stub()
17-
snapshot = cy.stub().returns({ end })
19+
error = cy.stub()
20+
snapshot = cy.stub().returns({ end, error })
1821
log = cy.stub().returns({ snapshot })
22+
action = cy.stub()
1923

20-
downloads = $Downloads.create({ log })
24+
downloads = $Downloads.create({ action, log })
2125
})
2226

2327
context('#start', () => {
@@ -51,9 +55,21 @@ describe('src/cypress/downloads', () => {
5155
downloads.start(downloadItem)
5256
downloads.end({ id: '1' })
5357

58+
expect(action).to.be.calledWith('app:download:received')
59+
expect(snapshot).to.be.called
5460
expect(end).to.be.called
5561
})
5662

63+
it('fails with snapshot if matching log exists', () => {
64+
downloads.start(downloadItem)
65+
downloads.end({ id: '1' }, true)
66+
67+
expect(action).to.be.calledWith('app:download:received')
68+
expect(snapshot).to.be.called
69+
expect(end).not.to.be.called
70+
expect(error).to.be.called
71+
})
72+
5773
it('is a noop if matching log does not exist', () => {
5874
downloads.end({ id: '1' })
5975

@@ -62,3 +78,35 @@ describe('src/cypress/downloads', () => {
6278
})
6379
})
6480
})
81+
82+
describe('download behavior', () => {
83+
beforeEach(() => {
84+
cy.visit('/fixtures/downloads.html')
85+
})
86+
87+
it('downloads from anchor tag with download attribute', () => {
88+
cy.exec(`rm -f ${Cypress.config('downloadsFolder')}/downloads_records.csv`)
89+
cy.readFile(`${Cypress.config('downloadsFolder')}/downloads_records.csv`).should('not.exist')
90+
91+
// trigger download
92+
cy.get('[data-cy=download-csv]').click()
93+
cy.readFile(`${Cypress.config('downloadsFolder')}/downloads_records.csv`)
94+
.should('contain', '"Joe","Smith"')
95+
})
96+
97+
it('downloads from anchor tag without download attribute', { browser: '!webkit' }, () => {
98+
cy.exec(`rm -f ${Cypress.config('downloadsFolder')}/downloads_records.csv`)
99+
cy.readFile(`${Cypress.config('downloadsFolder')}/downloads_records.csv`).should('not.exist')
100+
101+
// trigger download
102+
cy.get('[data-cy=download-without-download-attr]').click()
103+
cy.readFile(`${Cypress.config('downloadsFolder')}/downloads_records.csv`)
104+
.should('contain', '"Joe","Smith"')
105+
})
106+
107+
it('invalid download path from anchor tag with download attribute', () => {
108+
// attempt to download
109+
cy.get('[data-cy=invalid-download]').click()
110+
cy.readFile(`${Cypress.config('downloadsFolder')}/downloads_does_not_exist.csv`).should('not.exist')
111+
})
112+
})
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
</head>
5+
<body>
6+
<h3>Download CSV</h3>
7+
<a data-cy="download-csv" href="downloads_records.csv" download>downloads_records.csv</a>
8+
9+
<h3>Download CSV</h3>
10+
<a data-cy="download-without-download-attr" href="downloads_records.csv">download csv from anchor tag without download attr</a>
11+
12+
<h3>Download CSV</h3>
13+
<a data-cy="invalid-download" href="downloads_does_not_exist.csv" download>broken download link</a>
14+
</body>
15+
</html>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"First name","Last name","Occupation","Age","City","State"
2+
"Joe","Smith","student",20,"Boston","MA"

packages/driver/src/cy/commands/navigation.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,23 @@ const stabilityChanged = async (Cypress, state, config, stable) => {
302302
debug('waiting for window:load')
303303

304304
const promise = new Promise((resolve) => {
305+
const handleDownloadUnloadEvent = () => {
306+
cy.state('onPageLoadErr', null)
307+
cy.isStable(true, 'download')
308+
309+
options._log
310+
.set({
311+
message: 'download fired beforeUnload event',
312+
consoleProps () {
313+
return {
314+
Note: 'This event fired when the download was initiated.',
315+
}
316+
},
317+
}).snapshot().end()
318+
319+
resolve()
320+
}
321+
305322
const onWindowLoad = ({ url }) => {
306323
const href = state('autLocation').href
307324
const count = getRedirectionCount(href)
@@ -351,6 +368,7 @@ const stabilityChanged = async (Cypress, state, config, stable) => {
351368
}
352369
}
353370

371+
cy.once('download:received', handleDownloadUnloadEvent)
354372
cy.once('internal:window:load', onInternalWindowLoad)
355373

356374
// If this request is still pending after the test run, resolve it, no commands were waiting on its result.

packages/driver/src/cypress.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,9 @@ class $Cypress {
712712
case 'app:navigation:changed':
713713
return this.emit('navigation:changed', ...args)
714714

715+
case 'app:download:received':
716+
return this.emit('download:received')
717+
715718
case 'app:form:submitted':
716719
return this.emit('form:submitted', args[0])
717720

packages/driver/src/cypress/downloads.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,17 @@ export default {
2525
return log.snapshot()
2626
}
2727

28-
const end = ({ id }) => {
28+
const end = ({ id }, isCanceled = false) => {
29+
Cypress.action('app:download:received')
30+
2931
const log = logs[id]
3032

3133
if (log) {
32-
log.snapshot().end()
34+
if (isCanceled) {
35+
log.snapshot().error(new Error('Download was canceled.'))
36+
} else {
37+
log.snapshot().end()
38+
}
3339

3440
// don't need this anymore since the download has ended
3541
// and won't change anymore

packages/extension/app/v2/background.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,19 @@ const connect = function (host, path, extraOpts) {
5151
})
5252

5353
browser.downloads.onChanged.addListener((downloadDelta) => {
54-
if ((downloadDelta.state || {}).current !== 'complete') return
54+
const state = (downloadDelta.state || {}).current
5555

56-
ws.emit('automation:push:request', 'complete:download', {
57-
id: `${downloadDelta.id}`,
58-
})
56+
if (state === 'complete') {
57+
ws.emit('automation:push:request', 'complete:download', {
58+
id: `${downloadDelta.id}`,
59+
})
60+
}
61+
62+
if (state === 'canceled') {
63+
ws.emit('automation:push:request', 'canceled:download', {
64+
id: `${downloadDelta.id}`,
65+
})
66+
}
5967
})
6068
})
6169

packages/extension/test/integration/background_spec.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,23 @@ describe('app/background', () => {
231231
})
232232
})
233233

234+
it('onChanged emits automation:push:request canceled:download', async function () {
235+
const downloadDelta = {
236+
id: '1',
237+
state: {
238+
current: 'canceled',
239+
},
240+
}
241+
242+
sinon.stub(browser.downloads.onChanged, 'addListener').yields(downloadDelta)
243+
244+
const ws = await this.connect()
245+
246+
expect(ws.emit).to.be.calledWith('automation:push:request', 'canceled:download', {
247+
id: `${downloadDelta.id}`,
248+
})
249+
})
250+
234251
it('onChanged does not emit if state does not exist', async function () {
235252
const downloadDelta = {
236253
id: '1',

0 commit comments

Comments
 (0)