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 };