Using Cypress App Action With ngrx/store Angular Applications
Dispatching the actions from Cypress end-to-end tests to avoid the need to the complicated page objects.
Let's say you are writing end-to-end tests for a modern Angular application. The app is showing a list of customers, so your first test is checking if adding a customer works. You are testing the application the way the user would do it: by filling the input fields and clicking the "Save" button.
The test is passing. For clarity, I am using cypress-slow-down plugin to slow down each Cypress command by 100ms.
Nice.
The second test
Once we have tested adding a customer, we can test so many other things. For example, we can test deleting a customer. To delete a customer, we need to ... add a customer first. Will you write almost the same test as before?
Hmm. If you look at the Command Log column, 24 out of 36 commands are the same as in the "adds a customer" test and are adding a new customer record using the page elements. It is slow too.
Will we write a page object to abstract adding a customer? We could.
I don't like such page objects. All they do is "hide" individual cy.contains and cy.get commands in their tiny methods. Of course, we could create an abstraction on top of these tiny methods.
Even with multiple levels of abstractions, we still have a test that repeats 2/3 of its commands between the test "adds a customer" and "deletes a customer". We also built up levels of testing code in the page objects ... that the actual user of our web page does not see or benefit from.
You are building levels of code on top of the elements on the page. The end user does not benefit from this code, and the tests simply repeat actions on the page from the other tests. By the time you get to the "deletes a customer" test, you know that adding a customer works. So repeating the same clicks and typing in the test simply spends time.
A better way
Inside the application's code, the event handlers take the input from the page and pass it on to the business logic. For example, when you add a customer, here is what the application code is doing:
If you print the constructed action, it looks like this:
Ok, so all those UI clicks and typings lead to calling this.#store.dispatch(action). We can do it ourselves from the test. First, we need to expose the store object so that the test can access it. No biggie. We can expose the global store from any top-level component.
ngOnInit() { // expose the store instance if (window.Cypress) { window.store = this.#store; } ... } };
Technical detail: in Angular applications we need to dispatch the actions using ngZone wrapper. This will force all components to update their user interface after propagating our action. Thus we need to expose the ngZone instance.
We build an object with the customer information, then dispatch an NgRx action. We wait for the window object to have the store instance - then we know the app has finished loading.
Because of the design of our application, we need to dispatch the load event too in order to update the entire list. Here is how the test looks.
The test loads the page and immediately creates a customer record, simply by calling win.store!.dispatch(addCustomer);. Then we can use the page UI and delete that customer, just like a real user. We lost nothing in our testing by removing the duplicate commands between the two tests. The "adds a customer" still exercises the page object flow for adding a new record. The current test simply reaches into the app and calls the same production code as the regular page ui would. Want to have a better test experience? Improve the application code, don't create levels of "better" testing code.
We can still use some small Page Object utilities, like opening a specific menu
But in general, we can do larger actions to add new data by calling the app's code. Cypress tests run in the same browser as the app code, so no problems with passing the real object references, circular objects, etc.
🎓 If you want to see what I think a Page Object should have, check out a free lesson "Lesson b5: Write a Page Object" in my course Write Cypress Tests Using GitHub Copilot.
A note about types: We need to "tell" the application and the testing code that the window object might have properties Cypress, ngZone, and store. We can use src/index.d.ts file for this:
src/index.d.ts
1 2 3 4 5 6 7 8 9 10
importtype { NgZone } from'@angular/core'; importtype { Store } from'@ngrx/store';