diff --git a/package-lock.json b/package-lock.json index 834823ad..e992a591 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21963,7 +21963,8 @@ }, "ssri": { "version": "6.0.1", - "resolved": "", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", "requires": { "figgy-pudding": "^3.5.1" } diff --git a/src/_helpers/route.helpers.ts b/src/_helpers/route.helpers.ts index d884df78..3847fcb7 100644 --- a/src/_helpers/route.helpers.ts +++ b/src/_helpers/route.helpers.ts @@ -4,10 +4,12 @@ import qs from "qs"; export const buildProjectPageUrl = (projectId: string) => `${routes.HOME}${projectId}`; -export const buildTestRunLocation = (buildId: string, testRunId?: string) => ({ - search: testRunId - ? `buildId=${buildId}&testId=${testRunId}` - : `buildId=${buildId}`, +export const buildTestRunLocation = (buildId?: string, testRunId?: string) => ({ + search: buildId + ? testRunId + ? `buildId=${buildId}&testId=${testRunId}` + : `buildId=${buildId}` + : "", }); export interface QueryParams { diff --git a/src/_test/stub.helper.ts b/src/_test/stub.helper.ts index 79cd72eb..74f1b0c5 100644 --- a/src/_test/stub.helper.ts +++ b/src/_test/stub.helper.ts @@ -34,6 +34,8 @@ const buildsServiceStub = { const testRunServiceStub = { getList: (testRuns: Array) => cy.stub(testRunService, "getList").resolves(testRuns), + getDetails: (testRun: TestRun) => + cy.stub(testRunService, "getDetails").resolves(testRun), }; const staticServiceStub = { diff --git a/src/components/BuildList/index.tsx b/src/components/BuildList/index.tsx index 43f09ff8..76c9e46a 100644 --- a/src/components/BuildList/index.tsx +++ b/src/components/BuildList/index.tsx @@ -21,8 +21,6 @@ import { useBuildState, useBuildDispatch, deleteBuild, - selectBuild, - getBuildList, useProjectState, } from "../../contexts"; import { BuildStatusChip } from "../BuildStatusChip"; @@ -34,6 +32,8 @@ import { Pagination } from "@material-ui/lab"; import { Build } from "../../types"; import { BaseModal } from "../BaseModal"; import { buildsService } from "../../services"; +import { useHistory } from "react-router"; +import { buildTestRunLocation } from "../../_helpers/route.helpers"; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -54,6 +54,7 @@ const useStyles = makeStyles((theme: Theme) => const BuildList: FunctionComponent = () => { const classes = useStyles(); + const history = useHistory(); const { buildList, selectedBuild, loading, total, take } = useBuildState(); const buildDispatch = useBuildDispatch(); const { enqueueSnackbar } = useSnackbar(); @@ -85,23 +86,28 @@ const BuildList: FunctionComponent = () => { setEditDialogOpen(!editDialogOpen); }; - React.useEffect(() => { - if (!selectedBuild || selectedBuild.projectId !== selectedProjectId) { - const buildId = buildList.length > 0 ? buildList[0].id : null; - selectBuild(buildDispatch, buildId); - } - }, [buildDispatch, selectedBuild, buildList, selectedProjectId]); + const selectBuildCalback = React.useCallback( + (id?: string) => history.push(buildTestRunLocation(id)), + [history] + ); - const getBuildListCalback: any = React.useCallback( - (page: number) => - selectedProjectId && - getBuildList(buildDispatch, selectedProjectId, page).catch( - (err: string) => - enqueueSnackbar(err, { - variant: "error", + const getBuildListCalback = React.useCallback( + (page: number) => { + if (selectedProjectId) { + buildDispatch({ type: "request" }); + buildsService + .getList(selectedProjectId, take, take * (page - 1)) + .then((payload) => { + buildDispatch({ type: "get", payload }); }) - ), - [buildDispatch, enqueueSnackbar, selectedProjectId] + .catch((err: string) => + enqueueSnackbar(err, { + variant: "error", + }) + ); + } + }, + [buildDispatch, enqueueSnackbar, selectedProjectId, take] ); React.useEffect(() => { @@ -122,9 +128,7 @@ const BuildList: FunctionComponent = () => { { - selectBuild(buildDispatch, build.id); - }} + onClick={() => selectBuildCalback(build.id)} classes={{ container: classes.listItem, }} @@ -273,7 +277,7 @@ const BuildList: FunctionComponent = () => { } onSubmit={() => { deleteBuild(buildDispatch, menuBuild.id) - .then((b) => { + .then((build) => { toggleDeleteDialogOpen(); enqueueSnackbar( `Build #${menuBuild.number || menuBuild.id} deleted`, @@ -281,6 +285,9 @@ const BuildList: FunctionComponent = () => { variant: "success", } ); + if (menuBuild.id === selectedBuild?.id) { + selectBuildCalback(); + } }) .catch((err) => enqueueSnackbar(err, { diff --git a/src/components/TestDetailsDialog/TestDetailsModal.tsx b/src/components/TestDetailsDialog/TestDetailsModal.tsx index 6176ef81..72a6c9e6 100644 --- a/src/components/TestDetailsDialog/TestDetailsModal.tsx +++ b/src/components/TestDetailsDialog/TestDetailsModal.tsx @@ -85,7 +85,9 @@ const TestDetailsModal: React.FunctionComponent<{ const [isDiffShown, setIsDiffShown] = useState(!!testRun.diffName); const [selectedRectId, setSelectedRectId] = React.useState(); const [ignoreAreas, setIgnoreAreas] = React.useState([]); - const [applyIgnoreDialogOpen, setApplyIgnoreDialogOpen] = React.useState(false); + const [applyIgnoreDialogOpen, setApplyIgnoreDialogOpen] = React.useState( + false + ); const toggleApplyIgnoreDialogOpen = () => { setApplyIgnoreDialogOpen(!applyIgnoreDialogOpen); @@ -101,7 +103,8 @@ const TestDetailsModal: React.FunctionComponent<{ staticService.getImage(testRun.diffName) ); - const applyIgnoreAreaText = 'Apply selected ignore area to all images in this build.'; + const applyIgnoreAreaText = + "Apply selected ignore area to all images in this build."; React.useEffect(() => { fitStageToScreen(); @@ -195,9 +198,9 @@ const TestDetailsModal: React.FunctionComponent<{ const fitStageToScreen = () => { const scale = image ? Math.min( - stageWidth < image.width ? stageWidth / image.width : 1, - stageHeigth < image.height ? stageHeigth / image.height : 1 - ) + stageWidth < image.width ? stageWidth / image.width : 1, + stageHeigth < image.height ? stageHeigth / image.height : 1 + ) : 1; setStageScale(scale); resetPositioin(); @@ -274,10 +277,10 @@ const TestDetailsModal: React.FunctionComponent<{ )} {(testRun.status === TestStatus.unresolved || testRun.status === TestStatus.new) && ( - - - - )} + + + + )} @@ -492,7 +495,7 @@ const TestDetailsModal: React.FunctionComponent<{ @@ -502,8 +505,7 @@ const TestDetailsModal: React.FunctionComponent<{ onSubmit={() => { toggleApplyIgnoreDialogOpen(); applyIgnoreArea(); - } - } + }} /> ); diff --git a/src/components/TestDetailsDialog/index.tsx b/src/components/TestDetailsDialog/index.tsx index e2deb329..46a74448 100644 --- a/src/components/TestDetailsDialog/index.tsx +++ b/src/components/TestDetailsDialog/index.tsx @@ -1,10 +1,8 @@ import { Dialog, makeStyles, Typography } from "@material-ui/core"; import React from "react"; -import { - selectTestRun, - useTestRunDispatch, - useTestRunState, -} from "../../contexts"; +import { useHistory } from "react-router"; +import { useBuildState, useTestRunState } from "../../contexts"; +import { buildTestRunLocation } from "../../_helpers/route.helpers"; import { BaseModal } from "../BaseModal"; import { ArrowButtons } from "./ArrowButtons"; import TestDetailsModal from "./TestDetailsModal"; @@ -18,14 +16,14 @@ const useStyles = makeStyles((theme) => ({ export const TestDetailsDialog: React.FunctionComponent = () => { const classes = useStyles(); const { - testRun, + selectedTestRun, touched, testRuns: allTestRuns, filteredTestRunIds, sortedTestRunIds, - selectedTestRunId, } = useTestRunState(); - const testRunDispatch = useTestRunDispatch(); + const { selectedBuild } = useBuildState(); + const history = useHistory(); const [notSavedChangesModal, setNotSavedChangesModal] = React.useState(false); const [navigationTargetId, setNavigationTargetId] = React.useState(); @@ -46,8 +44,8 @@ export const TestDetailsDialog: React.FunctionComponent = () => { }, [allTestRuns, filteredTestRunIds, sortedTestRunIds]); const selectedTestRunIndex = React.useMemo( - () => testRuns.findIndex((t) => t.id === selectedTestRunId), - [testRuns, selectedTestRunId] + () => testRuns.findIndex((t) => t.id === selectedTestRun?.id), + [testRuns, selectedTestRun?.id] ); const handleNavigation = React.useCallback( @@ -56,20 +54,20 @@ export const TestDetailsDialog: React.FunctionComponent = () => { setNavigationTargetId(id); setNotSavedChangesModal(true); } else { - selectTestRun(testRunDispatch, id); + history.push(buildTestRunLocation(selectedBuild?.id, id)); } }, - [testRunDispatch, touched] + [touched, history, selectedBuild?.id] ); - if (!testRun) { + if (!selectedTestRun) { return null; } return ( handleNavigation()} /> @@ -87,7 +85,9 @@ export const TestDetailsDialog: React.FunctionComponent = () => { {`Are you sure you want to discard changes?`} } onSubmit={() => { - selectTestRun(testRunDispatch, navigationTargetId); + history.push( + buildTestRunLocation(selectedBuild?.id, navigationTargetId) + ); setNotSavedChangesModal(false); }} /> diff --git a/src/components/TestRunList/index.tsx b/src/components/TestRunList/index.tsx index effa30aa..6aa34afe 100644 --- a/src/components/TestRunList/index.tsx +++ b/src/components/TestRunList/index.tsx @@ -1,12 +1,10 @@ import React from "react"; -import { Chip } from "@material-ui/core"; +import { Chip, Typography } from "@material-ui/core"; import TestStatusChip from "../TestStatusChip"; import { useTestRunState, useTestRunDispatch, - getTestRunList, useBuildState, - selectTestRun, } from "../../contexts"; import { useSnackbar } from "notistack"; import { @@ -26,6 +24,9 @@ import { DataGridCustomToolbar } from "./DataGridCustomToolbar"; import { StatusFilterOperators } from "./StatusFilterOperators"; import { TagFilterOperators } from "./TagFilterOperators"; import { TestStatus } from "../../types"; +import { testRunService } from "../../services"; +import { useHistory } from "react-router"; +import { buildTestRunLocation } from "../../_helpers/route.helpers"; const columnsDef: GridColDef[] = [ { field: "id", hide: true, filterable: false }, @@ -97,8 +98,9 @@ const columnsDef: GridColDef[] = [ const TestRunList: React.FunctionComponent = () => { const { enqueueSnackbar } = useSnackbar(); - const { testRun, testRuns, loading } = useTestRunState(); - const { selectedBuildId } = useBuildState(); + const history = useHistory(); + const { selectedTestRun, testRuns, loading } = useTestRunState(); + const { selectedBuild } = useBuildState(); const testRunDispatch = useTestRunDispatch(); const [sortModel, setSortModel] = React.useState([ @@ -108,16 +110,21 @@ const TestRunList: React.FunctionComponent = () => { }, ]); - const getTestRunListCallback = React.useCallback( - () => - selectedBuildId && - getTestRunList(testRunDispatch, selectedBuildId).catch((err: string) => - enqueueSnackbar(err, { - variant: "error", - }) - ), - [testRunDispatch, enqueueSnackbar, selectedBuildId] - ); + const getTestRunListCallback = React.useCallback(() => { + testRunDispatch({ type: "request" }); + if (selectedBuild?.id) { + testRunService + .getList(selectedBuild.id) + .then((payload) => testRunDispatch({ type: "get", payload })) + .catch((err: string) => + enqueueSnackbar(err, { + variant: "error", + }) + ); + } else { + testRunDispatch({ type: "get", payload: [] }); + } + }, [testRunDispatch, enqueueSnackbar, selectedBuild?.id]); React.useEffect(() => { getTestRunListCallback(); @@ -125,7 +132,7 @@ const TestRunList: React.FunctionComponent = () => { return ( - {selectedBuildId && ( + {selectedBuild ? ( { sortModel={sortModel} onSortModelChange={(model) => setSortModel(model)} onRowClick={(param: GridRowParams) => { - selectTestRun( - testRunDispatch, - param.getValue(param.id, "id")?.toString() + history.push( + buildTestRunLocation( + selectedBuild.id, + param.getValue(param.id, "id")?.toString() + ) ); }} onStateChange={(props: GridStateChangeParams) => { - if (!testRun) { + if (!selectedTestRun) { // only if testRun modal is not shown testRunDispatch({ type: "filter", @@ -161,6 +170,8 @@ const TestRunList: React.FunctionComponent = () => { } }} /> + ) : ( + Select build from list )} ); diff --git a/src/contexts/build.context.tsx b/src/contexts/build.context.tsx index 3adf2f3e..82723c54 100644 --- a/src/contexts/build.context.tsx +++ b/src/contexts/build.context.tsx @@ -1,15 +1,11 @@ import React from "react"; import { Build, PaginatedData } from "../types"; import { buildsService } from "../services"; -import { - buildTestRunLocation, - getQueryParams, -} from "../_helpers/route.helpers"; -import { useHistory, useLocation } from "react-router-dom"; +import { useLocation } from "react-router-dom"; +import { getQueryParams } from "../_helpers/route.helpers"; interface IRequestAction { type: "request"; - payload?: undefined; } interface IGetAction { @@ -19,7 +15,7 @@ interface IGetAction { interface ISelectAction { type: "select"; - payload: Build | null; + payload?: Build; } interface IDeleteAction { @@ -47,8 +43,7 @@ type IAction = type Dispatch = (action: IAction) => void; type State = { - selectedBuildId: string | null; - selectedBuild: Build | null; + selectedBuild?: Build; buildList: Build[]; total: number; take: number; @@ -64,8 +59,6 @@ const BuildDispatchContext = React.createContext( ); const initialState: State = { - selectedBuildId: null, - selectedBuild: null, buildList: [], take: 10, skip: 0, @@ -76,19 +69,10 @@ const initialState: State = { function buildReducer(state: State, action: IAction): State { switch (action.type) { case "select": - if (action.payload === null) { - return { - ...state, - selectedBuildId: null, - selectedBuild: null, - }; - } else { - return { - ...state, - selectedBuildId: action.payload.id, - selectedBuild: action.payload, - }; - } + return { + ...state, + selectedBuild: action.payload, + }; case "request": return { ...state, @@ -105,28 +89,12 @@ function buildReducer(state: State, action: IAction): State { total, loading: false, }; - case "delete": - { - let buildList = state.buildList; - let indexOfBuildDeleted = buildList.findIndex( - (e) => e.id === action.payload - ); - let indexOfSelectedBuild = buildList.findIndex( - (e) => e.id === state.selectedBuildId - ); - if (indexOfBuildDeleted === indexOfSelectedBuild) { - let buildToSelect = null; - if (buildList.length > 1) { - buildToSelect = (buildList.length === 0) ? buildList[1] : buildList[indexOfSelectedBuild - 1]; - } - state.selectedBuild = buildToSelect; - state.selectedBuildId = buildToSelect?.id ?? null; - } - return { - ...state, - buildList: state.buildList.filter((p) => p.id !== action.payload), - }; - } + case "delete": { + return { + ...state, + buildList: state.buildList.filter((p) => p.id !== action.payload), + }; + } case "add": return { ...state, @@ -153,24 +121,16 @@ function buildReducer(state: State, action: IAction): State { function BuildProvider({ children }: BuildProviderProps) { const [state, dispatch] = React.useReducer(buildReducer, initialState); + const [buildId, setBuildId] = React.useState(); const location = useLocation(); - const history = useHistory(); - // get id from url in case none in state React.useEffect(() => { - const idFromUrl = getQueryParams(location.search).buildId; - if (!state.selectedBuildId && idFromUrl) { - selectBuild(dispatch, idFromUrl); + const { buildId: id } = getQueryParams(location.search); + if (buildId !== id) { + selectBuild(dispatch, id); + setBuildId(id); } - // eslint-disable-next-line - }, []); - - // update url - React.useEffect(() => { - if (state.selectedBuildId) { - history.push(buildTestRunLocation(state.selectedBuildId)); - } - }, [history, state.selectedBuildId]); + }, [location.search, buildId]); return ( @@ -197,16 +157,6 @@ function useBuildDispatch() { return context; } -async function getBuildList(dispatch: Dispatch, id: string, page: number) { - dispatch({ type: "request" }); - - return buildsService - .getList(id, initialState.take, initialState.take * (page - 1)) - .then((response) => { - dispatch({ type: "get", payload: response }); - }); -} - async function deleteBuild(dispatch: Dispatch, id: string) { return buildsService.remove(id).then((build) => { dispatch({ type: "delete", payload: id }); @@ -214,9 +164,9 @@ async function deleteBuild(dispatch: Dispatch, id: string) { }); } -async function selectBuild(dispatch: Dispatch, id: string | null) { - if (id === null) { - dispatch({ type: "select", payload: null }); +async function selectBuild(dispatch: Dispatch, id?: string) { + if (!id) { + dispatch({ type: "select" }); } else { return buildsService.getDetails(id).then((build) => { dispatch({ type: "select", payload: build }); @@ -236,7 +186,6 @@ export { BuildProvider, useBuildState, useBuildDispatch, - getBuildList, deleteBuild, selectBuild, addBuild, diff --git a/src/contexts/testRun.context.tsx b/src/contexts/testRun.context.tsx index 6f24e1ed..26557409 100644 --- a/src/contexts/testRun.context.tsx +++ b/src/contexts/testRun.context.tsx @@ -1,12 +1,8 @@ import React from "react"; import { TestRun } from "../types"; +import { useLocation } from "react-router-dom"; +import { getQueryParams } from "../_helpers/route.helpers"; import { testRunService } from "../services"; -import { useHistory, useLocation } from "react-router-dom"; -import { - buildTestRunLocation, - getQueryParams, -} from "../_helpers/route.helpers"; -import { useBuildState } from "."; interface IRequestAction { type: "request"; @@ -19,7 +15,7 @@ interface IGetAction { interface ISelectAction { type: "select"; - payload?: string; + payload?: TestRun; } interface IDeleteAction { @@ -77,11 +73,10 @@ type IAction = type Dispatch = (action: IAction) => void; type State = { - selectedTestRunId?: string; + selectedTestRun?: TestRun; sortedTestRunIds?: Array; filteredTestRunIds?: Array; testRuns: Array; - testRun?: TestRun; touched: boolean; loading: boolean; }; @@ -105,8 +100,7 @@ function testRunReducer(state: State, action: IAction): State { return { ...state, touched: false, - selectedTestRunId: action.payload, - testRun: state.testRuns.find((item) => item.id === action.payload), + selectedTestRun: action.payload, }; case "filter": return { @@ -156,9 +150,9 @@ function testRunReducer(state: State, action: IAction): State { } return t; }), - testRun: - state.testRun && - action.payload.find((item) => item.id === state.testRun!.id), + selectedTestRun: + action.payload.find((i) => i.id === state.selectedTestRun?.id) ?? + state.selectedTestRun, }; case "touched": return { @@ -173,26 +167,17 @@ function testRunReducer(state: State, action: IAction): State { function TestRunProvider({ children }: TestRunProviderProps) { const [state, dispatch] = React.useReducer(testRunReducer, initialState); const location = useLocation(); - const history = useHistory(); - const { selectedBuildId } = useBuildState(); - // get id from url in case none in state React.useEffect(() => { - const idFromUrl = getQueryParams(location.search).testId; - if (!state.selectedTestRunId && idFromUrl) { - selectTestRun(dispatch, idFromUrl); + const { testId } = getQueryParams(location.search); + if (!testId) { + dispatch({ type: "select" }); + } else { + testRunService.getDetails(testId).then((payload) => { + dispatch({ type: "select", payload }); + }); } - // eslint-disable-next-line - }, []); - - // update url - React.useEffect(() => { - if (selectedBuildId) { - history.push( - buildTestRunLocation(selectedBuildId, state.selectedTestRunId) - ); - } - }, [history, selectedBuildId, state.selectedTestRunId]); + }, [location.search]); return ( @@ -219,25 +204,10 @@ function useTestRunDispatch() { return context; } -async function getTestRunList( - dispatch: Dispatch, - buildId: string -): Promise { - dispatch({ type: "request" }); - - return testRunService.getList(buildId).then((response) => { - dispatch({ type: "get", payload: response }); - }); -} - async function deleteTestRun(dispatch: Dispatch, ids: Array) { dispatch({ type: "delete", payload: ids }); } -async function selectTestRun(dispatch: Dispatch, id?: string) { - dispatch({ type: "select", payload: id }); -} - async function addTestRun(dispatch: Dispatch, testRuns: Array) { dispatch({ type: "add", payload: testRuns }); } @@ -250,8 +220,6 @@ export { TestRunProvider, useTestRunState, useTestRunDispatch, - getTestRunList, - selectTestRun, addTestRun, deleteTestRun, updateTestRun, diff --git a/src/pages/ProjectPage.spec.tsx b/src/pages/ProjectPage.spec.tsx index 77d26270..a56dde2e 100644 --- a/src/pages/ProjectPage.spec.tsx +++ b/src/pages/ProjectPage.spec.tsx @@ -63,6 +63,7 @@ describe("Project page", () => { id: "some test run id7", }, ]); + testRunServiceStub.getDetails(TEST_UNRESOLVED); mountVrtComponent({ component: , @@ -72,6 +73,8 @@ describe("Project page", () => { path: "/:projectId", }); + cy.contains(TEST_BUILD_FAILED.ciBuildId).click(); + cy.vrtTrack("Project page"); cy.contains(TEST_UNRESOLVED.name).click(); diff --git a/src/pages/TestVariationDetailsPage.tsx b/src/pages/TestVariationDetailsPage.tsx index 8c03e5ee..33846d6f 100644 --- a/src/pages/TestVariationDetailsPage.tsx +++ b/src/pages/TestVariationDetailsPage.tsx @@ -18,12 +18,6 @@ import { buildTestRunLocation, } from "../_helpers/route.helpers"; import { TestVariationDetails } from "../components/TestVariationDetails"; -import { - selectBuild, - useBuildDispatch, - useTestRunDispatch, - selectTestRun, -} from "../contexts"; import { useSnackbar } from "notistack"; import { formatDateTime } from "../_helpers/format.helper"; import TestStatusChip from "../components/TestStatusChip"; @@ -40,8 +34,6 @@ const TestVariationDetailsPage: React.FunctionComponent = () => { const classes = useStyles(); const history = useHistory(); const { enqueueSnackbar } = useSnackbar(); - const buildDispatch = useBuildDispatch(); - const testRunDispatch = useTestRunDispatch(); const { testVariationId } = useParams<{ testVariationId: string }>(); const [testVariation, setTestVariation] = React.useState(); @@ -88,12 +80,6 @@ const TestVariationDetailsPage: React.FunctionComponent = () => { testRun.id ), }); - selectBuild( - buildDispatch, - testRun.buildId - ).then(() => - selectTestRun(testRunDispatch, testRun.id) - ); } }} > diff --git a/src/services/testRun.service.ts b/src/services/testRun.service.ts index d57349a5..f0782121 100644 --- a/src/services/testRun.service.ts +++ b/src/services/testRun.service.ts @@ -17,6 +17,17 @@ async function getList(buildId: string): Promise { ).then(handleResponse); } +async function getDetails(id: string): Promise { + const requestOptions = { + method: "GET", + headers: authHeader(), + }; + + return fetch(`${API_URL}${ENDPOINT_URL}/${id}`, requestOptions).then( + handleResponse + ); +} + async function removeBulk(ids: (string | number)[]): Promise { const requestOptions = { method: "POST", @@ -97,6 +108,7 @@ async function update(id: string, data: { comment: string }): Promise { export const testRunService = { getList, + getDetails, removeBulk, rejectBulk, approveBulk,