From 1c436b7db65736a691ea84e6e7f8cf9ed69eb102 Mon Sep 17 00:00:00 2001 From: Takeshi Kimata <117462761+kimatata@users.noreply.github.com> Date: Mon, 15 Jul 2024 23:07:50 +0900 Subject: [PATCH] include RunCases on testCase property --- backend/index.js | 2 + backend/routes/cases/indexByProjectId.js | 54 ++++++ .../[projectId]/folders/foldersControl.ts | 2 +- .../[projectId]/runs/[runId]/RunEditor.tsx | 87 ++++----- .../runs/[runId]/TestCaseSelector.tsx | 33 +++- .../projects/[projectId]/runs/runsControl.ts | 171 ++++++++++++++---- frontend/types/case.ts | 9 +- 7 files changed, 258 insertions(+), 100 deletions(-) create mode 100644 backend/routes/cases/indexByProjectId.js diff --git a/backend/index.js b/backend/index.js index b1427f9..5acd9ad 100644 --- a/backend/index.js +++ b/backend/index.js @@ -63,11 +63,13 @@ app.use('/folders', foldersDeleteRoute); // "/cases" const casesIndexRoute = require('./routes/cases/index')(sequelize); +const casesIndexByProjectIdRoute = require('./routes/cases/indexByProjectId')(sequelize); const casesShowRoute = require('./routes/cases/show')(sequelize); const casesNewRoute = require('./routes/cases/new')(sequelize); const casesEditRoute = require('./routes/cases/edit')(sequelize); const casesDeleteRoute = require('./routes/cases/delete')(sequelize); app.use('/cases', casesIndexRoute); +app.use('/cases', casesIndexByProjectIdRoute); app.use('/cases', casesShowRoute); app.use('/cases', casesNewRoute); app.use('/cases', casesEditRoute); diff --git a/backend/routes/cases/indexByProjectId.js b/backend/routes/cases/indexByProjectId.js new file mode 100644 index 0000000..1bfe0b6 --- /dev/null +++ b/backend/routes/cases/indexByProjectId.js @@ -0,0 +1,54 @@ +const express = require('express'); +const router = express.Router(); +const defineProject = require('../../models/projects'); +const defineFolder = require('../../models/folders'); +const defineCase = require('../../models/cases'); +const defineRunCase = require('../../models/runCases'); +const { DataTypes } = require('sequelize'); + +module.exports = function (sequelize) { + const Project = defineProject(sequelize, DataTypes); + const Folder = defineFolder(sequelize, DataTypes); + const Case = defineCase(sequelize, DataTypes); + const RunCase = defineRunCase(sequelize, DataTypes); + Project.hasMany(Folder, { foreignKey: 'projectId' }); + Folder.hasMany(Case, { foreignKey: 'folderId' }); + Folder.belongsTo(Project, { foreignKey: 'projectId' }); + Case.belongsTo(Folder, { foreignKey: 'folderId' }); + Case.hasMany(RunCase, { foreignKey: 'caseId' }); + RunCase.belongsTo(Case, { foreignKey: 'caseId' }); + const { verifySignedIn } = require('../../middleware/auth')(sequelize); + const { verifyProjectVisibleFromProjectId } = require('../../middleware/verifyVisible')(sequelize); + + router.get('/byproject', verifySignedIn, verifyProjectVisibleFromProjectId, async (req, res) => { + const { projectId } = req.query; + + if (!projectId) { + return res.status(400).json({ error: 'projectId is required' }); + } + + try { + const cases = await Case.findAll({ + include: [ + { + model: Folder, + where: { + projectId: projectId, + }, + attributes: [], + }, + { + model: RunCase, + attributes: ['runId', 'status'], + }, + ], + }); + res.json(cases); + } catch (error) { + console.error(error); + res.status(500).send('Internal Server Error'); + } + }); + + return router; +}; diff --git a/frontend/src/app/[locale]/projects/[projectId]/folders/foldersControl.ts b/frontend/src/app/[locale]/projects/[projectId]/folders/foldersControl.ts index 452b18b..f20f635 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/folders/foldersControl.ts +++ b/frontend/src/app/[locale]/projects/[projectId]/folders/foldersControl.ts @@ -4,7 +4,7 @@ const apiServer = Config.apiServer; /** * fetch folder records */ -async function fetchFolders(jwt: string, projectId: string) { +async function fetchFolders(jwt: string, projectId: number) { try { const url = `${apiServer}/folders?projectId=${projectId}`; const response = await fetch(url, { diff --git a/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/RunEditor.tsx b/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/RunEditor.tsx index bbae39c..5730e46 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/RunEditor.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/RunEditor.tsx @@ -22,12 +22,18 @@ import { Save, ArrowLeft, Folder, ChevronDown, CopyPlus, CopyMinus, RotateCw } f import RunProgressChart from './RunPregressDonutChart'; import TestCaseSelector from './TestCaseSelector'; import { testRunStatus } from '@/config/selection'; -import { RunType, RunCaseType, RunStatusCountType, RunMessages } from '@/types/run'; +import { RunType, RunStatusCountType, RunMessages } from '@/types/run'; import { CaseType } from '@/types/case'; import { FolderType } from '@/types/folder'; -import { fetchRun, updateRun, fetchRunCases, updateRunCases, processRunCases } from '../runsControl'; +import { + fetchRun, + updateRun, + updateRunCases, + processRunCases, + fetchProjectCases, + processTestCases, +} from '../runsControl'; import { fetchFolders } from '../../folders/foldersControl'; -import { fetchCases } from '@/utils/caseControl'; import { TokenContext } from '@/utils/TokenProvider'; import { useTheme } from 'next-themes'; import { useFormGuard } from '@/utils/formGuard'; @@ -55,11 +61,11 @@ export default function RunEditor({ projectId, runId, messages, locale }: Props) const { theme, setTheme } = useTheme(); const [testRun, setTestRun] = useState(defaultTestRun); const [folders, setFolders] = useState([]); - const [runCases, setRunCases] = useState([]); const [runStatusCounts, setRunStatusCounts] = useState(); const [selectedKeys, setSelectedKeys] = useState(new Set([])); const [selectedFolder, setSelectedFolder] = useState(null); - const [testcases, setTestCases] = useState([]); + const [testCases, setTestCases] = useState([]); + const [filteredTestCases, setFilteredTestCases] = useState([]); const [isNameInvalid, setIsNameInvalid] = useState(false); const [isUpdating, setIsUpdating] = useState(false); const [isDirty, setIsDirty] = useState(false); @@ -80,9 +86,17 @@ export default function RunEditor({ projectId, runId, messages, locale }: Props) try { await fetchRunAndStatusCount(); - const foldersData = await fetchFolders(context.token.access_token, projectId); + const foldersData = await fetchFolders(context.token.access_token, Number(projectId)); setFolders(foldersData); setSelectedFolder(foldersData[0]); + + const casesData = await fetchProjectCases(context.token.access_token, Number(projectId)); + casesData.forEach((testCase: CaseType) => { + if (testCase.RunCases && testCase.RunCases.length > 0) { + testCase.RunCases[0].editState = 'notChanged'; + } + }); + setTestCases(casesData); } catch (error: any) { console.error('Error in effect:', error.message); } @@ -92,39 +106,19 @@ export default function RunEditor({ projectId, runId, messages, locale }: Props) }, [context]); useEffect(() => { - async function fetchCasesData() { + function onFilter() { if (selectedFolder && selectedFolder.id) { try { - const latestRunCases: RunCaseType[] = await fetchRunCases(context.token.access_token, Number(runId)); - latestRunCases.forEach((runCase: RunCaseType) => { - runCase.editState = 'notChanged'; - }); - setRunCases(latestRunCases); - - const testCasesData: CaseType[] = await fetchCases(context.token.access_token, selectedFolder.id); - // Check if each testCase has an association with testRun - // and add "isIncluded" property - const updatedTestCasesData = testCasesData.map((testCase) => { - const runCase = latestRunCases.find((runCase) => runCase.caseId === testCase.id); - - const isIncluded = runCase ? true : false; - const runStatus = runCase ? runCase.status : 0; - return { - ...testCase, - isIncluded, - runStatus, - }; - }); - - setTestCases(updatedTestCasesData); + const filteredData = testCases.filter((testCase) => testCase.folderId === selectedFolder.id); + setFilteredTestCases(filteredData); } catch (error: any) { console.error('Error fetching cases data:', error.message); } } } - fetchCasesData(); - }, [selectedFolder]); + onFilter(); + }, [selectedFolder, testCases]); const handleChangeStatus = async (changeCaseId: number, status: number) => { setIsDirty(true); @@ -141,38 +135,21 @@ export default function RunEditor({ projectId, runId, messages, locale }: Props) const handleIncludeExcludeCase = async (isInclude: boolean, clickedTestCaseId: number) => { setIsDirty(true); const keys = [clickedTestCaseId]; - const newRunCases = processRunCases(isInclude, keys, Number(runId), runCases); - setRunCases(newRunCases); - - const updatedTestCases = testcases.map((testcase) => { - if (testcase.id === clickedTestCaseId) { - return { ...testcase, isIncluded: isInclude }; - } - return testcase; - }); - setTestCases(updatedTestCases); + const newTestCases = processTestCases(isInclude, keys, Number(runId), testCases); + setTestCases(newTestCases); }; const handleBulkIncludeExcludeCases = async (isInclude: boolean) => { setIsDirty(true); let keys: number[] = []; if (selectedKeys === 'all') { - keys = testcases.map((item) => item.id); + keys = testCases.map((item) => item.id); } else { keys = Array.from(selectedKeys).map(Number); } - const newRunCases = processRunCases(isInclude, keys, Number(runId), runCases); - setRunCases(newRunCases); - - const updatedTestCases = testcases.map((testcase) => { - if (keys.includes(testcase.id)) { - return { ...testcase, isIncluded: isInclude }; - } - return testcase; - }); - setTestCases(updatedTestCases); - + const newTestCases = processTestCases(isInclude, keys, Number(runId), testCases); + setTestCases(newTestCases); setSelectedKeys(new Set([])); }; @@ -204,7 +181,7 @@ export default function RunEditor({ projectId, runId, messages, locale }: Props) onPress={async () => { setIsUpdating(true); await updateRun(context.token.access_token, testRun); - await updateRunCases(context.token.access_token, Number(runId), runCases); + await updateRunCases(context.token.access_token, Number(runId), testCases); setIsUpdating(false); setIsDirty(false); }} @@ -335,7 +312,7 @@ export default function RunEditor({ projectId, runId, messages, locale }: Props)
{}; onIncludeCase: (includeCaseId: number) => {}; onExcludeCase: (excludeCaseId: number) => {}; - messages: RunsMessages; + messages: RunMessages; }; export default function TestCaseSelector({ @@ -106,9 +106,22 @@ export default function TestCaseSelector({ } }; + const isCaseIncluded = (testCase: CaseType) => { + let isIncluded = false; + if (testCase.RunCases && testCase.RunCases.length > 0) { + if (testCase.RunCases[0].editState !== 'deleted') { + // Even if RunCase[0] exists, if 'deleted' it will be as not included. + isIncluded = true; + } + } + + return isIncluded; + }; + const renderCell = (testCase: CaseType, columnKey: Key) => { const cellValue = testCase[columnKey as keyof CaseType]; - const isIncluded = testCase.isIncluded; + const isIncluded = isCaseIncluded(testCase); + const runStatus = testCase.RunCases && testCase.RunCases.length > 0 ? testCase.RunCases[0].status : 0; switch (columnKey) { case 'priority': @@ -130,10 +143,10 @@ export default function TestCaseSelector({ size="sm" variant="light" isDisabled={!isIncluded} - startContent={isIncluded && renderStatusIcon(testRunCaseStatus[cellValue].uid)} + startContent={isIncluded && renderStatusIcon(testRunCaseStatus[runStatus].uid)} endContent={isIncluded && } > - {isIncluded && messages[testRunCaseStatus[cellValue].uid]} + {isIncluded && messages[testRunCaseStatus[runStatus].uid]} @@ -143,7 +156,7 @@ export default function TestCaseSelector({ startContent={renderStatusIcon(runCaseStatus.uid)} onPress={() => onStatusChange(testCase.id, index)} > - {messages[runCaseStatus.uid]} + {messages[runStatus.uid]} ))} @@ -162,7 +175,7 @@ export default function TestCaseSelector({ key="include" startContent={} onPress={() => { - if (testCase.isIncluded) { + if (isIncluded) { return; } onIncludeCase(testCase.id); @@ -174,7 +187,7 @@ export default function TestCaseSelector({ key="exclude" startContent={} onPress={() => { - if (!testCase.isIncluded) { + if (!isIncluded) { return; } onExcludeCase(testCase.id); @@ -239,8 +252,8 @@ export default function TestCaseSelector({ {(item) => ( - - {(columnKey) => {renderCell(item, columnKey)}} + + {(columnKey) => {renderCell(item, columnKey)}} )} diff --git a/frontend/src/app/[locale]/projects/[projectId]/runs/runsControl.ts b/frontend/src/app/[locale]/projects/[projectId]/runs/runsControl.ts index 1b31c1f..692b1ca 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/runs/runsControl.ts +++ b/frontend/src/app/[locale]/projects/[projectId]/runs/runsControl.ts @@ -1,6 +1,7 @@ +import { CaseType } from '@/types/case'; +import { RunType, RunCaseType } from '@/types/run'; import Config from '@/config/config'; const apiServer = Config.apiServer; -import { RunType, RunCaseType } from '@/types/run'; async function fetchRun(jwt: string, runId: number) { const url = `${apiServer}/runs/${runId}`; @@ -150,61 +151,134 @@ async function fetchRunCases(jwt: string, runId: number) { } } -function processRunCases( - isInclude: boolean, - keys: number[], - runId: number, - currentRunCases: RunCaseType[] -): RunCaseType[] { - const updatedRunCases = [...currentRunCases]; +function processTestCases(isInclude: boolean, keys: number[], runId: number, currentTestCases: CaseType[]): CaseType[] { + const updatedTestCases = [...currentTestCases]; if (isInclude) { keys.forEach((caseId) => { - const existingRunCase = currentRunCases.find((runCase) => runCase.caseId === caseId); - if (existingRunCase) { + const targetCase = updatedTestCases.find((testCase) => testCase.id === caseId); + if (!targetCase) { + console.error('failed to find target case'); + return; + } + + if (targetCase.RunCases && targetCase.RunCases.length > 0) { // already included - if (existingRunCase.editState === 'notChanged') { + if (targetCase.RunCases[0].editState === 'notChanged') { // do nothing - } else if (existingRunCase.editState === 'changed') { + } else if (targetCase.RunCases[0].editState === 'changed') { // do nothing - } else if (existingRunCase.editState === 'new') { + } else if (targetCase.RunCases[0].editState === 'new') { // do nothing - } else if (existingRunCase.editState === 'deleted') { - existingRunCase.editState = 'changed'; + } else if (targetCase.RunCases[0].editState === 'deleted') { + targetCase.RunCases[0].editState = 'changed'; } } else { - updatedRunCases.push({ - id: -1, + const newRunCase = { runId: runId, - caseId: caseId, - status: -1, + status: 0, editState: 'new', - }); + } as RunCaseType; + targetCase.RunCases = [newRunCase]; } }); } else { keys.forEach((caseId) => { - const existingRunCase = currentRunCases.find((runCase) => runCase.caseId === caseId); - if (!existingRunCase) { + const targetCase = updatedTestCases.find((testCase) => testCase.id === caseId); + if (!targetCase) { + console.error('failed to find target case'); + return; + } + + if (!targetCase.RunCases || targetCase.RunCases.length == 0) { // already excluded } else { - if (existingRunCase.editState === 'notChanged') { - existingRunCase.editState = 'deleted'; - } else if (existingRunCase.editState === 'changed') { - existingRunCase.editState = 'deleted'; - } else if (existingRunCase.editState === 'new') { - existingRunCase.editState = 'deleted'; - } else if (existingRunCase.editState === 'deleted') { + if (targetCase.RunCases[0].editState === 'notChanged') { + targetCase.RunCases[0].editState = 'deleted'; + } else if (targetCase.RunCases[0].editState === 'changed') { + targetCase.RunCases[0].editState = 'deleted'; + } else if (targetCase.RunCases[0].editState === 'new') { + targetCase.RunCases[0].editState = 'deleted'; + } else if (targetCase.RunCases[0].editState === 'deleted') { // do nothing } } }); } - return updatedRunCases; + return updatedTestCases; } -async function updateRunCases(jwt: string, runId: number, runCases: RunCaseType[]) { +// function processRunCases( +// isInclude: boolean, +// keys: number[], +// runId: number, +// currentRunCases: RunCaseType[] +// ): RunCaseType[] { +// const updatedRunCases = [...currentRunCases]; + +// if (isInclude) { +// keys.forEach((caseId) => { +// const existingRunCase = currentRunCases.find((runCase) => runCase.caseId === caseId); +// if (existingRunCase) { +// // already included +// if (existingRunCase.editState === 'notChanged') { +// // do nothing +// } else if (existingRunCase.editState === 'changed') { +// // do nothing +// } else if (existingRunCase.editState === 'new') { +// // do nothing +// } else if (existingRunCase.editState === 'deleted') { +// existingRunCase.editState = 'changed'; +// } +// } else { +// updatedRunCases.push({ +// id: -1, +// runId: runId, +// caseId: caseId, +// status: 0, +// editState: 'new', +// }); +// } +// }); +// } else { +// keys.forEach((caseId) => { +// const existingRunCase = currentRunCases.find((runCase) => runCase.caseId === caseId); +// if (!existingRunCase) { +// // already excluded +// } else { +// if (existingRunCase.editState === 'notChanged') { +// existingRunCase.editState = 'deleted'; +// } else if (existingRunCase.editState === 'changed') { +// existingRunCase.editState = 'deleted'; +// } else if (existingRunCase.editState === 'new') { +// existingRunCase.editState = 'deleted'; +// } else if (existingRunCase.editState === 'deleted') { +// // do nothing +// } +// } +// }); +// } + +// return updatedRunCases; +// } + +async function updateRunCases(jwt: string, runId: number, testCases: CaseType[]) { + const runCases: RunCaseType[] = []; + testCases.forEach((itr) => { + if (itr.RunCases && itr.RunCases.length > 0) { + runCases.push({ + id: -1, + caseId: itr.id, + runId: runId, + status: itr.RunCases[0].status, + editState: itr.RunCases[0].editState, + }); + } + }); + + console.log(testCases, runCases); + const fetchOptions = { method: 'POST', headers: { @@ -227,4 +301,37 @@ async function updateRunCases(jwt: string, runId: number, runCases: RunCaseType[ } } -export { fetchRun, fetchRuns, createRun, updateRun, deleteRun, fetchRunCases, processRunCases, updateRunCases }; +async function fetchProjectCases(jwt: string, projectId: number) { + const url = `${apiServer}/cases/byproject?projectId=${projectId}`; + + try { + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const data = await response.json(); + return data; + } catch (error: any) { + console.error('Error fetching data:', error.message); + } +} + +export { + fetchRun, + fetchRuns, + createRun, + updateRun, + deleteRun, + fetchRunCases, + processTestCases, + updateRunCases, + fetchProjectCases, +}; diff --git a/frontend/types/case.ts b/frontend/types/case.ts index 87a0e29..2558bd8 100644 --- a/frontend/types/case.ts +++ b/frontend/types/case.ts @@ -11,9 +11,8 @@ type CaseType = { expectedResults: string; folderId: number; Steps?: StepType[]; + RunCases?: RunCaseType[]; Attachments?: AttachmentType[]; - isIncluded?: boolean; - runStatus?: number; }; type CaseStepType = { @@ -35,6 +34,12 @@ type StepType = { editState: 'notChanged' | 'changed' | 'new' | 'deleted'; }; +type RunCaseType = { + runId: number; + status: number; + editState: 'notChanged' | 'changed' | 'new' | 'deleted'; +}; + type CaseAttachmentType = { createdAt: Date; updatedAt: Date;