Skip to content

Commit

Permalink
Use data attributes to 'sanity-test' the costs pie chart
Browse files Browse the repository at this point in the history
  • Loading branch information
david-mears-2 committed Dec 12, 2024
1 parent 8ae603e commit 422c8f6
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 22 deletions.
14 changes: 8 additions & 6 deletions components/CostsPie.client.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
:id="chartContainerId"
ref="chartContainer"
:class="[props.hideTooltips ? hideTooltipsClassName : '']"
:data-test="JSON.stringify(costsData)"
/>
</div>
</template>
Expand Down Expand Up @@ -36,7 +37,7 @@ const hideTooltipsClassName = "hide-tooltips";
const chartBackgroundColor = "transparent";
const chartBackgroundColorOnExporting = "white";
let chart: Highcharts.Chart;
let costsData: pieCost[] = [];
const costsData = ref<pieCost[]>([]); // This is only implemented as ref for testing purposes, via the data-test attribute.
const pieSize = computed(() => appStore.largeScreen ? 450 : 300);
const chartContainer = ref<HTMLElement | null>(null);
Expand Down Expand Up @@ -112,7 +113,7 @@ const chartLevelsOptions = (isDrillingDown: boolean = false): Array<Highcharts.P
const chartSeries = () => {
return {
type: "sunburst",
data: costsData, // Empty at initialisation, populated later
data: costsData.value, // Empty at initialisation, populated later
name: "Root",
allowDrillToNode: true,
borderRadius: 0,
Expand Down Expand Up @@ -210,7 +211,7 @@ const populateCostsDataIntoPie = () => {
if (!appStore.totalCost) {
return;
}
costsData = [{
const data = [{
id: appStore.totalCost.id,
parent: "",
name: appStore.getCostLabel(appStore.totalCost.id),
Expand All @@ -220,7 +221,7 @@ const populateCostsDataIntoPie = () => {
// so that earlier pieCostsColors are assigned to top-level children before
// the next level of children. (Using recursion changes the color assignment order.)
appStore.totalCost.children?.forEach((cost) => {
costsData.push({
data.push({
id: cost.id,
parent: appStore.totalCost!.id,
name: appStore.getCostLabel(cost.id),
Expand All @@ -233,15 +234,16 @@ const populateCostsDataIntoPie = () => {
appStore.totalCost.children?.forEach((cost) => {
// Omit sub-costs with a value of zero
cost.children?.filter(subCost => subCost.value !== 0)?.forEach((subCost) => {
costsData.push({
data.push({
id: subCost.id,
parent: cost.id,
name: appStore.getCostLabel(subCost.id),
value: subCost.value,
});
});
});
chart.series[0].setData(costsData);
costsData.value = data;
chart.series[0].setData(costsData.value);
};
watch(() => appStore.costsData, () => {
Expand Down
4 changes: 2 additions & 2 deletions components/TimeSeries.client.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import accessibilityInitialize from "highcharts/modules/accessibility";
import exportDataInitialize from "highcharts/modules/export-data";
import exportingInitialize from "highcharts/modules/exporting";
import offlineExportingInitialize from "highcharts/modules/offline-exporting";
import { debounce } from "perfect-debounce";
import type { DisplayInfo } from "~/types/apiResponseTypes";
import type { TimeSeriesDataPoint } from "~/types/dataTypes";
import { plotBandsColor, plotLinesColor, timeSeriesColors } from "./utils/highCharts";
const props = defineProps<{
Expand Down Expand Up @@ -59,7 +59,7 @@ const seriesMetadata = computed((): DisplayInfo | undefined => {
});
// Assign an x-position to y-values. Nth value corresponds to "N+1th day" of simulation.
const data = computed(() => {
const data = computed((): TimeSeriesDataPoint[] => {
return appStore.timeSeriesData![props.seriesId].map((value, index) => [index + 1, value]);
});
Expand Down
18 changes: 6 additions & 12 deletions tests/e2e/helpers/checkTimeSeriesDataPoints.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
import type { Locator } from "playwright/test";
import { expect } from "@playwright/test";

type DataPoint = [number, number];

const checkValueIsInRange = (value: number, expected: number, tolerance: number) => {
expect(value).toBeGreaterThanOrEqual(expected * (1 - tolerance));
expect(value).toBeLessThanOrEqual(expected * (1 + tolerance));
};
import type { TimeSeriesDataPoint } from "~/types/dataTypes";
import { checkValueIsInRange } from "./checkValueIsInRange";

export const checkTimeSeriesDataPoints = async (
locator: Locator,
expectedFirstDataPoint: DataPoint,
expectedLastDataPoint: DataPoint,
expectedFirstDataPoint: TimeSeriesDataPoint,
expectedLastDataPoint: TimeSeriesDataPoint,
tolerance = 0.5,
) => {
const testData = await locator.getAttribute("data-test");
const data = JSON.parse(testData!);
const dataString = await locator.getAttribute("data-test");
const data = JSON.parse(dataString!);

const [firstX, firstY] = data.firstDataPoint;
const [lastX, lastY] = data.lastDataPoint;
Expand Down
6 changes: 6 additions & 0 deletions tests/e2e/helpers/checkValueIsInRange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { expect } from "@playwright/test";

export const checkValueIsInRange = (value: number, expected: number, tolerance: number) => {
expect(value).toBeGreaterThanOrEqual(expected * (1 - tolerance));
expect(value).toBeLessThanOrEqual(expected * (1 + tolerance));
};
49 changes: 47 additions & 2 deletions tests/e2e/runAnalysis.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import selectParameterOption from "~/tests/e2e/helpers/selectParameterOption";
import waitForNewScenarioPage from "~/tests/e2e/helpers/waitForNewScenarioPage";
import checkRApiServer from "./helpers/checkRApiServer";
import { checkTimeSeriesDataPoints } from "./helpers/checkTimeSeriesDataPoints";
import { checkValueIsInRange } from "./helpers/checkValueIsInRange";

const parameterLabels = {
country: "Country",
Expand Down Expand Up @@ -89,13 +90,43 @@ test("Can request a scenario analysis run", async ({ page, baseURL }) => {
await expect(page.locator("#vaccinated-container .highcharts-yaxis-labels")).toBeVisible();
await expect(page.locator("#vaccinated-container").getByLabel("View chart menu, Chart")).toBeVisible();

await expect(page.locator("#costsChartContainer rect").first()).toBeVisible();
const prevalence1DataStr = await page.locator("#prevalence-container").getAttribute("data-test");
const prevalence1Data = JSON.parse(prevalence1DataStr!);
const prevalenceTimeSeries1LastY = prevalence1Data.lastDataPoint[1];

await checkTimeSeriesDataPoints(page.locator("#prevalence-container"), [1, 331.0026], [600, 110_000]);
await checkTimeSeriesDataPoints(page.locator("#hospitalised-container"), [1, 0], [600, 55_000]);
await checkTimeSeriesDataPoints(page.locator("#dead-container"), [1, 0], [600, 800_000]);
await checkTimeSeriesDataPoints(page.locator("#vaccinated-container"), [1, 0], [600, 200_000_000]);

await expect(page.locator("#costsChartContainer rect").first()).toBeVisible();

const costsPieDataStr = await page.locator("#costsChartContainer").getAttribute("data-test");
const costsPieData = JSON.parse(costsPieDataStr!);
expect(costsPieData).toHaveLength(12);

const expectedCostData = [
{ id: "total", parent: "", name: "Total", value: 16_000_000 },
{ id: "gdp", parent: "total", name: "GDP", value: 8_000_000 },
{ id: "education", parent: "total", name: "Education", value: 6_000_000 },
{ id: "life_years", parent: "total", name: "Life years", value: 2_250_000 },
{ id: "gdp_closures", parent: "gdp", name: "Closures", value: 8_000_000 },
{ id: "gdp_absences", parent: "gdp", name: "Absences", value: 50_000 },
{ id: "education_closures", parent: "education", name: "Closures", value: 6_000_000 },
{ id: "education_absences", parent: "education", name: "Absences", value: 100 },
{ id: "life_years_pre_school", parent: "life_years", name: "Preschool-age children", value: 250_000 },
{ id: "life_years_school_age", parent: "life_years", name: "School-age children", value: 1_200_000 },
{ id: "life_years_working_age", parent: "life_years", name: "Working-age adults", value: 600_000 },
{ id: "life_years_retirement_age", parent: "life_years", name: "Retirement-age adults", value: 220_000 },
];

expectedCostData.forEach((expectedCost) => {
const cost = costsPieData.find((cost: any) => cost.id === expectedCost.id);
expect(cost.name).toEqual(expectedCost.name);
expect(cost.parent).toBe(expectedCost.parent);
checkValueIsInRange(cost.value, expectedCost.value, 0.5);
});

// Run a second analysis with a different parameter, using the parameters form on the results page.
await page.getByRole("button", { name: "Parameters" }).first().click();
// The following line has been known to fail locally on webkit, but pass on CI.
Expand Down Expand Up @@ -140,11 +171,25 @@ test("Can request a scenario analysis run", async ({ page, baseURL }) => {
const closeButton = page.getByLabel("Edit parameters").getByLabel("Close");
await closeButton.click();

// Test that the second analysis results page has charts of both types.
// Test that the second analysis results page has visible charts of both types.
await expect(page.locator("#prevalence-container")).toBeVisible({ timeout: 20000 });
await expect(page.locator("#prevalence-container .highcharts-xaxis-labels")).toBeVisible();
await expect(page.locator("#costsChartContainer rect").first()).toBeVisible();

// Test that one of the time series charts for the second analysis has different data from the first analysis.
const prevalence2DataStr = await page.locator("#prevalence-container").getAttribute("data-test");
const prevalence2Data = JSON.parse(prevalence2DataStr!);
const prevalenceTimeSeries2LastY = prevalence2Data.lastDataPoint[1];
expect(prevalenceTimeSeries2LastY).not.toEqual(prevalenceTimeSeries1LastY);

// Test that the second analysis' costs pie chart has different data from the first.
const costsPie2DataStr = await page.locator("#costsChartContainer").getAttribute("data-test");
const costsPie2Data = JSON.parse(costsPie2DataStr!);
expect(costsPie2Data).toHaveLength(12);
costsPie2Data.forEach((cost: any, index: number) => {
expect(cost.value).not.toEqual(costsPieData[index].value);
});

// Test that the user can navigate to previously-run analyses, including when the page is initially rendered server-side.
await page.goto(urlOfFirstAnalysis);
await page.waitForURL(urlOfFirstAnalysis);
Expand Down
1 change: 1 addition & 0 deletions types/dataTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type TimeSeriesDataPoint = [number, number];

0 comments on commit 422c8f6

Please sign in to comment.