diff --git a/src/RootContext.ts b/src/RootContext.ts
index dbad753..453e706 100644
--- a/src/RootContext.ts
+++ b/src/RootContext.ts
@@ -14,6 +14,7 @@ export const initialState: AppState = {
export enum ActionType {
ERROR_ADDED = "ERROR_ADDED",
ERROR_DISMISSED = "ERROR_DISMISSED",
+ CLEAR_ALL_ERRORS = "CLEAR_ALL_ERRORS",
UPLOAD_ERROR_ADDED = "UPLOAD_ERROR_ADDED",
UPLOAD_ERROR_DISMISSED = "UPLOAD_ERROR_DISMISSED",
DATASET_NAMES_FETCHED = "DATASET_NAMES_FETCHED",
@@ -21,7 +22,8 @@ export enum ActionType {
DATASET_SELECTED = "DATASET_SELECTED",
SELECT_COVARIATE = "SELECT_COVARIATE",
UNSELECT_COVARIATE = "UNSELECT_COVARIATE",
- SELECT_SCALE = "SELECT_SCALE"
+ SELECT_SCALE = "SELECT_SCALE",
+ SET_SPLINE_OPTIONS = "SET_SPLINE_OPTIONS",
}
export interface RootAction {
diff --git a/src/components/App.tsx b/src/components/App.tsx
index d565cda..c2aef2b 100644
--- a/src/components/App.tsx
+++ b/src/components/App.tsx
@@ -24,6 +24,7 @@ export default function App() {
}
}, [state.selectedDataset, state.language, dispatch]);
+
useEffect(() => {
setInterval(() => {
dataService("en", () => {
diff --git a/src/components/ExploreDataset.tsx b/src/components/ExploreDataset.tsx
index a9a2b36..bd5d2c2 100644
--- a/src/components/ExploreDataset.tsx
+++ b/src/components/ExploreDataset.tsx
@@ -3,7 +3,7 @@ import {RootContext} from "../RootContext";
import {Col, Row} from "react-bootstrap";
import SideBar from "./SideBar";
import LinePlot from "./LinePlot";
-import {calculateFacets} from "../services/plotUtils";
+import {calculateFacets} from "../services/utils";
export function ExploreDataset() {
diff --git a/src/components/LinePlot.tsx b/src/components/LinePlot.tsx
index 6eec4f4..2179cb9 100644
--- a/src/components/LinePlot.tsx
+++ b/src/components/LinePlot.tsx
@@ -1,9 +1,10 @@
-import React, {useContext, useEffect, useState} from 'react';
+import React, {useContext, useState} from 'react';
import Plot from 'react-plotly.js';
import {RootContext, RootDispatchContext} from "../RootContext";
import {DataSeries} from "../generated";
import {dataService} from "../services/dataService";
-import {toFilename} from "../services/plotUtils";
+import {toFilename} from "../services/utils";
+import {useDebouncedEffect} from "../hooks/useDebouncedEffect";
interface Props {
biomarker: string
@@ -45,11 +46,12 @@ export default function LinePlot({
const covariateSettings = state.datasetSettings[state.selectedDataset].covariateSettings;
const scale = state.datasetSettings[state.selectedDataset].scale;
- useEffect(() => {
+ const splineSettings = state.datasetSettings[state.selectedDataset].splineSettings;
+ useDebouncedEffect(() => {
const fetchData = async () => {
const result = await dataService(state.language, dispatch)
.getDataSeries(state.selectedDataset,
- biomarker, facetDefinition, covariateSettings, scale);
+ biomarker, facetDefinition, covariateSettings, scale, splineSettings);
if (result && result.data) {
setSeries(result.data)
@@ -58,7 +60,7 @@ export default function LinePlot({
}
}
fetchData();
- }, [state.language, dispatch, state.selectedDataset, biomarker, facetDefinition, covariateSettings, scale]);
+ }, [state.language, dispatch, state.selectedDataset, biomarker, facetDefinition, covariateSettings, scale, splineSettings], 100);
let series: any[] = [];
diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx
index 2951340..eeed3ff 100644
--- a/src/components/SideBar.tsx
+++ b/src/components/SideBar.tsx
@@ -6,6 +6,7 @@ import CovariateOptions from "./CovariateOptions";
import SelectedCovariate from "./SelectedCovariate";
import ChooseDataset from "./ChooseDataset";
import ChooseScale from "./ChooseScale";
+import SplineOptions from "./SplineOptions";
export default function SideBar() {
@@ -39,6 +40,10 @@ export default function SideBar() {
+
+ Spline options
+
+
{availableCovariates.length > 0 &&
Disaggregate by
diff --git a/src/components/SplineOptions.tsx b/src/components/SplineOptions.tsx
new file mode 100644
index 0000000..595fd61
--- /dev/null
+++ b/src/components/SplineOptions.tsx
@@ -0,0 +1,82 @@
+import React, {useContext} from "react";
+import {ActionType, RootContext, RootDispatchContext} from "../RootContext";
+import Form from "react-bootstrap/Form";
+import {Col, Row} from "react-bootstrap";
+import {between} from "../services/utils";
+
+export default function SplineOptions() {
+
+ const state = useContext(RootContext);
+ const dispatch = useContext(RootDispatchContext);
+
+ const onChangeMethod = (event: any) => {
+ dispatch({
+ type: ActionType.SET_SPLINE_OPTIONS,
+ payload: {method: event.target.value}
+ });
+ }
+
+ const onChangeSpan = (event: any) => {
+ dispatch({
+ type: ActionType.SET_SPLINE_OPTIONS,
+ payload: {span: between(event.target.value, 0, 1)}
+ });
+ }
+
+ const onChangeKnots = (event: any) => {
+ dispatch({
+ type: ActionType.SET_SPLINE_OPTIONS,
+ payload: {k: between(event.target.value, 5, 30)}
+ });
+ }
+
+ const settings = state.datasetSettings[state.selectedDataset].splineSettings;
+
+ return
+
+
+ Method:
+
+
+
+
+
+
+
+
+
+
+
+ Span:
+
+
+
+
+
+
+
+
+ k:
+
+
+
+
+
+
+
+}
diff --git a/src/hooks/useDebouncedEffect.ts b/src/hooks/useDebouncedEffect.ts
new file mode 100644
index 0000000..8f417ef
--- /dev/null
+++ b/src/hooks/useDebouncedEffect.ts
@@ -0,0 +1,10 @@
+import { useEffect } from "react";
+
+export const useDebouncedEffect = (effect: () => void, deps: any[], delay: number) => {
+ useEffect(() => {
+ const handler = setTimeout(() => effect(), delay);
+
+ return () => clearTimeout(handler);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [...(deps || []), delay]);
+}
diff --git a/src/reducers/datasetReducer.ts b/src/reducers/datasetReducer.ts
index d72708e..b0abe40 100644
--- a/src/reducers/datasetReducer.ts
+++ b/src/reducers/datasetReducer.ts
@@ -1,4 +1,4 @@
-import {AppState, DatasetSettings} from "../types";
+import {AppState, DatasetSettings, SplineSettings} from "../types";
import {ActionType, RootAction} from "../RootContext";
export const datasetReducer = (state: AppState, action: RootAction): AppState => {
@@ -13,14 +13,23 @@ export const datasetReducer = (state: AppState, action: RootAction): AppState =>
return unselectCovariate(state, action)
case ActionType.SELECT_SCALE:
return selectScale(state, action)
+ case ActionType.SET_SPLINE_OPTIONS:
+ return setSpline(state, action)
default:
return state
}
}
+const splineSettings = (): SplineSettings => ({
+ method: "auto",
+ span: 0.75,
+ k: 10
+})
+
const datasetSettings = (): DatasetSettings => ({
covariateSettings: [],
- scale: "natural"
+ scale: "natural",
+ splineSettings: splineSettings()
})
const selectDataset = (state: AppState, action: RootAction): AppState => {
@@ -53,3 +62,10 @@ const selectScale = (state: AppState, action: RootAction): AppState => {
newState.datasetSettings[state.selectedDataset].scale = action.payload
return newState
}
+
+const setSpline = (state: AppState, action: RootAction): AppState => {
+ const newState = {...state}
+ const settings = newState.datasetSettings[state.selectedDataset].splineSettings;
+ newState.datasetSettings[state.selectedDataset].splineSettings = {...settings, ...action.payload}
+ return newState
+}
diff --git a/src/reducers/rootReducer.ts b/src/reducers/rootReducer.ts
index 2abe2bf..1cc71fe 100644
--- a/src/reducers/rootReducer.ts
+++ b/src/reducers/rootReducer.ts
@@ -15,6 +15,11 @@ export const rootReducer = (state: AppState, action: RootAction): AppState => {
...state,
genericErrors: state.genericErrors.filter(e => e.detail !== action.payload.detail || e.error !== action.payload.error)
}
+ case ActionType.CLEAR_ALL_ERRORS:
+ return {
+ ...state,
+ genericErrors: []
+ }
case ActionType.UPLOAD_ERROR_ADDED:
return {...state, uploadError: action.payload}
case ActionType.UPLOAD_ERROR_DISMISSED:
diff --git a/src/services/apiService.ts b/src/services/apiService.ts
index 336dc63..592c314 100644
--- a/src/services/apiService.ts
+++ b/src/services/apiService.ts
@@ -25,7 +25,6 @@ export interface API {
postAndReturn(url: string, data: any): Promise>
get(url: string): Promise>
- delete(url: string): Promise
}
export class APIService implements API {
@@ -120,6 +119,10 @@ export class APIService implements API {
this._dispatch({type: ActionType.ERROR_ADDED, payload: error});
};
+ private _clearErrors = () => {
+ this._dispatch({type: ActionType.CLEAR_ALL_ERRORS, payload: null});
+ };
+
private _verifyHandlers(url: string) {
if (this._onError == null && !this._ignoreErrors) {
console.warn(`No error handler registered for request ${url}.`)
@@ -131,6 +134,7 @@ export class APIService implements API {
async get(url: string): Promise> {
this._verifyHandlers(url);
+ this._clearErrors();
const fullUrl = this._buildFullUrl(url);
return this._handleAxiosResponse(axios.get(fullUrl, {headers: this._headers}));
}
@@ -147,6 +151,7 @@ export class APIService implements API {
async postAndReturn(url: string, data?: any): Promise> {
this._verifyHandlers(url);
+ this._clearErrors();
const fullUrl = this._buildFullUrl(url);
// this allows us to pass data of type FormData in both the browser and
@@ -158,11 +163,6 @@ export class APIService implements API {
return this._handleAxiosResponse(axios.post(fullUrl, data, {headers}));
}
- async delete(url: string) {
- const fullUrl = this._buildFullUrl(url);
- return this._handleAxiosResponse(axios.delete(fullUrl));
- }
-
}
export const api = (lang: string, dispatch: (action: RootAction) => void) => new APIService(lang, dispatch);
diff --git a/src/services/dataService.ts b/src/services/dataService.ts
index c34c2bb..5b97b14 100644
--- a/src/services/dataService.ts
+++ b/src/services/dataService.ts
@@ -7,7 +7,7 @@ import {
UploadResult
} from "../generated";
import {
- GenericResponse, CovariateSettings,
+ GenericResponse, CovariateSettings, SplineSettings,
} from "../types";
import {Dispatch} from "react";
@@ -51,7 +51,8 @@ export class DataService {
biomarker: string,
facetDefinition: string,
covariateSettings: CovariateSettings[],
- scale: "log" | "natural" | "log2") {
+ scale: "log" | "natural" | "log2",
+ splineSettings: SplineSettings) {
const traces = covariateSettings
@@ -67,7 +68,7 @@ export class DataService {
queryString += `disaggregate=${encodeURIComponent(traces)}&`
}
- queryString += `scale=${scale}`
+ queryString += `scale=${scale}&method=${splineSettings.method}&span=${splineSettings.span}&k=${splineSettings.k}`
return await this._api
.ignoreSuccess()
diff --git a/src/services/plotUtils.ts b/src/services/utils.ts
similarity index 79%
rename from src/services/plotUtils.ts
rename to src/services/utils.ts
index 7f388af..585ac33 100644
--- a/src/services/plotUtils.ts
+++ b/src/services/utils.ts
@@ -10,3 +10,7 @@ export const toFilename = (title: string) => title
.replaceAll(/\W+/g, " ")
.trim()
.replaceAll(/\s+/g, "_")
+
+export const between = (x: number, min: number, max: number) => {
+ return Math.min(Math.max(x, min), max)
+}
diff --git a/src/types.ts b/src/types.ts
index 3aad6d0..91440b7 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -23,9 +23,16 @@ export interface CovariateSettings extends Variable {
display: PlotDisplay
}
+export interface SplineSettings {
+ method: "gam" | "loess" | "auto"
+ k: number
+ span: number
+}
+
export interface DatasetSettings {
covariateSettings: CovariateSettings[]
scale: "log" | "natural" | "log2"
+ splineSettings: SplineSettings
}
export interface AppState {
diff --git a/test/components/ChooseDataset.test.tsx b/test/components/ChooseDataset.test.tsx
index 343cb9c..da124ed 100644
--- a/test/components/ChooseDataset.test.tsx
+++ b/test/components/ChooseDataset.test.tsx
@@ -27,9 +27,9 @@ describe("", () => {
);
- await waitFor(() => expect(dispatch.mock.calls.length).toBe(1));
+ await waitFor(() => expect(dispatch.mock.calls.length).toBe(2));
- expect(dispatch.mock.calls[0][0]).toEqual({
+ expect(dispatch.mock.calls[1][0]).toEqual({
type: ActionType.DATASET_NAMES_FETCHED,
payload: ["d1", "d2"]
});
@@ -98,7 +98,7 @@ describe("", () => {
const submit = screen.getByText("Go");
await user.click(submit);
- expect(dispatch.mock.calls[1][0]).toEqual({
+ expect(dispatch.mock.calls[2][0]).toEqual({
type: ActionType.DATASET_SELECTED,
payload: "d2"
});
@@ -152,11 +152,14 @@ describe("", () => {
const testFile = new File(['hello'], 'hello.csv', {type: 'text/csv'});
await user.upload(fileInput, testFile);
- expect(dispatch.mock.calls.length).toBe(4);
- expect(dispatch.mock.calls[0][0].type).toBe(ActionType.DATASET_NAMES_FETCHED);
- expect(dispatch.mock.calls[1][0].type).toBe(ActionType.UPLOAD_ERROR_DISMISSED);
- expect(dispatch.mock.calls[2][0].type).toBe(ActionType.DATASET_NAMES_FETCHED);
- expect(dispatch.mock.calls[3][0].type).toBe(ActionType.DATASET_SELECTED);
- expect(dispatch.mock.calls[3][0].payload).toBe("hello");
+ expect(dispatch.mock.calls.length).toBe(7);
+ expect(dispatch.mock.calls[0][0].type).toBe(ActionType.CLEAR_ALL_ERRORS);
+ expect(dispatch.mock.calls[1][0].type).toBe(ActionType.DATASET_NAMES_FETCHED);
+ expect(dispatch.mock.calls[2][0].type).toBe(ActionType.UPLOAD_ERROR_DISMISSED);
+ expect(dispatch.mock.calls[3][0].type).toBe(ActionType.CLEAR_ALL_ERRORS);
+ expect(dispatch.mock.calls[4][0].type).toBe(ActionType.CLEAR_ALL_ERRORS);
+ expect(dispatch.mock.calls[5][0].type).toBe(ActionType.DATASET_NAMES_FETCHED);
+ expect(dispatch.mock.calls[6][0].type).toBe(ActionType.DATASET_SELECTED);
+ expect(dispatch.mock.calls[6][0].payload).toBe("hello");
});
});
diff --git a/test/components/LinePlot.test.tsx b/test/components/LinePlot.test.tsx
index 197dbf2..6449b89 100644
--- a/test/components/LinePlot.test.tsx
+++ b/test/components/LinePlot.test.tsx
@@ -24,7 +24,7 @@ describe("", () => {
});
test("requests data for given biomarker", async () => {
- mockAxios.onGet(`/dataset/d1/trace/ab/?scale=natural`)
+ mockAxios.onGet(`/dataset/d1/trace/ab/?scale=natural&method=auto&span=0.75&k=10`)
.reply(200, mockSuccess([{
name: "all",
model: {
@@ -94,7 +94,7 @@ describe("", () => {
});
test("requests data for given facet variables", async () => {
- mockAxios.onGet(`/dataset/d1/trace/ab/?filter=age%3A0%2Bsex%3AF&scale=natural`)
+ mockAxios.onGet(`/dataset/d1/trace/ab/?filter=age%3A0%2Bsex%3AF&scale=natural&method=auto&span=0.75&k=10`)
.reply(200, mockSuccess([{
name: "all",
model: {
@@ -164,7 +164,7 @@ describe("", () => {
});
test("clears plot data if request to API fails", async () => {
- mockAxios.onGet("/dataset/d1/trace/ab/?scale=natural")
+ mockAxios.onGet("/dataset/d1/trace/ab/?scale=natural&method=auto&span=0.75&k=10")
.reply(200, mockSuccess([{
name: "all",
model: {
@@ -177,7 +177,7 @@ describe("", () => {
}
}]));
- mockAxios.onGet("/dataset/d1/trace/ab/?filter=sex%3AF&scale=natural")
+ mockAxios.onGet("/dataset/d1/trace/ab/?filter=sex%3AF&scale=natural&method=auto&span=0.75&k=10")
.reply(404, mockFailure("bad"));
const dispatch = jest.fn();
diff --git a/test/components/SideBar.test.tsx b/test/components/SideBar.test.tsx
index d4c5a76..bbd19aa 100644
--- a/test/components/SideBar.test.tsx
+++ b/test/components/SideBar.test.tsx
@@ -37,7 +37,7 @@ describe("", () => {
);
- const selectVariable = screen.getAllByRole("listbox")[0] as HTMLSelectElement;
+ const selectVariable = screen.getAllByRole("listbox")[1] as HTMLSelectElement;
let items = selectVariable.options;
expect(items.length).toBe(2);
expect(items[0].value).toBe("b");
@@ -97,4 +97,21 @@ describe("", () => {
payload: "d2"
});
});
+
+ test("user can change spline settings", async () => {
+ const state = mockAppState({
+ datasetNames: ["d1", "d2"],
+ selectedDataset: "d1",
+ datasetSettings: {"d1": mockDatasetSettings()}
+ });
+ const dispatch = jest.fn();
+ const {container} = render(
+
+
+
+
+ );
+
+ expect(container.textContent).toContain("Spline options")
+ });
});
diff --git a/test/components/SplineOptions.test.tsx b/test/components/SplineOptions.test.tsx
new file mode 100644
index 0000000..f123bde
--- /dev/null
+++ b/test/components/SplineOptions.test.tsx
@@ -0,0 +1,94 @@
+import React from "react";
+import {render, screen} from "@testing-library/react";
+import {
+ mockAppState,
+ mockDatasetSettings
+} from "../mocks";
+import {
+ RootDispatchContext,
+ RootContext,
+ ActionType
+} from "../../src/RootContext";
+import {userEvent} from "@testing-library/user-event";
+import SplineOptions from "../../src/components/SplineOptions";
+
+describe("", () => {
+
+ test("can change method", async () => {
+ const dispatch = jest.fn();
+ const state = mockAppState({
+ selectedDataset: "d1",
+ datasetSettings: {
+ "d1": mockDatasetSettings()
+ }
+ });
+ render(
+
+
+
+
+ );
+
+ const select = screen.getByRole("listbox") as HTMLSelectElement;
+ expect(select.value).toBe("auto");
+ expect(select.item(1)!!.value).toBe("gam");
+ expect(select.item(2)!!.value).toBe("loess");
+ await userEvent.selectOptions(select, "gam");
+
+ expect(dispatch.mock.calls[0][0]).toEqual({
+ type: ActionType.SET_SPLINE_OPTIONS,
+ payload: {method: "gam"}
+ });
+ });
+
+ test("can change span", async () => {
+ const dispatch = jest.fn();
+ const state = mockAppState({
+ selectedDataset: "d1",
+ datasetSettings: {
+ "d1": mockDatasetSettings()
+ }
+ });
+ render(
+
+
+
+
+ );
+
+ const span = screen.getAllByRole("spinbutton")[0] as HTMLInputElement;
+ expect(span.value).toBe("0.75");
+
+ await userEvent.clear(span);
+
+ expect(dispatch.mock.calls[0][0]).toEqual({
+ type: ActionType.SET_SPLINE_OPTIONS,
+ payload: {span: 0}
+ });
+ });
+
+ test("can change k", async () => {
+ const dispatch = jest.fn();
+ const state = mockAppState({
+ selectedDataset: "d1",
+ datasetSettings: {
+ "d1": mockDatasetSettings()
+ }
+ });
+ render(
+
+
+
+
+ );
+
+ const k = screen.getAllByRole("spinbutton")[1] as HTMLSelectElement;
+ expect(k.value).toBe("10");
+ await userEvent.clear(k);
+
+ expect(dispatch.mock.calls[0][0]).toEqual({
+ type: ActionType.SET_SPLINE_OPTIONS,
+ payload: {k: 5}
+ });
+ });
+});
diff --git a/test/integration/dataService.itest.ts b/test/integration/dataService.itest.ts
index 9e357c3..3582926 100644
--- a/test/integration/dataService.itest.ts
+++ b/test/integration/dataService.itest.ts
@@ -15,7 +15,7 @@ describe("DataService", () => {
const formData = await getFormData("testpopulation.csv");
const res = await sut.uploadDataset(formData) as GenericResponse;
expect(res.data).toEqual("testpopulation");
- expect(dispatch.mock.calls.length).toBe(0);
+ expect(dispatch.mock.calls.length).toBe(1);
});
test("it can fetch root", async () => {
@@ -30,8 +30,8 @@ describe("DataService", () => {
const sut = dataService("en", dispatch);
const res = await sut.getDatasetNames() as GenericResponse;
expect(res.data).toEqual(["testpopulation"]);
- expect(dispatch.mock.calls[0][0].type).toBe(ActionType.DATASET_NAMES_FETCHED);
- expect(dispatch.mock.calls[0][0].payload).toEqual(["testpopulation"]);
+ expect(dispatch.mock.calls[1][0].type).toBe(ActionType.DATASET_NAMES_FETCHED);
+ expect(dispatch.mock.calls[1][0].payload).toEqual(["testpopulation"]);
});
test("it can fetch dataset metadata", async () => {
@@ -52,44 +52,58 @@ describe("DataService", () => {
xcol: "day"
}
expect(res.data).toEqual(expectedPayload);
- expect(dispatch.mock.calls[0][0].type).toBe(ActionType.DATASET_METADATA_FETCHED);
- expect(dispatch.mock.calls[0][0].payload).toEqual(expectedPayload);
+ expect(dispatch.mock.calls[1][0].type).toBe(ActionType.DATASET_METADATA_FETCHED);
+ expect(dispatch.mock.calls[1][0].payload).toEqual(expectedPayload);
});
- test("it can fetch dataset metadata", async () => {
+ test("it can fetch data series", async () => {
const dispatch = jest.fn();
const sut = dataService("en", dispatch);
- const res = await sut.getDataSeries("testpopulation", "ab_units", "", [], "natural") as GenericResponse;
+ const res = await sut.getDataSeries("testpopulation", "ab_units", "", [], "natural",
+ {
+ method: "auto",
+ span: 0.75,
+ k: 10
+ }) as GenericResponse;
expect(res.data!![0].name).toBe("all");
expect(res.data!![0].raw.x).toEqual([1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4]);
- expect(dispatch.mock.calls.length).toBe(0);
+ expect(dispatch.mock.calls.length).toBe(1);
});
- test("it can fetch dataset metadata with facet definition", async () => {
+ test("it can fetch data series with facet definition", async () => {
const dispatch = jest.fn();
const sut = dataService("en", dispatch);
- const res = await sut.getDataSeries("testpopulation", "ab_units", "sex:F", [], "natural") as GenericResponse;
+ const res = await sut.getDataSeries("testpopulation", "ab_units", "sex:F", [], "natural",
+ {
+ method: "auto",
+ span: 0.75,
+ k: 10
+ }) as GenericResponse;
expect(res.data!!.length).toBe(1);
expect(res.data!![0].name).toBe("sex:F");
expect(res.data!![0].raw.x).toEqual([1, 2, 1, 2, 1, 2, 1, 2]);
- expect(dispatch.mock.calls.length).toBe(0);
+ expect(dispatch.mock.calls.length).toBe(1);
});
- test("it can fetch dataset metadata with facet definition and trace", async () => {
+ test("it can fetch data series with facet definition and trace", async () => {
const dispatch = jest.fn();
const sut = dataService("en", dispatch);
const res = await sut.getDataSeries("testpopulation", "ab_units", "sex:F", [{
name: "age",
display: "trace",
levels: ["0-5"]
- }], "natural") as GenericResponse;
+ }], "natural", {
+ method: "auto",
+ span: 0.75,
+ k: 10
+ }) as GenericResponse;
expect(res.data!!.length).toBe(2);
expect(res.data!![0].name).toBe("0-5");
expect(res.data!![0].raw.x).toEqual([1, 2, 1, 2]);
expect(res.data!![1].name).toBe("5+");
expect(res.data!![1].raw.x).toEqual([1, 2, 1, 2]);
- expect(dispatch.mock.calls.length).toBe(0);
+ expect(dispatch.mock.calls.length).toBe(1);
});
});
diff --git a/test/mocks.ts b/test/mocks.ts
index a993586..8cf9379 100644
--- a/test/mocks.ts
+++ b/test/mocks.ts
@@ -3,7 +3,7 @@ import MockAdapter from "axios-mock-adapter";
import {
AppState,
ResponseSuccess,
- CovariateSettings, DatasetSettings
+ CovariateSettings, DatasetSettings, SplineSettings
} from "../src/types";
import {
DataSeries,
@@ -39,10 +39,20 @@ export function mockDatasetMetadata(datasetMetadata: Partial =
}
}
+export function mockSplineSettings(settings: Partial = {}): SplineSettings {
+ return {
+ method: "auto",
+ span: 0.75,
+ k: 10,
+ ...settings
+ }
+}
+
export function mockDatasetSettings(settings: Partial = {}): DatasetSettings {
return {
covariateSettings: [],
scale: "natural",
+ splineSettings: mockSplineSettings(),
...settings
}
}
diff --git a/test/rootReducer.test.ts b/test/rootReducer.test.ts
index ae2569d..f9df56d 100644
--- a/test/rootReducer.test.ts
+++ b/test/rootReducer.test.ts
@@ -109,7 +109,6 @@ describe("rootReducer", () => {
expect(newState.datasetSettings["d1"].covariateSettings.length).toBe(0);
});
-
it("should select scale on SELECT_SCALE", () => {
const state = mockAppState({
selectedDataset: "d1",
@@ -122,4 +121,31 @@ describe("rootReducer", () => {
{type: ActionType.SELECT_SCALE, payload: "log"});
expect(newState.datasetSettings["d1"].scale).toBe("log");
});
+
+ it("should clear all errors on CLEAR_ALL_ERRORS", () => {
+ const state = mockAppState({
+ genericErrors: [mockError("1"), mockError("2")]
+ });
+ const newState = rootReducer(state,
+ {type: ActionType.CLEAR_ALL_ERRORS, payload: null});
+ expect(newState.genericErrors.length).toBe(0);
+ });
+
+ it("should set spline options on SET_SPLINE_OPTIONS", () => {
+ const state = mockAppState({selectedDataset: "d1",
+ datasetSettings: {
+ "d1": mockDatasetSettings()
+ }});
+ let newState = rootReducer(state,
+ {type: ActionType.SET_SPLINE_OPTIONS, payload: { method: "gam"}});
+ expect(newState.datasetSettings["d1"].splineSettings.method).toBe("gam");
+
+ newState = rootReducer(state,
+ {type: ActionType.SET_SPLINE_OPTIONS, payload: { span: 0.1}});
+ expect(newState.datasetSettings["d1"].splineSettings.span).toBe(0.1);
+
+ newState = rootReducer(state,
+ {type: ActionType.SET_SPLINE_OPTIONS, payload: { k: 30}});
+ expect(newState.datasetSettings["d1"].splineSettings.k).toBe(30);
+ });
});
diff --git a/test/services/apiService.test.ts b/test/services/apiService.test.ts
index c95cb8e..f87437f 100644
--- a/test/services/apiService.test.ts
+++ b/test/services/apiService.test.ts
@@ -33,6 +33,29 @@ describe("ApiService", () => {
.toBe("some error message");
});
+ it("clears all before making a call", async () => {
+
+ mockAxios.onGet(`/`)
+ .reply(200, mockSuccess(true));
+
+ mockAxios.onPost(`/`)
+ .reply(200, mockSuccess(true));
+
+ const dispatch = jest.fn();
+
+ await api(rootState.language, dispatch as any)
+ .get("/");
+
+ expect(dispatch.mock.calls.length).toBe(1);
+ expect(dispatch.mock.calls[0][0].type).toBe(ActionType.CLEAR_ALL_ERRORS);
+
+ await api(rootState.language, dispatch as any)
+ .postAndReturn("/");
+
+ expect(dispatch.mock.calls.length).toBe(2);
+ expect(dispatch.mock.calls[1][0].type).toBe(ActionType.CLEAR_ALL_ERRORS);
+ });
+
it("dispatches the the first error message by default", async () => {
mockAxios.onGet(`/unusual/`)
@@ -46,9 +69,9 @@ describe("ApiService", () => {
expect((console.warn as jest.Mock).mock.calls[0][0])
.toBe("No error handler registered for request /unusual/.");
- expect(dispatch.mock.calls.length).toBe(1);
- expect(dispatch.mock.calls[0][0].type).toBe(ActionType.ERROR_ADDED);
- expect(dispatch.mock.calls[0][0].payload).toStrictEqual(mockError("some error message"));
+ expect(dispatch.mock.calls.length).toBe(2);
+ expect(dispatch.mock.calls[1][0].type).toBe(ActionType.ERROR_ADDED);
+ expect(dispatch.mock.calls[1][0].payload).toStrictEqual(mockError("some error message"));
});
it("dispatches an extra SESSION_EXPIRED error on 404", async () => {
@@ -61,11 +84,11 @@ describe("ApiService", () => {
await api(rootState.language, dispatch as any)
.get("/bad/");
- expect(dispatch.mock.calls.length).toBe(2);
- expect(dispatch.mock.calls[0][0].type).toBe(ActionType.ERROR_ADDED);
- expect(dispatch.mock.calls[0][0].payload).toStrictEqual(mockError("some error message"));
+ expect(dispatch.mock.calls.length).toBe(3);
expect(dispatch.mock.calls[1][0].type).toBe(ActionType.ERROR_ADDED);
- expect(dispatch.mock.calls[1][0].payload).toStrictEqual(mockError("Your session may have expired.", "SESSION_EXPIRED"))
+ expect(dispatch.mock.calls[1][0].payload).toStrictEqual(mockError("some error message"));
+ expect(dispatch.mock.calls[2][0].type).toBe(ActionType.ERROR_ADDED);
+ expect(dispatch.mock.calls[2][0].payload).toStrictEqual(mockError("Your session may have expired.", "SESSION_EXPIRED"))
});
it("if no first error message, dispatches a default error message to errors module by default", async () => {
@@ -86,9 +109,9 @@ describe("ApiService", () => {
expect((console.warn as jest.Mock).mock.calls[0][0])
.toBe("No error handler registered for request /unusual/.");
- expect(dispatch.mock.calls.length).toBe(1);
- expect(dispatch.mock.calls[0][0].type).toBe(ActionType.ERROR_ADDED);
- expect(dispatch.mock.calls[0][0].payload).toStrictEqual({
+ expect(dispatch.mock.calls.length).toBe(2);
+ expect(dispatch.mock.calls[1][0].type).toBe(ActionType.ERROR_ADDED);
+ expect(dispatch.mock.calls[1][0].payload).toStrictEqual({
error: "MALFORMED_RESPONSE",
detail: "API response failed but did not contain any error information. If error persists, please contact support.",
});
@@ -249,9 +272,9 @@ describe("ApiService", () => {
await api(rootState.language, dispatch as any)
.get("/datasets/");
- expect(dispatch.mock.calls.length).toBe(1);
- expect(dispatch.mock.calls[0][0].type).toBe(ActionType.ERROR_ADDED);
- expect(dispatch.mock.calls[0][0].payload).toStrictEqual({
+ expect(dispatch.mock.calls.length).toBe(2);
+ expect(dispatch.mock.calls[1][0].type).toBe(ActionType.ERROR_ADDED);
+ expect(dispatch.mock.calls[1][0].payload).toStrictEqual({
error: "MALFORMED_RESPONSE",
detail: "Could not parse API response. If error persists, please contact support."
});
diff --git a/test/services/plotUtils.test.ts b/test/services/utils.test.ts
similarity index 84%
rename from test/services/plotUtils.test.ts
rename to test/services/utils.test.ts
index 350d16f..388397e 100644
--- a/test/services/plotUtils.test.ts
+++ b/test/services/utils.test.ts
@@ -1,4 +1,4 @@
-import {calculateFacets, toFilename} from "../../src/services/plotUtils";
+import {between, calculateFacets, toFilename} from "../../src/services/utils";
import {Variable} from "../../src/generated";
describe("plotUtils", () => {
@@ -36,4 +36,11 @@ describe("plotUtils", () => {
expect(toFilename("ab_units")).toBe("ab_units")
expect(toFilename("ABunits")).toBe("abunits")
})
+
+
+ it("can bound number by min and max", () => {
+ expect(between(1.1, 0, 1)).toBe(1);
+ expect(between(-0.1, 0, 1)).toBe(0);
+ expect(between(0.1, 0, 1)).toBe(0.1);
+ })
});