Merge branch '36-tests-webapp'
test(web-app): provide valid test and CI Closes #36 See merge request team-raclette/project-softweng!21
This commit is contained in:
commit
9967aef1f5
23 files changed
+243
-808
No files matched your search
@@ -8,3 +8,5 @@ MQTT_USERNAME=
|
|||||||
MQTT_PASSWORD=
|
MQTT_PASSWORD=
|
||||||
REST_USERNAME=
|
REST_USERNAME=
|
||||||
REST_PASSWORD=
|
REST_PASSWORD=
|
||||||
|
REST_URL=
|
||||||
|
REST_PAGE=
|
||||||
+4
-2
@@ -109,8 +109,10 @@ services:
|
|||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
- VUE_APP_INFLUXDB_USER=$REST_USERNAME
|
- VUE_APP_REST_USER=$REST_USERNAME
|
||||||
- VUE_APP_INFLUXDB_PASSWORD=$REST_PASSWORD
|
- VUE_APP_REST_PASSWORD=$REST_PASSWORD
|
||||||
|
- VUE_APP_REST_URL=$REST_URL
|
||||||
|
- VUE_APP_REST_PAGE=$REST_PAGE
|
||||||
|
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
|
|||||||
@@ -1,66 +1,69 @@
|
|||||||
@startuml web-app
|
@startuml web-app
|
||||||
|
|
||||||
skinparam linetype ortho
|
skinparam linetype ortho
|
||||||
|
skinparam nodesep 50
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
' principale vuejs component
|
' principale vuejs component
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Client {
|
class ChartComponent {
|
||||||
+getValues()
|
|
||||||
+getNewValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
class SeriesManager {
|
|
||||||
' get data, put data in graphs, ask new measure
|
|
||||||
|
|
||||||
+getNewValue()
|
|
||||||
+getAllValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
class SerieConverter {
|
|
||||||
' convert timeseries from backend for graphs
|
|
||||||
+jsonToSerie()
|
|
||||||
}
|
|
||||||
|
|
||||||
class TimeSeries {
|
|
||||||
' simplify the duplication of series in graphs
|
|
||||||
-Series
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class Button {
|
|
||||||
' vue js component
|
' vue js component
|
||||||
' ask new measure
|
scatter
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ControlPanel {
|
||||||
|
' vue js component
|
||||||
|
action-button
|
||||||
|
refresh-button
|
||||||
|
user-multiselect
|
||||||
|
room-multiselect
|
||||||
|
device-multiselect
|
||||||
|
}
|
||||||
|
|
||||||
|
class Client {
|
||||||
|
+isconnected()
|
||||||
|
+getValues(user, room, device)
|
||||||
|
+getNewValue()
|
||||||
|
-ping()
|
||||||
|
-getAuthHeader()
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimeSeriesManager {
|
||||||
|
' get data, put data in graphs, ask new measure
|
||||||
|
+selected_user
|
||||||
|
+selected_room
|
||||||
|
+selected_device
|
||||||
|
+series
|
||||||
|
|
||||||
|
+getTimeSeriesData()
|
||||||
|
+getNewValue()
|
||||||
|
}
|
||||||
|
|
||||||
class Serie {
|
class Serie {
|
||||||
' contains table of measure
|
' contains table of measure
|
||||||
' convert the table to graphs format
|
' convert the table to graphs format
|
||||||
-type
|
-type
|
||||||
-values
|
-data
|
||||||
-numberOfLastValus
|
-user
|
||||||
-id
|
-room
|
||||||
-place
|
-device
|
||||||
-name
|
|
||||||
|
|
||||||
+getGraphFormat()
|
+getLabel()
|
||||||
' needs to limit the number of value
|
+getSerie()
|
||||||
+addNewValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
class Plot {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
App *-r- SeriesManager
|
|
||||||
|
App *-r- TimeSeriesManager
|
||||||
App *-u- Client
|
App *-u- Client
|
||||||
SeriesManager *-r- TimeSeries
|
App *-d- ChartComponent
|
||||||
Plot -l-* TimeSeries
|
App *-d- ControlPanel
|
||||||
Serie -u-* TimeSeries
|
TimeSeriesManager *---r--- Serie
|
||||||
Serie *-- Button
|
|
||||||
SerieConverter .u.> SeriesManager
|
|
||||||
|
|
||||||
|
|
||||||
@enduml
|
@enduml
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="us-ascii" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" height="601px" preserveAspectRatio="none" style="width:571px;height:601px;background:#FFFFFF;" version="1.1" viewBox="0 0 571 601" width="571px" zoomAndPan="magnify"><defs/><g><!--class App--><g id="elem_App"><rect codeLine="5" fill="#F1F1F1" height="48" id="App" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="59" x="146" y="276.5"/><ellipse cx="161" cy="292.5" fill="#ADD1B2" rx="11" ry="11" style="stroke:#181818;stroke-width:1.0;"/><path d="M163.9688,298.1406 Q163.3906,298.4375 162.75,298.5781 Q162.1094,298.7344 161.4063,298.7344 Q158.9063,298.7344 157.5781,297.0938 Q156.2656,295.4375 156.2656,292.3125 Q156.2656,289.1875 157.5781,287.5313 Q158.9063,285.875 161.4063,285.875 Q162.1094,285.875 162.75,286.0313 Q163.4063,286.1875 163.9688,286.4844 L163.9688,289.2031 Q163.3438,288.625 162.75,288.3594 Q162.1563,288.0781 161.5313,288.0781 Q160.1875,288.0781 159.5,289.1563 Q158.8125,290.2188 158.8125,292.3125 Q158.8125,294.4063 159.5,295.4844 Q160.1875,296.5469 161.5313,296.5469 Q162.1563,296.5469 162.75,296.2813 Q163.3438,296 163.9688,295.4219 L163.9688,298.1406 Z " fill="#000000"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="27" x="175" y="297.932">App</text><line style="stroke:#181818;stroke-width:0.5;" x1="147" x2="204" y1="308.5" y2="308.5"/><line style="stroke:#181818;stroke-width:0.5;" x1="147" x2="204" y1="316.5" y2="316.5"/></g><!--class ChartComponent--><g id="elem_ChartComponent"><rect codeLine="10" fill="#F1F1F1" height="67.0679" id="ChartComponent" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="147" x="7" y="489"/><ellipse cx="22" cy="505" fill="#ADD1B2" rx="11" ry="11" style="stroke:#181818;stroke-width:1.0;"/><path d="M24.9688,510.6406 Q24.3906,510.9375 23.75,511.0781 Q23.1094,511.2344 22.4063,511.2344 Q19.9063,511.2344 18.5781,509.5938 Q17.2656,507.9375 17.2656,504.8125 Q17.2656,501.6875 18.5781,500.0313 Q19.9063,498.375 22.4063,498.375 Q23.1094,498.375 23.75,498.5313 Q24.4063,498.6875 24.9688,498.9844 L24.9688,501.7031 Q24.3438,501.125 23.75,500.8594 Q23.1563,500.5781 22.5313,500.5781 Q21.1875,500.5781 20.5,501.6563 Q19.8125,502.7188 19.8125,504.8125 Q19.8125,506.9063 20.5,507.9844 Q21.1875,509.0469 22.5313,509.0469 Q23.1563,509.0469 23.75,508.7813 Q24.3438,508.5 24.9688,507.9219 L24.9688,510.6406 Z " fill="#000000"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="115" x="36" y="510.432">ChartComponent</text><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="153" y1="521" y2="521"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="46" x="13" y="539.9659">scatter</text><line style="stroke:#181818;stroke-width:0.5;" x1="8" x2="153" y1="548.0679" y2="548.0679"/></g><!--class ControlPanel--><g id="elem_ControlPanel"><rect codeLine="16" fill="#F1F1F1" height="143.3396" id="ControlPanel" rx="2.5" ry="2.5" style="stroke:#181818;stroke-width:0.5;" width="134" x="204.5" y="451"/><ellipse cx="226.7" cy="467" fill="#ADD1B2" rx="11" ry="11" style="stroke:#181818;stroke-width:1.0;"/><path d="M229.6688,472.6406 Q229.0906,472.9375 228.45,473.0781 Q227.8094,473.2344 227.1063,473.2344 Q224.6063,473.2344 223.2781,471.5938 Q221.9656,469.9375 221.9656,466.8125 Q221.9656,463.6875 223.2781,462.0313 Q224.6063,460.375 227.1063,460.375 Q227.8094,460.375 228.45,460.5313 Q229.1063,460.6875 229.6688,460.9844 L229.6688,463.7031 Q229.0438,463.125 228.45,462.8594 Q227.8563,462.5781 227.2313,462.5781 Q225.8875,462.5781 225.2,463.6563 Q224.5125,464.7188 224.5125,466.8125 Q224.5125,468.9063 225.2,469.9844 Q225.8875,471.0469 227.2313,471.0469 Q227.8563,471.0469 228.45,470.7813 Q229.0438,470.5 229.6688,469.9219 L229.6688,472.6406 Z " fill="#000000"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="86" x="242.3" y="472.432">ControlPanel</text><line style="stroke:#181818;stroke-width:0.5;" x1="205.5" x2="337.5" y1="483" y2="483"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="91" x="210.5" y="501.9659">action-button</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="99" x="210.5" y="521.0339">refresh-button</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="109" x="210.5" y="540.1018">user-multiselect</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="114" x="210.5" y="559.1697">room-multiselect</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="122" x="210.5" y="578.2376">device-multiselect</text><line style="stroke:#181818;stroke-width:0.5;" x1="205.5" x2="337.5" y1="586.3396" y2="586.339Line truncated
|
||||||
|
After Width: | Height: | Size: 15 KiB |
+11
-5
@@ -2,6 +2,11 @@ variables:
|
|||||||
DOCKER_IMAGE: registry.forge.hefr.ch/team-raclette/project-softweng/web-app:latest
|
DOCKER_IMAGE: registry.forge.hefr.ch/team-raclette/project-softweng/web-app:latest
|
||||||
NODE_IMAGE: cypress/included:cypress-14.4.1-node-22.16.0-chrome-137.0.7151.68-1-ff-139.0.1-edge-137.0.3296.62-1
|
NODE_IMAGE: cypress/included:cypress-14.4.1-node-22.16.0-chrome-137.0.7151.68-1-ff-139.0.1-edge-137.0.3296.62-1
|
||||||
|
|
||||||
|
VUE_APP_REST_USER: $REST_USER
|
||||||
|
VUE_APP_REST_PASSWORD: $REST_PASSWORD
|
||||||
|
VUE_APP_REST_URL: $REST_URL
|
||||||
|
VUE_APP_REST_PAGE: $TEST_PAGE
|
||||||
|
|
||||||
default:
|
default:
|
||||||
image: $DOCKER_IMAGE
|
image: $DOCKER_IMAGE
|
||||||
|
|
||||||
@@ -35,9 +40,10 @@ web-app-unit-tests:
|
|||||||
web-app-e2e-tests:
|
web-app-e2e-tests:
|
||||||
image: $NODE_IMAGE
|
image: $NODE_IMAGE
|
||||||
stage: web-app-tests
|
stage: web-app-tests
|
||||||
services:
|
|
||||||
- name: $DOCKER_IMAGE
|
# services:
|
||||||
alias: app
|
# - name: $DOCKER_IMAGE
|
||||||
|
# alias: app
|
||||||
script:
|
script:
|
||||||
- echo "Running e2e tests"
|
- echo "Running e2e tests"
|
||||||
- apt-get update
|
- apt-get update
|
||||||
@@ -45,6 +51,6 @@ web-app-e2e-tests:
|
|||||||
- cd ./web-app
|
- cd ./web-app
|
||||||
- npm install --include=dev
|
- npm install --include=dev
|
||||||
- echo "Wait web-app app is running"
|
- echo "Wait web-app app is running"
|
||||||
- for i in {1..6}; do curl -f http://app:8080 && break || sleep 5; done
|
- for i in {1..6}; do curl -f https://app.mse.kb28.ch/ && break || sleep 5; done
|
||||||
- echo "Flask app is running"
|
- echo "App is running"
|
||||||
- npm run test:e2e
|
- npm run test:e2e
|
||||||
+9
-2
@@ -39,8 +39,10 @@ Setup the environment:
|
|||||||
|
|
||||||
linux:
|
linux:
|
||||||
```terminal
|
```terminal
|
||||||
export VUE_APP_API_KEY=your_api_key
|
export VUE_APP_REST_USER=
|
||||||
export VUE_APP_API_SECRET=your_api_secret
|
export VUE_APP_REST_PASSWORD=
|
||||||
|
export VUE_APP_REST_URL=
|
||||||
|
export VUE_APP_REST_PAGE=
|
||||||
```
|
```
|
||||||
windows:
|
windows:
|
||||||
```terminal
|
```terminal
|
||||||
@@ -58,6 +60,11 @@ npm run serve
|
|||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
### Class diagram
|
||||||
|
|
||||||
|
<p align="center"> 
|
||||||
|
|
||||||
### Links
|
### Links
|
||||||
See [Configuration Reference](https://cli.vuejs.org/config/).\
|
See [Configuration Reference](https://cli.vuejs.org/config/).\
|
||||||
See [Vue.js](https://vuejs.org/guide/introduction.html).\
|
See [Vue.js](https://vuejs.org/guide/introduction.html).\
|
||||||
|
|||||||
@@ -2,21 +2,12 @@ import { defineConfig } from "cypress";
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
e2e: {
|
e2e: {
|
||||||
specPattern: "tests/e2e/*.spec.ts",
|
setupNodeEvents(on, config) {
|
||||||
baseUrl: "http://localhost:8080",
|
// implement node event listeners here
|
||||||
supportFile: "tests/support/e2e.ts",
|
|
||||||
// Don't skip server checks - we want to ensure the app is running
|
|
||||||
// Wait up to 10 seconds for the server to be available
|
|
||||||
defaultCommandTimeout: 10000,
|
|
||||||
requestTimeout: 10000,
|
|
||||||
responseTimeout: 10000,
|
|
||||||
pageLoadTimeout: 30000,
|
|
||||||
},
|
},
|
||||||
component: {
|
specPattern: [
|
||||||
devServer: {
|
"cypress/e2e/*.cy.{js,jsx,ts,tsx}",
|
||||||
framework: "vue",
|
"cypress/unit/*.cy.{js,jsx,ts,tsx}",
|
||||||
bundler: "webpack",
|
],
|
||||||
},
|
|
||||||
specPattern: "src/**/*.cy.ts",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
const { defineConfig } = require('cypress');
|
|
||||||
|
|
||||||
module.exports = defineConfig({
|
|
||||||
// Shared settings for all test types
|
|
||||||
watchForFileChanges: false,
|
|
||||||
screenshotOnRunFailure: false,
|
|
||||||
video: false,
|
|
||||||
defaultCommandTimeout: 10000,
|
|
||||||
chromeWebSecurity: false,
|
|
||||||
retries: {
|
|
||||||
runMode: 2,
|
|
||||||
openMode: 0,
|
|
||||||
},
|
|
||||||
|
|
||||||
// E2E test configuration
|
|
||||||
e2e: {
|
|
||||||
specPattern: ['tests/e2e/*.spec.ts', 'tests/unit/*.spec.ts'],
|
|
||||||
baseUrl: null, // No baseUrl to prevent server checks in headless mode
|
|
||||||
supportFile: 'tests/support/e2e.ts',
|
|
||||||
experimentalRunAllSpecs: true,
|
|
||||||
testIsolation: false, // Allow shared context for unit tests
|
|
||||||
setupNodeEvents(on, config) {
|
|
||||||
// Disable baseUrl verification for headless testing
|
|
||||||
on('before:browser:launch', (browser, launchOptions) => {
|
|
||||||
return launchOptions;
|
|
||||||
});
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Component test configuration (for Vue components)
|
|
||||||
component: {
|
|
||||||
devServer: {
|
|
||||||
framework: 'vue',
|
|
||||||
bundler: 'webpack',
|
|
||||||
},
|
|
||||||
specPattern: 'tests/unit/*.spec.ts', // Component tests are in unit directory
|
|
||||||
supportFile: 'tests/support/e2e.ts',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
describe("Test fetch measurments button in the main page", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit("https://app.mse.kb28.ch");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Fetch timeseries with valid user-room-device", () => {
|
||||||
|
cy.get('[data-cy="fetch-measurements-button"]').should("be.visible");
|
||||||
|
// keep default mulitselector value for the test
|
||||||
|
cy.get('[data-cy="fetch-measurements-button"]').click();
|
||||||
|
|
||||||
|
cy.get('[data-cy="main-chart"]').should("be.visible");
|
||||||
|
cy.get('[data-cy="no-data-display"]').should("not.exist");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Fetch timeseries with invalid user-room-device", () => {
|
||||||
|
//change the defualt user to "Sylvan"
|
||||||
|
cy.get('[data-cy="user-mulitselect"]').should("be.visible").click();
|
||||||
|
cy.get(
|
||||||
|
'[data-cy="user-mulitselect"] .multiselect__content-wrapper .multiselect__element'
|
||||||
|
)
|
||||||
|
.contains("Sylvan")
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.get('[data-cy="fetch-measurements-button"]').should("be.visible");
|
||||||
|
// keep default mulitselector value for the test
|
||||||
|
cy.get('[data-cy="fetch-measurements-button"]').click();
|
||||||
|
|
||||||
|
cy.get('[data-cy="main-chart"]').should("not.exist");
|
||||||
|
cy.get('[data-cy="no-data-display"]').should("be.visible");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
// ***********************************************
|
||||||
|
// This example commands.ts shows you how to
|
||||||
|
// create various custom commands and overwrite
|
||||||
|
// existing commands.
|
||||||
|
//
|
||||||
|
// For more comprehensive examples of custom
|
||||||
|
// commands please read more here:
|
||||||
|
// https://on.cypress.io/custom-commands
|
||||||
|
// ***********************************************
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a parent command --
|
||||||
|
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a child command --
|
||||||
|
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a dual command --
|
||||||
|
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This will overwrite an existing command --
|
||||||
|
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||||
|
//
|
||||||
|
// declare global {
|
||||||
|
// namespace Cypress {
|
||||||
|
// interface Chainable {
|
||||||
|
// login(email: string, password: string): Chainable<void>
|
||||||
|
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||||
|
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||||
|
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// ***********************************************************
|
||||||
|
// This example support/e2e.ts is processed and
|
||||||
|
// loaded automatically before your test files.
|
||||||
|
//
|
||||||
|
// This is a great place to put global configuration and
|
||||||
|
// behavior that modifies Cypress.
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off
|
||||||
|
// automatically serving support files with the
|
||||||
|
// 'supportFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/configuration
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// Import commands.js using ES2015 syntax:
|
||||||
|
import './commands'
|
||||||
@@ -8,12 +8,12 @@ describe("Serie Component Tests", () => {
|
|||||||
// Mock data for tests
|
// Mock data for tests
|
||||||
const mockDataTemperature = [
|
const mockDataTemperature = [
|
||||||
{ time: 1625097600000, value: new Date(22.5) },
|
{ time: 1625097600000, value: new Date(22.5) },
|
||||||
{ time: 1625184000000, value: new Date(23.8) }
|
{ time: 1625184000000, value: new Date(23.8) },
|
||||||
];
|
];
|
||||||
|
|
||||||
const mockDataHumidity = [
|
const mockDataHumidity = [
|
||||||
{ time: 1625097600000, value: new Date(45) },
|
{ time: 1625097600000, value: new Date(45) },
|
||||||
{ time: 1625184000000, value: new Date(48) }
|
{ time: 1625184000000, value: new Date(48) },
|
||||||
];
|
];
|
||||||
|
|
||||||
it("should initialize with correct properties", () => {
|
it("should initialize with correct properties", () => {
|
||||||
@@ -69,7 +69,10 @@ describe("Serie Component Tests", () => {
|
|||||||
it("should format temperature data correctly with getSerie()", () => {
|
it("should format temperature data correctly with getSerie()", () => {
|
||||||
const serie = new Serie(
|
const serie = new Serie(
|
||||||
TEMPERATURE,
|
TEMPERATURE,
|
||||||
mockDataTemperature.map(item => ({ time: item.time, value: new Date(item.value) })),
|
mockDataTemperature.map((item) => ({
|
||||||
|
time: item.time,
|
||||||
|
value: new Date(item.value),
|
||||||
|
})),
|
||||||
"user1",
|
"user1",
|
||||||
"bedroom",
|
"bedroom",
|
||||||
"sensor1"
|
"sensor1"
|
||||||
@@ -7,8 +7,8 @@
|
|||||||
"build": "vue-cli-service build",
|
"build": "vue-cli-service build",
|
||||||
"cypress:open": "cypress open",
|
"cypress:open": "cypress open",
|
||||||
"cypress:run": "cypress run",
|
"cypress:run": "cypress run",
|
||||||
"test:unit": "cypress run --config-file cypress.unit.js --spec 'tests/unit/*.spec.ts'",
|
"test:e2e": "cypress run --spec 'cypress/e2e/*.cy.ts'",
|
||||||
"test:e2e": "cypress run --spec 'tests/e2e/*.spec.ts'",
|
"test:unit": "cypress run --spec 'cypress/unit/*.cy.ts'",
|
||||||
"test": "npm run test:unit && npm run test:e2e"
|
"test": "npm run test:unit && npm run test:e2e"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Prop } from "vue";
|
|
||||||
import { Serie } from "./Serie";
|
import { Serie } from "./Serie";
|
||||||
import { HttpClient } from "../Services/HttpClient";
|
import { HttpClient } from "../Services/HttpClient";
|
||||||
import { TEMPERATURE, HUMIDITY, TYPE, VALUE } from "../const";
|
import { TEMPERATURE, HUMIDITY, TYPE } from "../const";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
|
||||||
export class TimeSeriesManager {
|
export class TimeSeriesManager {
|
||||||
@@ -57,8 +56,6 @@ export class TimeSeriesManager {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
error = ref(false);
|
|
||||||
loading = ref("");
|
|
||||||
|
|
||||||
series = ref<Serie[]>([]);
|
series = ref<Serie[]>([]);
|
||||||
|
|
||||||
@@ -125,12 +122,11 @@ export class TimeSeriesManager {
|
|||||||
this.selected_room.room,
|
this.selected_room.room,
|
||||||
this.selected_device.device
|
this.selected_device.device
|
||||||
);
|
);
|
||||||
|
|
||||||
this.series.value = [temperatureSerie, humiditySerie];
|
this.series.value = [temperatureSerie, humiditySerie];
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Error fetching time series data:", error);
|
console.error("Error fetching time series data:", error);
|
||||||
throw error; // Re-throw to allow calling code to handle it
|
this.series.value = [];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import axios, { AxiosResponse } from "axios";
|
import axios, { AxiosResponse } from "axios";
|
||||||
import { BASE, PING, RACLETTE, PONG } from "../const";
|
import { BASE, PING, PAGE, PONG } from "../const";
|
||||||
|
|
||||||
export class HttpClient {
|
export class HttpClient {
|
||||||
private _url: string;
|
private _url: string;
|
||||||
@@ -45,7 +45,7 @@ export class HttpClient {
|
|||||||
room: string,
|
room: string,
|
||||||
device: string
|
device: string
|
||||||
): Promise<AxiosResponse<any, any>> {
|
): Promise<AxiosResponse<any, any>> {
|
||||||
const response = await axios.get(`${BASE}${this._url}/${RACLETTE}`, {
|
const response = await axios.get(`${BASE}${this._url}/${PAGE}`, {
|
||||||
headers: this.getAuthHeader(),
|
headers: this.getAuthHeader(),
|
||||||
params: {
|
params: {
|
||||||
user: user,
|
user: user,
|
||||||
@@ -62,7 +62,7 @@ export class HttpClient {
|
|||||||
device: string
|
device: string
|
||||||
): Promise<AxiosResponse<any, any>> {
|
): Promise<AxiosResponse<any, any>> {
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${BASE}${this._url}/${RACLETTE}`,
|
`${BASE}${this._url}/${PAGE}`,
|
||||||
{
|
{
|
||||||
command: "MEASURE_NEW",
|
command: "MEASURE_NEW",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
<!-- src/components/ChartComponent.vue -->
|
<!-- src/components/ChartComponent.vue -->
|
||||||
<template>
|
<template>
|
||||||
<div class="chart-wrapper">
|
<div v-if="isTimeSeries" class="chart-wrapper">
|
||||||
<div v-if="manager.loading" class="loading-state">Loading data...</div>
|
<Scatter data-cy="main-chart" :data="chartData" :options="chartOptions" />
|
||||||
<div v-else-if="manager.error" class="error-state">{{ manager.error }}</div>
|
|
||||||
<div v-else class="chart-container">
|
|
||||||
<Scatter
|
|
||||||
v-if="chartData.datasets.length > 0"
|
|
||||||
:data="chartData"
|
|
||||||
:options="chartOptions"
|
|
||||||
/>
|
|
||||||
<div v-else class="no-data-state">No data to display</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="no-data-state" data-cy="no-data-display">
|
||||||
|
No data to display
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
</template>
|
</template>
|
||||||
@@ -133,9 +127,17 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isTimeSeries = computed(() => {
|
||||||
|
return (
|
||||||
|
props.manager.series.value &&
|
||||||
|
(props.manager.series.value as Serie[]).length > 0
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chartOptions,
|
chartOptions,
|
||||||
chartData,
|
chartData,
|
||||||
|
isTimeSeries,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="control-panel">
|
<div class="control-panel">
|
||||||
<button @click="newMeasurments" class="action-button">
|
<button @click="newMeasurments" class="action-button" data-cy="new-measurements-button">
|
||||||
New Measurments
|
New Measurments
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button @click="fetchData" class="refresh-button">
|
<button
|
||||||
|
@click="fetchData"
|
||||||
|
class="refresh-button"
|
||||||
|
data-cy="fetch-measurements-button"
|
||||||
|
>
|
||||||
Fetch Measurments
|
Fetch Measurments
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -12,6 +16,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<span>
|
<span>
|
||||||
<multiselect
|
<multiselect
|
||||||
|
data-cy="user-mulitselect"
|
||||||
id="user-select"
|
id="user-select"
|
||||||
v-model="manager.selected_user"
|
v-model="manager.selected_user"
|
||||||
:options="manager.user_options"
|
:options="manager.user_options"
|
||||||
@@ -24,6 +29,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<multiselect
|
<multiselect
|
||||||
|
data-cy="room-mulitselect"
|
||||||
id="room-select"
|
id="room-select"
|
||||||
v-model="manager.selected_room"
|
v-model="manager.selected_room"
|
||||||
:options="manager.room_options"
|
:options="manager.room_options"
|
||||||
@@ -36,6 +42,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<multiselect
|
<multiselect
|
||||||
|
data-cy="device-mulitselect"
|
||||||
id="device-select"
|
id="device-select"
|
||||||
v-model="manager.selected_device"
|
v-model="manager.selected_device"
|
||||||
:options="manager.device_options"
|
:options="manager.device_options"
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
export const APP_NAME = "Home Monitor";
|
export const APP_NAME = "Home Monitor";
|
||||||
|
|
||||||
export const URL = "rest.mse.kb28.ch";
|
export const URL = process.env.VUE_APP_REST_URL;
|
||||||
|
|
||||||
// load environment varaibles - need to have the prefix VUE_APP
|
// load environment varaibles - need to have the prefix VUE_APP
|
||||||
export const USERNAME = process.env.VUE_APP_INFLUXDB_USER;
|
export const USERNAME = process.env.VUE_APP_REST_USER;
|
||||||
export const PASSWORD = process.env.VUE_APP_INFLUXDB_PASSWORD;
|
export const PASSWORD = process.env.VUE_APP_REST_PASSWORD;
|
||||||
|
|
||||||
export const HUMIDITY = "humidity";
|
export const HUMIDITY = "humidity";
|
||||||
export const TEMPERATURE = "temperature";
|
export const TEMPERATURE = "temperature";
|
||||||
@@ -17,5 +17,5 @@ export const DEVICE = "Shed";
|
|||||||
|
|
||||||
export const BASE = "https://";
|
export const BASE = "https://";
|
||||||
export const PING = "ping";
|
export const PING = "ping";
|
||||||
export const RACLETTE = "raclette";
|
export const PAGE = process.env.VUE_APP_REST_PAGE;
|
||||||
export const PONG = "pong";
|
export const PONG = "pong";
|
||||||
+40
-246
@@ -1,274 +1,68 @@
|
|||||||
# End-to-End Testing Documentation for Home Monitor
|
# End-to-End and Unit Testing Documentation for Home Monitor
|
||||||
|
|
||||||
## Table of Contents
|
## Test Description
|
||||||
- [Introduction](#introduction)
|
|
||||||
- [Testing Strategy](#testing-strategy)
|
|
||||||
- [Test Environment Setup](#test-environment-setup)
|
|
||||||
- [Running Tests](#running-tests)
|
|
||||||
- [Test Structure](#test-structure)
|
|
||||||
- [Test Cases Overview](#test-cases-overview)
|
|
||||||
- [Writing New Tests](#writing-new-tests)
|
|
||||||
- [Best Practices](#best-practices)
|
|
||||||
- [Troubleshooting](#troubleshooting)
|
|
||||||
- [References](#references)
|
|
||||||
|
|
||||||
## Introduction
|
The Home Monitor project uses Cypress to automate both End-to-End (E2E) and unit tests. These tests ensure that the main features of the application work correctly, both from the user’s and the developer’s perspectives.
|
||||||
|
|
||||||
End-to-End (E2E) testing verifies that the application functions correctly from a user's perspective by testing the complete application workflow. This document outlines the E2E testing approach for the Home Monitor application.
|
### Types of Tests
|
||||||
|
|
||||||
### Goals of E2E Testing
|
- **End-to-End (E2E) Tests**: Simulate real user behavior on the web interface. They check critical user flows, data display, error handling, and the responsiveness of the application.
|
||||||
|
- **Unit Tests**: Verify the correct functioning of TypeScript components and functions in isolation.
|
||||||
|
|
||||||
- Validate critical user flows function correctly
|
### Test Structure
|
||||||
- Ensure components work together as expected
|
|
||||||
- Detect regression issues before production deployment
|
|
||||||
- Verify application behavior in real-world scenarios
|
|
||||||
|
|
||||||
## Testing Strategy
|
- E2E tests are located in `web-app/cypress/e2e/`
|
||||||
|
- Unit tests are located in `web-app/cypress/unit/`
|
||||||
|
- Custom commands and global setup are in `web-app/cypress/support/`
|
||||||
|
- Cypress configuration is in `web-app/cypress.config.ts`
|
||||||
|
|
||||||
Home Monitor uses Cypress for E2E testing with two primary approaches:
|
## How to Run the Tests
|
||||||
|
|
||||||
1. **Mock Testing (Without Server)**: Fast, reliable tests using mocked DOM elements and data
|
### Install Dependencies
|
||||||
2. **Real Environment Testing (With Server)**: Full application testing with a running development server
|
|
||||||
|
|
||||||
### Mock vs. Real Testing Comparison
|
|
||||||
|
|
||||||
| Aspect | Mock Testing | Real Environment Testing |
|
|
||||||
|--------|-------------|--------------------------|
|
|
||||||
| Speed | Very fast (< 1s per test) | Slower (depends on app loading time) |
|
|
||||||
| Reliability | Highly stable | May be affected by server/network issues |
|
|
||||||
| Coverage | Limited to structure and basic interactions | Tests actual rendering and behaviors |
|
|
||||||
| Dependencies | None (no server needed) | Requires development server |
|
|
||||||
| Best for | CI/CD, quick verification | Final validation, regression testing |
|
|
||||||
|
|
||||||
## Test Environment Setup
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- Node.js (v14+)
|
|
||||||
- npm or yarn
|
|
||||||
- Chrome browser (for visual testing)
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install dependencies
|
cd ./web-app
|
||||||
cd project-softweng/web-app
|
npm install --inlude=dev
|
||||||
npm install
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running Tests
|
### Run unit Tests
|
||||||
|
```bash
|
||||||
|
npm run test:unit
|
||||||
|
```
|
||||||
|
|
||||||
### Option 1: Tests with Mock Environment (No Server)
|
### Run E2E Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run all tests in headless mode
|
|
||||||
export CYPRESS_SKIP_SERVER_CHECK=true
|
|
||||||
npm run test:e2e
|
npm run test:e2e
|
||||||
|
|
||||||
# Run specific test file
|
|
||||||
export CYPRESS_SKIP_SERVER_CHECK=true
|
|
||||||
npx cypress run --spec "tests/e2e/app.spec.ts"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Option 2: Tests with Real Application (Server Required)
|
### Run All Tests
|
||||||
|
|
||||||
#### Automated Script Method
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Use the automated script (starts server, runs tests, stops server)
|
npm run test
|
||||||
./tests/e2e/run-e2e-tests.sh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Manual Method
|
### Note
|
||||||
|
The cypress GUI can be launched with the command:
|
||||||
1. Start the development server:
|
|
||||||
```bash
|
|
||||||
# Terminal 1
|
|
||||||
npm run serve
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Run the tests:
|
|
||||||
```bash
|
|
||||||
# Terminal 2
|
|
||||||
# Headless mode
|
|
||||||
npx cypress run --spec "tests/e2e/app.spec.ts"
|
|
||||||
|
|
||||||
# Interactive mode
|
|
||||||
npx cypress open
|
|
||||||
```
|
|
||||||
|
|
||||||
### Interactive Mode (Development)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run cypress:open
|
npx cypress open
|
||||||
```
|
```
|
||||||
|
|
||||||
This opens the Cypress Test Runner where you can:
|
## Test Coverage
|
||||||
- Select individual tests to run
|
|
||||||
- See test execution in real-time
|
|
||||||
- Debug failing tests with time-travel debugging
|
|
||||||
|
|
||||||
## Test Structure
|
### End-to-End (E2E) Tests
|
||||||
|
|
||||||
### Directory Structure
|
- **Fetch Measurements Button**:
|
||||||
|
- Verifies that the "Fetch Measurements" button is visible and functional on the main page.
|
||||||
|
- Tests fetching timeseries data with a valid user-room-device selection, ensuring the main chart is displayed and no "no data" message appears.
|
||||||
|
- Tests fetching timeseries data with an invalid user-room-device selection (e.g., changing the user to "Sylvan"), ensuring the main chart is not displayed and the "no data" message is shown.
|
||||||
|
|
||||||
```
|
#### Important Notes
|
||||||
web-app/
|
The E2E tests use a web-app development image deployed on https://app.mse.kb28.ch/ for the moment. Because there is a problem with the service image in the CI. The environment variables cannot be set correctly to the job's service. An issue is opened to fix this problem: [#37].
|
||||||
├── tests/
|
|
||||||
│ ├── e2e/ # E2E test files
|
|
||||||
│ │ ├── app.spec.ts # Main application tests
|
|
||||||
│ │ └── run-e2e-tests.sh # Script for running tests with server
|
|
||||||
│ └── support/ # Support files
|
|
||||||
│ ├── commands.ts # Custom Cypress commands
|
|
||||||
│ └── e2e.ts # Global setup for E2E tests
|
|
||||||
└── cypress.config.ts # Cypress configuration
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test Files Organization
|
### Unit Tests
|
||||||
|
|
||||||
Each test file follows this structure:
|
- **Serie Component**:
|
||||||
1. **Setup**: Import dependencies and set up the test environment
|
- Checks that a `Serie` instance initializes with the correct properties.
|
||||||
2. **Beforehooks**: Prepare the application state before each test
|
- Validates that the correct label is returned for temperature and humidity types.
|
||||||
3. **Test Cases**: Individual test scenarios grouped by feature
|
- Ensures the label "Unknown" is returned for unknown types.
|
||||||
4. **Helper Functions**: Support functions for test cases
|
- Verifies that temperature and humidity data are formatted correctly by `getSerie()`, including label, color, and data structure.
|
||||||
|
- Ensures unknown types are handled appropriately in `getSerie()`, defaulting to the correct color and axis.
|
||||||
## Test Cases Overview
|
|
||||||
|
|
||||||
The E2E test suite covers the following scenarios:
|
|
||||||
|
|
||||||
1. **Application Loading**
|
|
||||||
- Verify application loads with correct title
|
|
||||||
- Confirm main layout sections are visible
|
|
||||||
|
|
||||||
2. **Control Panel Functionality**
|
|
||||||
- Verify control panel buttons are present
|
|
||||||
- Test "Fetch Measurements" button works
|
|
||||||
- Test "New Measurements" button works
|
|
||||||
|
|
||||||
3. **Filtering Controls**
|
|
||||||
- Test user selection dropdown
|
|
||||||
- Test room selection dropdown
|
|
||||||
- Test device selection dropdown
|
|
||||||
|
|
||||||
4. **Data Visualization**
|
|
||||||
- Verify chart displays correctly
|
|
||||||
- Test chart updates when data changes
|
|
||||||
|
|
||||||
5. **Error Handling**
|
|
||||||
- Test error state display
|
|
||||||
- Test empty data state display
|
|
||||||
|
|
||||||
6. **Responsive Design**
|
|
||||||
- Verify layout adapts to desktop viewport
|
|
||||||
- Verify layout adapts to mobile viewport
|
|
||||||
|
|
||||||
## Writing New Tests
|
|
||||||
|
|
||||||
### Adding a New Test Case
|
|
||||||
|
|
||||||
1. Identify the feature or flow to test
|
|
||||||
2. Determine the expected behavior
|
|
||||||
3. Add a new test case to the appropriate spec file:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
it('should [describe expected behavior]', () => {
|
|
||||||
// Setup any preconditions
|
|
||||||
|
|
||||||
// Perform actions
|
|
||||||
|
|
||||||
// Assert expected outcomes
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom Commands
|
|
||||||
|
|
||||||
Custom commands are available to simplify test writing:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Select an option from a multiselect dropdown
|
|
||||||
cy.selectMultiselectOption('user-select', 'user1');
|
|
||||||
|
|
||||||
// Wait for chart to load and be visible
|
|
||||||
cy.waitForChart();
|
|
||||||
|
|
||||||
// Check application loading state
|
|
||||||
cy.checkLoadingState(false);
|
|
||||||
```
|
|
||||||
|
|
||||||
### API Mocking
|
|
||||||
|
|
||||||
Use Cypress's intercept feature to mock API responses:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Mock GET request
|
|
||||||
cy.intercept('GET', '**/api/measurements*', {
|
|
||||||
statusCode: 200,
|
|
||||||
body: {
|
|
||||||
data: [
|
|
||||||
{ time: new Date(2023, 0, 1, 10, 0).getTime(), value: 22.5, type: 'temperature' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}).as('getMeasurements');
|
|
||||||
|
|
||||||
// Wait for the intercepted request
|
|
||||||
cy.wait('@getMeasurements');
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
1. **Test Independence**
|
|
||||||
- Each test should be able to run independently
|
|
||||||
- Avoid dependencies between tests
|
|
||||||
- Reset state between tests
|
|
||||||
|
|
||||||
2. **Selector Strategy**
|
|
||||||
- Prefer data attributes for test selectors (e.g., `data-cy`, `data-testid`)
|
|
||||||
- Avoid using CSS classes that might change with styling updates
|
|
||||||
- Establish a consistent selector naming convention
|
|
||||||
|
|
||||||
3. **Handling Asynchronous Operations**
|
|
||||||
- Use explicit waits rather than arbitrary timeouts
|
|
||||||
- Wait for specific elements or network requests rather than fixed delays
|
|
||||||
- Handle loading states appropriately
|
|
||||||
|
|
||||||
4. **Test Data Management**
|
|
||||||
- Use consistent test data
|
|
||||||
- Mock external dependencies
|
|
||||||
- Consider using fixtures for complex data structures
|
|
||||||
|
|
||||||
5. **Error Handling**
|
|
||||||
- Add proper error handling in tests
|
|
||||||
- Use `Cypress.on('uncaught:exception')` for expected application errors
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues and Solutions
|
|
||||||
|
|
||||||
| Issue | Solution |
|
|
||||||
|-------|----------|
|
|
||||||
| Tests fail with 401 errors | Add `Cypress.on('uncaught:exception', () => false)` to handle authentication errors |
|
|
||||||
| Elements not found | Increase timeouts or check selectors; ensure elements are in the DOM |
|
|
||||||
| Click actions failing | Use `{ force: true }` option if elements might be covered by overlays |
|
|
||||||
| Tests passing locally but failing in CI | Check environment differences; ensure CI has all required dependencies |
|
|
||||||
| Timeouts on waiting for elements | Increase `defaultCommandTimeout` in cypress.config.ts |
|
|
||||||
|
|
||||||
### Debugging Strategies
|
|
||||||
|
|
||||||
1. **Use Cypress's Debug Tools**
|
|
||||||
- Add `.debug()` to pause execution at a specific point
|
|
||||||
- Use the time-travel debugger in interactive mode
|
|
||||||
|
|
||||||
2. **Add Logging**
|
|
||||||
- Use `cy.log()` to add informative messages in the test
|
|
||||||
- Check browser console for application errors
|
|
||||||
|
|
||||||
3. **Visualize Test State**
|
|
||||||
- Enable screenshots and videos for failed tests
|
|
||||||
- Use `cy.screenshot()` at critical points
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Cypress Documentation](https://docs.cypress.io/)
|
|
||||||
- [Vue Test Utils](https://vue-test-utils.vuejs.org/)
|
|
||||||
- [Testing Library](https://testing-library.com/)
|
|
||||||
- [Cypress Best Practices](https://docs.cypress.io/guides/references/best-practices)
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
import { TEMPERATURE, HUMIDITY } from "../../src/const";
|
|
||||||
|
|
||||||
describe('Home Monitor Application (E2E)', () => {
|
|
||||||
// Prevent tests from failing when app throws uncaught exceptions (like 401 errors)
|
|
||||||
Cypress.on('uncaught:exception', (err) => {
|
|
||||||
// Returning false prevents Cypress from failing the test
|
|
||||||
console.log('Ignoring uncaught exception:', err.message);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// Visit the application running on the development server
|
|
||||||
cy.visit('/');
|
|
||||||
|
|
||||||
// Mock ALL API calls to avoid authentication issues
|
|
||||||
cy.intercept('GET', '**/rest.mse.kb28.ch/**', (req) => {
|
|
||||||
// Generic mock for all GET requests to the API server
|
|
||||||
const mockData = {
|
|
||||||
data: [
|
|
||||||
{ time: new Date(2023, 0, 1, 10, 0).getTime(), value: 22.5, type: 'temperature' },
|
|
||||||
{ time: new Date(2023, 0, 1, 10, 30).getTime(), value: 23.8, type: 'temperature' },
|
|
||||||
{ time: new Date(2023, 0, 1, 11, 0).getTime(), value: 24.2, type: 'temperature' },
|
|
||||||
{ time: new Date(2023, 0, 1, 10, 0).getTime(), value: 45, type: 'humidity' },
|
|
||||||
{ time: new Date(2023, 0, 1, 10, 30).getTime(), value: 48, type: 'humidity' },
|
|
||||||
{ time: new Date(2023, 0, 1, 11, 0).getTime(), value: 51, type: 'humidity' }
|
|
||||||
],
|
|
||||||
users: ['user1', 'user2'],
|
|
||||||
rooms: ['living-room', 'bedroom', 'kitchen'],
|
|
||||||
devices: ['sensor1', 'sensor2', 'sensor3']
|
|
||||||
};
|
|
||||||
req.reply({ status: 200, body: mockData });
|
|
||||||
}).as('apiGetRequest');
|
|
||||||
|
|
||||||
// More specific intercept for measurements endpoint
|
|
||||||
cy.intercept('GET', '**/api/measurements*', {
|
|
||||||
statusCode: 200,
|
|
||||||
body: {
|
|
||||||
data: [
|
|
||||||
{ time: new Date(2023, 0, 1, 10, 0).getTime(), value: 22.5, type: 'temperature' },
|
|
||||||
{ time: new Date(2023, 0, 1, 10, 30).getTime(), value: 23.8, type: 'temperature' },
|
|
||||||
{ time: new Date(2023, 0, 1, 11, 0).getTime(), value: 24.2, type: 'temperature' },
|
|
||||||
{ time: new Date(2023, 0, 1, 10, 0).getTime(), value: 45, type: 'humidity' },
|
|
||||||
{ time: new Date(2023, 0, 1, 10, 30).getTime(), value: 48, type: 'humidity' },
|
|
||||||
{ time: new Date(2023, 0, 1, 11, 0).getTime(), value: 51, type: 'humidity' }
|
|
||||||
],
|
|
||||||
users: ['user1', 'user2'],
|
|
||||||
rooms: ['living-room', 'bedroom', 'kitchen'],
|
|
||||||
devices: ['sensor1', 'sensor2', 'sensor3']
|
|
||||||
}
|
|
||||||
}).as('getMeasurements');
|
|
||||||
|
|
||||||
// Mock all POST requests to avoid auth issues
|
|
||||||
cy.intercept('POST', '**/rest.mse.kb28.ch/**', {
|
|
||||||
statusCode: 200,
|
|
||||||
body: { success: true }
|
|
||||||
}).as('apiPostRequest');
|
|
||||||
|
|
||||||
// More specific intercept for new measurements endpoint
|
|
||||||
cy.intercept('POST', '**/api/measurements*', {
|
|
||||||
statusCode: 200,
|
|
||||||
body: { success: true }
|
|
||||||
}).as('postNewMeasurement');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should load the application with correct title', () => {
|
|
||||||
// Check page title
|
|
||||||
cy.contains('h1', 'Home Monitor').should('be.visible');
|
|
||||||
|
|
||||||
// Verify main layout sections are present
|
|
||||||
cy.get('.sidebar').should('be.visible');
|
|
||||||
cy.get('.chart-area').should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should display the control panel with all interactive elements', () => {
|
|
||||||
// Verify control panel buttons
|
|
||||||
cy.contains('button', 'New Measurments').should('be.visible');
|
|
||||||
cy.contains('button', 'Fetch Measurments').should('be.visible');
|
|
||||||
|
|
||||||
// Verify dropdowns
|
|
||||||
cy.get('#user-select').should('exist');
|
|
||||||
cy.get('#room-select').should('exist');
|
|
||||||
cy.get('#device-select').should('exist');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should load and display chart with data', () => {
|
|
||||||
// We can't reliably wait for specific API calls, so we'll give the app time to load
|
|
||||||
cy.wait(1000);
|
|
||||||
|
|
||||||
// Verify the app structure is loaded
|
|
||||||
cy.get('.app-container').should('be.visible');
|
|
||||||
|
|
||||||
// Chart.js creates a canvas element, which is difficult to test in detail
|
|
||||||
// But we can verify that the container exists
|
|
||||||
cy.get('.chart-area').should('be.visible');
|
|
||||||
cy.get('.chart-container').should('exist');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fetch new data when "Fetch Measurements" is clicked', () => {
|
|
||||||
// Give the app time to stabilize
|
|
||||||
cy.wait(500);
|
|
||||||
|
|
||||||
// Click the fetch button (force: true to bypass overlay issues)
|
|
||||||
cy.contains('button', 'Fetch Measurments').click({ force: true });
|
|
||||||
|
|
||||||
// Give the app time to process the click
|
|
||||||
cy.wait(500);
|
|
||||||
|
|
||||||
// Verify the app is still responsive
|
|
||||||
cy.get('.app-container').should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should request new measurements when "New Measurements" is clicked', () => {
|
|
||||||
// Click the New Measurements button (force: true to bypass overlay issues)
|
|
||||||
cy.contains('button', 'New Measurments').click({ force: true });
|
|
||||||
|
|
||||||
// Give the app time to process the click
|
|
||||||
cy.wait(500);
|
|
||||||
|
|
||||||
// Verify the app is still responsive
|
|
||||||
cy.get('.app-container').should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow selecting different users from dropdown', () => {
|
|
||||||
// Give the app time to load
|
|
||||||
cy.wait(500);
|
|
||||||
|
|
||||||
// Test if user-select exists
|
|
||||||
cy.get('#user-select').should('exist');
|
|
||||||
|
|
||||||
// We can't reliably test multiselect component in this environment,
|
|
||||||
// so we'll just verify that the component exists
|
|
||||||
cy.log('User selection dropdown is present in the DOM');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow selecting different rooms from dropdown', () => {
|
|
||||||
// Give the app time to load
|
|
||||||
cy.wait(500);
|
|
||||||
|
|
||||||
// Test if room-select exists
|
|
||||||
cy.get('#room-select').should('exist');
|
|
||||||
|
|
||||||
// We can't reliably test multiselect component in this environment,
|
|
||||||
// so we'll just verify that the component exists
|
|
||||||
cy.log('Room selection dropdown is present in the DOM');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow selecting different devices from dropdown', () => {
|
|
||||||
// Give the app time to load
|
|
||||||
cy.wait(500);
|
|
||||||
|
|
||||||
// Test if device-select exists
|
|
||||||
cy.get('#device-select').should('exist');
|
|
||||||
|
|
||||||
// We can't reliably test multiselect component in this environment,
|
|
||||||
// so we'll just verify that the component exists
|
|
||||||
cy.log('Device selection dropdown is present in the DOM');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle error states appropriately', () => {
|
|
||||||
// In a real test, we would check error states
|
|
||||||
// Here we're just verifying the app structure
|
|
||||||
cy.get('.app-container').should('be.visible');
|
|
||||||
|
|
||||||
// We need to skip this test for now since we can't reliably create error states
|
|
||||||
// in this environment due to the authentication issues
|
|
||||||
cy.log('Error state testing is skipped in this environment');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle empty data appropriately', () => {
|
|
||||||
// In a real test, we would check empty data states
|
|
||||||
// Here we're just verifying the app structure
|
|
||||||
cy.get('.app-container').should('be.visible');
|
|
||||||
|
|
||||||
// We need to skip this test for now since we can't reliably create empty data states
|
|
||||||
// in this environment due to the authentication issues
|
|
||||||
cy.log('Empty data state testing is skipped in this environment');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have responsive layout that adapts to different screen sizes', () => {
|
|
||||||
// Test responsive behavior at desktop size
|
|
||||||
cy.viewport(1200, 800);
|
|
||||||
cy.get('.main-content').should('have.css', 'grid-template-columns')
|
|
||||||
.and('not.eq', '1fr');
|
|
||||||
|
|
||||||
// Test at mobile size
|
|
||||||
cy.viewport(600, 800);
|
|
||||||
|
|
||||||
// On mobile, the layout should change, but we can't reliably test CSS in this environment
|
|
||||||
// Instead, we'll just verify that the elements still exist at the different viewport size
|
|
||||||
cy.get('.sidebar').should('exist');
|
|
||||||
cy.get('.chart-area').should('exist');
|
|
||||||
cy.log('Responsive layout verified at mobile viewport size');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
// ***********************************************
|
|
||||||
// This example commands.ts shows you how to
|
|
||||||
// create various custom commands and overwrite
|
|
||||||
// existing commands.
|
|
||||||
//
|
|
||||||
// For more comprehensive examples of custom
|
|
||||||
// commands please read more here:
|
|
||||||
// https://on.cypress.io/custom-commands
|
|
||||||
// ***********************************************
|
|
||||||
|
|
||||||
// -- This is a parent command --
|
|
||||||
Cypress.Commands.add('login', (email, password) => {
|
|
||||||
// Implementation example for a login command
|
|
||||||
cy.visit('/login')
|
|
||||||
cy.get('[data-cy=email]').type(email)
|
|
||||||
cy.get('[data-cy=password]').type(password)
|
|
||||||
cy.get('[data-cy=submit]').click()
|
|
||||||
})
|
|
||||||
|
|
||||||
// -- This is a child command --
|
|
||||||
Cypress.Commands.add('getByDataCy', { prevSubject: 'element' }, (subject, value) => {
|
|
||||||
return cy.wrap(subject).find(`[data-cy=${value}]`)
|
|
||||||
})
|
|
||||||
|
|
||||||
// -- This is a dual command --
|
|
||||||
Cypress.Commands.add('getByTestId', { prevSubject: 'optional' }, (subject, value) => {
|
|
||||||
return subject
|
|
||||||
? cy.wrap(subject).find(`[data-testid=${value}]`)
|
|
||||||
: cy.get(`[data-testid=${value}]`)
|
|
||||||
})
|
|
||||||
|
|
||||||
// -- Example command for chart testing --
|
|
||||||
Cypress.Commands.add('getChartCanvas', (selector = 'canvas') => {
|
|
||||||
return cy.get(selector)
|
|
||||||
})
|
|
||||||
|
|
||||||
// -- Command to select an option from a multiselect dropdown --
|
|
||||||
Cypress.Commands.add('selectMultiselectOption', (selectId, optionText) => {
|
|
||||||
cy.get(`#${selectId}`).click()
|
|
||||||
cy.get('.multiselect__content-wrapper').contains(optionText).click()
|
|
||||||
})
|
|
||||||
|
|
||||||
// -- Command to wait for chart to load and be visible --
|
|
||||||
Cypress.Commands.add('waitForChart', () => {
|
|
||||||
cy.get('.loading-state').should('not.exist')
|
|
||||||
cy.get('.chart-container canvas').should('be.visible')
|
|
||||||
})
|
|
||||||
|
|
||||||
// -- Command to check app loading state --
|
|
||||||
Cypress.Commands.add('checkLoadingState', (isLoading) => {
|
|
||||||
if (isLoading) {
|
|
||||||
cy.get('.loading-state').should('be.visible')
|
|
||||||
} else {
|
|
||||||
cy.get('.loading-state').should('not.exist')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Declare the types for the custom commands
|
|
||||||
declare global {
|
|
||||||
namespace Cypress {
|
|
||||||
interface Chainable {
|
|
||||||
login(email: string, password: string): Chainable<void>
|
|
||||||
getByDataCy(value: string): Chainable<JQuery<HTMLElement>>
|
|
||||||
getByTestId(value: string): Chainable<JQuery<HTMLElement>>
|
|
||||||
getChartCanvas(selector?: string): Chainable<JQuery<HTMLElement>>
|
|
||||||
selectMultiselectOption(selectId: string, optionText: string): Chainable<void>
|
|
||||||
waitForChart(): Chainable<void>
|
|
||||||
checkLoadingState(isLoading: boolean): Chainable<void>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
// Import cypress commands
|
|
||||||
import "./commands";
|
|
||||||
|
|
||||||
// Add Cypress types
|
|
||||||
declare global {
|
|
||||||
namespace Cypress {
|
|
||||||
interface Chainable {
|
|
||||||
// Add custom commands here if needed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
# Test Documentation
|
|
||||||
|
|
||||||
This document describes the automated tests implemented for the Home Monitor project. The tests are written with Cypress and are organized into two distinct categories: unit tests and end-to-end (E2E) tests.
|
|
||||||
|
|
||||||
## Test Types and Structure
|
|
||||||
|
|
||||||
The tests are organized in separate folders according to their type:
|
|
||||||
|
|
||||||
### 1. Unit Tests (`tests/unit/`)
|
|
||||||
|
|
||||||
Unit tests focus on testing individual components or classes in isolation, without dependencies on other parts of the system.
|
|
||||||
|
|
||||||
- **Example**: `Serie.spec.ts` - Tests the `Serie` class methods and functionality in isolation
|
|
||||||
- **Purpose**: Verify that individual components work correctly on their own
|
|
||||||
- **Characteristics**: Fast, focused, and test a single unit of code
|
|
||||||
|
|
||||||
### 2. End-to-End Tests (`tests/e2e/`)
|
|
||||||
|
|
||||||
E2E tests simulate real user scenarios by testing the entire application flow from start to finish.
|
|
||||||
|
|
||||||
- **Example**: `app.spec.ts` - Tests complete user workflows like navigating the application and interacting with charts
|
|
||||||
- **Purpose**: Verify that the entire application works as expected from a user's perspective
|
|
||||||
- **Characteristics**: Most comprehensive, test the entire system, slower than other test types
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Tests use a custom configuration in `cypress.unit.js` that allows different test types to run with appropriate settings. Key features:
|
|
||||||
|
|
||||||
- Unit tests run without isolation (can share context)
|
|
||||||
- E2E tests run with isolation (fresh environment for each test)
|
|
||||||
- All tests can run without requiring a server (baseUrl is set to null)
|
|
||||||
- Optimized settings for performance (no videos, screenshots disabled by default)
|
|
||||||
|
|
||||||
## Running Tests
|
|
||||||
|
|
||||||
### Running Specific Test Types
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run unit tests
|
|
||||||
npm run test:unit
|
|
||||||
|
|
||||||
# Run E2E tests
|
|
||||||
npm run test:e2e
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running All Tests
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run test
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using Cypress GUI
|
|
||||||
|
|
||||||
To run tests through the Cypress graphical interface:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run cypress:open
|
|
||||||
```
|
|
||||||
|
|
||||||
This method is particularly useful during development, as it allows you to:
|
|
||||||
- View tests in real-time
|
|
||||||
- Inspect DOM elements
|
|
||||||
- Replay individual tests
|
|
||||||
- See a detailed overview of the executed steps
|
|
||||||
|
|
||||||
## Detailed Test Examples
|
|
||||||
|
|
||||||
### Unit Tests for Serie Class
|
|
||||||
|
|
||||||
The unit tests (`tests/unit/Serie.spec.ts`) verify that the Serie class works correctly in isolation:
|
|
||||||
|
|
||||||
- **Initialization**: Tests that the class initializes with the correct properties
|
|
||||||
- **Label Generation**: Tests that the `getLabel()` method returns the correct label based on series type
|
|
||||||
- **Data Formatting**: Tests that the `getSerie()` method correctly formats data for the chart library
|
|
||||||
|
|
||||||
### E2E Tests for Application
|
|
||||||
|
|
||||||
The E2E tests (`tests/e2e/app.spec.ts`) verify complete user workflows:
|
|
||||||
|
|
||||||
- **Application Loading**: Tests that the application loads correctly
|
|
||||||
- **Data Display**: Tests that temperature and humidity data are displayed properly
|
|
||||||
- **Filtering**: Tests that users can filter data by date range
|
|
||||||
- **Room Selection**: Tests that users can switch between different rooms
|
|
||||||
- **Error Handling**: Tests that errors are handled gracefully
|
|
||||||
|
|
||||||
## Test Data Structure
|
|
||||||
|
|
||||||
The tests use mock data in the following format:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const mockData = [
|
|
||||||
{ time: 1625097600000, value: new Date(22.5) },
|
|
||||||
{ time: 1625184000000, value: new Date(23.8) }
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
Where:
|
|
||||||
- `time`: Unix timestamp in milliseconds
|
|
||||||
- `value`: Value encapsulated in a Date object (according to the current implementation)
|
|
||||||
|
|
||||||
## Special Note on Date Usage
|
|
||||||
|
|
||||||
The `Serie` class uses `Date` objects to store temperature and humidity values, which is a unique approach. In the test code, we create Date objects by directly passing the numeric values to the constructor:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
new Date(22.5) // For a temperature of 22.5°C
|
|
||||||
new Date(45) // For a humidity of 45%
|
|
||||||
```
|
|
||||||
|
|
||||||
This approach is specific to the current implementation of the `Serie` class and might be modified in the future to directly use numeric values.
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
## Test Organization
|
|
||||||
|
|
||||||
1. **Test Hierarchy**:
|
|
||||||
- Unit tests -> E2E tests
|
|
||||||
- Tests should get progressively more comprehensive
|
|
||||||
- Most of your tests should be unit tests (faster and more focused)
|
|
||||||
|
|
||||||
2. **AAA Structure**: Tests follow the Arrange-Act-Assert pattern:
|
|
||||||
- **Arrange**: Prepare test data and environment
|
|
||||||
- **Act**: Execute the action being tested
|
|
||||||
- **Assert**: Verify the expected results
|
|
||||||
|
|
||||||
3. **Isolation**: Each test should be independent and not depend on shared state
|
|
||||||
|
|
||||||
### Test Troubleshooting
|
|
||||||
|
|
||||||
If tests fail, check the following:
|
|
||||||
|
|
||||||
1. **Server Issues**: If you're running E2E tests that require a server, make sure it's started on the appropriate port
|
|
||||||
|
|
||||||
2. **API Changes**: If the classes or components are modified, tests must be updated accordingly
|
|
||||||
|
|
||||||
3. **Synchronization Issues**: Cypress might need additional time for certain operations
|
|
||||||
|
|
||||||
4. **Environment Issues**: Verify that all dependencies are installed with `npm install`
|
|
||||||
|
|
||||||
## Future Test Extensions
|
|
||||||
|
|
||||||
The current tests cover basic functionality. In the future, it would be useful to add:
|
|
||||||
|
|
||||||
1. More unit tests for all components and services
|
|
||||||
2. More comprehensive E2E tests covering all user scenarios
|
|
||||||
3. Performance tests for operations on large data sets
|
|
||||||
Reference in new issue
Block a user