From 475fa1ced77af1b2318b414129066140fd670000 Mon Sep 17 00:00:00 2001 From: kimatata <117462761+kimatata@users.noreply.github.com> Date: Sat, 10 Jan 2026 18:17:07 +0900 Subject: [PATCH] feat: display tags in test run page (#378) --- backend/routes/runs/download.js | 18 ++++++++++++- backend/routes/runs/download.test.js | 20 ++++++++++++++ .../runs/[runId]/TestCaseDetailDialog.tsx | 27 ++++++++++++++++++- .../runs/[runId]/TestCaseSelector.tsx | 16 +++++++++++ 4 files changed, 79 insertions(+), 2 deletions(-) diff --git a/backend/routes/runs/download.js b/backend/routes/runs/download.js index 15a6b58..1a30ca5 100644 --- a/backend/routes/runs/download.js +++ b/backend/routes/runs/download.js @@ -7,6 +7,7 @@ import defineRun from '../../models/runs.js'; import defineRunCase from '../../models/runCases.js'; import defineCase from '../../models/cases.js'; import defineFolder from '../../models/folders.js'; +import defineTag from '../../models/tags.js'; import authMiddleware from '../../middleware/auth.js'; import visibilityMiddleware from '../../middleware/verifyVisible.js'; import { testRunCaseStatus, testRunStatus, priorities, testTypes, automationStatus } from '../../config/enums.js'; @@ -19,8 +20,11 @@ export default function (sequelize) { const RunCase = defineRunCase(sequelize, DataTypes); const Case = defineCase(sequelize, DataTypes); const Folder = defineFolder(sequelize, DataTypes); + const Tags = defineTag(sequelize, DataTypes); RunCase.belongsTo(Case, { foreignKey: 'caseId' }); + Case.belongsToMany(Tags, { through: 'caseTags', foreignKey: 'caseId', otherKey: 'tagId' }); + Tags.belongsToMany(Case, { through: 'caseTags', foreignKey: 'tagId', otherKey: 'caseId' }); router.get('/download/:runId', verifySignedIn, verifyProjectVisibleFromRunId, async (req, res) => { const { runId } = req.params; @@ -38,7 +42,18 @@ export default function (sequelize) { const runCases = await RunCase.findAll({ where: { runId }, - include: [{ model: Case }], + include: [ + { + model: Case, + include: [ + { + model: Tags, + attributes: ['id', 'name'], + through: { attributes: [] }, + }, + ], + }, + ], }); if (type === 'xml') { @@ -104,6 +119,7 @@ export default function (sequelize) { priority: priorities[rc.Case.priority] || rc.Case.priority, type: testTypes[rc.Case.type] || rc.Case.type, automationStatus: automationStatus[rc.Case.automationStatus] || rc.Case.automationStatus, + tags: rc.Case.Tags && rc.Case.Tags.length > 0 ? rc.Case.Tags.map((tag) => tag.name).join(', ') : '', status: testRunCaseStatus[rc.status] || rc.status, })); diff --git a/backend/routes/runs/download.test.js b/backend/routes/runs/download.test.js index 714e1c7..1c643a8 100644 --- a/backend/routes/runs/download.test.js +++ b/backend/routes/runs/download.test.js @@ -59,6 +59,7 @@ vi.mock('../../models/runCases.js', () => ({ // mock defineCase const mockCase = { belongsTo: vi.fn(), + belongsToMany: vi.fn(), }; vi.mock('../../models/cases.js', () => ({ default: () => mockCase, @@ -72,6 +73,14 @@ vi.mock('../../models/folders.js', () => ({ default: () => mockFolder, })); +// mock defineTag +const mockTags = { + belongsToMany: vi.fn(), +}; +vi.mock('../../models/tags.js', () => ({ + default: () => mockTags, +})); + describe('GET /download/:runId with type=csv', () => { let app; const sequelize = new Sequelize({ @@ -105,6 +114,10 @@ describe('GET /download/:runId with type=csv', () => { priority: 0, // critical type: 4, // functional automationStatus: 0, // automated + Tags: [ + { id: 1, name: 'tag1' }, + { id: 2, name: 'tag2' }, + ], }, }, { @@ -119,6 +132,7 @@ describe('GET /download/:runId with type=csv', () => { priority: 1, // high type: 1, // security automationStatus: 1, // automation-not-required + Tags: [], }, }, { @@ -133,6 +147,7 @@ describe('GET /download/:runId with type=csv', () => { priority: 2, // medium type: 2, // performance automationStatus: 2, // cannot-be-automated + Tags: [{ id: 3, name: 'tag3' }], }, }, ]); @@ -162,6 +177,11 @@ describe('GET /download/:runId with type=csv', () => { expect(csvContent).toContain('inProgress'); expect(csvContent).toContain('underReview'); + // Check that tags are included in the CSV + expect(csvContent).toContain('tags'); + expect(csvContent).toContain('tag1, tag2'); + expect(csvContent).toContain('tag3'); + // Ensure numeric values are not present (except for id which should be numeric) const lines = csvContent.split('\n'); const dataLines = lines.slice(1).filter((line) => line.trim()); // Skip header diff --git a/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/TestCaseDetailDialog.tsx b/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/TestCaseDetailDialog.tsx index 0a7151b..aee94c1 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/TestCaseDetailDialog.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/TestCaseDetailDialog.tsx @@ -1,5 +1,15 @@ import { useState, useEffect, useContext } from 'react'; -import { Button, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Avatar, Textarea } from '@heroui/react'; +import { + Button, + Modal, + ModalContent, + ModalHeader, + ModalBody, + ModalFooter, + Avatar, + Textarea, + Chip, +} from '@heroui/react'; import { testTypes, templates } from '@/config/selection'; import { RunMessages } from '@/types/run'; import { CaseType, StepType } from '@/types/case'; @@ -103,6 +113,21 @@ export default function TestCaseDetailDialog({
{messages.tags}
+