admin管理员组

文章数量:1200819

I am very new to Cypress, so go easy.

We have a React application that, on initial load, presents a "Login with Spotify" button. The button constructs an auth URL according to the Spotify PKCE authorization flow, and directly sets window.location.href to that constructed URL:

//spotifyAuth.js

export const getAuthorizationURL = async () => {
  logMessage(`Getting authorization URL...`);
  const authUrl = new URL(";);
  const codeVerifier = generateCodeVerifier();
  const codeChallenge = await generateCodeChallenge(codeVerifier);

  // Save the codeVerifier in IndexedDB
  await setCachedEntry('auth', codeVerifier, 'spotify_code_verifier');

  logMessage(`Code verifier: ${codeVerifier}`);

  const state = generateState();
  const params = new URLSearchParams({
    response_type: 'code',
    client_id: CLIENT_ID,
    scope: 'user-library-read',
    state: state,
    redirect_uri: REDIRECT_URI,
    code_challenge_method: 'S256',
    code_challenge: codeChallenge,
  });
  authUrl.search = new URLSearchParams(params).toString();
  const authorizationURL = authUrl.toString();
  logMessage(`Authorization URL: ${authorizationURL}`);
  return { authorizationURL };
};

export const redirectToAuthorizationUrl = async () => {
  logMessage(`Redirecting to authorization URL...`);
  window.location.href = (await getAuthorizationURL()).authorizationURL;
}

To start off with, We are just trying to test that clicking that button redirects to the correct URL. We read that a single Cypress test cannot span across multiple origins. So, instead of allowing the redirect and attempting to read the new URL, we tried to stub the redirect, block it and read what the application is trying to set window.location.href to:

//login.cy.js

describe('Login Functionality', () => {
    
    describe('When you click Login to Spotify', () => {
        it('constructs the Spotify auth url', () => {
            cy.window().then(window => {
                cy.stub(window.location, 'href').as('redirect')
            })
            cy.visit('/');
            cy.get('.login-button').click();
            cy.get('@redirect').should('be.calledWithMatch', `accounts.spotify/authorize`)
        });
    });

});

That resulted an an AssertionError: Timed out retrying after 4000ms: expected redirect to have been called with arguments matching "accounts.spotify/authorize", but it was never called.

We then read that you cannot stub window.location.href directly like that. So, we tried to change the application code to set the auth URL in a function and stub that function instead:

//spotifyAuth.js

export const redirectToAuthorizationUrl = async () => {
  logMessage(`Redirecting to authorization URL...`);
  const authUrlToNavigateTo = (await getAuthorizationURL()).authorizationURL;
  window.redirectToSpotifyAuth(authUrlToNavigateTo);
}

window.redirectToSpotifyAuth = function (authUrl) {
  window.location.href = authUrl;
};
//login.cy.js

describe('Spotify Login Flow', () => {
    it('should construct correct Spotify auth URL', () => {
      // Stub the redirect function
      cy.visit('http://localhost:3000/', {
        onBeforeLoad(window) {
            window.redirectToSpotifyAuth = () => {};  
            cy.stub(window, 'redirectToSpotifyAuth').as('redirect');
        },
      });
  
      cy.get('.login-button').click();
  
      // Check that redirectToSpotifyAuth was called with the correct URL
      cy.get('@redirect').should('be.calledWithMatch', `accounts.spotify/authorize`);
    });
  });

This resulted in the same AssertionError.

What are we doing wrong?

I am very new to Cypress, so go easy.

We have a React application that, on initial load, presents a "Login with Spotify" button. The button constructs an auth URL according to the Spotify PKCE authorization flow, and directly sets window.location.href to that constructed URL:

//spotifyAuth.js

export const getAuthorizationURL = async () => {
  logMessage(`Getting authorization URL...`);
  const authUrl = new URL("https://accounts.spotify.com/authorize");
  const codeVerifier = generateCodeVerifier();
  const codeChallenge = await generateCodeChallenge(codeVerifier);

  // Save the codeVerifier in IndexedDB
  await setCachedEntry('auth', codeVerifier, 'spotify_code_verifier');

  logMessage(`Code verifier: ${codeVerifier}`);

  const state = generateState();
  const params = new URLSearchParams({
    response_type: 'code',
    client_id: CLIENT_ID,
    scope: 'user-library-read',
    state: state,
    redirect_uri: REDIRECT_URI,
    code_challenge_method: 'S256',
    code_challenge: codeChallenge,
  });
  authUrl.search = new URLSearchParams(params).toString();
  const authorizationURL = authUrl.toString();
  logMessage(`Authorization URL: ${authorizationURL}`);
  return { authorizationURL };
};

export const redirectToAuthorizationUrl = async () => {
  logMessage(`Redirecting to authorization URL...`);
  window.location.href = (await getAuthorizationURL()).authorizationURL;
}

To start off with, We are just trying to test that clicking that button redirects to the correct URL. We read that a single Cypress test cannot span across multiple origins. So, instead of allowing the redirect and attempting to read the new URL, we tried to stub the redirect, block it and read what the application is trying to set window.location.href to:

//login.cy.js

describe('Login Functionality', () => {
    
    describe('When you click Login to Spotify', () => {
        it('constructs the Spotify auth url', () => {
            cy.window().then(window => {
                cy.stub(window.location, 'href').as('redirect')
            })
            cy.visit('/');
            cy.get('.login-button').click();
            cy.get('@redirect').should('be.calledWithMatch', `accounts.spotify.com/authorize`)
        });
    });

});

That resulted an an AssertionError: Timed out retrying after 4000ms: expected redirect to have been called with arguments matching "accounts.spotify.com/authorize", but it was never called.

We then read that you cannot stub window.location.href directly like that. So, we tried to change the application code to set the auth URL in a function and stub that function instead:

//spotifyAuth.js

export const redirectToAuthorizationUrl = async () => {
  logMessage(`Redirecting to authorization URL...`);
  const authUrlToNavigateTo = (await getAuthorizationURL()).authorizationURL;
  window.redirectToSpotifyAuth(authUrlToNavigateTo);
}

window.redirectToSpotifyAuth = function (authUrl) {
  window.location.href = authUrl;
};
//login.cy.js

describe('Spotify Login Flow', () => {
    it('should construct correct Spotify auth URL', () => {
      // Stub the redirect function
      cy.visit('http://localhost:3000/', {
        onBeforeLoad(window) {
            window.redirectToSpotifyAuth = () => {};  
            cy.stub(window, 'redirectToSpotifyAuth').as('redirect');
        },
      });
  
      cy.get('.login-button').click();
  
      // Check that redirectToSpotifyAuth was called with the correct URL
      cy.get('@redirect').should('be.calledWithMatch', `accounts.spotify.com/authorize`);
    });
  });

This resulted in the same AssertionError.

What are we doing wrong?

Share Improve this question edited Jan 23 at 0:14 Aladin Spaz 9,6981 gold badge18 silver badges39 bronze badges asked Jan 22 at 21:01 Milo CowellMilo Cowell 374 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 6

In your 2nd example you have set the window.redirectToSpotifyAuth function in the same file as the handler that uses it.

The onBeforeLoad event is probably too early to to set the stub, as it's unlikely window.redirectToSpotifyAuth has been added at that point.

Your stub must be created after window.redirectToSpotifyAuth is initialized, and before the call to the click handler.

Try moving the setup of window.redirectToSpotifyAuth into a useEffect() of the component that owns the Login button,

function MyComponent() {

  useEffect(() => {
    window.redirectToSpotifyAuth = function (authUrl) {
      window.location.href = authUrl;
    }
  }, []);     // on mounting

  return <button onClick={window.redirectToSpotifyAuth(...)}>Login</button>;
}

that way it is set up well in advance of when the test clicks the Login button.


I mocked up a simple web app to simulate the situation.

<body>
  <button onclick="redirectToSpotifyAuth('somewhere')">Login</button>
  <script>
    window.redirectToSpotifyAuth = function (authUrl) {
      window.location.href = authUrl;
    }
  </script>
</body>

Then in the test I waited for the redirectToSpotifyAuth function to exist before stubbing it (to try and avoid any race condition).

Once the stub is in place, I clicked the button to invoke the method.

it('stubs a redirect', () => {

  cy.window().should('have.property', 'redirectToSpotifyAuth')
    
  cy.window().then(win => {
    cy.stub(win, 'redirectToSpotifyAuth')
      .callsFake(console.log)             // just for debugging
      .as('redirect');
  })

  cy.get('button').click()                // invoke the method

  cy.get('@redirect').should('calledWith', 'somewhere')
})


NOTE window.redirectToSpotifyAuth = () => {}; masks the real error in the second scenario, since it applies it's own version of the method.

If you remove it you may find the cy.stub() fails because the method is not yet set up, and that error is more informative than a fail on cy.get('@redirect').

If you're still having trouble with this, post the React app (a representative cut-down example) and I'll run a test for it.

本文标签: