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:
Yann Sierro committed 2025-06-24 16:12:01 +00:00
commit 9967aef1f5
23 files changed
+243 -808

No files matched your search

+2
View File
@@ -8,3 +8,5 @@ MQTT_USERNAME=
MQTT_PASSWORD= MQTT_PASSWORD=
REST_USERNAME= REST_USERNAME=
REST_PASSWORD= REST_PASSWORD=
REST_URL=
REST_PAGE=
+4 -2
View File
@@ -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"
+45 -42
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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"> ![Class Diagram](docs/class-diagramm-web-app.svg)
### 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).\
+6 -15
View File
@@ -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",
}, },
}); });
-40
View File
@@ -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',
},
});
+33
View File
@@ -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");
});
});
+37
View File
@@ -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>
// }
// }
// }
+17
View File
@@ -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"
+2 -2
View File
@@ -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": {
+2 -6
View File
@@ -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 = [];
}); });
} }
+3 -3
View File
@@ -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",
}, },
+12 -10
View File
@@ -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,
}; };
}, },
}); });
+9 -2
View File
@@ -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"
+4 -4
View File
@@ -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
View File
@@ -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)
-196
View File
@@ -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');
});
});
-73
View File
@@ -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 {}
-11
View File
@@ -1,11 +0,0 @@
// Import cypress commands
import "./commands";
// Add Cypress types
declare global {
namespace Cypress {
interface Chainable {
// Add custom commands here if needed
}
}
}
-146
View File
@@ -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