test(frontend): couvre la vue détail sur le contrat SiteCurrentResponse
Le cas « site sans mesure » ne passe plus par une réponse nulle mais par `timestamp` à null : il ne doit alors pas interroger `/readings`, et la vue annonce l'absence de mesure au lieu de six métriques de cause inconnue.
This commit is contained in:
@@ -18,33 +18,11 @@ describe('ReadingsService', () => {
|
|||||||
|
|
||||||
afterEach(() => httpMock.verify());
|
afterEach(() => httpMock.verify());
|
||||||
|
|
||||||
it("demande la dernière lecture connue du site, quel que soit son âge", () => {
|
|
||||||
let result: unknown;
|
|
||||||
service.getLatest('SITE001').subscribe((r) => (result = r));
|
|
||||||
|
|
||||||
const req = httpMock.expectOne(`${environment.apiUrl}/sites/SITE001/current`);
|
|
||||||
expect(req.request.method).toBe('GET');
|
|
||||||
|
|
||||||
req.flush({ reading_id: 1, site_id: 'SITE001', consumption_kw: 12.5 });
|
|
||||||
|
|
||||||
expect((result as { reading_id: number }).reading_id).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("retourne null quand le site n'a aucune lecture", () => {
|
|
||||||
let result: unknown;
|
|
||||||
service.getLatest('SITE001').subscribe((r) => (result = r));
|
|
||||||
|
|
||||||
const req = httpMock.expectOne(`${environment.apiUrl}/sites/SITE001/current`);
|
|
||||||
req.flush(null);
|
|
||||||
|
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("demande l'historique du site avec la fenêtre temporelle donnée", () => {
|
it("demande l'historique du site avec la fenêtre temporelle donnée", () => {
|
||||||
let result: unknown;
|
let result: unknown;
|
||||||
service.getHistory('SITE001', '2026-09-16T00:00:00Z', '2026-09-17T00:00:00Z').subscribe(
|
service
|
||||||
(r) => (result = r),
|
.getHistory('SITE001', '2026-09-16T00:00:00Z', '2026-09-17T00:00:00Z')
|
||||||
);
|
.subscribe((r) => (result = r));
|
||||||
|
|
||||||
const req = httpMock.expectOne(
|
const req = httpMock.expectOne(
|
||||||
(r) => r.url === `${environment.apiUrl}/readings` && r.method === 'GET',
|
(r) => r.url === `${environment.apiUrl}/readings` && r.method === 'GET',
|
||||||
@@ -58,7 +36,7 @@ describe('ReadingsService', () => {
|
|||||||
expect((result as unknown[]).length).toBe(1);
|
expect((result as unknown[]).length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ne pose pas de paramètres start/end quand ils sont omis", () => {
|
it('ne pose pas de paramètres start/end quand ils sont omis', () => {
|
||||||
service.getHistory('SITE001').subscribe();
|
service.getHistory('SITE001').subscribe();
|
||||||
|
|
||||||
const req = httpMock.expectOne(
|
const req = httpMock.expectOne(
|
||||||
|
|||||||
@@ -57,4 +57,31 @@ describe('SitesService', () => {
|
|||||||
|
|
||||||
expect((result as { site_id: string }).site_id).toBe('SITE001');
|
expect((result as { site_id: string }).site_id).toBe('SITE001');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('appelle le bon endpoint et retourne la mesure courante du site', () => {
|
||||||
|
let result: unknown;
|
||||||
|
service.getCurrent('SITE001').subscribe((r) => (result = r));
|
||||||
|
|
||||||
|
const req = httpMock.expectOne(`${environment.apiUrl}/sites/SITE001/current`);
|
||||||
|
expect(req.request.method).toBe('GET');
|
||||||
|
|
||||||
|
req.flush({
|
||||||
|
timestamp: '2026-09-17T10:00:00Z',
|
||||||
|
site_id: 'SITE001',
|
||||||
|
site_type: 'industriel',
|
||||||
|
consumption_kw: 120,
|
||||||
|
consumption_kwh: null,
|
||||||
|
voltage_v: null,
|
||||||
|
current_a: null,
|
||||||
|
power_factor: null,
|
||||||
|
temperature_celsius: 22,
|
||||||
|
humidity_percent: 55,
|
||||||
|
null_reasons: ['electrical_sensor_failure'],
|
||||||
|
data_quality: 'partial',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect((result as { null_reasons: string[] }).null_reasons).toEqual([
|
||||||
|
'electrical_sensor_failure',
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,11 +28,23 @@
|
|||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.site-detail__badges {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.banner-error {
|
.banner-error {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 0 1.5rem;
|
margin: 0 0 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.banner-empty {
|
||||||
|
display: block;
|
||||||
|
margin: 0 0 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.overview {
|
.overview {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
|||||||
@@ -15,12 +15,39 @@ const SITE = {
|
|||||||
status: 'actif',
|
status: 'actif',
|
||||||
};
|
};
|
||||||
|
|
||||||
const READING_COMPLETE = {
|
const CURRENT_COMPLET = {
|
||||||
|
timestamp: '2026-09-17T10:00:00Z',
|
||||||
|
site_id: 'SITE001',
|
||||||
|
site_type: 'industriel',
|
||||||
|
consumption_kw: 120,
|
||||||
|
consumption_kwh: null,
|
||||||
|
voltage_v: 230,
|
||||||
|
current_a: 12,
|
||||||
|
power_factor: 0.95,
|
||||||
|
temperature_celsius: 22,
|
||||||
|
humidity_percent: 55,
|
||||||
|
null_reasons: [] as string[],
|
||||||
|
data_quality: 'good' as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
const SANS_MESURE = {
|
||||||
|
...CURRENT_COMPLET,
|
||||||
|
timestamp: null,
|
||||||
|
consumption_kw: null,
|
||||||
|
voltage_v: null,
|
||||||
|
current_a: null,
|
||||||
|
power_factor: null,
|
||||||
|
temperature_celsius: null,
|
||||||
|
humidity_percent: null,
|
||||||
|
data_quality: 'critical' as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
const LECTURE = {
|
||||||
reading_id: 1,
|
reading_id: 1,
|
||||||
site_id: 'SITE001',
|
site_id: 'SITE001',
|
||||||
timestamp: '2026-09-17T10:00:00Z',
|
timestamp: '2026-09-17T09:00:00Z',
|
||||||
source: 'api_current' as const,
|
source: 'api_history' as const,
|
||||||
consumption_kw: 120,
|
consumption_kw: 118,
|
||||||
consumption_kwh: null,
|
consumption_kwh: null,
|
||||||
consumption_euros: null,
|
consumption_euros: null,
|
||||||
voltage_v: 230,
|
voltage_v: 230,
|
||||||
@@ -55,20 +82,20 @@ function setup(
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('SiteDetail', () => {
|
describe('SiteDetail', () => {
|
||||||
it('charge le site, la dernière lecture et son historique au démarrage', () => {
|
it('charge le site, la mesure courante et son historique au démarrage', () => {
|
||||||
const { fixture } = setup(
|
const { fixture } = setup(
|
||||||
'SITE001',
|
'SITE001',
|
||||||
{ getSite: vi.fn().mockReturnValue(of(SITE)) },
|
|
||||||
{
|
{
|
||||||
getLatest: vi.fn().mockReturnValue(of(READING_COMPLETE)),
|
getSite: vi.fn().mockReturnValue(of(SITE)),
|
||||||
getHistory: vi.fn().mockReturnValue(of([READING_COMPLETE])),
|
getCurrent: vi.fn().mockReturnValue(of(CURRENT_COMPLET)),
|
||||||
},
|
},
|
||||||
|
{ getHistory: vi.fn().mockReturnValue(of([LECTURE])) },
|
||||||
);
|
);
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
expect(fixture.componentInstance.site()?.site_id).toBe('SITE001');
|
expect(fixture.componentInstance.site()?.site_id).toBe('SITE001');
|
||||||
expect(fixture.componentInstance.latestReading()?.consumption_kw).toBe(120);
|
expect(fixture.componentInstance.current()?.consumption_kw).toBe(120);
|
||||||
expect(fixture.componentInstance.history().length).toBe(1);
|
expect(fixture.componentInstance.history().length).toBe(1);
|
||||||
expect(fixture.componentInstance.error()).toBeNull();
|
expect(fixture.componentInstance.error()).toBeNull();
|
||||||
});
|
});
|
||||||
@@ -76,11 +103,11 @@ describe('SiteDetail', () => {
|
|||||||
it("signale l'indisponibilité quand un des appels échoue", () => {
|
it("signale l'indisponibilité quand un des appels échoue", () => {
|
||||||
const { fixture } = setup(
|
const { fixture } = setup(
|
||||||
'SITE001',
|
'SITE001',
|
||||||
{ getSite: vi.fn().mockReturnValue(throwError(() => new Error('nope'))) },
|
|
||||||
{
|
{
|
||||||
getLatest: vi.fn().mockReturnValue(of(READING_COMPLETE)),
|
getSite: vi.fn().mockReturnValue(throwError(() => new Error('nope'))),
|
||||||
getHistory: vi.fn().mockReturnValue(of([])),
|
getCurrent: vi.fn().mockReturnValue(of(CURRENT_COMPLET)),
|
||||||
},
|
},
|
||||||
|
{ getHistory: vi.fn().mockReturnValue(of([])) },
|
||||||
);
|
);
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -90,44 +117,41 @@ describe('SiteDetail', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('affiche explicitement les champs null avec leur raison plutôt que de les masquer', () => {
|
it('affiche explicitement les champs null avec leur raison plutôt que de les masquer', () => {
|
||||||
const readingPartielle = {
|
const partielle = {
|
||||||
...READING_COMPLETE,
|
...CURRENT_COMPLET,
|
||||||
voltage_v: null,
|
voltage_v: null,
|
||||||
current_a: null,
|
current_a: null,
|
||||||
power_factor: null,
|
power_factor: null,
|
||||||
null_reasons: ['electrical_sensor_failure'],
|
null_reasons: ['electrical_sensor_failure'],
|
||||||
|
data_quality: 'partial' as const,
|
||||||
};
|
};
|
||||||
const { fixture } = setup(
|
const { fixture } = setup(
|
||||||
'SITE001',
|
'SITE001',
|
||||||
{ getSite: vi.fn().mockReturnValue(of(SITE)) },
|
|
||||||
{
|
{
|
||||||
getLatest: vi.fn().mockReturnValue(of(readingPartielle)),
|
getSite: vi.fn().mockReturnValue(of(SITE)),
|
||||||
getHistory: vi.fn().mockReturnValue(of([readingPartielle])),
|
getCurrent: vi.fn().mockReturnValue(of(partielle)),
|
||||||
},
|
},
|
||||||
|
{ getHistory: vi.fn().mockReturnValue(of([LECTURE])) },
|
||||||
);
|
);
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const tension = fixture.componentInstance
|
const tension = fixture.componentInstance.metrics().find((m) => m.key === 'voltage_v');
|
||||||
.metrics()
|
|
||||||
.find((m) => m.key === 'voltage_v');
|
|
||||||
expect(tension?.value).toBeNull();
|
expect(tension?.value).toBeNull();
|
||||||
expect(tension?.reason).toBe('capteur électrique en panne');
|
expect(tension?.reason).toBe('capteur électrique en panne');
|
||||||
|
|
||||||
const html = fixture.nativeElement.textContent;
|
const texte = fixture.nativeElement.textContent;
|
||||||
expect(html).toContain('Indisponible');
|
expect(texte).toContain('Indisponible');
|
||||||
expect(html).toContain('capteur électrique en panne');
|
expect(texte).toContain('capteur électrique en panne');
|
||||||
|
expect(texte).toContain('Données partielles');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('recharge les données quand le paramètre de route siteId change', () => {
|
it('recharge les données quand le paramètre de route siteId change', () => {
|
||||||
const getSite = vi.fn().mockReturnValue(of(SITE));
|
const getSite = vi.fn().mockReturnValue(of(SITE));
|
||||||
const { fixture, paramMap } = setup(
|
const { fixture, paramMap } = setup(
|
||||||
'SITE001',
|
'SITE001',
|
||||||
{ getSite },
|
{ getSite, getCurrent: vi.fn().mockReturnValue(of(CURRENT_COMPLET)) },
|
||||||
{
|
{ getHistory: vi.fn().mockReturnValue(of([])) },
|
||||||
getLatest: vi.fn().mockReturnValue(of(READING_COMPLETE)),
|
|
||||||
getHistory: vi.fn().mockReturnValue(of([])),
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -141,8 +165,11 @@ describe('SiteDetail', () => {
|
|||||||
const getHistory = vi.fn().mockReturnValue(of([]));
|
const getHistory = vi.fn().mockReturnValue(of([]));
|
||||||
const { fixture } = setup(
|
const { fixture } = setup(
|
||||||
'SITE001',
|
'SITE001',
|
||||||
{ getSite: vi.fn().mockReturnValue(of(SITE)) },
|
{
|
||||||
{ getLatest: vi.fn().mockReturnValue(of(READING_COMPLETE)), getHistory },
|
getSite: vi.fn().mockReturnValue(of(SITE)),
|
||||||
|
getCurrent: vi.fn().mockReturnValue(of(CURRENT_COMPLET)),
|
||||||
|
},
|
||||||
|
{ getHistory },
|
||||||
);
|
);
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -154,17 +181,21 @@ describe('SiteDetail', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ne fixe aucune fenêtre d'historique quand le site n'a aucune lecture", () => {
|
it("annonce l'absence de mesure sans interroger l'historique quand timestamp est null", () => {
|
||||||
const getHistory = vi.fn().mockReturnValue(of([]));
|
const getHistory = vi.fn().mockReturnValue(of([]));
|
||||||
const { fixture } = setup(
|
const { fixture } = setup(
|
||||||
'SITE001',
|
'SITE001',
|
||||||
{ getSite: vi.fn().mockReturnValue(of(SITE)) },
|
{
|
||||||
{ getLatest: vi.fn().mockReturnValue(of(null)), getHistory },
|
getSite: vi.fn().mockReturnValue(of(SITE)),
|
||||||
|
getCurrent: vi.fn().mockReturnValue(of(SANS_MESURE)),
|
||||||
|
},
|
||||||
|
{ getHistory },
|
||||||
);
|
);
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
expect(getHistory).toHaveBeenCalledWith('SITE001', undefined, undefined);
|
expect(getHistory).not.toHaveBeenCalled();
|
||||||
expect(fixture.componentInstance.latestReading()).toBeNull();
|
expect(fixture.componentInstance.hasMeasurement()).toBe(false);
|
||||||
|
expect(fixture.nativeElement.textContent).toContain('Aucune mesure remontée pour ce site.');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ const LIBELLE_PAR_RAISON: Record<string, string> = {
|
|||||||
consumption_sensor_failure: 'capteur de consommation en panne',
|
consumption_sensor_failure: 'capteur de consommation en panne',
|
||||||
electrical_sensor_failure: 'capteur électrique en panne',
|
electrical_sensor_failure: 'capteur électrique en panne',
|
||||||
temperature_sensor_failure: 'capteur de température en panne',
|
temperature_sensor_failure: 'capteur de température en panne',
|
||||||
humidity_sensor_failure: 'capteur d\'humidité en panne',
|
humidity_sensor_failure: "capteur d'humidité en panne",
|
||||||
network_loss: 'perte réseau',
|
network_loss: 'perte réseau',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user