From 056719530dca7837574aafe071c4eaa5bf5c2e9f Mon Sep 17 00:00:00 2001 From: Tamoor Shahid Date: Tue, 24 Feb 2026 16:21:30 +0000 Subject: [PATCH 1/9] Removed empty log and set polling to false. --- src/components/OavVideoStream.tsx | 2 -- src/config_server/configServer.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/OavVideoStream.tsx b/src/components/OavVideoStream.tsx index 585d71f..8e5f439 100644 --- a/src/components/OavVideoStream.tsx +++ b/src/components/OavVideoStream.tsx @@ -116,8 +116,6 @@ function VideoBoxWithOverlay(props: { drawCanvas(canvasRef, props.crosshairX, props.crosshairY); }, [props.crosshairX, props.crosshairY, width, height]); - console.info(); - return ( Date: Tue, 24 Feb 2026 17:17:24 +0000 Subject: [PATCH 2/9] Added useEffect to refetch each time zoomValue is changed --- src/screens/OavMover/OAVStageController.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/screens/OavMover/OAVStageController.tsx b/src/screens/OavMover/OAVStageController.tsx index 0098340..6d55ee6 100644 --- a/src/screens/OavMover/OAVStageController.tsx +++ b/src/screens/OavMover/OAVStageController.tsx @@ -6,7 +6,7 @@ import { OavVideoStream } from "#/components/OavVideoStream.tsx"; import { useConfigCall } from "#/config_server/configServer.ts"; import { forceString, useParsedPvConnection } from "#/pv/util.ts"; import { ZoomLevels } from "#/pv/enumPvValues.ts"; -import { useMemo } from "react"; +import { useMemo, useEffect } from "react"; const DISPLAY_CONFIG_ENDPOINT = "/dls_sw/i24/software/daq_configuration/domain/display.configuration"; @@ -20,6 +20,9 @@ export function OavMover() { transformValue: forceString, }), ); + useEffect(() => { + beamCenterQuery.refetch(); + }, [currentZoomValue]); const zoomIndex = ZoomLevels.findIndex( (element: string) => element == currentZoomValue, ); From afa52ff04358980ac55fe1b6ef4e05be3c464c62 Mon Sep 17 00:00:00 2001 From: Tamoor Shahid Date: Wed, 25 Feb 2026 17:39:36 +0000 Subject: [PATCH 3/9] Refactored OAVStageController into smaller functions --- src/screens/OavMover/OAVStageController.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/screens/OavMover/OAVStageController.tsx b/src/screens/OavMover/OAVStageController.tsx index 6d55ee6..d47b9ea 100644 --- a/src/screens/OavMover/OAVStageController.tsx +++ b/src/screens/OavMover/OAVStageController.tsx @@ -10,19 +10,22 @@ import { useMemo, useEffect } from "react"; const DISPLAY_CONFIG_ENDPOINT = "/dls_sw/i24/software/daq_configuration/domain/display.configuration"; +const ZOOM_PV = "ca://BL24I-EA-OAV-01:FZOOM:MP:SELECT"; -export function OavMover() { +function useZoomAndCrosshair() { const beamCenterQuery = useConfigCall(DISPLAY_CONFIG_ENDPOINT); const currentZoomValue = String( useParsedPvConnection({ - pv: "ca://BL24I-EA-OAV-01:FZOOM:MP:SELECT", + pv: ZOOM_PV, label: "zoom-level", transformValue: forceString, }), ); + useEffect(() => { beamCenterQuery.refetch(); }, [currentZoomValue]); + const zoomIndex = ZoomLevels.findIndex( (element: string) => element == currentZoomValue, ); @@ -43,6 +46,12 @@ export function OavMover() { return [Number(xLine.split(" ")[2]), Number(yLine.split(" ")[2])]; }, [beamCenterQuery.data, zoomIndex]); + return { crosshairX, crosshairY }; +} + +export function OavMover() { + const { crosshairX, crosshairY } = useZoomAndCrosshair(); + const theme = useTheme(); const bgColor = theme.palette.background.paper; From 1178cbcc58b3fef962505c5d701c01b7e19df221 Mon Sep 17 00:00:00 2001 From: Tamoor Shahid Date: Fri, 27 Feb 2026 15:12:04 +0000 Subject: [PATCH 4/9] Created BeamCenter Context and some refactoring --- src/context/BeamCenterContext.ts | 8 ++++++++ src/context/BeamCenterProvider.tsx | 16 ++++++++++++++++ src/routes/BeamlineI24.tsx | 9 ++++++--- src/screens/OavMover/OAVStageController.tsx | 16 ++++++++-------- 4 files changed, 38 insertions(+), 11 deletions(-) create mode 100644 src/context/BeamCenterContext.ts create mode 100644 src/context/BeamCenterProvider.tsx diff --git a/src/context/BeamCenterContext.ts b/src/context/BeamCenterContext.ts new file mode 100644 index 0000000..37ce7e2 --- /dev/null +++ b/src/context/BeamCenterContext.ts @@ -0,0 +1,8 @@ +import { createContext } from "react"; +import { UseQueryResult } from "react-query"; + +type BeamCenterQueryResult = UseQueryResult; + +export const BeamCenterContext = createContext( + null as unknown as BeamCenterQueryResult, +); diff --git a/src/context/BeamCenterProvider.tsx b/src/context/BeamCenterProvider.tsx new file mode 100644 index 0000000..a9db5f2 --- /dev/null +++ b/src/context/BeamCenterProvider.tsx @@ -0,0 +1,16 @@ +import { ReactNode } from "react"; +import { useConfigCall } from "#/config_server/configServer.ts"; +import { BeamCenterContext } from "./BeamCenterContext"; + +const DISPLAY_CONFIG_ENDPOINT = + "/dls_sw/i24/software/daq_configuration/domain/display.configuration"; + +export const BeamCenterProvider = ({ children }: { children: ReactNode }) => { + const beamCenterQuery = useConfigCall(DISPLAY_CONFIG_ENDPOINT); + + return ( + + {children} + + ); +}; diff --git a/src/routes/BeamlineI24.tsx b/src/routes/BeamlineI24.tsx index 40bea1b..3f14120 100644 --- a/src/routes/BeamlineI24.tsx +++ b/src/routes/BeamlineI24.tsx @@ -2,9 +2,10 @@ import { BeamlineStatsTabPanel } from "#/screens/BeamlineStats.tsx"; import { DetectorMotionTabPanel } from "#/screens/DetectorMotion.tsx"; import { FallbackScreen } from "#/screens/FallbackScreen.tsx"; import { OavMover } from "#/screens/OavMover/OAVStageController.tsx"; +import { BeamCenterProvider } from "#/context/BeamCenterProvider.tsx"; import { Box, Tab, Tabs, useTheme } from "@mui/material"; -import React from "react"; import { ErrorBoundary } from "react-error-boundary"; +import { useState } from "react"; interface TabPanelProps { children?: React.ReactNode; @@ -37,7 +38,7 @@ function CustomTabPanel(props: TabPanelProps) { export function BeamlineI24() { const theme = useTheme(); - const [tab, setTab] = React.useState(0); + const [tab, setTab] = useState(0); const handleChange = (_event: React.SyntheticEvent, newTab: number) => { setTab(newTab); @@ -73,7 +74,9 @@ export function BeamlineI24() { - + + + diff --git a/src/screens/OavMover/OAVStageController.tsx b/src/screens/OavMover/OAVStageController.tsx index d47b9ea..8773c0e 100644 --- a/src/screens/OavMover/OAVStageController.tsx +++ b/src/screens/OavMover/OAVStageController.tsx @@ -1,19 +1,19 @@ import { Grid2, useTheme } from "@mui/material"; +import { useContext } from "react"; import { OAVSideBar } from "./OAVSideBar"; import { submitAndRunPlanImmediately } from "#/blueapi/blueapi.ts"; import { readVisitFromPv, parseInstrumentSession } from "#/blueapi/visit.ts"; import { OavVideoStream } from "#/components/OavVideoStream.tsx"; -import { useConfigCall } from "#/config_server/configServer.ts"; import { forceString, useParsedPvConnection } from "#/pv/util.ts"; import { ZoomLevels } from "#/pv/enumPvValues.ts"; import { useMemo, useEffect } from "react"; +import { BeamCenterContext } from "#/context/BeamCenterContext.ts"; -const DISPLAY_CONFIG_ENDPOINT = - "/dls_sw/i24/software/daq_configuration/domain/display.configuration"; const ZOOM_PV = "ca://BL24I-EA-OAV-01:FZOOM:MP:SELECT"; +const BEAM_CENTER_LINES_PER_ZOOM = 7; function useZoomAndCrosshair() { - const beamCenterQuery = useConfigCall(DISPLAY_CONFIG_ENDPOINT); + const beamCenterQuery = useContext(BeamCenterContext); const currentZoomValue = String( useParsedPvConnection({ pv: ZOOM_PV, @@ -24,7 +24,7 @@ function useZoomAndCrosshair() { useEffect(() => { beamCenterQuery.refetch(); - }, [currentZoomValue]); + }, [currentZoomValue, beamCenterQuery]); const zoomIndex = ZoomLevels.findIndex( (element: string) => element == currentZoomValue, @@ -36,8 +36,8 @@ function useZoomAndCrosshair() { } const lines = beamCenterQuery.data.split("\n"); - const xLine = lines[zoomIndex * 7 + 1]; - const yLine = lines[zoomIndex * 7 + 2]; + const xLine = lines[zoomIndex * BEAM_CENTER_LINES_PER_ZOOM + 1]; + const yLine = lines[zoomIndex * BEAM_CENTER_LINES_PER_ZOOM + 2]; if (!xLine || !yLine) { return [NaN, NaN]; @@ -64,7 +64,7 @@ export function OavMover() { instrumentSession: parseInstrumentSession(fullVisit), }).catch((error) => { console.log( - `Failed to run plan , see console and logs for full error. Reason: ${error}`, + `Failed to run plan, see console and logs for full error. Reason: ${error}`, ); }); } From 6d65ffef28a45bc130baff287e6c6ead6ea8f786 Mon Sep 17 00:00:00 2001 From: Tamoor Shahid Date: Fri, 27 Feb 2026 15:26:54 +0000 Subject: [PATCH 5/9] Added refetch to canvas click --- src/components/OavVideoStream.tsx | 5 ++++- src/screens/OavMover/OAVStageController.tsx | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/OavVideoStream.tsx b/src/components/OavVideoStream.tsx index 8e5f439..0283471 100644 --- a/src/components/OavVideoStream.tsx +++ b/src/components/OavVideoStream.tsx @@ -1,5 +1,5 @@ import { Box } from "@mui/material"; -import React, { useEffect } from "react"; +import React, { useContext, useEffect } from "react"; import { useContainerDimensions } from "./OavVideoStreamHelper"; import { PvComponent } from "#/pv/PvComponent.tsx"; import { PvDescription, PvItem } from "#/pv/types.ts"; @@ -8,6 +8,7 @@ import { parseNumericPv, pvIntArrayToString, } from "#/pv/util.ts"; +import { BeamCenterContext } from "#/context/BeamCenterContext.ts"; /* * A viewer which allows overlaying a crosshair (takes numbers which could be the values from a react useState hook) @@ -111,6 +112,7 @@ function VideoBoxWithOverlay(props: { const canvasRef = React.useRef(null); const videoBoxRef = React.useRef(null); const { width, height } = useContainerDimensions(videoBoxRef); + const beamCenterQuery = useContext(BeamCenterContext); useEffect(() => { drawCanvas(canvasRef, props.crosshairX, props.crosshairY); @@ -137,6 +139,7 @@ function VideoBoxWithOverlay(props: { const rect = canvas.getBoundingClientRect(); const [x, y] = [e.clientX - rect.left, e.clientY - rect.top]; props.onCoordClick(x, y); + beamCenterQuery.refetch(); } } }} diff --git a/src/screens/OavMover/OAVStageController.tsx b/src/screens/OavMover/OAVStageController.tsx index 8773c0e..8590f4f 100644 --- a/src/screens/OavMover/OAVStageController.tsx +++ b/src/screens/OavMover/OAVStageController.tsx @@ -24,7 +24,7 @@ function useZoomAndCrosshair() { useEffect(() => { beamCenterQuery.refetch(); - }, [currentZoomValue, beamCenterQuery]); + }, [currentZoomValue]); const zoomIndex = ZoomLevels.findIndex( (element: string) => element == currentZoomValue, From 60ee32518bd1fdaf813e5ee965045f75711947bd Mon Sep 17 00:00:00 2001 From: Tamoor Shahid Date: Fri, 27 Feb 2026 15:55:04 +0000 Subject: [PATCH 6/9] Buttons added to run refetch on click --- src/blueapi/BlueapiComponents.tsx | 19 ++++--- src/screens/OavMover/OAVMoveController.tsx | 58 ++++++++++++++++++---- 2 files changed, 61 insertions(+), 16 deletions(-) diff --git a/src/blueapi/BlueapiComponents.tsx b/src/blueapi/BlueapiComponents.tsx index c11fae6..e3ef084 100644 --- a/src/blueapi/BlueapiComponents.tsx +++ b/src/blueapi/BlueapiComponents.tsx @@ -30,6 +30,7 @@ type RunPlanButtonProps = { sx?: object; tooltipSx?: object; typographySx?: object; + onSuccess?: () => void | Promise; // Optional callback after plan succeeds }; export function RunPlanButton(props: RunPlanButtonProps) { @@ -63,13 +64,17 @@ export function RunPlanButton(props: RunPlanButtonProps) { planName: props.planName, planParams: params, instrumentSession: instrumentSession, - }).catch((error) => { - setSeverity("error"); - setMsg( - `Failed to run plan ${props.planName}, see console and logs for full error`, - ); - console.log(`${msg}. Reason: ${error}`); - }); + }) + .then(() => { + props.onSuccess?.(); + }) + .catch((error) => { + setSeverity("error"); + setMsg( + `Failed to run plan ${props.planName}, see console and logs for full error`, + ); + console.log(`${msg}. Reason: ${error}`); + }); } catch (error) { setSeverity("error"); setMsg( diff --git a/src/screens/OavMover/OAVMoveController.tsx b/src/screens/OavMover/OAVMoveController.tsx index 9c139a8..50571ca 100644 --- a/src/screens/OavMover/OAVMoveController.tsx +++ b/src/screens/OavMover/OAVMoveController.tsx @@ -1,4 +1,6 @@ import { RunPlanButton } from "#/blueapi/BlueapiComponents.tsx"; +import { useContext } from "react"; +import { BeamCenterContext } from "#/context/BeamCenterContext.ts"; import { KeyboardDoubleArrowUp, KeyboardArrowUp, @@ -37,7 +39,7 @@ const arrowsScreenSizing = { }, }; -function BlockMove(props: TabPanelProps) { +function BlockMove(props: TabPanelProps & { onMoveSuccess?: () => void }) { if (props.value !== props.index) return null; return ( @@ -48,6 +50,7 @@ function BlockMove(props: TabPanelProps) { planName={"move_block_on_arrow_click"} planParams={{ direction: "up" }} btnVariant="outlined" + onSuccess={props.onMoveSuccess} /> ); } -function NudgeMove(props: TabPanelProps) { +function NudgeMove(props: TabPanelProps & { onMoveSuccess?: () => void }) { if (props.value !== props.index) return null; return ( @@ -87,6 +93,7 @@ function NudgeMove(props: TabPanelProps) { planParams={{ direction: "up", size_of_move: "big" }} btnVariant="outlined" sx={arrowsScreenSizing} + onSuccess={props.onMoveSuccess} /> } @@ -110,6 +119,7 @@ function NudgeMove(props: TabPanelProps) { planParams={{ direction: "left", size_of_move: "small" }} btnVariant="outlined" sx={arrowsScreenSizing} + onSuccess={props.onMoveSuccess} /> } @@ -125,6 +136,7 @@ function NudgeMove(props: TabPanelProps) { planParams={{ direction: "right", size_of_move: "big" }} btnVariant="outlined" sx={arrowsScreenSizing} + onSuccess={props.onMoveSuccess} /> ); } -function WindowMove(props: TabPanelProps) { +function WindowMove(props: TabPanelProps & { onMoveSuccess?: () => void }) { if (props.value !== props.index) return null; return ( @@ -158,6 +172,7 @@ function WindowMove(props: TabPanelProps) { planParams={{ direction: "up", size_of_move: "big" }} btnVariant="outlined" sx={arrowsScreenSizing} + onSuccess={props.onMoveSuccess} /> } @@ -181,6 +198,7 @@ function WindowMove(props: TabPanelProps) { planParams={{ direction: "left", size_of_move: "small" }} btnVariant="outlined" sx={arrowsScreenSizing} + onSuccess={props.onMoveSuccess} /> } @@ -196,6 +215,7 @@ function WindowMove(props: TabPanelProps) { planParams={{ direction: "right", size_of_move: "big" }} btnVariant="outlined" sx={arrowsScreenSizing} + onSuccess={props.onMoveSuccess} /> ); } -function FocusMove(props: TabPanelProps) { +function FocusMove(props: TabPanelProps & { onMoveSuccess?: () => void }) { if (props.value !== props.index) return null; const focus_move = [ { direction: "in", size_of_move: "big", label: "IN x3" }, @@ -252,7 +274,7 @@ function FocusMove(props: TabPanelProps) { export function MoveArrows() { const theme = useTheme(); - + const beamCenterQuery = useContext(BeamCenterContext); const [value, setValue] = useState(0); const isSmall = useMediaQuery(theme.breakpoints.down("xl")); @@ -292,10 +314,28 @@ export function MoveArrows() { - - - - + { + beamCenterQuery?.refetch(); + }} + /> + beamCenterQuery?.refetch()} + /> + beamCenterQuery?.refetch()} + /> + beamCenterQuery?.refetch()} + /> ); } From 6a8a36424cd430d64da6ecd5b0632d6b5cff9199 Mon Sep 17 00:00:00 2001 From: Tamoor Shahid Date: Mon, 2 Mar 2026 15:33:13 +0000 Subject: [PATCH 7/9] Moved context files and added tests --- src/components/OavVideoStream.tsx | 2 +- .../{ => beamcenter}/BeamCenterContext.ts | 0 .../beamcenter/BeamCenterProvider.test.tsx | 80 +++++++++++++++++++ .../{ => beamcenter}/BeamCenterProvider.tsx | 0 src/routes/BeamlineI24.tsx | 2 +- src/screens/OavMover/OAVMoveController.tsx | 2 +- src/screens/OavMover/OAVStageController.tsx | 2 +- 7 files changed, 84 insertions(+), 4 deletions(-) rename src/context/{ => beamcenter}/BeamCenterContext.ts (100%) create mode 100644 src/context/beamcenter/BeamCenterProvider.test.tsx rename src/context/{ => beamcenter}/BeamCenterProvider.tsx (100%) diff --git a/src/components/OavVideoStream.tsx b/src/components/OavVideoStream.tsx index 0283471..9754619 100644 --- a/src/components/OavVideoStream.tsx +++ b/src/components/OavVideoStream.tsx @@ -8,7 +8,7 @@ import { parseNumericPv, pvIntArrayToString, } from "#/pv/util.ts"; -import { BeamCenterContext } from "#/context/BeamCenterContext.ts"; +import { BeamCenterContext } from "#/context/beamcenter/BeamCenterContext.ts"; /* * A viewer which allows overlaying a crosshair (takes numbers which could be the values from a react useState hook) diff --git a/src/context/BeamCenterContext.ts b/src/context/beamcenter/BeamCenterContext.ts similarity index 100% rename from src/context/BeamCenterContext.ts rename to src/context/beamcenter/BeamCenterContext.ts diff --git a/src/context/beamcenter/BeamCenterProvider.test.tsx b/src/context/beamcenter/BeamCenterProvider.test.tsx new file mode 100644 index 0000000..4cca674 --- /dev/null +++ b/src/context/beamcenter/BeamCenterProvider.test.tsx @@ -0,0 +1,80 @@ +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { describe, it, expect, vi, afterEach, beforeEach } from "vitest"; +import { useContext } from "react"; +import "@testing-library/jest-dom/vitest"; +import { BeamCenterProvider } from "./BeamCenterProvider"; +import { BeamCenterContext } from "./BeamCenterContext"; +import { useConfigCall } from "#/config_server/configServer.ts"; +import type { UseQueryResult } from "react-query"; + +vi.mock("#/config_server/configServer.ts", () => ({ + useConfigCall: vi.fn(), +})); + +const TestConsumer = () => { + const value = useContext(BeamCenterContext); + return ( + <> +
{value.data}
+ + + ); +}; + +describe("BeamCenterProvider", () => { + afterEach(() => { + cleanup(); + vi.clearAllMocks(); + }); + + const mockRefetch = vi.fn(); + const mockQueryResult = { + data: "mock config text", + refetch: mockRefetch, + }; + + beforeEach(() => + vi + .mocked(useConfigCall) + .mockReturnValue( + mockQueryResult as unknown as UseQueryResult, + ), + ); + + it("calls useConfigCall with the correct endpoint", () => { + render( + + + , + ); + + expect(useConfigCall).toHaveBeenCalledWith( + "/dls_sw/i24/software/daq_configuration/domain/display.configuration", + ); + }); + + it("provides the data to consumers via context", () => { + render( + + + , + ); + + expect(screen.getByTestId("context-value")).toHaveTextContent( + "mock config text", + ); + }); + + it("passes refetch function through context and it can be called", () => { + render( + + + , + ); + + fireEvent.click(screen.getByTestId("refetch-button")); + expect(mockRefetch).toHaveBeenCalled(); + }); +}); diff --git a/src/context/BeamCenterProvider.tsx b/src/context/beamcenter/BeamCenterProvider.tsx similarity index 100% rename from src/context/BeamCenterProvider.tsx rename to src/context/beamcenter/BeamCenterProvider.tsx diff --git a/src/routes/BeamlineI24.tsx b/src/routes/BeamlineI24.tsx index 3f14120..c684cbb 100644 --- a/src/routes/BeamlineI24.tsx +++ b/src/routes/BeamlineI24.tsx @@ -2,7 +2,7 @@ import { BeamlineStatsTabPanel } from "#/screens/BeamlineStats.tsx"; import { DetectorMotionTabPanel } from "#/screens/DetectorMotion.tsx"; import { FallbackScreen } from "#/screens/FallbackScreen.tsx"; import { OavMover } from "#/screens/OavMover/OAVStageController.tsx"; -import { BeamCenterProvider } from "#/context/BeamCenterProvider.tsx"; +import { BeamCenterProvider } from "#/context/beamcenter/BeamCenterProvider.tsx"; import { Box, Tab, Tabs, useTheme } from "@mui/material"; import { ErrorBoundary } from "react-error-boundary"; import { useState } from "react"; diff --git a/src/screens/OavMover/OAVMoveController.tsx b/src/screens/OavMover/OAVMoveController.tsx index 50571ca..4429f17 100644 --- a/src/screens/OavMover/OAVMoveController.tsx +++ b/src/screens/OavMover/OAVMoveController.tsx @@ -1,6 +1,6 @@ import { RunPlanButton } from "#/blueapi/BlueapiComponents.tsx"; import { useContext } from "react"; -import { BeamCenterContext } from "#/context/BeamCenterContext.ts"; +import { BeamCenterContext } from "#/context/beamcenter/BeamCenterContext.ts"; import { KeyboardDoubleArrowUp, KeyboardArrowUp, diff --git a/src/screens/OavMover/OAVStageController.tsx b/src/screens/OavMover/OAVStageController.tsx index 8590f4f..da8791c 100644 --- a/src/screens/OavMover/OAVStageController.tsx +++ b/src/screens/OavMover/OAVStageController.tsx @@ -7,7 +7,7 @@ import { OavVideoStream } from "#/components/OavVideoStream.tsx"; import { forceString, useParsedPvConnection } from "#/pv/util.ts"; import { ZoomLevels } from "#/pv/enumPvValues.ts"; import { useMemo, useEffect } from "react"; -import { BeamCenterContext } from "#/context/BeamCenterContext.ts"; +import { BeamCenterContext } from "#/context/beamcenter/BeamCenterContext.ts"; const ZOOM_PV = "ca://BL24I-EA-OAV-01:FZOOM:MP:SELECT"; const BEAM_CENTER_LINES_PER_ZOOM = 7; From f7acd31a5d12a274d7b6d76f72a2d5e9ba630320 Mon Sep 17 00:00:00 2001 From: Tamoor Shahid Date: Mon, 2 Mar 2026 16:11:23 +0000 Subject: [PATCH 8/9] Refactor of context --- src/components/OavVideoStream.tsx | 5 +---- src/screens/OavMover/OAVStageController.tsx | 9 +++++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/OavVideoStream.tsx b/src/components/OavVideoStream.tsx index 9754619..8e5f439 100644 --- a/src/components/OavVideoStream.tsx +++ b/src/components/OavVideoStream.tsx @@ -1,5 +1,5 @@ import { Box } from "@mui/material"; -import React, { useContext, useEffect } from "react"; +import React, { useEffect } from "react"; import { useContainerDimensions } from "./OavVideoStreamHelper"; import { PvComponent } from "#/pv/PvComponent.tsx"; import { PvDescription, PvItem } from "#/pv/types.ts"; @@ -8,7 +8,6 @@ import { parseNumericPv, pvIntArrayToString, } from "#/pv/util.ts"; -import { BeamCenterContext } from "#/context/beamcenter/BeamCenterContext.ts"; /* * A viewer which allows overlaying a crosshair (takes numbers which could be the values from a react useState hook) @@ -112,7 +111,6 @@ function VideoBoxWithOverlay(props: { const canvasRef = React.useRef(null); const videoBoxRef = React.useRef(null); const { width, height } = useContainerDimensions(videoBoxRef); - const beamCenterQuery = useContext(BeamCenterContext); useEffect(() => { drawCanvas(canvasRef, props.crosshairX, props.crosshairY); @@ -139,7 +137,6 @@ function VideoBoxWithOverlay(props: { const rect = canvas.getBoundingClientRect(); const [x, y] = [e.clientX - rect.left, e.clientY - rect.top]; props.onCoordClick(x, y); - beamCenterQuery.refetch(); } } }} diff --git a/src/screens/OavMover/OAVStageController.tsx b/src/screens/OavMover/OAVStageController.tsx index da8791c..4729c35 100644 --- a/src/screens/OavMover/OAVStageController.tsx +++ b/src/screens/OavMover/OAVStageController.tsx @@ -1,5 +1,5 @@ import { Grid2, useTheme } from "@mui/material"; -import { useContext } from "react"; +import { useContext, useRef } from "react"; import { OAVSideBar } from "./OAVSideBar"; import { submitAndRunPlanImmediately } from "#/blueapi/blueapi.ts"; import { readVisitFromPv, parseInstrumentSession } from "#/blueapi/visit.ts"; @@ -22,8 +22,10 @@ function useZoomAndCrosshair() { }), ); + const beamCenterQueryRef = useRef(beamCenterQuery); + useEffect(() => { - beamCenterQuery.refetch(); + beamCenterQueryRef.current.refetch(); }, [currentZoomValue]); const zoomIndex = ZoomLevels.findIndex( @@ -56,6 +58,7 @@ export function OavMover() { const bgColor = theme.palette.background.paper; const fullVisit = readVisitFromPv(); + const beamCenterQuery = useContext(BeamCenterContext); function onCoordClick(x: number, y: number) { submitAndRunPlanImmediately({ @@ -67,6 +70,8 @@ export function OavMover() { `Failed to run plan, see console and logs for full error. Reason: ${error}`, ); }); + + beamCenterQuery.refetch(); } return ( From 1c2dadd7b60728e85c7ab3125dfd1a47009f8bcf Mon Sep 17 00:00:00 2001 From: Tamoor Shahid Date: Wed, 4 Mar 2026 12:11:29 +0000 Subject: [PATCH 9/9] Created tests for the fetching of beamCenterQuery --- src/context/beamcenter/BeamCenterContext.ts | 13 +- .../OavMover/OAVStageController.test.tsx | 119 ++++++++++++++++++ src/screens/OavMover/OAVStageController.tsx | 2 +- 3 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 src/screens/OavMover/OAVStageController.test.tsx diff --git a/src/context/beamcenter/BeamCenterContext.ts b/src/context/beamcenter/BeamCenterContext.ts index 37ce7e2..6552751 100644 --- a/src/context/beamcenter/BeamCenterContext.ts +++ b/src/context/beamcenter/BeamCenterContext.ts @@ -1,8 +1,11 @@ import { createContext } from "react"; -import { UseQueryResult } from "react-query"; -type BeamCenterQueryResult = UseQueryResult; +type BeamCenterQueryResult = { + data: string | null | undefined; + refetch: () => void; +}; -export const BeamCenterContext = createContext( - null as unknown as BeamCenterQueryResult, -); +export const BeamCenterContext = createContext({ + data: null, + refetch: () => {}, +}); diff --git a/src/screens/OavMover/OAVStageController.test.tsx b/src/screens/OavMover/OAVStageController.test.tsx new file mode 100644 index 0000000..e4acd21 --- /dev/null +++ b/src/screens/OavMover/OAVStageController.test.tsx @@ -0,0 +1,119 @@ +import { renderHook } from "@testing-library/react"; +import { describe, it, vi, beforeEach, expect } from "vitest"; +import { useZoomAndCrosshair } from "./OAVStageController"; +import { useParsedPvConnection } from "#/pv/util.ts"; +import { BeamCenterContext } from "#/context/beamcenter/BeamCenterContext.ts"; +import type { RawValue } from "#/pv/types.ts"; + +vi.mock("#/pv/util.ts", () => ({ + ...vi.importActual("#/pv/util.ts"), + useParsedPvConnection: vi.fn(), + forceString: (x: RawValue | string | number) => String(x), +})); + +type validateZoomTestType = { + zoomLevel: string; + expectedX: number; + expectedY: number; +}; + +describe("useZoomAndCrosshair", () => { + const mockRefetch = vi.fn(); + const mockBeamCenterData = [ + "zoomLevel = 1.0", + "crosshairX = 561", + "crosshairY = 321", + "topLeftX = 611", + "topLeftY = 441", + "bottomRightX = 631", + "bottomRightY = 461", + "zoomLevel = 2.0", + "crosshairX = 562", + "crosshairY = 322", + "topLeftX = 612", + "topLeftY = 442", + "bottomRightX = 632", + "bottomRightY = 462", + "zoomLevel = 3.0", + "crosshairX = 563", + "crosshairY = 323", + "topLeftX = 613", + "topLeftY = 443", + "bottomRightX = 633", + "bottomRightY = 463", + ].join("\n"); + + beforeEach(() => { + vi.mocked(useParsedPvConnection).mockReturnValue("2.0"); + }); + + it.each` + zoomLevel | expectedX | expectedY + ${"1.0"} | ${561} | ${321} + ${"2.0"} | ${562} | ${322} + ${"3.0"} | ${563} | ${323} + `( + "returns ( $expectedX , $expectedY ) for zoom level '$zoomLevel'", + ({ zoomLevel, expectedX, expectedY }: validateZoomTestType) => { + vi.mocked(useParsedPvConnection).mockReturnValue(zoomLevel); + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); + + const { result } = renderHook(() => useZoomAndCrosshair(), { wrapper }); + + expect(result.current.crosshairX).toBe(expectedX); + expect(result.current.crosshairY).toBe(expectedY); + }, + ); + + it("returns NaN for crosshair if zoomIndex is not found", () => { + vi.mocked(useParsedPvConnection).mockReturnValue("99.0"); + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); + + const { result } = renderHook(() => useZoomAndCrosshair(), { wrapper }); + + expect(result.current.crosshairX).toBeNaN(); + expect(result.current.crosshairY).toBeNaN(); + }); + + it("returns NaN if beamCenter data is missing", () => { + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); + + const { result } = renderHook(() => useZoomAndCrosshair(), { wrapper }); + + expect(result.current.crosshairX).toBeNaN(); + expect(result.current.crosshairY).toBeNaN(); + }); + + it("calls refetch when zoom level changes", () => { + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); + + renderHook(() => useZoomAndCrosshair(), { wrapper }); + vi.mocked(useParsedPvConnection).mockReturnValue("3.0"); + expect(mockRefetch).toHaveBeenCalled(); + }); +}); diff --git a/src/screens/OavMover/OAVStageController.tsx b/src/screens/OavMover/OAVStageController.tsx index 4729c35..0cf4037 100644 --- a/src/screens/OavMover/OAVStageController.tsx +++ b/src/screens/OavMover/OAVStageController.tsx @@ -12,7 +12,7 @@ import { BeamCenterContext } from "#/context/beamcenter/BeamCenterContext.ts"; const ZOOM_PV = "ca://BL24I-EA-OAV-01:FZOOM:MP:SELECT"; const BEAM_CENTER_LINES_PER_ZOOM = 7; -function useZoomAndCrosshair() { +export function useZoomAndCrosshair() { const beamCenterQuery = useContext(BeamCenterContext); const currentZoomValue = String( useParsedPvConnection({