From b986f39c420db79ee680cc0e4f1887809f294416 Mon Sep 17 00:00:00 2001 From: kimatata <117462761+kimatata@users.noreply.github.com> Date: Sat, 22 Nov 2025 10:32:18 +0900 Subject: [PATCH] feat: import test cases UI using Excel (#339) --- backend/package-lock.json | 43 +----- backend/routes/cases/import.js | 2 +- frontend/messages/en.json | 10 +- frontend/messages/ja.json | 10 +- frontend/messages/pt-BR.json | 10 +- frontend/package-lock.json | 9 +- .../template/unittcms-import-template-v1.xlsx | Bin 0 -> 17586 bytes .../[folderId]/cases/CaseImportDialog.tsx | 125 ++++++++++++++++++ .../folders/[folderId]/cases/CasesPane.tsx | 79 +++++++---- .../[folderId]/cases/TestCaseTable.tsx | 12 ++ .../folders/[folderId]/cases/page.tsx | 8 ++ frontend/types/case.ts | 8 ++ frontend/utils/caseControl.ts | 23 +++- 13 files changed, 263 insertions(+), 76 deletions(-) create mode 100644 frontend/public/template/unittcms-import-template-v1.xlsx create mode 100644 frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/CaseImportDialog.tsx diff --git a/backend/package-lock.json b/backend/package-lock.json index 5523736..e5ae6ac 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1010,27 +1010,6 @@ "node": ">= 0.8" } }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -1120,6 +1099,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -4286,26 +4266,6 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" }, - "encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "optional": true, - "requires": { - "iconv-lite": "^0.6.2" - }, - "dependencies": { - "iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - } - } - }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -4368,6 +4328,7 @@ "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "peer": true, "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", diff --git a/backend/routes/cases/import.js b/backend/routes/cases/import.js index cfd6088..9b07cfd 100644 --- a/backend/routes/cases/import.js +++ b/backend/routes/cases/import.js @@ -68,7 +68,7 @@ export default function (sequelize) { let errorMessage = null; const casesToCreate = []; - const requiredFields = ['title', 'preConditions', 'expectedResults']; + const requiredFields = ['title', 'priority', 'type', 'template']; for (const [index, row] of jsonData.entries()) { const rowNumber = index + 2; for (const field of requiredFields) { diff --git a/frontend/messages/en.json b/frontend/messages/en.json index d94ee65..eee362c 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -240,7 +240,15 @@ "cases_moved": "Test cases moved", "cases_cloned": "Test cases cloned", "tags": "Tags", - "select_tags": "Select tags" + "select_tags": "Select tags", + "import": "Import", + "import_cases": "Import Test Cases", + "import_available": "You can import test cases from Excel file (xlsx, xls). The Excel file to be imported must follow the specified format.", + "download_template": "Download Template", + "click_to_upload": "Click to upload", + "or_drag_and_drop": " or drag and drop", + "max_file_size": "Max. file size", + "cases_imported": "Test cases imported" }, "Case": { "back_to_cases": "Back to test cases", diff --git a/frontend/messages/ja.json b/frontend/messages/ja.json index 46d64e8..53229ec 100644 --- a/frontend/messages/ja.json +++ b/frontend/messages/ja.json @@ -240,7 +240,15 @@ "cases_moved": "テストケースを移動しました", "cases_cloned": "テストケースをクローンしました", "tags": "タグ", - "select_tags": "タグを選択" + "select_tags": "タグを選択", + "import": "インポート", + "import_cases": "テストケースのインポート", + "import_available": "Excelファイル(xlsx, xls)からテストケースをインポートできます。インポートするExcelファイルは既定のフォーマットに従う必要があります。", + "download_template": "テンプレートをダウンロード", + "click_to_upload": "クリックしてアップロード", + "or_drag_and_drop": "またはドラッグアンドドロップ", + "max_file_size": "最大ファイルサイズ", + "cases_imported": "テストケースがインポートされました" }, "Case": { "back_to_cases": "テストケース一覧に戻る", diff --git a/frontend/messages/pt-BR.json b/frontend/messages/pt-BR.json index 881c5ca..3f3b767 100644 --- a/frontend/messages/pt-BR.json +++ b/frontend/messages/pt-BR.json @@ -240,7 +240,15 @@ "cases_moved": "Casos de teste movidos", "cases_cloned": "Casos de teste clonados", "tags": "Tags", - "select_tags": "Selecionar tags" + "select_tags": "Selecionar tags", + "import": "Importar", + "import_cases": "Importar Casos de Teste", + "import_available": "Você pode importar casos de teste a partir de um arquivo Excel (xlsx, xls). O arquivo Excel a ser importado deve seguir o formato especificado.", + "download_template": "Baixar Modelo", + "click_to_upload": "Clique para enviar", + "or_drag_and_drop": " ou arraste e solte", + "max_file_size": "Tamanho máx. do arquivo", + "cases_imported": "Casos de teste importados" }, "Case": { "back_to_cases": "Voltar para os casos de teste", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d93f099..2b167f8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9988,9 +9988,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001700", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz", - "integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==", + "version": "1.0.30001754", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", + "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==", "funding": [ { "type": "opencollective", @@ -10004,7 +10004,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chokidar": { "version": "3.6.0", diff --git a/frontend/public/template/unittcms-import-template-v1.xlsx b/frontend/public/template/unittcms-import-template-v1.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..9a89fe33ac17f49f0a71c7b5f5db14a77e86991b GIT binary patch literal 17586 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 j*XTd8H}n31{U0l~oFwFrSQ`Wl`1c3_0+P=6 void; + onCancel: () => void; + messages: CasesMessages; + token: string; +}; + +export default function CaseImportDialog({ isOpen, folderId, isDisabled, onImport, onCancel, messages, token }: Props) { + const [isProcessing, setIsProcessing] = useState(false); + const [importError, setImportError] = useState(null); + + const handleDrop = (event: DragEvent) => { + event.preventDefault(); + if (event.dataTransfer) { + const filesArray = Array.from(event.dataTransfer.files); + handleFiles(filesArray); + } + }; + + const handleInput = (event: ChangeEvent) => { + if (event.target) { + const input = event.target as HTMLInputElement; + if (input.files) { + handleFiles(Array.from(input.files)); + } + } + }; + + const handleFiles = async (filesArray: File[]) => { + setIsProcessing(true); + + if (filesArray.length !== 1) { + console.error('Error multiple file'); + } else { + const ret = await importCases(token, folderId, filesArray[0]); + if (ret.error) { + setImportError(ret.error); + } else { + onImport(); + } + } + setIsProcessing(false); + }; + + const onCloseDialog = () => { + setImportError(null); + onCancel(); + }; + + return ( + { + onCloseDialog(); + }} + > + + {messages.importCases} + +
+
+
{messages.importAvailable}
+ + {messages.downloadTemplate} + +
+
+ {importError && } +
{ + if (isDisabled) { + return; + } + handleDrop(event); + }} + onDragOver={(event) => event.preventDefault()} + > + +
+
+ + {isProcessing ? ( + + ) : ( + <> + + + )} + +
+
+ ); +} diff --git a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/CasesPane.tsx b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/CasesPane.tsx index fef0bc6..5be48ed 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/CasesPane.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/CasesPane.tsx @@ -1,9 +1,11 @@ 'use client'; import { useState, useEffect, useContext, useCallback } from 'react'; +import { addToast } from '@heroui/react'; import { useRouter, useSearchParams } from 'next/navigation'; import TestCaseTable from './TestCaseTable'; import CaseDialog from './CaseDialog'; import CaseMoveDialog from './CaseMoveDialog'; +import CaseImportDialog from './CaseImportDialog'; import DeleteConfirmDialog from '@/components/DeleteConfirmDialog'; import { TokenContext } from '@/utils/TokenProvider'; import { fetchCases, createCase, deleteCases, exportCases } from '@/utils/caseControl'; @@ -76,38 +78,38 @@ export default function CasesPane({ router.push(newUrl, { scroll: false }); }; - useEffect(() => { - async function fetchDataEffect() { - if (!context.isSignedIn()) return; + const refreshCases = useCallback(async () => { + if (!context.isSignedIn()) return; - const searchParam = searchParams.get('search') || ''; - const priorityParam = parseQueryParam(searchParams.get('priority')); - const typeParam = parseQueryParam(searchParams.get('type')); - const tagParam = parseQueryParam(searchParams.get('tag')); + const searchParam = searchParams.get('search') || ''; + const priorityParam = parseQueryParam(searchParams.get('priority')); + const typeParam = parseQueryParam(searchParams.get('type')); + const tagParam = parseQueryParam(searchParams.get('tag')); - setSearchFilter(searchParam); - setPriorityFilter(priorityParam); - setTypeFilter(typeParam); - setTagFilter(tagParam); + setSearchFilter(searchParam); + setPriorityFilter(priorityParam); + setTypeFilter(typeParam); + setTagFilter(tagParam); - try { - const data = await fetchCases( - context.token.access_token, - Number(folderId), - searchParam || undefined, - priorityParam.length > 0 ? priorityParam : undefined, - typeParam.length > 0 ? typeParam : undefined, - tagParam.length > 0 ? tagParam : undefined - ); - setCases(data); - } catch (error: unknown) { - logError('Error fetching cases:', error); - } + try { + const data = await fetchCases( + context.token.access_token, + Number(folderId), + searchParam || undefined, + priorityParam.length > 0 ? priorityParam : undefined, + typeParam.length > 0 ? typeParam : undefined, + tagParam.length > 0 ? tagParam : undefined + ); + setCases(data); + } catch (error: unknown) { + logError('Error fetching cases:', error); } - - fetchDataEffect(); }, [context, folderId, searchParams]); + useEffect(() => { + refreshCases(); + }, [refreshCases]); + const closeDialog = () => setIsCaseDialogOpen(false); const onSubmit = async (title: string, description: string) => { @@ -175,6 +177,20 @@ export default function CasesPane({ return unsubscribe; }, [openMoveDialog]); + // ************************************************************************** + // Import cases + // ************************************************************************** + const [isImportDialogOpen, setIsImportDialogOpen] = useState(false); + const handleImport = () => { + refreshCases(); + setIsImportDialogOpen(false); + addToast({ + title: 'Success', + color: 'success', + description: messages.casesImported, + }); + }; + return ( <> setIsCaseDialogOpen(true)} onDeleteCase={onDeleteCase} onDeleteCases={onDeleteCases} + onShowImportDialog={() => setIsImportDialogOpen(true)} onExportCases={onExportCases} onFilterChange={handleFilterChange} activeSearchFilter={searchFilter} @@ -210,6 +227,16 @@ export default function CasesPane({ token={context.token.access_token} /> + setIsImportDialogOpen(false)} + messages={messages} + token={context.token.access_token} + /> + void; onDeleteCase: (caseId: number) => void; onDeleteCases: (caseIds: number[]) => void; + onShowImportDialog: () => void; onExportCases: (type: string) => void; onFilterChange: (query: string, priorities: number[], types: number[], tag: number[]) => void; activeSearchFilter: string; @@ -64,6 +66,7 @@ export default function TestCaseTable({ onCreateCase, onDeleteCase, onDeleteCases, + onShowImportDialog, onExportCases, onFilterChange, activeSearchFilter, @@ -355,6 +358,15 @@ export default function TestCaseTable({ +