You’ve just completed a deployment and then the client emails that editors cannot access content moderation. Now you’re manually logging in as five different roles trying to reproduce the issue. There’s a better way. When your authentication testing code is scattered across test files (or nonexistent) regressions slip through to production. A custom Cypress command called cy.drupalLogin() centralizes the entire authentication flow in one place, making it reusable across your test suite and fast enough to run before every single test.
A custom Cypress command is a reusable function that encapsulates Drupal’s login process and makes it available throughout your test suite. To define these commands we place our code in a 'cypress/support/commands.js' file which then becomes callable anywhere in the test suite as cy.commandName().
This guide assumes basic familiarity with Cypress: writing tests, executing the test runner, and using cy.visit() and cy.get(). If you’re new to Cypress, the resources at the end of this article will help you get started. This article assumes version 12 or newer where cy.session() is stable. If you’re on an older version, ensure session support is enabled or upgrade Cypress before adopting this approach.
Prerequisites:
- Cypress installed
- Drupal site running locally
- Test users created on the site
Our cy.drupalLogin() command handles the complete authentication flow: navigate to Drupal’s login form, fill in the credentials, submit and verify successful authentication. Importantly, the custom command will use Cypress’s session caching to store state. This means subsequent tests using the same credentials will skip the full login process and save you precious time. This optimization pays off quickly: On a suite of 30 tests that each require a login you’ve just cut two minutes off every test run. Over the course of a sprint that’s a good amount of time your team gets back.
Here is the complete custom command which handles authenticating with Drupal via the login form and includes session caching:
Cypress.Commands.add('drupalLogin', (username, password) => {
cy.session([username, password], () => {
cy.visit('/user/login');
cy.get('#edit-name').type(username);
cy.get('#edit-pass').type(password);
cy.get('#edit-submit').click();
// Verify login was successful.
cy.url().should('not.include', '/user/login');
cy.getCookies().then((cookies) => {
const sessionCookie = cookies.find(cookie => cookie.name.startsWith('SESS'));
expect(sessionCookie).to.exist;
});
}, {
validate() {
cy.getCookies().then((cookies) => {
const sessionCookie = cookies.find(c => c.name.startsWith('SESS'));
expect(sessionCookie).to.exist;
});
}
});
});How It Works
The first login takes 2 - 3 seconds. Every login after that? Under 200 milliseconds. The cy.session() wrapper is key to making this test performant. It takes a unique identifier (in this case the username and password array) and a callback function. The first time Cypress encounters a particular session identifier, it runs the callback and performs the full login flow. When a later test calls for a login with the same credentials, Cypress will restore the cached session cookies automatically, skipping the login entirely.
The #edit-name and #edit-pass fields follow Drupal’s naming conventions and #edit-submit is the submit button. If your theme has been heavily modified you may need to adjust these ID values. Once we click submit we verify two things: the URL no longer includes /user/login (to show we have been redirected to a new page), and that a Drupal session cookie exists.
Drupal’s session cookie follows the pattern ‘SESS’ followed by a hash value. The hash varies by site, so we simply check for cookies whose name starts with the string ‘SESS’ rather than try to match the full name. This allows our custom command to be portable across different Drupal installations.
There is a subtle gotcha with session caching: what if the session expires between tests? Cypress handles this with the validate() function: a hook that runs before restoring a cached session. If the ‘SESS’ cookie is no longer present, Cypress re-runs the full login automatically. One less subtle hang-up to worry about.
Using The Command
Once defined, you can use cy.drupalLogin() anywhere in your test suite. Cypress provides a handy beforeEach() function which allows us to fire our new custom command before each test, making our test file more concise.
describe('Blog Post creation tests', () => {
beforeEach(() => {
cy.drupalLogin('editor_user', 'editor_password');
});
it('Creates a new article', () => {
cy.visit('/node/add/article');
cy.get('#edit-title-0-value').type('Test Article');
});
it('Saves article as draft', () => {
cy.visit('/node/add/article');
cy.get('#edit-title-0-value').type('Draft Article');
cy.get('#edit-moderation-state-0-state').select('draft');
});
});In this example we are using credentials in the test code for simplicity. In practice, you will want to store your credentials in a ‘cypress.env.json’ file which is excluded from version control and reference them as Cypress.env(‘editor_user’) and Cypress.env(‘editor_password’). Best practice dictates that we never commit passwords to the repository, even for test accounts.
You now have a reusable, performant authentication command that can be inserted into any existing Cypress tests with a single line of code. The cy.drupalLogin() command handles authentication, caches sessions intelligently, and gives you a solid foundation for testing all the content moderation, permissions and administrative interface functionality that truly matters on your Drupal site.
This login command is just the beginning of a mature Drupal testing strategy. The pattern behind this command of identifying a repeated Drupal interaction and wrapping it in a custom command can be applied beyond authentication. Content creation, cache management, and role assignment are all candidates for optimization. Each command you add centralizes Drupal-specific logic and keeps your test files focused on what they are actually testing. The next time you catch yourself copy-pasting login code into a new test file, pause. That is your signal to build a custom command.
Resources
An introduction to Cypress testing
Documentation about environment variables and secrets in Cypress