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:
Johan LEROY
2026-09-18 11:11:12 +02:00
parent 7db57f162c
commit 9d3e402ca4
5 changed files with 110 additions and 62 deletions
@@ -18,33 +18,11 @@ describe('ReadingsService', () => {
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", () => {
let result: unknown;
service.getHistory('SITE001', '2026-09-16T00:00:00Z', '2026-09-17T00:00:00Z').subscribe(
(r) => (result = r),
);
service
.getHistory('SITE001', '2026-09-16T00:00:00Z', '2026-09-17T00:00:00Z')
.subscribe((r) => (result = r));
const req = httpMock.expectOne(
(r) => r.url === `${environment.apiUrl}/readings` && r.method === 'GET',
@@ -58,7 +36,7 @@ describe('ReadingsService', () => {
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();
const req = httpMock.expectOne(
@@ -57,4 +57,31 @@ describe('SitesService', () => {
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);
}
.site-detail__badges {
display: flex;
align-items: center;
gap: 0.5rem;
margin-left: auto;
}
.banner-error {
display: block;
margin: 0 0 1.5rem;
}
.banner-empty {
display: block;
margin: 0 0 1.5rem;
}
.overview {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
@@ -15,12 +15,39 @@ const SITE = {
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,
site_id: 'SITE001',
timestamp: '2026-09-17T10:00:00Z',
source: 'api_current' as const,
consumption_kw: 120,
timestamp: '2026-09-17T09:00:00Z',
source: 'api_history' as const,
consumption_kw: 118,
consumption_kwh: null,
consumption_euros: null,
voltage_v: 230,
@@ -55,20 +82,20 @@ function setup(
}
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(
'SITE001',
{ getSite: vi.fn().mockReturnValue(of(SITE)) },
{
getLatest: vi.fn().mockReturnValue(of(READING_COMPLETE)),
getHistory: vi.fn().mockReturnValue(of([READING_COMPLETE])),
getSite: vi.fn().mockReturnValue(of(SITE)),
getCurrent: vi.fn().mockReturnValue(of(CURRENT_COMPLET)),
},
{ getHistory: vi.fn().mockReturnValue(of([LECTURE])) },
);
fixture.detectChanges();
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.error()).toBeNull();
});
@@ -76,11 +103,11 @@ describe('SiteDetail', () => {
it("signale l'indisponibilité quand un des appels échoue", () => {
const { fixture } = setup(
'SITE001',
{ getSite: vi.fn().mockReturnValue(throwError(() => new Error('nope'))) },
{
getLatest: vi.fn().mockReturnValue(of(READING_COMPLETE)),
getHistory: vi.fn().mockReturnValue(of([])),
getSite: vi.fn().mockReturnValue(throwError(() => new Error('nope'))),
getCurrent: vi.fn().mockReturnValue(of(CURRENT_COMPLET)),
},
{ getHistory: vi.fn().mockReturnValue(of([])) },
);
fixture.detectChanges();
@@ -90,44 +117,41 @@ describe('SiteDetail', () => {
});
it('affiche explicitement les champs null avec leur raison plutôt que de les masquer', () => {
const readingPartielle = {
...READING_COMPLETE,
const partielle = {
...CURRENT_COMPLET,
voltage_v: null,
current_a: null,
power_factor: null,
null_reasons: ['electrical_sensor_failure'],
data_quality: 'partial' as const,
};
const { fixture } = setup(
'SITE001',
{ getSite: vi.fn().mockReturnValue(of(SITE)) },
{
getLatest: vi.fn().mockReturnValue(of(readingPartielle)),
getHistory: vi.fn().mockReturnValue(of([readingPartielle])),
getSite: vi.fn().mockReturnValue(of(SITE)),
getCurrent: vi.fn().mockReturnValue(of(partielle)),
},
{ getHistory: vi.fn().mockReturnValue(of([LECTURE])) },
);
fixture.detectChanges();
const tension = fixture.componentInstance
.metrics()
.find((m) => m.key === 'voltage_v');
const tension = fixture.componentInstance.metrics().find((m) => m.key === 'voltage_v');
expect(tension?.value).toBeNull();
expect(tension?.reason).toBe('capteur électrique en panne');
const html = fixture.nativeElement.textContent;
expect(html).toContain('Indisponible');
expect(html).toContain('capteur électrique en panne');
const texte = fixture.nativeElement.textContent;
expect(texte).toContain('Indisponible');
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', () => {
const getSite = vi.fn().mockReturnValue(of(SITE));
const { fixture, paramMap } = setup(
'SITE001',
{ getSite },
{
getLatest: vi.fn().mockReturnValue(of(READING_COMPLETE)),
getHistory: vi.fn().mockReturnValue(of([])),
},
{ getSite, getCurrent: vi.fn().mockReturnValue(of(CURRENT_COMPLET)) },
{ getHistory: vi.fn().mockReturnValue(of([])) },
);
fixture.detectChanges();
@@ -141,8 +165,11 @@ describe('SiteDetail', () => {
const getHistory = vi.fn().mockReturnValue(of([]));
const { fixture } = setup(
'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();
@@ -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 { fixture } = setup(
'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();
expect(getHistory).toHaveBeenCalledWith('SITE001', undefined, undefined);
expect(fixture.componentInstance.latestReading()).toBeNull();
expect(getHistory).not.toHaveBeenCalled();
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',
electrical_sensor_failure: 'capteur électrique 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',
};