fix(frontend): detruit les graphiques avec leur composant
Chart.js conserve chaque instance dans un registre lie au canvas et lui attache un observateur de redimensionnement. Sans destroy, tout survit a la destruction du composant, et une re-creation sur le meme canvas echoue avec "Canvas is already in use". Les doubles de test gagnent destroy : TestBed detruit les fixtures apres chaque test, un mock sans cette methode fait tomber les specs existantes.
This commit is contained in:
+27
-2
@@ -1,16 +1,28 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { vi } from 'vitest';
|
import { vi } from 'vitest';
|
||||||
|
import { Chart } from 'chart.js';
|
||||||
import { ConsumptionGauge } from './consumption-gauge';
|
import { ConsumptionGauge } from './consumption-gauge';
|
||||||
|
|
||||||
vi.mock('chart.js', () => {
|
vi.mock('chart.js', () => {
|
||||||
class ChartMock {
|
class ChartMock {
|
||||||
update = vi.fn();
|
static instances: ChartMock[] = [];
|
||||||
data = { datasets: [{}] };
|
|
||||||
static register = vi.fn();
|
static register = vi.fn();
|
||||||
|
update = vi.fn();
|
||||||
|
destroy = vi.fn();
|
||||||
|
data = { datasets: [{}] };
|
||||||
|
constructor() {
|
||||||
|
ChartMock.instances.push(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { Chart: ChartMock, registerables: [] };
|
return { Chart: ChartMock, registerables: [] };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type ChartDouble = { destroy: ReturnType<typeof vi.fn> };
|
||||||
|
|
||||||
|
function lastChart(): ChartDouble | undefined {
|
||||||
|
return (Chart as unknown as { instances: ChartDouble[] }).instances.at(-1);
|
||||||
|
}
|
||||||
|
|
||||||
describe('ConsumptionGauge', () => {
|
describe('ConsumptionGauge', () => {
|
||||||
it('se crée sans erreur avec des entrées valides', () => {
|
it('se crée sans erreur avec des entrées valides', () => {
|
||||||
TestBed.configureTestingModule({ imports: [ConsumptionGauge] });
|
TestBed.configureTestingModule({ imports: [ConsumptionGauge] });
|
||||||
@@ -31,4 +43,17 @@ describe('ConsumptionGauge', () => {
|
|||||||
|
|
||||||
expect(() => fixture.detectChanges()).not.toThrow();
|
expect(() => fixture.detectChanges()).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('détruit le graphique quand le composant est détruit', () => {
|
||||||
|
TestBed.configureTestingModule({ imports: [ConsumptionGauge] });
|
||||||
|
const fixture = TestBed.createComponent(ConsumptionGauge);
|
||||||
|
fixture.componentRef.setInput('consumption', 300);
|
||||||
|
fixture.componentRef.setInput('capacity', 1000);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const chart = lastChart();
|
||||||
|
fixture.destroy();
|
||||||
|
|
||||||
|
expect(chart?.destroy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { Component, ElementRef, ViewChild, input, effect, AfterViewInit } from '@angular/core';
|
import {
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
ViewChild,
|
||||||
|
input,
|
||||||
|
effect,
|
||||||
|
AfterViewInit,
|
||||||
|
OnDestroy,
|
||||||
|
} from '@angular/core';
|
||||||
import { Chart, registerables } from 'chart.js';
|
import { Chart, registerables } from 'chart.js';
|
||||||
|
|
||||||
Chart.register(...registerables);
|
Chart.register(...registerables);
|
||||||
@@ -9,7 +17,7 @@ Chart.register(...registerables);
|
|||||||
templateUrl: './consumption-gauge.html',
|
templateUrl: './consumption-gauge.html',
|
||||||
styleUrl: './consumption-gauge.scss',
|
styleUrl: './consumption-gauge.scss',
|
||||||
})
|
})
|
||||||
export class ConsumptionGauge implements AfterViewInit {
|
export class ConsumptionGauge implements AfterViewInit, OnDestroy {
|
||||||
consumption = input.required<number>();
|
consumption = input.required<number>();
|
||||||
capacity = input.required<number>();
|
capacity = input.required<number>();
|
||||||
|
|
||||||
@@ -52,4 +60,8 @@ export class ConsumptionGauge implements AfterViewInit {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.chart?.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,28 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { vi } from 'vitest';
|
import { vi } from 'vitest';
|
||||||
|
import { Chart } from 'chart.js';
|
||||||
import { SiteLoadChart } from './site-load-chart';
|
import { SiteLoadChart } from './site-load-chart';
|
||||||
|
|
||||||
vi.mock('chart.js', () => {
|
vi.mock('chart.js', () => {
|
||||||
class ChartMock {
|
class ChartMock {
|
||||||
update = vi.fn();
|
static instances: ChartMock[] = [];
|
||||||
data = { datasets: [{}] };
|
|
||||||
static register = vi.fn();
|
static register = vi.fn();
|
||||||
|
update = vi.fn();
|
||||||
|
destroy = vi.fn();
|
||||||
|
data = { datasets: [{}] };
|
||||||
|
constructor() {
|
||||||
|
ChartMock.instances.push(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { Chart: ChartMock, registerables: [] };
|
return { Chart: ChartMock, registerables: [] };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type ChartDouble = { destroy: ReturnType<typeof vi.fn> };
|
||||||
|
|
||||||
|
function lastChart(): ChartDouble | undefined {
|
||||||
|
return (Chart as unknown as { instances: ChartDouble[] }).instances.at(-1);
|
||||||
|
}
|
||||||
|
|
||||||
describe('SiteLoadChart', () => {
|
describe('SiteLoadChart', () => {
|
||||||
it('se crée sans erreur avec une liste de sites valide', () => {
|
it('se crée sans erreur avec une liste de sites valide', () => {
|
||||||
TestBed.configureTestingModule({ imports: [SiteLoadChart] });
|
TestBed.configureTestingModule({ imports: [SiteLoadChart] });
|
||||||
@@ -35,4 +47,25 @@ describe('SiteLoadChart', () => {
|
|||||||
|
|
||||||
expect(() => fixture.detectChanges()).not.toThrow();
|
expect(() => fixture.detectChanges()).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('détruit le graphique quand le composant est détruit', () => {
|
||||||
|
TestBed.configureTestingModule({ imports: [SiteLoadChart] });
|
||||||
|
const fixture = TestBed.createComponent(SiteLoadChart);
|
||||||
|
fixture.componentRef.setInput('sites', [
|
||||||
|
{
|
||||||
|
site_id: 'S1',
|
||||||
|
site_name: 'A',
|
||||||
|
current_consumption_kw: 50,
|
||||||
|
capacity_kw: 100,
|
||||||
|
load_percent: 50,
|
||||||
|
data_quality: 'good',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const chart = lastChart();
|
||||||
|
fixture.destroy();
|
||||||
|
|
||||||
|
expect(chart?.destroy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { Component, ElementRef, ViewChild, input, effect, AfterViewInit } from '@angular/core';
|
import {
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
ViewChild,
|
||||||
|
input,
|
||||||
|
effect,
|
||||||
|
AfterViewInit,
|
||||||
|
OnDestroy,
|
||||||
|
} from '@angular/core';
|
||||||
import { Chart, registerables } from 'chart.js';
|
import { Chart, registerables } from 'chart.js';
|
||||||
import { SiteSummary } from '../../models/stats.model';
|
import { SiteSummary } from '../../models/stats.model';
|
||||||
|
|
||||||
@@ -17,7 +25,7 @@ const QUALITY_COLORS: Record<SiteSummary['data_quality'], string> = {
|
|||||||
templateUrl: './site-load-chart.html',
|
templateUrl: './site-load-chart.html',
|
||||||
styleUrl: './site-load-chart.scss',
|
styleUrl: './site-load-chart.scss',
|
||||||
})
|
})
|
||||||
export class SiteLoadChart implements AfterViewInit {
|
export class SiteLoadChart implements AfterViewInit, OnDestroy {
|
||||||
sites = input.required<SiteSummary[]>();
|
sites = input.required<SiteSummary[]>();
|
||||||
|
|
||||||
@ViewChild('canvas') private canvasRef!: ElementRef<HTMLCanvasElement>;
|
@ViewChild('canvas') private canvasRef!: ElementRef<HTMLCanvasElement>;
|
||||||
@@ -59,4 +67,8 @@ export class SiteLoadChart implements AfterViewInit {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.chart?.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user