diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js
index 3fc95f9b..8de78d35 100644
--- a/docs/.vitepress/config.js
+++ b/docs/.vitepress/config.js
@@ -120,6 +120,7 @@ export default ({ mode }) => {
items: [
{ text: 'Adding Core Widgets', link: '/contributing/widgets/core-widgets' },
{ text: 'Third Party Widgets', link: '/contributing/widgets/third-party' },
+ { text: 'Testing', link: '/contributing/widgets/testing' },
{ text: 'Debugging', link: '/contributing/widgets/debugging' }
]
},
diff --git a/docs/contributing/widgets/testing.md b/docs/contributing/widgets/testing.md
new file mode 100644
index 00000000..9500dc85
--- /dev/null
+++ b/docs/contributing/widgets/testing.md
@@ -0,0 +1,53 @@
+# E2E Testing
+
+E2E Testing consists of runnig a local environment, and automating interaction with the browser to test the widgets behaviour.
+
+With Dashboard 2.0, we have the following commands which are used for testing:
+
+- `npm run cy:server` - Runs an instance of Node-RED with Dashboard 2.0 installed.
+- `npm run cy:run` - Runs all of the Cypress tests in headless mode.
+- `npm run cy:open` - Opens the Cypress test runner, whereby you can explicitly choose which tests to run locally.
+
+
+## Cypress Test Helper
+
+In order to make it easier to write tests, we have created a helper function which can be used to test the output from particular widgets.
+
+]
+
+The "Store Latest Msg" function node here contains:
+
+```js
+global.set('msg', msg)
+return msg;
+```
+
+When a button is clicked in the Dashboard, the value emitted by that button is then stored in a global `msg` variable. We can then use this in conjuction with:
+
+
+
+In order to make an API call from Cypress after we've clicked the button and checked the stored value against that which we expect the button to have emitted.
+
+For example, from our button tests:
+
+```js
+describe('Node-RED Dashboard 2.0 - Buttons', () => {
+ beforeEach(() => {
+ cy.deployFixture('dashboard-buttons') // reads in a flow.json and deploys it to the local Node-RED instance
+ cy.visit('/dashboard/page1')
+ })
+
+ it('can be clicked and outputs the correct payload & topic are emitted', () => {
+ // Emitting strings
+ cy.get('button').contains('Button 1 (str)').click()
+ // checkOutput calls our helper endpoints to checks the values against the stored msg
+ cy.checkOutput('msg.payload', 'button 1 clicked')
+ cy.checkOutput('msg.topic', 'button-str-topic')
+
+ // Emitting JSON
+ cy.get('button').contains('Button 1 (json)').click()
+ cy.checkOutput('msg.payload.hello', 'world')
+ cy.checkOutput('msg.topic', 'button-json-topic')
+ })
+})
+```
\ No newline at end of file