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:
@@ -8,3 +8,5 @@ MQTT_USERNAME=
|
||||
MQTT_PASSWORD=
|
||||
REST_USERNAME=
|
||||
REST_PASSWORD=
|
||||
REST_URL=
|
||||
REST_PAGE=
|
||||
|
||||
@@ -109,8 +109,10 @@ services:
|
||||
- "8080:8080"
|
||||
|
||||
environment:
|
||||
- VUE_APP_INFLUXDB_USER=$REST_USERNAME
|
||||
- VUE_APP_INFLUXDB_PASSWORD=$REST_PASSWORD
|
||||
- VUE_APP_REST_USER=$REST_USERNAME
|
||||
- VUE_APP_REST_PASSWORD=$REST_PASSWORD
|
||||
- VUE_APP_REST_URL=$REST_URL
|
||||
- VUE_APP_REST_PAGE=$REST_PAGE
|
||||
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
|
||||
@@ -1,66 +1,69 @@
|
||||
@startuml web-app
|
||||
|
||||
skinparam linetype ortho
|
||||
skinparam nodesep 50
|
||||
|
||||
class App {
|
||||
' principale vuejs component
|
||||
|
||||
}
|
||||
|
||||
class Client {
|
||||
+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 {
|
||||
class ChartComponent {
|
||||
' 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 {
|
||||
' contains table of measure
|
||||
' convert the table to graphs format
|
||||
-type
|
||||
-values
|
||||
-numberOfLastValus
|
||||
-id
|
||||
-place
|
||||
-name
|
||||
-data
|
||||
-user
|
||||
-room
|
||||
-device
|
||||
|
||||
+getGraphFormat()
|
||||
' needs to limit the number of value
|
||||
+addNewValue()
|
||||
}
|
||||
|
||||
class Plot {
|
||||
+getLabel()
|
||||
+getSerie()
|
||||
|
||||
}
|
||||
|
||||
App *-r- SeriesManager
|
||||
|
||||
App *-r- TimeSeriesManager
|
||||
App *-u- Client
|
||||
SeriesManager *-r- TimeSeries
|
||||
Plot -l-* TimeSeries
|
||||
Serie -u-* TimeSeries
|
||||
Serie *-- Button
|
||||
SerieConverter .u.> SeriesManager
|
||||
App *-d- ChartComponent
|
||||
App *-d- ControlPanel
|
||||
TimeSeriesManager *---r--- Serie
|
||||
|
||||
|
||||
|
||||
|
||||
@enduml
|
||||
1
docs/class-diagramm-web-app.svg
Normal file
1
docs/class-diagramm-web-app.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 15 KiB |
@@ -2,6 +2,11 @@ variables:
|
||||
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
|
||||
|
||||
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:
|
||||
image: $DOCKER_IMAGE
|
||||
|
||||
@@ -35,9 +40,10 @@ web-app-unit-tests:
|
||||
web-app-e2e-tests:
|
||||
image: $NODE_IMAGE
|
||||
stage: web-app-tests
|
||||
services:
|
||||
- name: $DOCKER_IMAGE
|
||||
alias: app
|
||||
|
||||
# services:
|
||||
# - name: $DOCKER_IMAGE
|
||||
# alias: app
|
||||
script:
|
||||
- echo "Running e2e tests"
|
||||
- apt-get update
|
||||
@@ -45,6 +51,6 @@ web-app-e2e-tests:
|
||||
- cd ./web-app
|
||||
- npm install --include=dev
|
||||
- echo "Wait web-app app is running"
|
||||
- for i in {1..6}; do curl -f http://app:8080 && break || sleep 5; done
|
||||
- echo "Flask app is running"
|
||||
- for i in {1..6}; do curl -f https://app.mse.kb28.ch/ && break || sleep 5; done
|
||||
- echo "App is running"
|
||||
- npm run test:e2e
|
||||
|
||||
@@ -39,8 +39,10 @@ Setup the environment:
|
||||
|
||||
linux:
|
||||
```terminal
|
||||
export VUE_APP_API_KEY=your_api_key
|
||||
export VUE_APP_API_SECRET=your_api_secret
|
||||
export VUE_APP_REST_USER=
|
||||
export VUE_APP_REST_PASSWORD=
|
||||
export VUE_APP_REST_URL=
|
||||
export VUE_APP_REST_PAGE=
|
||||
```
|
||||
windows:
|
||||
```terminal
|
||||
@@ -58,6 +60,11 @@ npm run serve
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Documentation
|
||||
### Class diagram
|
||||
|
||||
<p align="center"> 
|
||||
|
||||
### Links
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).\
|
||||
See [Vue.js](https://vuejs.org/guide/introduction.html).\
|
||||
|
||||
@@ -2,21 +2,12 @@ import { defineConfig } from "cypress";
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
specPattern: "tests/e2e/*.spec.ts",
|
||||
baseUrl: "http://localhost:8080",
|
||||
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,
|
||||
setupNodeEvents(on, config) {
|
||||
// implement node event listeners here
|
||||
},
|
||||
component: {
|
||||
devServer: {
|
||||
framework: "vue",
|
||||
bundler: "webpack",
|
||||
},
|
||||
specPattern: "src/**/*.cy.ts",
|
||||
specPattern: [
|
||||
"cypress/e2e/*.cy.{js,jsx,ts,tsx}",
|
||||
"cypress/unit/*.cy.{js,jsx,ts,tsx}",
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
web-app/cypress/e2e/fetch_process.cy.ts
Normal file
33
web-app/cypress/e2e/fetch_process.cy.ts
Normal 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
web-app/cypress/support/commands.ts
Normal file
37
web-app/cypress/support/commands.ts
Normal 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
web-app/cypress/support/e2e.ts
Normal file
17
web-app/cypress/support/e2e.ts
Normal 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
|
||||
const mockDataTemperature = [
|
||||
{ time: 1625097600000, value: new Date(22.5) },
|
||||
{ time: 1625184000000, value: new Date(23.8) }
|
||||
{ time: 1625184000000, value: new Date(23.8) },
|
||||
];
|
||||
|
||||
const mockDataHumidity = [
|
||||
{ time: 1625097600000, value: new Date(45) },
|
||||
{ time: 1625184000000, value: new Date(48) }
|
||||
{ time: 1625184000000, value: new Date(48) },
|
||||
];
|
||||
|
||||
it("should initialize with correct properties", () => {
|
||||
@@ -69,7 +69,10 @@ describe("Serie Component Tests", () => {
|
||||
it("should format temperature data correctly with getSerie()", () => {
|
||||
const serie = new Serie(
|
||||
TEMPERATURE,
|
||||
mockDataTemperature.map(item => ({ time: item.time, value: new Date(item.value) })),
|
||||
mockDataTemperature.map((item) => ({
|
||||
time: item.time,
|
||||
value: new Date(item.value),
|
||||
})),
|
||||
"user1",
|
||||
"bedroom",
|
||||
"sensor1"
|
||||
@@ -7,8 +7,8 @@
|
||||
"build": "vue-cli-service build",
|
||||
"cypress:open": "cypress open",
|
||||
"cypress:run": "cypress run",
|
||||
"test:unit": "cypress run --config-file cypress.unit.js --spec 'tests/unit/*.spec.ts'",
|
||||
"test:e2e": "cypress run --spec 'tests/e2e/*.spec.ts'",
|
||||
"test:e2e": "cypress run --spec 'cypress/e2e/*.cy.ts'",
|
||||
"test:unit": "cypress run --spec 'cypress/unit/*.cy.ts'",
|
||||
"test": "npm run test:unit && npm run test:e2e"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Prop } from "vue";
|
||||
import { Serie } from "./Serie";
|
||||
import { HttpClient } from "../Services/HttpClient";
|
||||
import { TEMPERATURE, HUMIDITY, TYPE, VALUE } from "../const";
|
||||
import { TEMPERATURE, HUMIDITY, TYPE } from "../const";
|
||||
import { ref } from "vue";
|
||||
|
||||
export class TimeSeriesManager {
|
||||
@@ -57,8 +56,6 @@ export class TimeSeriesManager {
|
||||
},
|
||||
];
|
||||
|
||||
error = ref(false);
|
||||
loading = ref("");
|
||||
|
||||
series = ref<Serie[]>([]);
|
||||
|
||||
@@ -125,12 +122,11 @@ export class TimeSeriesManager {
|
||||
this.selected_room.room,
|
||||
this.selected_device.device
|
||||
);
|
||||
|
||||
this.series.value = [temperatureSerie, humiditySerie];
|
||||
})
|
||||
.catch((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 { BASE, PING, RACLETTE, PONG } from "../const";
|
||||
import { BASE, PING, PAGE, PONG } from "../const";
|
||||
|
||||
export class HttpClient {
|
||||
private _url: string;
|
||||
@@ -45,7 +45,7 @@ export class HttpClient {
|
||||
room: string,
|
||||
device: string
|
||||
): 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(),
|
||||
params: {
|
||||
user: user,
|
||||
@@ -62,7 +62,7 @@ export class HttpClient {
|
||||
device: string
|
||||
): Promise<AxiosResponse<any, any>> {
|
||||
const response = await axios.post(
|
||||
`${BASE}${this._url}/${RACLETTE}`,
|
||||
`${BASE}${this._url}/${PAGE}`,
|
||||
{
|
||||
command: "MEASURE_NEW",
|
||||
},
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
<!-- src/components/ChartComponent.vue -->
|
||||
<template>
|
||||
<div class="chart-wrapper">
|
||||
<div v-if="manager.loading" class="loading-state">Loading data...</div>
|
||||
<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 v-if="isTimeSeries" class="chart-wrapper">
|
||||
<Scatter data-cy="main-chart" :data="chartData" :options="chartOptions" />
|
||||
</div>
|
||||
<div v-else class="no-data-state" data-cy="no-data-display">
|
||||
No data to display
|
||||
</div>
|
||||
<br />
|
||||
</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 {
|
||||
chartOptions,
|
||||
chartData,
|
||||
isTimeSeries,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<template>
|
||||
<div class="control-panel">
|
||||
<button @click="newMeasurments" class="action-button">
|
||||
<button @click="newMeasurments" class="action-button" data-cy="new-measurements-button">
|
||||
New Measurments
|
||||
</button>
|
||||
|
||||
<button @click="fetchData" class="refresh-button">
|
||||
<button
|
||||
@click="fetchData"
|
||||
class="refresh-button"
|
||||
data-cy="fetch-measurements-button"
|
||||
>
|
||||
Fetch Measurments
|
||||
</button>
|
||||
</div>
|
||||
@@ -12,6 +16,7 @@
|
||||
<div>
|
||||
<span>
|
||||
<multiselect
|
||||
data-cy="user-mulitselect"
|
||||
id="user-select"
|
||||
v-model="manager.selected_user"
|
||||
:options="manager.user_options"
|
||||
@@ -24,6 +29,7 @@
|
||||
</span>
|
||||
<span>
|
||||
<multiselect
|
||||
data-cy="room-mulitselect"
|
||||
id="room-select"
|
||||
v-model="manager.selected_room"
|
||||
:options="manager.room_options"
|
||||
@@ -36,6 +42,7 @@
|
||||
</span>
|
||||
<span>
|
||||
<multiselect
|
||||
data-cy="device-mulitselect"
|
||||
id="device-select"
|
||||
v-model="manager.selected_device"
|
||||
:options="manager.device_options"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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
|
||||
export const USERNAME = process.env.VUE_APP_INFLUXDB_USER;
|
||||
export const PASSWORD = process.env.VUE_APP_INFLUXDB_PASSWORD;
|
||||
export const USERNAME = process.env.VUE_APP_REST_USER;
|
||||
export const PASSWORD = process.env.VUE_APP_REST_PASSWORD;
|
||||
|
||||
export const HUMIDITY = "humidity";
|
||||
export const TEMPERATURE = "temperature";
|
||||
@@ -17,5 +17,5 @@ export const DEVICE = "Shed";
|
||||
|
||||
export const BASE = "https://";
|
||||
export const PING = "ping";
|
||||
export const RACLETTE = "raclette";
|
||||
export const PAGE = process.env.VUE_APP_REST_PAGE;
|
||||
export const PONG = "pong";
|
||||
|
||||
288
web-app/test.md
288
web-app/test.md
@@ -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
|
||||
- [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)
|
||||
## Test Description
|
||||
|
||||
## 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
|
||||
- Ensure components work together as expected
|
||||
- Detect regression issues before production deployment
|
||||
- Verify application behavior in real-world scenarios
|
||||
### Test Structure
|
||||
|
||||
## 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
|
||||
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
|
||||
### Install Dependencies
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
cd project-softweng/web-app
|
||||
npm install
|
||||
cd ./web-app
|
||||
npm install --inlude=dev
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Option 1: Tests with Mock Environment (No Server)
|
||||
|
||||
### Run unit Tests
|
||||
```bash
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
### Run E2E Tests
|
||||
```bash
|
||||
# Run all tests in headless mode
|
||||
export CYPRESS_SKIP_SERVER_CHECK=true
|
||||
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)
|
||||
|
||||
#### Automated Script Method
|
||||
|
||||
### Run All Tests
|
||||
```bash
|
||||
# Use the automated script (starts server, runs tests, stops server)
|
||||
./tests/e2e/run-e2e-tests.sh
|
||||
npm run test
|
||||
```
|
||||
|
||||
#### Manual Method
|
||||
|
||||
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)
|
||||
|
||||
### Note
|
||||
The cypress GUI can be launched with the command:
|
||||
```bash
|
||||
npm run cypress:open
|
||||
npx cypress open
|
||||
```
|
||||
|
||||
This opens the Cypress Test Runner where you can:
|
||||
- Select individual tests to run
|
||||
- See test execution in real-time
|
||||
- Debug failing tests with time-travel debugging
|
||||
## Test Coverage
|
||||
|
||||
## 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.
|
||||
|
||||
```
|
||||
web-app/
|
||||
├── 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
|
||||
```
|
||||
#### Important Notes
|
||||
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].
|
||||
|
||||
### Test Files Organization
|
||||
### Unit Tests
|
||||
|
||||
Each test file follows this structure:
|
||||
1. **Setup**: Import dependencies and set up the test environment
|
||||
2. **Beforehooks**: Prepare the application state before each test
|
||||
3. **Test Cases**: Individual test scenarios grouped by feature
|
||||
4. **Helper Functions**: Support functions for test cases
|
||||
|
||||
## 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)
|
||||
- **Serie Component**:
|
||||
- Checks that a `Serie` instance initializes with the correct properties.
|
||||
- Validates that the correct label is returned for temperature and humidity types.
|
||||
- Ensures the label "Unknown" is returned for unknown types.
|
||||
- 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.
|
||||
|
||||
@@ -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