Use Copilot Instructions And Page Objects

Make Copilot more likely to successfully write end-to-end tests by providing the instructions file and using a page object.

Ask Copilot agent to write the full end-to-end test and it is likely to write nonsense. For example, let's test the "zero teams / zero players" message on my soccer web app.

Zero teams and zero players messages

I can start a new test in the existing "Teams" spec file

cypress/e2e/teams.cy.js
1
2
3
describe('Teams', () => {
it.only('shows zero teams and zero players message', () => {})
})

If I ask Copilot to write this test, what are the chances of Copilot writing a good end-to-end test?

Prompting Copilot to write this test

The agent's solution is not too bad

cypress/e2e/teams.cy.js
1
2
3
4
5
6
7
8
9
10
11
12
/// <reference types="cypress" />

describe('Teams', () => {
it.only('shows zero teams and zero players message', () => {
// Visit the teams page (adjust route if needed)
cy.visit('/team')
// Check for zero teams message
cy.contains(/zero teams/i).should('be.visible')
// Check for zero players message
cy.contains(/zero players/i).should('be.visible')
})
})

Let's immediately run this test to see how well it works.

Cypress running the test above

The test is failing

  • the check for zero teams message is wrong. Copilot does not "know" what the "zero teams" selector should be
  • the test is incomplete. It does not check if the "zero teams" component goes away when we add a team

Copilot has no idea about your project. It is like a developer who just sees the spec file and "guesses" the test steps and selectors based on their previous work experience, but without any idea how your project works. Let's improve it.

Add comments

Notice how Copilot added comments to the generated test code? That's a very good idea to describe what the test is trying to do. We should guide Copilot using comments ourselves.

cypress/e2e/teams.cy.js
1
2
3
4
5
6
7
8
9
10
11
describe('Teams', () => {
it('shows zero teams and zero players message', () => {
// Visit the team page
// Check if the zero teams and zero players components are visible
// Add a team
// Confirm the zero teams component is gone
// zero players should still be visible
// Add a player
// Confirm the zero players component is gone
})
})

Using code comments in my opinion is preferably to putting more context into the Agent prompt, since prompts are transient, while the comments stay with the code.

Let's use the same prompt and check the generated code.

Copilot prompt plus test comments

Much much better. Does it work?

Almost a half of the second test works

Let's give Copilot shortcuts.

Use a page object

Instead of hoping that Copilot can discover in our test how to add a team and a player, why don't we give it a shortcut: by using a page object we can simplify Copilot's task. I will create a new static object that simply implements a few common actions on the page, like adding a team.

cypress/e2e/gametime.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import 'cypress-plugin-steps'

const Gametime = {
home() {
cy.visit('/')
},

teamPage() {
cy.visit('/team/')
},

addTeam(name: string) {
cy.step(`Add team ${name}`)
cy.location('pathname').should('include', '/team/')
cy.get('[name=teamName]').type(name)
cy.contains('button', 'Add team').click()
cy.get('[data-cy="zero-teams"]').should('not.exist')
cy.get('li.team').find('.name').should('have.text', name)
},

addPlayer(first: string, last: string) {
cy.step(`Add player ${first} ${last}`)
cy.location('pathname').should('include', '/team/')
cy.get('[name=firstName]').type(first)
cy.get('[name=lastName]').type(last)
cy.contains('button', '+').click()
cy.get('li.player')
.find('[data-cy="name"]')
.should('have.text', `${first} ${last}`)
},
}

export default Gametime

Tip: I am using cypress-plugin-steps to create better visual test log.

Ok, so how can Copilot take the advantage of "gametime" page object? Let's include it in the prompt.

Copilot prompt including the page object reference

Bingo. The generated test looks reasonable and is passing.

The generated test is working

Use Copilot instructions file

Typing the same "Use the page object from gametime.ts file to do common actions." in each prompt quickly becomes tiresome. We can use the common Copilot instructions Markdown file instead. Here is my instructions file that gives Copilot general instructions for code generation. Notice how I use the project-specific references.

Copilot instructions file

Let's see if Copilot Agent can write a useful test with the minimal prompt "Implement this test". Wow, the Agent actually does the two-step. First it suggests using only the page object.

The agent generates the initial code

The generated test is bad, but the Copilot Agent is not done yet. It now checks if the syntax is correct and modifies the code to mix page object method calls with custom UI assertions!

The agent finished generating the test

The finished test is ... good and passing.

The passing final test

Here is the finished spec file for reference

cypress/e2e/teams.cy.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/// <reference types="cypress" />

// @ts-check
import gametime from './gametime'

describe('Teams', () => {
it('shows zero teams and zero players message', () => {
// Visit the team page
gametime.teamPage()

// Check if the zero teams and zero players components are visible
cy.get('[data-cy="zero-teams"]').should('be.visible')
cy.get('[data-cy="zero-players"]').should('be.visible')

// Add a team
gametime.addTeam('Team 1')

// Confirm the zero teams component is gone
cy.get('[data-cy="zero-teams"]').should('not.exist')
// zero players should still be visible
cy.get('[data-cy="zero-players"]').should('be.visible')

// Add a player
gametime.addPlayer('Player', 'One')

// Confirm the zero players component is gone
cy.get('[data-cy="zero-players"]').should('not.exist')
})
})

Pretty sweet. I would accept this test if someone opened a pull request review with this code. It uses mostly page object methods for general actions on the page plus correct stable selectors for checking the "zero" components. No complains.

🎓 Want to learn how to use Cypress and Copilot or Cursor to quickly write useful end-to-end tests? Check out my online courses on these topics at https://cypress.tips/courses.

Tips

Add a TypeScript check

Often Copilot "invents" non-existent page object methods

Copilot Agent suggests a non-existent method

To prevent such simple hallucinations, I include the following in my prompt / instructions

1
2
When using the gametime.ts page object, check if the method names and their parameters are correct.
There should be no TypeScript warnings or "any" unknown types in the generated code.

I also use // @ts-check in my spec files to type-check even JavaScript specs.