test(web-app): add e2e fetch process
It tests the normal request case of timeserie
This commit is contained in:
@@ -2,21 +2,8 @@ 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: {
|
|
||||||
devServer: {
|
|
||||||
framework: "vue",
|
|
||||||
bundler: "webpack",
|
|
||||||
},
|
|
||||||
specPattern: "src/**/*.cy.ts",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
const { defineConfig } = require('cypress');
|
|
||||||
|
|
||||||
module.exports = defineConfig({
|
|
||||||
// Shared settings for all test types
|
|
||||||
watchForFileChanges: false,
|
|
||||||
screenshotOnRunFailure: false,
|
|
||||||
video: false,
|
|
||||||
defaultCommandTimeout: 10000,
|
|
||||||
chromeWebSecurity: false,
|
|
||||||
retries: {
|
|
||||||
runMode: 2,
|
|
||||||
openMode: 0,
|
|
||||||
},
|
|
||||||
|
|
||||||
// E2E test configuration
|
|
||||||
e2e: {
|
|
||||||
specPattern: ['tests/e2e/*.spec.ts', 'tests/unit/*.spec.ts'],
|
|
||||||
baseUrl: null, // No baseUrl to prevent server checks in headless mode
|
|
||||||
supportFile: 'tests/support/e2e.ts',
|
|
||||||
experimentalRunAllSpecs: true,
|
|
||||||
testIsolation: false, // Allow shared context for unit tests
|
|
||||||
setupNodeEvents(on, config) {
|
|
||||||
// Disable baseUrl verification for headless testing
|
|
||||||
on('before:browser:launch', (browser, launchOptions) => {
|
|
||||||
return launchOptions;
|
|
||||||
});
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Component test configuration (for Vue components)
|
|
||||||
component: {
|
|
||||||
devServer: {
|
|
||||||
framework: 'vue',
|
|
||||||
bundler: 'webpack',
|
|
||||||
},
|
|
||||||
specPattern: 'tests/unit/*.spec.ts', // Component tests are in unit directory
|
|
||||||
supportFile: 'tests/support/e2e.ts',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
16
web-app/cypress/e2e/fetch_process.cy.ts
Normal file
16
web-app/cypress/e2e/fetch_process.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
describe("Test fetch measurments button in the main page", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit("http://localhost:8080/");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Fetch timeseries with valid user-room-device", () => {
|
||||||
|
cy.get('[data-cy="new-measurements-button"]').should("be.visible");
|
||||||
|
// keep default mulitselector value for the test
|
||||||
|
cy.get('[data-cy="new-measurements-button"]').click();
|
||||||
|
|
||||||
|
cy.get('[data-cy="main-chart"]').should("be.visible");
|
||||||
|
cy.get('[data-cy="no-data-display"]').should("not.exist");
|
||||||
|
});
|
||||||
|
});
|
||||||
5
web-app/cypress/fixtures/example.json
Normal file
5
web-app/cypress/fixtures/example.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "Using fixtures to represent data",
|
||||||
|
"email": "hello@cypress.io",
|
||||||
|
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||||
|
}
|
||||||
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'
|
||||||
@@ -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:unit": "cypress run --config-file cypress.unit.ts --spec 'cypress/unit/*.cy.ts'",
|
||||||
"test:e2e": "cypress run --spec 'tests/e2e/*.spec.ts'",
|
"test:e2e": "cypress run --spec 'cypress/e2e/*.cy.ts'",
|
||||||
"test": "npm run test:unit && npm run test:e2e"
|
"test": "npm run test:unit && npm run test:e2e"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
<!-- src/components/ChartComponent.vue -->
|
<!-- src/components/ChartComponent.vue -->
|
||||||
<template>
|
<template>
|
||||||
<div class="chart-wrapper">
|
<div v-if="chartData.datasets.length > 0" 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>
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="control-panel">
|
<div class="control-panel">
|
||||||
<button @click="newMeasurments" class="action-button">
|
<button @click="newMeasurments" class="action-button" data-cy="new-measurements-button">
|
||||||
New Measurments
|
New Measurments
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button @click="fetchData" class="refresh-button">
|
<button
|
||||||
|
@click="fetchData"
|
||||||
|
class="refresh-button"
|
||||||
|
data-cy="fetch-measurements-button"
|
||||||
|
>
|
||||||
Fetch Measurments
|
Fetch Measurments
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -12,6 +16,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<span>
|
<span>
|
||||||
<multiselect
|
<multiselect
|
||||||
|
data-cy="user-mulitselect"
|
||||||
id="user-select"
|
id="user-select"
|
||||||
v-model="manager.selected_user"
|
v-model="manager.selected_user"
|
||||||
:options="manager.user_options"
|
:options="manager.user_options"
|
||||||
@@ -24,6 +29,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<multiselect
|
<multiselect
|
||||||
|
data-cy="room-mulitselect"
|
||||||
id="room-select"
|
id="room-select"
|
||||||
v-model="manager.selected_room"
|
v-model="manager.selected_room"
|
||||||
:options="manager.room_options"
|
:options="manager.room_options"
|
||||||
@@ -36,6 +42,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<multiselect
|
<multiselect
|
||||||
|
data-cy="device-mulitselect"
|
||||||
id="device-select"
|
id="device-select"
|
||||||
v-model="manager.selected_device"
|
v-model="manager.selected_device"
|
||||||
:options="manager.device_options"
|
:options="manager.device_options"
|
||||||
|
|||||||
@@ -1,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
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
import { Serie } from "../../src/Measures/Serie";
|
|
||||||
import { TEMPERATURE, HUMIDITY } from "../../src/const";
|
|
||||||
import { Colors } from "../../src/Measures/Utils";
|
|
||||||
|
|
||||||
describe("Serie Component Tests", () => {
|
|
||||||
// Mock data for tests
|
|
||||||
const mockDataTemperature = [
|
|
||||||
{ time: 1625097600000, value: new Date(22.5) },
|
|
||||||
{ time: 1625184000000, value: new Date(23.8) }
|
|
||||||
];
|
|
||||||
|
|
||||||
const mockDataHumidity = [
|
|
||||||
{ time: 1625097600000, value: new Date(45) },
|
|
||||||
{ time: 1625184000000, value: new Date(48) }
|
|
||||||
];
|
|
||||||
|
|
||||||
it("should initialize with correct properties", () => {
|
|
||||||
// Create a new Serie instance
|
|
||||||
const serie = new Serie(
|
|
||||||
TEMPERATURE,
|
|
||||||
mockDataTemperature,
|
|
||||||
"user1",
|
|
||||||
"bedroom",
|
|
||||||
"sensor1"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify that it has the correct type
|
|
||||||
expect(serie).to.be.instanceOf(Serie);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return the correct label for temperature", () => {
|
|
||||||
const serie = new Serie(
|
|
||||||
TEMPERATURE,
|
|
||||||
mockDataTemperature,
|
|
||||||
"user1",
|
|
||||||
"bedroom",
|
|
||||||
"sensor1"
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(serie.getLabel()).to.equal("Temperature [°C]");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return the correct label for humidity", () => {
|
|
||||||
const serie = new Serie(
|
|
||||||
HUMIDITY,
|
|
||||||
mockDataHumidity,
|
|
||||||
"user1",
|
|
||||||
"bedroom",
|
|
||||||
"sensor1"
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(serie.getLabel()).to.equal("Humidity [%]");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return 'Unknown' label for unknown type", () => {
|
|
||||||
const serie = new Serie(
|
|
||||||
"UNKNOWN_TYPE",
|
|
||||||
mockDataTemperature,
|
|
||||||
"user1",
|
|
||||||
"bedroom",
|
|
||||||
"sensor1"
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(serie.getLabel()).to.equal("Unknown");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should format temperature data correctly with getSerie()", () => {
|
|
||||||
const serie = new Serie(
|
|
||||||
TEMPERATURE,
|
|
||||||
mockDataTemperature.map(item => ({ time: item.time, value: new Date(item.value) })),
|
|
||||||
"user1",
|
|
||||||
"bedroom",
|
|
||||||
"sensor1"
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = serie.getSerie();
|
|
||||||
|
|
||||||
// Verify the properties of the formatted data
|
|
||||||
expect(result).to.have.property("label", "Temperature [°C]");
|
|
||||||
expect(result).to.have.property("borderColor", Colors.BLUE);
|
|
||||||
expect(result).to.have.property("backgroundColor", Colors.BLUE);
|
|
||||||
expect(result).to.have.property("borderWidth", 1);
|
|
||||||
expect(result).to.have.property("yAxisID", TEMPERATURE);
|
|
||||||
|
|
||||||
// Verify the data points
|
|
||||||
expect(result.data).to.have.length(mockDataTemperature.length);
|
|
||||||
expect(result.data[0]).to.have.property("x", mockDataTemperature[0].time);
|
|
||||||
expect(result.data[1]).to.have.property("x", mockDataTemperature[1].time);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should format humidity data correctly with getSerie()", () => {
|
|
||||||
const serie = new Serie(
|
|
||||||
HUMIDITY,
|
|
||||||
mockDataHumidity,
|
|
||||||
"user1",
|
|
||||||
"bedroom",
|
|
||||||
"sensor1"
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = serie.getSerie();
|
|
||||||
|
|
||||||
// Verify the properties of the formatted data
|
|
||||||
expect(result).to.have.property("label", "Humidity [%]");
|
|
||||||
expect(result).to.have.property("borderColor", Colors.GREEN);
|
|
||||||
expect(result).to.have.property("backgroundColor", Colors.GREEN);
|
|
||||||
expect(result).to.have.property("borderWidth", 1);
|
|
||||||
expect(result).to.have.property("yAxisID", HUMIDITY);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle unknown types appropriately in getSerie()", () => {
|
|
||||||
const serie = new Serie(
|
|
||||||
"UNKNOWN_TYPE",
|
|
||||||
mockDataTemperature,
|
|
||||||
"user1",
|
|
||||||
"bedroom",
|
|
||||||
"sensor1"
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = serie.getSerie();
|
|
||||||
|
|
||||||
// Verify the properties for unknown type
|
|
||||||
expect(result).to.have.property("label", "Unknown");
|
|
||||||
expect(result).to.have.property("borderColor", Colors.RED);
|
|
||||||
expect(result).to.have.property("backgroundColor", Colors.RED);
|
|
||||||
expect(result).to.have.property("yAxisID", TEMPERATURE); // Defaults to TEMPERATURE
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user