diff --git a/app/static/src/app/serialise.ts b/app/static/src/app/serialise.ts index 7a26c8ffe..fe8bb3628 100644 --- a/app/static/src/app/serialise.ts +++ b/app/static/src/app/serialise.ts @@ -3,7 +3,7 @@ import { FitState } from "./store/fit/state"; import { CodeState } from "./store/code/state"; import { ModelState } from "./store/model/state"; import { RunState } from "./store/run/state"; -import {BaseSensitivityState, SensitivityState} from "./store/sensitivity/state"; +import { BaseSensitivityState, SensitivityState } from "./store/sensitivity/state"; import { FitDataState } from "./store/fitData/state"; import { ModelFitState } from "./store/modelFit/state"; import { OdinFitResult, OdinRunResultDiscrete, OdinRunResultOde } from "./types/wrapperTypes"; @@ -15,7 +15,7 @@ import { } from "./types/serialisationTypes"; import { GraphSettingsState } from "./store/graphSettings/state"; import { Dict } from "./types/utilTypes"; -import {MultiSensitivityState} from "./store/multiSensitivity/state"; +import { MultiSensitivityState } from "./store/multiSensitivity/state"; function serialiseCode(code: CodeState) : CodeState { return { @@ -80,7 +80,7 @@ function serialiseBaseSensitivity(sensitivity: BaseSensitivityState) { hasResult: !!sensitivity.result.batch, error: sensitivity.result.error } : null - } + }; } function serialiseSensitivity(sensitivity: SensitivityState): SerialisedSensitivityState { diff --git a/app/static/src/app/store/sensitivity/actions.ts b/app/static/src/app/store/sensitivity/actions.ts index 6b84bf57e..ad43e8480 100644 --- a/app/static/src/app/store/sensitivity/actions.ts +++ b/app/static/src/app/store/sensitivity/actions.ts @@ -149,7 +149,7 @@ export const runSensitivityOnRehydrate = (context: ActionContext = { [BaseSensitivityAction.ComputeNext](context, batch: Batch) { diff --git a/app/static/src/app/store/sessions/actions.ts b/app/static/src/app/store/sessions/actions.ts index 6c4c7004d..16e167b05 100644 --- a/app/static/src/app/store/sessions/actions.ts +++ b/app/static/src/app/store/sessions/actions.ts @@ -12,7 +12,7 @@ import { SerialisedAppState } from "../../types/serialisationTypes"; import { deserialiseState } from "../../serialise"; import { SensitivityAction } from "../sensitivity/actions"; import { AppStateGetter } from "../appState/getters"; -import {MultiSensitivityAction} from "../multiSensitivity/actions"; +import { MultiSensitivityAction } from "../multiSensitivity/actions"; export enum SessionsAction { GetSessions = "GetSessions", @@ -69,7 +69,8 @@ export const actions: ActionTree = { dispatch(`sensitivity/${SensitivityAction.RunSensitivityOnRehydrate}`, null, rootOption); } if (sessionData.multiSensitivity.result?.hasResult) { - dispatch(`multiSensitivity/${MultiSensitivityAction.RunMultiSensitivityOnRehydrate}`, null, rootOption); + dispatch(`multiSensitivity/${MultiSensitivityAction.RunMultiSensitivityOnRehydrate}`, null, + rootOption); } } } diff --git a/app/static/tests/e2e/download.etest.ts b/app/static/tests/e2e/download.etest.ts index 84a767e8d..dbb29bf69 100644 --- a/app/static/tests/e2e/download.etest.ts +++ b/app/static/tests/e2e/download.etest.ts @@ -21,6 +21,7 @@ test.describe("Download tests", () => { }; test("can download from Run tab", async ({ page }) => { + console.log("start download"); await page.click("#download-btn"); await expect(await page.inputValue("#download-file-name input")).toContain("day2-run"); await testCanInputFileNameAndDownload(page); diff --git a/app/static/tests/e2e/multiSensitivity.etest.ts b/app/static/tests/e2e/multiSensitivity.etest.ts index 3bcaf7abe..8aae6b8ac 100644 --- a/app/static/tests/e2e/multiSensitivity.etest.ts +++ b/app/static/tests/e2e/multiSensitivity.etest.ts @@ -1,11 +1,11 @@ import { expect, Page, test } from "@playwright/test"; import { SensitivityScaleType, SensitivityVariationType } from "../../src/app/store/sensitivity/state"; import PlaywrightConfig from "../../playwright.config"; -import { writeCode } from "./utils"; +import { expectCanRunMultiSensitivity, writeCode } from "./utils"; -test.describe("Multi-sensitivity tests", () => { - const { timeout } = PlaywrightConfig; +const { timeout } = PlaywrightConfig; +test.describe("Multi-sensitivity tests", () => { test.beforeEach(async ({ page }) => { await page.goto("/apps/day1"); // Open Options tab @@ -122,22 +122,8 @@ test.describe("Multi-sensitivity tests", () => { SensitivityVariationType.Percentage, 5, null, null, 10, "1.900, 1.922, 1.944, ..., 2.100"); }); - const expectCanRunMultiSensitivity = async (page: Page) => { - // add a second varying parameter with default 10 values - should get 100 solutions from the 2 varying - await page.click("#add-param-settings"); - await expect(await page.locator("#edit-param-to-vary select")).toBeVisible(); - await page.click("#ok-settings"); - await expect(await page.locator(".sensitivity-options-settings").count()).toBe(2); - - await expect(await page.innerText(".multi-sensitivity-status")).toBe("Multi-sensitivity has not been run."); - await page.click("#run-multi-sens-btn"); - await expect(await page.locator("#run-multi-sens-btn")).toBeEnabled(); - await expect(await page.locator(".multi-sensitivity-status")) - .toHaveText("Multi-sensitivity run produced 100 solutions.", { timeout }); - }; - test("can run multi-sensitivity", async ({ page }) => { - await expectCanRunMultiSensitivity(page); + await expectCanRunMultiSensitivity(page, timeout); // shows update required message when update code await page.click(":nth-match(.wodin-left .nav-tabs a, 1)"); @@ -152,7 +138,7 @@ test.describe("Multi-sensitivity tests", () => { await page.click(":nth-match(.wodin-left .nav-tabs a, 2)"); await page.click(":nth-match(.wodin-right .nav-tabs a, 4)"); - await expectCanRunMultiSensitivity(page); + await expectCanRunMultiSensitivity(page, timeout); }); test("can show error in multi-sensitivity run", async ({ page }) => { diff --git a/app/static/tests/e2e/sessions.etest.ts b/app/static/tests/e2e/sessions.etest.ts index e4a9631c9..2d2a87dbe 100644 --- a/app/static/tests/e2e/sessions.etest.ts +++ b/app/static/tests/e2e/sessions.etest.ts @@ -1,5 +1,7 @@ import { - expect, test, chromium, Page + expect, test, + chromium, + Page } from "@playwright/test"; import * as os from "os"; import { @@ -7,7 +9,8 @@ import { newFitCode, realisticFitData, startModelFit, - waitForModelFitCompletion, expectWodinPlotDataSummary + waitForModelFitCompletion, expectWodinPlotDataSummary, + expectCanRunMultiSensitivity } from "./utils"; import PlaywrightConfig from "../../playwright.config"; @@ -64,6 +67,10 @@ test.describe("Sessions tests", () => { // 5 * 10 sensitivity traces, 5 central traces, 1 data plot expect(await page.locator(".wodin-plot-data-summary-series").count()).toBe(56); + // Run multi-sensitivity + await page.click(":nth-match(.wodin-right .nav-tabs a, 4)"); + expectCanRunMultiSensitivity(page, timeout); + // give the page a chance to save the session to back end await page.waitForTimeout(saveSessionTimeout); @@ -182,6 +189,13 @@ test.describe("Sessions tests", () => { const sensitivityDataSummary = await page.locator(":nth-match(.wodin-plot-data-summary-series, 56)"); await expectWodinPlotDataSummary(sensitivityDataSummary, "Cases", 32, 0, 31, 0, 13, "markers", null, "#cccc00"); + // Check multi-sensitivity result + await page.click(":nth-match(.wodin-right .nav-tabs a, 4)"); // Multi-sensitivity tab + await expect(await page.locator(".multi-sensitivity-status")) + .toHaveText("Multi-sensitivity run produced 100 solutions.", { timeout }); + await expect(await page.locator("#download-summary-btn")).toBeEnabled(); + await expect(await page.locator("#run-multi-sens-btn")).toBeEnabled(); + // Expect to be able to navigate to the share link we copied earlier - check it has some rehydrated data await page.goto(copiedLinkText); await expect(await page.innerText("#data-upload-success")).toBe(" Uploaded 32 rows and 2 columns"); diff --git a/app/static/tests/e2e/utils.ts b/app/static/tests/e2e/utils.ts index 632b98a9e..fa2728309 100644 --- a/app/static/tests/e2e/utils.ts +++ b/app/static/tests/e2e/utils.ts @@ -148,3 +148,18 @@ export const expectSummaryValues = async (page: Page, idx: number, name: string, expect(await page.getAttribute(locator, "y-max")).toBe(yMax); } }; + +export const expectCanRunMultiSensitivity = async (page: Page, timeout = 10000) => { + // add a second varying parameter with default 10 values - should get 100 solutions from the 2 varying + await page.click("#add-param-settings"); + await expect(await page.locator("#edit-param-to-vary select")).toBeVisible(); + await page.click("#ok-settings"); + await expect(await page.locator(".sensitivity-options-settings").count()).toBe(2); + + await expect(await page.innerText(".multi-sensitivity-status")).toBe("Multi-sensitivity has not been run."); + await page.click("#run-multi-sens-btn"); + await expect(await page.locator("#run-multi-sens-btn")).toBeEnabled(); + await expect(await page.locator(".multi-sensitivity-status")) + .toHaveText("Multi-sensitivity run produced 100 solutions.", { timeout }); + await expect(await page.locator("#download-summary-btn")).toBeEnabled(); +}; diff --git a/app/static/tests/unit/store/multiSensitivity/actions.test.ts b/app/static/tests/unit/store/multiSensitivity/actions.test.ts index e8402e110..876a097cf 100644 --- a/app/static/tests/unit/store/multiSensitivity/actions.test.ts +++ b/app/static/tests/unit/store/multiSensitivity/actions.test.ts @@ -10,7 +10,7 @@ import { } from "../sensitivity/actions.test"; import { AppType } from "../../../../src/app/store/appState/state"; import { BaseSensitivityMutation } from "../../../../src/app/store/sensitivity/mutations"; -import {SensitivityAction} from "../../../../src/app/store/sensitivity/actions"; +import { SensitivityAction } from "../../../../src/app/store/sensitivity/actions"; jest.mock("../../../../src/app/excel/wodinSensitivitySummaryDownload"); @@ -70,6 +70,7 @@ describe("multiSensitivity actions", () => { it("run multiSensitivity on rehydrate uses parameters from result", () => { expectRunOnRehydrateToUseParametersFromResult( - actions[MultiSensitivityAction.RunMultiSensitivityOnRehydrate] as any); + actions[MultiSensitivityAction.RunMultiSensitivityOnRehydrate] as any + ); }); }); diff --git a/app/static/tests/unit/store/sensitivity/actions.test.ts b/app/static/tests/unit/store/sensitivity/actions.test.ts index e7a7f7882..8667f124e 100644 --- a/app/static/tests/unit/store/sensitivity/actions.test.ts +++ b/app/static/tests/unit/store/sensitivity/actions.test.ts @@ -1,4 +1,4 @@ -import {Action, ActionTree} from "vuex"; +import { Action, ActionTree } from "vuex"; import { actions, BaseSensitivityAction, SensitivityAction } from "../../../../src/app/store/sensitivity/actions"; import { BaseSensitivityMutation, SensitivityMutation } from "../../../../src/app/store/sensitivity/mutations"; import { ModelGetter } from "../../../../src/app/store/model/getters"; @@ -8,7 +8,7 @@ import { AdvancedOptions } from "../../../../src/app/types/responseTypes"; import { AdvancedComponentType } from "../../../../src/app/store/run/state"; import { WodinSensitivitySummaryDownload } from "../../../../src/app/excel/wodinSensitivitySummaryDownload"; import Mock = jest.Mock; -import {BaseSensitivityState} from "../../../../src/app/store/sensitivity/state"; +import { BaseSensitivityState } from "../../../../src/app/store/sensitivity/state"; jest.mock("../../../../src/app/excel/wodinSensitivitySummaryDownload"); diff --git a/app/static/tests/unit/store/sessions/actions.test.ts b/app/static/tests/unit/store/sessions/actions.test.ts index e339509f1..5e50d725a 100644 --- a/app/static/tests/unit/store/sessions/actions.test.ts +++ b/app/static/tests/unit/store/sessions/actions.test.ts @@ -10,6 +10,7 @@ import { ModelAction } from "../../../../src/app/store/model/actions"; import { RunAction } from "../../../../src/app/store/run/actions"; import { SensitivityAction } from "../../../../src/app/store/sensitivity/actions"; import { AppStateGetter } from "../../../../src/app/store/appState/getters"; +import { MultiSensitivityAction } from "../../../../src/app/store/multiSensitivity/actions"; describe("SessionsActions", () => { const getSessionIdsSpy = jest.spyOn(localStorageManager, "getSessionIds") @@ -22,7 +23,8 @@ describe("SessionsActions", () => { }); const getSessionData = (hasOdin: boolean, compileRequired: boolean, - runHasResultOde: boolean, runHasResultDiscrete: boolean, sensitivityHasResult: boolean) => { + runHasResultOde: boolean, runHasResultDiscrete: boolean, sensitivityHasResult: boolean, + multiSensitivityHasResult: boolean) => { return { code: { currentCode: ["some saved code"] @@ -44,6 +46,11 @@ describe("SessionsActions", () => { result: { hasResult: sensitivityHasResult } + }, + multiSensitivity: { + result: { + hasResult: multiSensitivityHasResult + } } }; }; @@ -53,7 +60,7 @@ describe("SessionsActions", () => { }; const testRehydrate = async (stochastic: boolean) => { - const mockSessionData = getSessionData(true, false, !stochastic, stochastic, true); + const mockSessionData = getSessionData(true, false, !stochastic, stochastic, true, true); mockAxios.onGet("/apps/testApp/sessions/1234") .reply(200, mockSuccess(mockSessionData)); @@ -64,7 +71,7 @@ describe("SessionsActions", () => { expect(rootState.code.currentCode).toStrictEqual(["some saved code"]); expect(commit).toHaveBeenCalledTimes(1); expect(commit.mock.calls[0][0]).toBe(AppStateMutation.SetConfigured); - expect(dispatch).toHaveBeenCalledTimes(4); + expect(dispatch).toHaveBeenCalledTimes(5); expect(dispatch.mock.calls[0][0]).toBe(`model/${ModelAction.FetchOdinRunner}`); expect(dispatch.mock.calls[0][1]).toBe(null); expect(dispatch.mock.calls[0][2]).toStrictEqual({ root: true }); @@ -77,6 +84,10 @@ describe("SessionsActions", () => { expect(dispatch.mock.calls[3][0]).toBe(`sensitivity/${SensitivityAction.RunSensitivityOnRehydrate}`); expect(dispatch.mock.calls[3][1]).toBe(null); expect(dispatch.mock.calls[3][2]).toStrictEqual({ root: true }); + expect(dispatch.mock.calls[4][0]) + .toBe(`multiSensitivity/${MultiSensitivityAction.RunMultiSensitivityOnRehydrate}`); + expect(dispatch.mock.calls[4][1]).toBe(null); + expect(dispatch.mock.calls[4][2]).toStrictEqual({ root: true }); }; it("Rehydrates as expected for non-stochastic", async () => { @@ -88,7 +99,7 @@ describe("SessionsActions", () => { }); it("Rehydrate does not compile or run if no odin model", async () => { - const mockSessionData = getSessionData(false, false, true, false, true); + const mockSessionData = getSessionData(false, false, true, false, true, false); mockAxios.onGet("/apps/testApp/sessions/1234") .reply(200, mockSuccess(mockSessionData)); @@ -106,7 +117,7 @@ describe("SessionsActions", () => { }); it("Rehydrate does not compile or run if compile required is true", async () => { - const mockSessionData = getSessionData(true, true, true, false, true); + const mockSessionData = getSessionData(true, true, true, false, true, false); mockAxios.onGet("/apps/testApp/sessions/1234") .reply(200, mockSuccess(mockSessionData)); @@ -124,7 +135,7 @@ describe("SessionsActions", () => { }); it("Rehydrate does not run model if run has no result", async () => { - const mockSessionData = getSessionData(true, false, false, false, true); + const mockSessionData = getSessionData(true, false, false, false, true, false); mockAxios.onGet("/apps/testApp/sessions/1234") .reply(200, mockSuccess(mockSessionData)); @@ -148,7 +159,7 @@ describe("SessionsActions", () => { }); it("Rehydrate does not run sensitivity if sensitivity has no result", async () => { - const mockSessionData = getSessionData(true, false, true, false, false); + const mockSessionData = getSessionData(true, false, true, false, false, true); mockAxios.onGet("/apps/testApp/sessions/1234") .reply(200, mockSuccess(mockSessionData)); @@ -159,7 +170,7 @@ describe("SessionsActions", () => { expect(rootState.code.currentCode).toStrictEqual(["some saved code"]); expect(commit).toHaveBeenCalledTimes(1); expect(commit.mock.calls[0][0]).toBe(AppStateMutation.SetConfigured); - expect(dispatch).toHaveBeenCalledTimes(3); + expect(dispatch).toHaveBeenCalledTimes(4); expect(dispatch.mock.calls[0][0]).toBe(`model/${ModelAction.FetchOdinRunner}`); expect(dispatch.mock.calls[0][1]).toBe(null); expect(dispatch.mock.calls[0][2]).toStrictEqual({ root: true }); @@ -169,6 +180,37 @@ describe("SessionsActions", () => { expect(dispatch.mock.calls[2][0]).toBe(`run/${RunAction.RunModelOnRehydrate}`); expect(dispatch.mock.calls[2][1]).toBe(null); expect(dispatch.mock.calls[2][2]).toStrictEqual({ root: true }); + expect(dispatch.mock.calls[3][0]) + .toBe(`multiSensitivity/${MultiSensitivityAction.RunMultiSensitivityOnRehydrate}`); + expect(dispatch.mock.calls[3][1]).toBe(null); + expect(dispatch.mock.calls[3][2]).toStrictEqual({ root: true }); + }); + + it("Rehydrate does not run multiSensitivity if sensitivity has no result", async () => { + const mockSessionData = getSessionData(true, false, true, false, true, false); + mockAxios.onGet("/apps/testApp/sessions/1234") + .reply(200, mockSuccess(mockSessionData)); + + const commit = jest.fn(); + const dispatch = jest.fn(); + const rootState = { appName: "testApp", baseUrl: "", appsPath: "apps" } as any; + await (actions[SessionsAction.Rehydrate] as any)({ commit, dispatch, rootState }, "1234"); + expect(rootState.code.currentCode).toStrictEqual(["some saved code"]); + expect(commit).toHaveBeenCalledTimes(1); + expect(commit.mock.calls[0][0]).toBe(AppStateMutation.SetConfigured); + expect(dispatch).toHaveBeenCalledTimes(4); + expect(dispatch.mock.calls[0][0]).toBe(`model/${ModelAction.FetchOdinRunner}`); + expect(dispatch.mock.calls[0][1]).toBe(null); + expect(dispatch.mock.calls[0][2]).toStrictEqual({ root: true }); + expect(dispatch.mock.calls[1][0]).toBe(`model/${ModelAction.CompileModelOnRehydrate}`); + expect(dispatch.mock.calls[1][1]).toBe(null); + expect(dispatch.mock.calls[1][2]).toStrictEqual({ root: true }); + expect(dispatch.mock.calls[2][0]).toBe(`run/${RunAction.RunModelOnRehydrate}`); + expect(dispatch.mock.calls[2][1]).toBe(null); + expect(dispatch.mock.calls[2][2]).toStrictEqual({ root: true }); + expect(dispatch.mock.calls[3][0]).toBe(`sensitivity/${SensitivityAction.RunSensitivityOnRehydrate}`); + expect(dispatch.mock.calls[3][1]).toBe(null); + expect(dispatch.mock.calls[3][2]).toStrictEqual({ root: true }); }); it("GetSessions fetches and commits session metadata", async () => {