Skip to content

Commit 82077cf

Browse files
authored
chore: fix cy.location() not retrying chained its() then chained should() (#31928)
* chore: fix cy.location() to retry if its() assertion fails * return the cached URL while the url is being retried
1 parent 96e7472 commit 82077cf

File tree

5 files changed

+161
-6
lines changed

5 files changed

+161
-6
lines changed

packages/driver/cypress/e2e/commands/location.cy.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -416,11 +416,15 @@ describe('src/cy/commands/location', () => {
416416
it('eventually returns a given key', function () {
417417
cy.stub(Cypress, 'automation').withArgs('get:aut:url')
418418
.onFirstCall().resolves('http://localhost:3500')
419-
.onSecondCall().resolves('http://localhost:3500/my/path')
419+
.resolves('http://localhost:3500/my/path')
420420

421421
cy.location('pathname').should('equal', '/my/path')
422422
.then(() => {
423-
expect(Cypress.automation).to.have.been.calledTwice
423+
// should be called 3 times:
424+
// 1. initial call cy.location('pathname')
425+
// 2. the should() assertion
426+
// 3. the then() callback
427+
expect(Cypress.automation).to.have.been.calledThrice
424428
})
425429
})
426430

packages/driver/src/cy/commands/helpers/location.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export function getUrlFromAutomation (Cypress: Cypress.Cypress, options: Partial
1313
this.set('timeout', timeout)
1414

1515
let fullUrlObj: any = null
16+
let hasBeenInitiallyResolved = false
1617
let automationPromise: Promise<void> | null = null
1718
// need to set a valid type on this
1819
let mostRecentError = new UrlNotYetAvailableError()
@@ -22,8 +23,6 @@ export function getUrlFromAutomation (Cypress: Cypress.Cypress, options: Partial
2223
return automationPromise
2324
}
2425

25-
fullUrlObj = null
26-
2726
automationPromise = Cypress.automation('get:aut:url', {})
2827
.timeout(timeout)
2928
.then((url) => {
@@ -59,8 +58,29 @@ export function getUrlFromAutomation (Cypress: Cypress.Cypress, options: Partial
5958
}
6059
})
6160

62-
return () => {
61+
return (options: {
62+
retryAfterResolve?: boolean
63+
} = {
64+
retryAfterResolve: false,
65+
}) => {
6366
if (fullUrlObj) {
67+
// In some cases, Cypress will want to retry fetching the url object after it is resolved.
68+
// For instance, in the case of the command yielding an object, like cy.location().
69+
70+
// If cy.location().its('url').should('equal', 'https://www.foobar.com') initially fails the 'should' assertion,
71+
// Cypress will want to retry fetching the url object as the onFail handler is NOT called when the subject is chained after 'its'.
72+
73+
// This does NOT apply if the assertion is chained directly after the command, like cy.location().should('equal', 'https://www.foobar.com').
74+
// This examples DOES call the onFail handler and fetching the url will be retried from the context of the onFail handler.
75+
if (options?.retryAfterResolve && hasBeenInitiallyResolved) {
76+
// tslint:disable-next-line no-floating-promises
77+
getUrlFromAutomation()
78+
}
79+
80+
// We only want to retry if the url object has been resolved at least once.
81+
// Otherwise, this will always fetch n + 1 times which is usually unnecessary.
82+
hasBeenInitiallyResolved = true
83+
6484
return fullUrlObj
6585
}
6686

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export function locationQueryCommand (Cypress: Cypress.Cypress, cy: Cypress.Cypr
9393
const fn = Cypress.isBrowser('webkit') ? cy.getRemoteLocation : getUrlFromAutomation.bind(this)(Cypress, options)
9494

9595
return () => {
96-
const location = fn()
96+
const location = Cypress.isBrowser('webkit') ? fn() : fn({ retryAfterResolve: true })
9797

9898
if (location === '') {
9999
// maybe the page's domain is "invisible" to us

packages/driver/test/unit/cy/commands/helpers/location.spec.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,67 @@ describe('cy/commands/helpers/location', () => {
132132
})
133133
})
134134

135+
it('retries returning the url object after the automation promise is resolved and { retryAfterResolve: true } is passed', async () => {
136+
// @ts-expect-error
137+
mockCypress.automation.mockImplementationOnce(() => {
138+
// no-op promise to simulate the waiting for the automation client
139+
return new Bluebird.Promise((resolve) => resolve('https://www.example.com#foobar'))
140+
})
141+
142+
// @ts-expect-error
143+
mockCypress.automation.mockImplementation(() => {
144+
// no-op promise to simulate the waiting for the automation client
145+
return new Bluebird.Promise((resolve) => resolve('https://www.foobar.com#foobar'))
146+
})
147+
148+
const fn = getUrlFromAutomation.call(mockContext, mockCypress, mockOptions)
149+
150+
expect(() => {
151+
fn({ retryAfterResolve: true })
152+
}).toThrow()
153+
154+
// flush the microtask queue so we have a url value next time we call fn()
155+
await flushPromises()
156+
157+
const url = fn({ retryAfterResolve: true })
158+
159+
expect(url).toEqual({
160+
protocol: 'https:',
161+
host: 'www.example.com',
162+
hostname: 'www.example.com',
163+
hash: '#foobar',
164+
search: '',
165+
pathname: '/',
166+
port: '',
167+
origin: 'https://www.example.com',
168+
href: 'https://www.example.com/#foobar',
169+
searchParams: expect.any(Object),
170+
})
171+
172+
expect(() => {
173+
// in this case the fn will returned the cached url object until the new one is available
174+
fn({ retryAfterResolve: true })
175+
}).not.toThrow()
176+
177+
// flush the microtask queue so we have a url value next time we call fn()
178+
await flushPromises()
179+
180+
const url2 = fn({ retryAfterResolve: true })
181+
182+
expect(url2).toEqual({
183+
protocol: 'https:',
184+
host: 'www.foobar.com',
185+
hostname: 'www.foobar.com',
186+
hash: '#foobar',
187+
search: '',
188+
pathname: '/',
189+
port: '',
190+
origin: 'https://www.foobar.com',
191+
href: 'https://www.foobar.com/#foobar',
192+
searchParams: expect.any(Object),
193+
})
194+
})
195+
135196
it('throws an error when the automation promise is rejected and propagates the error', async () => {
136197
// @ts-expect-error
137198
mockCypress.automation.mockImplementation(() => {

packages/driver/test/unit/cy/commands/location.spec.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,76 @@ describe('cy/commands/location', () => {
303303
locationQueryCommand.call(mockContext, mockCypress, mockCy, 'doesnotexist', {})()
304304
}).toThrow('Location object does not have key: `doesnotexist`')
305305
})
306+
307+
it('retries the command even after the location has resolved', () => {
308+
// @ts-expect-error
309+
getUrlFromAutomation.mockReturnValueOnce((opts) => {
310+
expect(opts).toEqual({ retryAfterResolve: true })
311+
312+
return {
313+
protocol: 'https:',
314+
host: 'www.example.com',
315+
hostname: 'www.example.com',
316+
hash: '#foobar',
317+
search: '',
318+
pathname: '/',
319+
port: '',
320+
origin: 'https://www.example.com',
321+
href: 'https://www.example.com/#foobar',
322+
searchParams: expect.any(Object),
323+
}
324+
})
325+
326+
// @ts-expect-error
327+
getUrlFromAutomation.mockReturnValueOnce((opts) => {
328+
expect(opts).toEqual({ retryAfterResolve: true })
329+
330+
return {
331+
protocol: 'https:',
332+
host: 'www.foobar.com',
333+
hostname: 'www.foobar.com',
334+
hash: '#foobar',
335+
search: '',
336+
pathname: '/',
337+
port: '',
338+
origin: 'https://www.foobar.com',
339+
href: 'https://www.foobar.com/#foobar',
340+
searchParams: expect.any(Object),
341+
}
342+
})
343+
344+
const urlObj = locationQueryCommand.call(mockContext, mockCypress, mockCy, undefined, {})()
345+
346+
expect(urlObj).toEqual({
347+
protocol: 'https:',
348+
host: 'www.example.com',
349+
hostname: 'www.example.com',
350+
hash: '#foobar',
351+
search: '',
352+
pathname: '/',
353+
port: '',
354+
origin: 'https://www.example.com',
355+
href: 'https://www.example.com/#foobar',
356+
searchParams: expect.any(Object),
357+
})
358+
359+
const urlObj2 = locationQueryCommand.call(mockContext, mockCypress, mockCy, undefined, {})()
360+
361+
expect(urlObj2).toEqual({
362+
protocol: 'https:',
363+
host: 'www.foobar.com',
364+
hostname: 'www.foobar.com',
365+
hash: '#foobar',
366+
search: '',
367+
pathname: '/',
368+
port: '',
369+
origin: 'https://www.foobar.com',
370+
href: 'https://www.foobar.com/#foobar',
371+
searchParams: expect.any(Object),
372+
})
373+
374+
expect(getUrlFromAutomation).toHaveBeenCalledTimes(2)
375+
})
306376
})
307377

308378
describe('webkit', () => {

0 commit comments

Comments
 (0)