From 8a4183c98b0876d69d6f3e269524e9a8ecc81790 Mon Sep 17 00:00:00 2001 From: Takeshi Kimata <117462761+kimatata@users.noreply.github.com> Date: Sat, 20 Apr 2024 17:27:27 +0900 Subject: [PATCH] Implemented test run editor's test case selection --- backend/index.js | 6 ++ backend/routes/runcases/delete.js | 35 +++++++++ backend/routes/runcases/new.js | 30 ++++++++ .../[projectId]/runs/[runId]/RunEditor.tsx | 75 ++++++++++++------- .../runs/[runId]/TestCaseSelector.tsx | 23 +++++- .../projects/[projectId]/runs/runsControl.ts | 50 ++++++++++++- 6 files changed, 185 insertions(+), 34 deletions(-) create mode 100644 backend/routes/runcases/delete.js create mode 100644 backend/routes/runcases/new.js diff --git a/backend/index.js b/backend/index.js index b02c8f1..06c79e3 100644 --- a/backend/index.js +++ b/backend/index.js @@ -89,6 +89,12 @@ app.use("/runs", runsNewRoute); app.use("/runs", runsEditRoute); app.use("/runs", runDeleteRoute); +// "/runcases" +const runCaseNewRoute = require("./routes/runcases/new")(sequelize); +const runCaseDeleteRoute = require("./routes/runcases/delete")(sequelize); +app.use("/runcases", runCaseNewRoute); +app.use("/runcases", runCaseDeleteRoute); + const PORT = process.env.PORT || 3001; app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); diff --git a/backend/routes/runcases/delete.js b/backend/routes/runcases/delete.js new file mode 100644 index 0000000..9efbca3 --- /dev/null +++ b/backend/routes/runcases/delete.js @@ -0,0 +1,35 @@ +const express = require("express"); +const router = express.Router(); +const defineRunCase = require("../../models/runCases"); +const { DataTypes } = require("sequelize"); + +module.exports = function (sequelize) { + const RunCase = defineRunCase(sequelize, DataTypes); + + router.delete("/", async (req, res) => { + const runId = req.query.runId; + const caseId = req.query.caseId; + + try { + // Get RunCase to be deleted. + const deletingRunCase = await RunCase.findOne({ + where: { + runId: runId, + caseId: caseId, + }, + }); + + if (!deletingRunCase) { + return res.status(404).send("RunCase not found"); + } + + await deletingRunCase.destroy(); + res.status(204).send(); + } catch (error) { + console.error(error); + res.status(500).send("Internal Server Error"); + } + }); + + return router; +}; diff --git a/backend/routes/runcases/new.js b/backend/routes/runcases/new.js new file mode 100644 index 0000000..f8c7cd0 --- /dev/null +++ b/backend/routes/runcases/new.js @@ -0,0 +1,30 @@ +const express = require("express"); +const router = express.Router(); +const defineRunCase = require("../../models/runCases"); +const { DataTypes } = require("sequelize"); + +module.exports = function (sequelize) { + const RunCase = defineRunCase(sequelize, DataTypes); + + router.post("/", async (req, res) => { + const runId = req.query.runId; + const caseId = req.query.caseId; + + try { + const newRunCase = await RunCase.create( + { + runId: runId, + caseId: caseId, + status: 0, + }, + ); + + res.json(newRunCase); + } catch (error) { + console.error(error); + res.status(500).send("Internal Server Error"); + } + }); + + return router; +}; diff --git a/frontend/app/projects/[projectId]/runs/[runId]/RunEditor.tsx b/frontend/app/projects/[projectId]/runs/[runId]/RunEditor.tsx index 95f9291..79c9d0d 100644 --- a/frontend/app/projects/[projectId]/runs/[runId]/RunEditor.tsx +++ b/frontend/app/projects/[projectId]/runs/[runId]/RunEditor.tsx @@ -31,7 +31,12 @@ import { testRunStatus } from "@/config/selection"; import { RunType } from "@/types/run"; import { CaseType } from "@/types/case"; import { FolderType } from "@/types/folder"; -import { fetchRun, updateRun } from "../runsControl"; +import { + fetchRun, + updateRun, + createRunCase, + deleteRunCase, +} from "../runsControl"; import { fetchFolders } from "../../folders/foldersControl"; import { fetchCases } from "../../folders/[folderId]/cases/caseControl"; @@ -74,33 +79,37 @@ export default function RunEditor({ projectId, runId }: Props) { fetchDataEffect(); }, []); + const fetchAndUpdateCases = async () => { + try { + const testCasesData = await fetchCases(selectedFolder.id); + + // Check if each testCase has an association with testRun + // and add "isIncluded" property + testCasesData.forEach((caseItr: CaseType) => { + let isIncluded: boolean = false; + let runStatus: number = 0; + + testRun.Cases.forEach((runCaseItr: CaseType) => { + if (runCaseItr.id === caseItr.id) { + isIncluded = true; + runStatus = runCaseItr.runCases.status; + } + }); + + caseItr.isIncluded = isIncluded; + caseItr.runStatus = runStatus; + }); + + setTestCases(testCasesData); + } catch (error) { + console.error("Error fetching cases data:", error.message); + } + }; + useEffect(() => { async function fetchCasesData() { if (selectedFolder && selectedFolder.id) { - try { - const testCasesData = await fetchCases(selectedFolder.id); - - // Check if each testCase has an association with testRun - // and add "isIncluded" property - testCasesData.forEach((caseItr: CaseType) => { - let isIncluded: boolean = false; - let runStatus: number = 0; - - testRun.Cases.forEach((runCaseItr: CaseType) => { - if (runCaseItr.id === caseItr.id) { - isIncluded = true; - runStatus = runCaseItr.runCases.status; - } - }); - - caseItr.isIncluded = isIncluded; - caseItr.runStatus = runStatus; - }); - - setTestCases(testCasesData); - } catch (error) { - console.error("Error fetching cases data:", error.message); - } + await fetchAndUpdateCases(); } } @@ -119,6 +128,16 @@ export default function RunEditor({ projectId, runId }: Props) { setSelectedKeys(new Set([])); }; + const handleIncludeCase = async (includeTestCaseId: number) => { + await createRunCase(runId, includeTestCaseId); + fetchAndUpdateCases(); + }; + + const handleExcludeCase = async (excludeTestCaseId: number) => { + await deleteRunCase(runId, excludeTestCaseId); + fetchAndUpdateCases(); + }; + const baseClass = ""; const selectedClass = `${baseClass} bg-neutral-200 dark:bg-neutral-700`; @@ -224,13 +243,13 @@ export default function RunEditor({ projectId, runId }: Props) { startContent={} onClick={() => onIncludeExcludeClick("include")} > - Include in run + Include selected cases in run } onClick={() => onIncludeExcludeClick("exclude")} > - Exclude from run + Exclude selected cases from run @@ -266,6 +285,8 @@ export default function RunEditor({ projectId, runId }: Props) { cases={testcases} selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} + onIncludeCase={handleIncludeCase} + onExcludeCase={handleExcludeCase} /> diff --git a/frontend/app/projects/[projectId]/runs/[runId]/TestCaseSelector.tsx b/frontend/app/projects/[projectId]/runs/[runId]/TestCaseSelector.tsx index c36f899..d443e23 100644 --- a/frontend/app/projects/[projectId]/runs/[runId]/TestCaseSelector.tsx +++ b/frontend/app/projects/[projectId]/runs/[runId]/TestCaseSelector.tsx @@ -15,7 +15,7 @@ import { Selection, SortDescriptor, } from "@nextui-org/react"; -import { ChevronDown, MoreVertical } from "lucide-react"; +import { ChevronDown, MoreVertical, CopyPlus, CopyMinus } from "lucide-react"; import { priorities, testRunCaseStatus } from "@/config/selection"; import { CaseType } from "@/types/case"; @@ -31,12 +31,16 @@ type Props = { cases: CaseType[]; selectedKeys: Selection; onSelectionChange: React.Dispatch>; + onIncludeCase: (includeCaseId: number) => {}; + onExcludeCase: (excludeCaseId: number) => {}; }; export default function TestCaseSelector({ cases, selectedKeys, onSelectionChange, + onIncludeCase, + onExcludeCase }: Props) { const [sortDescriptor, setSortDescriptor] = useState({ column: "id", @@ -104,9 +108,20 @@ export default function TestCaseSelector({ - - {}}> - status change + + } + isDisabled={testCase.isIncluded} + onClick={() => onIncludeCase(testCase.id)} + > + Include in run + + } + isDisabled={!testCase.isIncluded} + onClick={() => {onExcludeCase(testCase.id)}} + > + Exclude from run diff --git a/frontend/app/projects/[projectId]/runs/runsControl.ts b/frontend/app/projects/[projectId]/runs/runsControl.ts index f14bbb0..a1d9122 100644 --- a/frontend/app/projects/[projectId]/runs/runsControl.ts +++ b/frontend/app/projects/[projectId]/runs/runsControl.ts @@ -97,7 +97,7 @@ async function updateRun(updateTestRun: RunType) { const data = await response.json(); return data; } catch (error) { - console.error("Error updating project:", error); + console.error("Error updating run:", error); throw error; } } @@ -118,9 +118,53 @@ async function deleteRun(runId: number) { throw new Error(`HTTP error! Status: ${response.status}`); } } catch (error) { - console.error("Error deleting project:", error); + console.error("Error deleting run:", error); throw error; } } -export { fetchRun, fetchRuns, createRun, updateRun, deleteRun }; +async function createRunCase(runId: string, caseId: number) { + const fetchOptions = { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }; + + const url = `${apiServer}/runcases?runId=${runId}&caseId=${caseId}`; + + try { + const response = await fetch(url, fetchOptions); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const data = await response.json(); + return data; + } catch (error) { + console.error("Error creating new runcase:", error); + throw error; + } +} + +async function deleteRunCase(runId: string, caseId: number) { + const fetchOptions = { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + }; + + const url = `${apiServer}/runcases?runId=${runId}&caseId=${caseId}`; + + try { + const response = await fetch(url, fetchOptions); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + } catch (error) { + console.error("Error deleting runcase:", error); + throw error; + } +} + +export { fetchRun, fetchRuns, createRun, updateRun, deleteRun, createRunCase, deleteRunCase };