Merge pull request #223 from kimatata/develop
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ coverage/
|
|||||||
/playwright-screenshots/
|
/playwright-screenshots/
|
||||||
/blob-report/
|
/blob-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
|
.DS_Store
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ There are many test case management tools available in the market, which can be
|
|||||||
|
|
||||||
Proprietary tools often come with modern, user-friendly interfaces but tend to be cloud-based, which may raise security concerns for some organizations. While some of them do offer on-premises options, these tend to be significantly more expensive.
|
Proprietary tools often come with modern, user-friendly interfaces but tend to be cloud-based, which may raise security concerns for some organizations. While some of them do offer on-premises options, these tend to be significantly more expensive.
|
||||||
|
|
||||||
There are also open-source tools, but many feature older user interfaces that involve frequent page reloads, which can hinder usability.
|
There are also open-source tools, but many feature older user interfaces that involve frequent full page reloads, which can hinder usability.
|
||||||
|
|
||||||
With these challenges in mind, I set out to develop a modern, user-friendly, open-source test case management tool that anyone can use for free in a secure, self-hosted environment.
|
With these challenges in mind, I set out to develop a modern, user-friendly, open-source test case management tool that anyone can use for free in a secure, self-hosted environment.
|
||||||
|
|
||||||
|
|||||||
@@ -78,12 +78,14 @@ app.use('/folders', foldersEditRoute);
|
|||||||
app.use('/folders', foldersDeleteRoute);
|
app.use('/folders', foldersDeleteRoute);
|
||||||
|
|
||||||
// "/cases"
|
// "/cases"
|
||||||
|
const casesDownloadRoute = require('./routes/cases/download')(sequelize);
|
||||||
const casesIndexRoute = require('./routes/cases/index')(sequelize);
|
const casesIndexRoute = require('./routes/cases/index')(sequelize);
|
||||||
const casesIndexByProjectIdRoute = require('./routes/cases/indexByProjectId')(sequelize);
|
const casesIndexByProjectIdRoute = require('./routes/cases/indexByProjectId')(sequelize);
|
||||||
const casesShowRoute = require('./routes/cases/show')(sequelize);
|
const casesShowRoute = require('./routes/cases/show')(sequelize);
|
||||||
const casesNewRoute = require('./routes/cases/new')(sequelize);
|
const casesNewRoute = require('./routes/cases/new')(sequelize);
|
||||||
const casesEditRoute = require('./routes/cases/edit')(sequelize);
|
const casesEditRoute = require('./routes/cases/edit')(sequelize);
|
||||||
const casesDeleteRoute = require('./routes/cases/delete')(sequelize);
|
const casesDeleteRoute = require('./routes/cases/delete')(sequelize);
|
||||||
|
app.use('/cases', casesDownloadRoute);
|
||||||
app.use('/cases', casesIndexRoute);
|
app.use('/cases', casesIndexRoute);
|
||||||
app.use('/cases', casesIndexByProjectIdRoute);
|
app.use('/cases', casesIndexByProjectIdRoute);
|
||||||
app.use('/cases', casesShowRoute);
|
app.use('/cases', casesShowRoute);
|
||||||
@@ -104,11 +106,13 @@ app.use('/attachments', attachmentsDeleteRoute);
|
|||||||
app.use('/attachments', attachmentsDownloadRoute);
|
app.use('/attachments', attachmentsDownloadRoute);
|
||||||
|
|
||||||
// "/runs"
|
// "/runs"
|
||||||
|
const runsDownloadRoute = require('./routes/runs/download')(sequelize);
|
||||||
const runsIndexRoute = require('./routes/runs/index')(sequelize);
|
const runsIndexRoute = require('./routes/runs/index')(sequelize);
|
||||||
const runsShowRoute = require('./routes/runs/show')(sequelize);
|
const runsShowRoute = require('./routes/runs/show')(sequelize);
|
||||||
const runsNewRoute = require('./routes/runs/new')(sequelize);
|
const runsNewRoute = require('./routes/runs/new')(sequelize);
|
||||||
const runsEditRoute = require('./routes/runs/edit')(sequelize);
|
const runsEditRoute = require('./routes/runs/edit')(sequelize);
|
||||||
const runDeleteRoute = require('./routes/runs/delete')(sequelize);
|
const runDeleteRoute = require('./routes/runs/delete')(sequelize);
|
||||||
|
app.use('/runs', runsDownloadRoute);
|
||||||
app.use('/runs', runsIndexRoute);
|
app.use('/runs', runsIndexRoute);
|
||||||
app.use('/runs', runsShowRoute);
|
app.use('/runs', runsShowRoute);
|
||||||
app.use('/runs', runsNewRoute);
|
app.use('/runs', runsNewRoute);
|
||||||
|
|||||||
181
backend/package-lock.json
generated
181
backend/package-lock.json
generated
@@ -15,9 +15,11 @@
|
|||||||
"express-rate-limit": "^7.4.1",
|
"express-rate-limit": "^7.4.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"papaparse": "^5.5.2",
|
||||||
"sequelize": "^6.37.5",
|
"sequelize": "^6.37.5",
|
||||||
"sequelize-cli": "^6.6.2",
|
"sequelize-cli": "^6.6.2",
|
||||||
"sqlite3": "^5.1.7"
|
"sqlite3": "^5.1.7",
|
||||||
|
"xmlbuilder2": "^3.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@gar/promisify": {
|
"node_modules/@gar/promisify": {
|
||||||
@@ -218,6 +220,50 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
|
||||||
"integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="
|
"integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@oozcitak/dom": {
|
||||||
|
"version": "1.15.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.10.tgz",
|
||||||
|
"integrity": "sha512-0JT29/LaxVgRcGKvHmSrUTEvZ8BXvZhGl2LASRUgHqDTC1M5g1pLmVv56IYNyt3bG2CUjDkc67wnyZC14pbQrQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@oozcitak/infra": "1.0.8",
|
||||||
|
"@oozcitak/url": "1.0.4",
|
||||||
|
"@oozcitak/util": "8.3.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oozcitak/infra": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@oozcitak/util": "8.3.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oozcitak/url": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@oozcitak/infra": "1.0.8",
|
||||||
|
"@oozcitak/util": "8.3.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oozcitak/util": {
|
||||||
|
"version": "8.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz",
|
||||||
|
"integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@pkgjs/parseargs": {
|
"node_modules/@pkgjs/parseargs": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
@@ -395,6 +441,19 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/argparse": {
|
||||||
|
"version": "1.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||||
|
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||||
|
"dependencies": {
|
||||||
|
"sprintf-js": "~1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/argparse/node_modules/sprintf-js": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
|
||||||
|
},
|
||||||
"node_modules/array-flatten": {
|
"node_modules/array-flatten": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
@@ -1086,6 +1145,18 @@
|
|||||||
"resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
|
||||||
"integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="
|
"integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/esprima": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
|
||||||
|
"bin": {
|
||||||
|
"esparse": "bin/esparse.js",
|
||||||
|
"esvalidate": "bin/esvalidate.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/etag": {
|
"node_modules/etag": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||||
@@ -1771,6 +1842,18 @@
|
|||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/js-yaml": {
|
||||||
|
"version": "3.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||||
|
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^1.0.7",
|
||||||
|
"esprima": "^4.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"js-yaml": "bin/js-yaml.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jsbn": {
|
"node_modules/jsbn": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||||
@@ -2358,6 +2441,11 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/papaparse": {
|
||||||
|
"version": "5.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.2.tgz",
|
||||||
|
"integrity": "sha512-PZXg8UuAc4PcVwLosEEDYjPyfWnTEhOrUfdv+3Bx+NuAb+5NhDmXzg5fHWmdCh1mP5p7JAZfFr3IMQfcntNAdA=="
|
||||||
|
},
|
||||||
"node_modules/parseurl": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
@@ -3430,6 +3518,20 @@
|
|||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/xmlbuilder2": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-WCSfbfZnQDdLQLiMdGUQpMxxckeQ4oZNMNhLVkcekTu7xhD4tuUDyAPoY8CwXvBYE6LwBHd6QW2WZXlOWr1vCw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@oozcitak/dom": "1.15.10",
|
||||||
|
"@oozcitak/infra": "1.0.8",
|
||||||
|
"@oozcitak/util": "8.3.8",
|
||||||
|
"js-yaml": "3.14.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xtend": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
@@ -3631,6 +3733,38 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
|
||||||
"integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="
|
"integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="
|
||||||
},
|
},
|
||||||
|
"@oozcitak/dom": {
|
||||||
|
"version": "1.15.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.10.tgz",
|
||||||
|
"integrity": "sha512-0JT29/LaxVgRcGKvHmSrUTEvZ8BXvZhGl2LASRUgHqDTC1M5g1pLmVv56IYNyt3bG2CUjDkc67wnyZC14pbQrQ==",
|
||||||
|
"requires": {
|
||||||
|
"@oozcitak/infra": "1.0.8",
|
||||||
|
"@oozcitak/url": "1.0.4",
|
||||||
|
"@oozcitak/util": "8.3.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@oozcitak/infra": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg==",
|
||||||
|
"requires": {
|
||||||
|
"@oozcitak/util": "8.3.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@oozcitak/url": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw==",
|
||||||
|
"requires": {
|
||||||
|
"@oozcitak/infra": "1.0.8",
|
||||||
|
"@oozcitak/util": "8.3.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@oozcitak/util": {
|
||||||
|
"version": "8.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz",
|
||||||
|
"integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ=="
|
||||||
|
},
|
||||||
"@pkgjs/parseargs": {
|
"@pkgjs/parseargs": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
@@ -3771,6 +3905,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"argparse": {
|
||||||
|
"version": "1.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||||
|
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||||
|
"requires": {
|
||||||
|
"sprintf-js": "~1.0.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"sprintf-js": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"array-flatten": {
|
"array-flatten": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
@@ -4317,6 +4466,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"esprima": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
|
||||||
|
},
|
||||||
"etag": {
|
"etag": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||||
@@ -4820,6 +4974,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"js-yaml": {
|
||||||
|
"version": "3.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||||
|
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||||
|
"requires": {
|
||||||
|
"argparse": "^1.0.7",
|
||||||
|
"esprima": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"jsbn": {
|
"jsbn": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||||
@@ -5265,6 +5428,11 @@
|
|||||||
"aggregate-error": "^3.0.0"
|
"aggregate-error": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"papaparse": {
|
||||||
|
"version": "5.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.2.tgz",
|
||||||
|
"integrity": "sha512-PZXg8UuAc4PcVwLosEEDYjPyfWnTEhOrUfdv+3Bx+NuAb+5NhDmXzg5fHWmdCh1mP5p7JAZfFr3IMQfcntNAdA=="
|
||||||
|
},
|
||||||
"parseurl": {
|
"parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
@@ -6038,6 +6206,17 @@
|
|||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||||
},
|
},
|
||||||
|
"xmlbuilder2": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-WCSfbfZnQDdLQLiMdGUQpMxxckeQ4oZNMNhLVkcekTu7xhD4tuUDyAPoY8CwXvBYE6LwBHd6QW2WZXlOWr1vCw==",
|
||||||
|
"requires": {
|
||||||
|
"@oozcitak/dom": "1.15.10",
|
||||||
|
"@oozcitak/infra": "1.0.8",
|
||||||
|
"@oozcitak/util": "8.3.8",
|
||||||
|
"js-yaml": "3.14.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"xtend": {
|
"xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
|||||||
@@ -18,8 +18,10 @@
|
|||||||
"express-rate-limit": "^7.4.1",
|
"express-rate-limit": "^7.4.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"papaparse": "^5.5.2",
|
||||||
"sequelize": "^6.37.5",
|
"sequelize": "^6.37.5",
|
||||||
"sequelize-cli": "^6.6.2",
|
"sequelize-cli": "^6.6.2",
|
||||||
"sqlite3": "^5.1.7"
|
"sqlite3": "^5.1.7",
|
||||||
|
"xmlbuilder2": "^3.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
54
backend/routes/cases/download.js
Normal file
54
backend/routes/cases/download.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
const Papa = require('papaparse');
|
||||||
|
const defineCase = require('../../models/cases');
|
||||||
|
|
||||||
|
module.exports = function (sequelize) {
|
||||||
|
const Case = defineCase(sequelize, DataTypes);
|
||||||
|
const { verifySignedIn } = require('../../middleware/auth')(sequelize);
|
||||||
|
const { verifyProjectVisibleFromFolderId } = require('../../middleware/verifyVisible')(sequelize);
|
||||||
|
|
||||||
|
router.get('/download', verifySignedIn, verifyProjectVisibleFromFolderId, async (req, res) => {
|
||||||
|
const { folderId, type } = req.query;
|
||||||
|
|
||||||
|
if (!folderId) {
|
||||||
|
return res.status(400).json({ error: 'folderId is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
return res.status(400).json({ error: 'download type is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cases = await Case.findAll({
|
||||||
|
where: { folderId },
|
||||||
|
raw: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cases.length === 0) {
|
||||||
|
return res.status(404).send('No cases found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'json') {
|
||||||
|
return res.json(cases);
|
||||||
|
} else if (type === 'csv') {
|
||||||
|
const csv = Papa.unparse(cases, {
|
||||||
|
quotes: true,
|
||||||
|
skipEmptyLines: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||||
|
res.setHeader('Content-Disposition', `attachment; filename=cases_folder_${folderId}.csv`);
|
||||||
|
return res.send(csv);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(400).json({ error: 'Unsupported type. Use ?type=json or ?type=csv' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
||||||
126
backend/routes/runs/download.js
Normal file
126
backend/routes/runs/download.js
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const Papa = require('papaparse');
|
||||||
|
const { create } = require('xmlbuilder2');
|
||||||
|
|
||||||
|
const defineRun = require('../../models/runs');
|
||||||
|
const defineRunCase = require('../../models/runCases');
|
||||||
|
const defineCase = require('../../models/cases');
|
||||||
|
const defineFolder = require('../../models/folders');
|
||||||
|
|
||||||
|
module.exports = function (sequelize) {
|
||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
const { verifySignedIn } = require('../../middleware/auth')(sequelize);
|
||||||
|
const { verifyProjectVisibleFromRunId } = require('../../middleware/verifyVisible')(sequelize);
|
||||||
|
|
||||||
|
const Run = defineRun(sequelize, DataTypes);
|
||||||
|
const RunCase = defineRunCase(sequelize, DataTypes);
|
||||||
|
const Case = defineCase(sequelize, DataTypes);
|
||||||
|
const Folder = defineFolder(sequelize, DataTypes);
|
||||||
|
|
||||||
|
RunCase.belongsTo(Case, { foreignKey: 'caseId' });
|
||||||
|
|
||||||
|
router.get('/download/:runId', verifySignedIn, verifyProjectVisibleFromRunId, async (req, res) => {
|
||||||
|
const { runId } = req.params;
|
||||||
|
const { type } = req.query;
|
||||||
|
|
||||||
|
if (!runId) {
|
||||||
|
return res.status(400).json({ error: 'runId is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const run = await Run.findByPk(runId);
|
||||||
|
if (!run) {
|
||||||
|
return res.status(404).send('Run not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const runCases = await RunCase.findAll({
|
||||||
|
where: { runId },
|
||||||
|
include: [{ model: Case }],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (type === 'xml') {
|
||||||
|
// JUnit xml valid status
|
||||||
|
const validStatuses = [1, 2, 4]; // 0: untested, 1: passed, 2 failed, 3: retest, 4: skipped
|
||||||
|
const filteredRunCases = runCases.filter((rc) => validStatuses.includes(rc.status));
|
||||||
|
|
||||||
|
// group cases by folder
|
||||||
|
const folderMap = new Map();
|
||||||
|
for (const rc of filteredRunCases) {
|
||||||
|
const folderId = rc.Case.folderId;
|
||||||
|
if (!folderMap.has(folderId)) {
|
||||||
|
folderMap.set(folderId, []);
|
||||||
|
}
|
||||||
|
folderMap.get(folderId).push(rc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct JUnit xml
|
||||||
|
const xml = create({ version: '1.0' });
|
||||||
|
const root = xml.ele('testsuites');
|
||||||
|
|
||||||
|
for (const [folderId, cases] of folderMap.entries()) {
|
||||||
|
let folderName = '';
|
||||||
|
const folder = await Folder.findByPk(folderId);
|
||||||
|
if (folder) {
|
||||||
|
folderName = folder.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const suite = root.ele('testsuite', {
|
||||||
|
name: folderName,
|
||||||
|
tests: cases.length,
|
||||||
|
failures: cases.filter((c) => c.status === 2).length,
|
||||||
|
skipped: cases.filter((c) => c.status === 4).length,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const rc of cases) {
|
||||||
|
const testCase = suite.ele('testcase', {
|
||||||
|
name: rc.Case.title,
|
||||||
|
classname: folderName,
|
||||||
|
time: '0',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rc.status === 2) {
|
||||||
|
testCase.ele('failure', { message: 'Test failed' }).txt('Test case failed.');
|
||||||
|
} else if (rc.status === 4) {
|
||||||
|
testCase.ele('skipped', { message: 'skipped' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const xmlString = xml.end({ prettyPrint: true });
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'application/xml');
|
||||||
|
res.setHeader('Content-Disposition', `attachment; filename=run_${runId}.xml`);
|
||||||
|
return res.send(xmlString);
|
||||||
|
} else if (type === 'json') {
|
||||||
|
return res.json(runCases);
|
||||||
|
} else if (type === 'csv') {
|
||||||
|
const records = runCases.map((rc) => ({
|
||||||
|
id: rc.Case.id,
|
||||||
|
title: rc.Case.title,
|
||||||
|
state: rc.Case.state,
|
||||||
|
priority: rc.Case.priority,
|
||||||
|
type: rc.Case.type,
|
||||||
|
automationStatus: rc.Case.automationStatus,
|
||||||
|
status: rc.status,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const csv = Papa.unparse(records, {
|
||||||
|
quotes: true,
|
||||||
|
skipEmptyLines: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/csv');
|
||||||
|
res.setHeader('Content-Disposition', `attachment; filename=run_${runId}.csv`);
|
||||||
|
return res.send(csv);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(400).json({ error: 'Unsupported type. Use ?type=xml or ?type=json or ?type=csv' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
||||||
@@ -4,22 +4,6 @@ sidebar_position: 2
|
|||||||
|
|
||||||
# Backend
|
# Backend
|
||||||
|
|
||||||
## Environmental variables
|
|
||||||
|
|
||||||
:::warning[Strongly Recommended]
|
|
||||||
|
|
||||||
Although the system will work with default settings without setting environment variables, it is strongly recommended to set SECRET_KEY in production.
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
Create `.env` File on `backend/`
|
|
||||||
|
|
||||||
```.env
|
|
||||||
FRONTEND_ORIGIN=http://localhost:8000
|
|
||||||
PORT=8001
|
|
||||||
SECRET_KEY=your-secret-key
|
|
||||||
```
|
|
||||||
|
|
||||||
## Set up database
|
## Set up database
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -4,20 +4,6 @@ sidebar_position: 3
|
|||||||
|
|
||||||
# Frontend
|
# Frontend
|
||||||
|
|
||||||
## Environmental variables
|
|
||||||
|
|
||||||
:::info
|
|
||||||
|
|
||||||
Although the system will work with default settings without setting environment variables, but you can override the settings by setting environment variables.
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
Create `.env` File on `frontend/`
|
|
||||||
|
|
||||||
```
|
|
||||||
NEXT_PUBLIC_BACKEND_ORIGIN=http://localhost:8001
|
|
||||||
```
|
|
||||||
|
|
||||||
## Start frontend server with dev mode
|
## Start frontend server with dev mode
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
31
docs/docs/getstarted/environment.md
Normal file
31
docs/docs/getstarted/environment.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 5
|
||||||
|
---
|
||||||
|
|
||||||
|
# Override settings
|
||||||
|
|
||||||
|
The system will work with default settings without setting environment variables, but you can override the settings by placing `.env` file.
|
||||||
|
|
||||||
|
## setting frontend environment variables
|
||||||
|
|
||||||
|
Create `.env` File on `frontend/`
|
||||||
|
|
||||||
|
```.env title="frontend/.env"
|
||||||
|
NEXT_PUBLIC_BACKEND_ORIGIN=http://localhost:8001
|
||||||
|
```
|
||||||
|
|
||||||
|
## setting backend environment variables
|
||||||
|
|
||||||
|
Create `.env` File on `backend/`
|
||||||
|
|
||||||
|
:::warning[Strongly Recommended]
|
||||||
|
|
||||||
|
It is strongly recommended to set SECRET_KEY in production.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
```.env title="backend/.env"
|
||||||
|
FRONTEND_ORIGIN=http://localhost:8000
|
||||||
|
PORT=8001
|
||||||
|
SECRET_KEY=your-secret-key
|
||||||
|
```
|
||||||
@@ -4,4 +4,29 @@ sidebar_position: 1
|
|||||||
|
|
||||||
# UnitTCMS
|
# UnitTCMS
|
||||||
|
|
||||||
Open Source Test Case Management System
|
Open source test case management system designed for self-hosted use
|
||||||
|
|
||||||
|
## Why UnitTCMS
|
||||||
|
|
||||||
|
No matter the product, it is difficult to manage all testing with automated tests alone. Depending on the environment, Some portion of testing often involves manual test cases. In my team, we have been managing test cases using Excel. While Excel is a highly versatile tool, it is not specifically designed for test case management, which can make it inconvenient in certain scenarios.
|
||||||
|
|
||||||
|
For example,
|
||||||
|
|
||||||
|
- writing long test procedures can lead to oversized cells
|
||||||
|
- forced horizontal scrolling
|
||||||
|
- difficult to attach image files.
|
||||||
|
- the Excel file may become heavy
|
||||||
|
- Need to aggregate test cases and visualization
|
||||||
|
- Not ideal for simultaneous editing by multiple people.
|
||||||
|
|
||||||
|
Due to these challenges, it becomes clear that a dedicated tool is necessary when managing a substantial number of manual tests.
|
||||||
|
|
||||||
|
## Other tools
|
||||||
|
|
||||||
|
There are many test case management tools available in the market, which can be categorized into proprietary and open-source solutions.
|
||||||
|
|
||||||
|
Proprietary tools often come with modern, user-friendly interfaces but tend to be cloud-based, which may raise security concerns for some organizations. While some of them do offer on-premises options, these tend to be significantly more expensive.
|
||||||
|
|
||||||
|
There are also open-source tools, but many feature older user interfaces that involve frequent full page reloads, which can hinder usability.
|
||||||
|
|
||||||
|
With these challenges in mind, I set out to develop a modern, user-friendly, open-source test case management tool that anyone can use for free in a secure, self-hosted environment.
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
---
|
|
||||||
sidebar_position: 2
|
|
||||||
---
|
|
||||||
|
|
||||||
# Why UnitTCMS
|
|
||||||
|
|
||||||
No matter the product, it is difficult to manage all testing with automated tests alone. Depending on the environment, Some portion of testing often involves manual test cases. In my team, we have been managing test cases using Excel. While Excel is a highly versatile tool, it is not specifically designed for test case management, which can make it inconvenient in certain scenarios.
|
|
||||||
|
|
||||||
For example,
|
|
||||||
|
|
||||||
- writing long test procedures can lead to oversized cells
|
|
||||||
- forced horizontal scrolling
|
|
||||||
- difficult to attach image files.
|
|
||||||
- the Excel file may become heavy
|
|
||||||
- Need to aggregate test cases and visualization
|
|
||||||
- Not ideal for simultaneous editing by multiple people.
|
|
||||||
|
|
||||||
Due to these challenges, it becomes clear that a dedicated tool is necessary when managing a substantial number of manual tests.
|
|
||||||
|
|
||||||
## Other tools
|
|
||||||
|
|
||||||
There are many test case management tools available in the market, which can be categorized into proprietary and open-source solutions.
|
|
||||||
|
|
||||||
Proprietary tools often come with modern, user-friendly interfaces but tend to be cloud-based, which may raise security concerns for some organizations. While some of them do offer on-premises options, these tend to be significantly more expensive.
|
|
||||||
|
|
||||||
There are also open-source tools, but many feature older user interfaces that involve frequent page reloads, which can hinder usability.
|
|
||||||
|
|
||||||
With these challenges in mind, I set out to develop a modern, user-friendly, open-source test case management tool that anyone can use for free in a secure, self-hosted environment.
|
|
||||||
10
frontend/components/ClientLink.tsx
Normal file
10
frontend/components/ClientLink.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
'use client';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
import { Link } from '@/src/i18n/routing';
|
||||||
|
type Props = React.ComponentPropsWithoutRef<typeof Link>;
|
||||||
|
const ClientLink = forwardRef<HTMLAnchorElement, Props>(({ href, ...props }, ref) => (
|
||||||
|
<Link ref={ref} href={href} {...props} />
|
||||||
|
));
|
||||||
|
ClientLink.displayName = 'ClientLink';
|
||||||
|
|
||||||
|
export default ClientLink;
|
||||||
@@ -188,6 +188,7 @@
|
|||||||
"close": "Close",
|
"close": "Close",
|
||||||
"are_you_sure": "Are you sure you want to delete test cases?",
|
"are_you_sure": "Are you sure you want to delete test cases?",
|
||||||
"new_test_case": "New Test Case",
|
"new_test_case": "New Test Case",
|
||||||
|
"export": "Export",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"no_cases_found": "No test cases found",
|
"no_cases_found": "No test cases found",
|
||||||
"case_title": "Test Case Title",
|
"case_title": "Test Case Title",
|
||||||
@@ -253,6 +254,7 @@
|
|||||||
"updating": "Updating...",
|
"updating": "Updating...",
|
||||||
"update": "Update",
|
"update": "Update",
|
||||||
"updated_test_run": "Updated test run",
|
"updated_test_run": "Updated test run",
|
||||||
|
"export": "Export",
|
||||||
"progress": "Progress",
|
"progress": "Progress",
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
|
|||||||
@@ -189,6 +189,7 @@
|
|||||||
"close": "閉じる",
|
"close": "閉じる",
|
||||||
"are_you_sure": "テストケースを削除してもよろしいですか?",
|
"are_you_sure": "テストケースを削除してもよろしいですか?",
|
||||||
"new_test_case": "新規テストケース",
|
"new_test_case": "新規テストケース",
|
||||||
|
"export": "エクスポート",
|
||||||
"status": "ステータス",
|
"status": "ステータス",
|
||||||
"no_cases_found": "テストケースがありません",
|
"no_cases_found": "テストケースがありません",
|
||||||
"case_title": "テストケースタイトル",
|
"case_title": "テストケースタイトル",
|
||||||
@@ -254,6 +255,7 @@
|
|||||||
"updating": "更新中...",
|
"updating": "更新中...",
|
||||||
"update": "更新",
|
"update": "更新",
|
||||||
"updated_test_run": "テストランを更新しました",
|
"updated_test_run": "テストランを更新しました",
|
||||||
|
"export": "エクスポート",
|
||||||
"progress": "進捗",
|
"progress": "進捗",
|
||||||
"refresh": "再読み込み",
|
"refresh": "再読み込み",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
|
|||||||
@@ -94,19 +94,14 @@ export default function HeaderNavbarMenu({ messages, locale }: Props) {
|
|||||||
<NavbarItem className="hidden md:block">
|
<NavbarItem className="hidden md:block">
|
||||||
<Chip size="sm" variant="flat">
|
<Chip size="sm" variant="flat">
|
||||||
<Link className="data-[active=true]:text-primary data-[active=true]:font-medium" href="/" locale={locale}>
|
<Link className="data-[active=true]:text-primary data-[active=true]:font-medium" href="/" locale={locale}>
|
||||||
1.0.0-beta.11
|
1.0.0-beta.12
|
||||||
</Link>
|
</Link>
|
||||||
</Chip>
|
</Chip>
|
||||||
</NavbarItem>
|
</NavbarItem>
|
||||||
{commonLinks.map((link) =>
|
{commonLinks.map((link) =>
|
||||||
link.isExternal ? (
|
link.isExternal ? (
|
||||||
<NavbarItem key={link.uid} className="hidden md:block">
|
<NavbarItem key={link.uid} className="hidden md:block">
|
||||||
<NextUiLink
|
<NextUiLink isExternal href={link.href} showAnchorIcon>
|
||||||
isExternal
|
|
||||||
href={link.href}
|
|
||||||
showAnchorIcon
|
|
||||||
anchorIcon={<MoveUpRight size={12} className="ms-1" />}
|
|
||||||
>
|
|
||||||
{link.label}
|
{link.label}
|
||||||
</NextUiLink>
|
</NextUiLink>
|
||||||
</NavbarItem>
|
</NavbarItem>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { title, subtitle } from '@/components/primitives';
|
|
||||||
import { Button, Link as NextUiLink } from '@heroui/react';
|
import { Button, Link as NextUiLink } from '@heroui/react';
|
||||||
import { MoveUpRight } from 'lucide-react';
|
import { MoveUpRight } from 'lucide-react';
|
||||||
import { Link } from '@/src/i18n/routing';
|
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { title, subtitle } from '@/components/primitives';
|
||||||
import { LocaleCodeType } from '@/types/locale';
|
import { LocaleCodeType } from '@/types/locale';
|
||||||
|
import ClientLink from '@/components/ClientLink';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
locale: LocaleCodeType;
|
locale: LocaleCodeType;
|
||||||
@@ -34,35 +34,37 @@ export default function MainTitle({ locale }: Props) {
|
|||||||
<h4 className={subtitle({ class: 'mt-4' })}>{t('integrate_and_manage')}</h4>
|
<h4 className={subtitle({ class: 'mt-4' })}>{t('integrate_and_manage')}</h4>
|
||||||
|
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
<Link href={`/projects/`} locale={locale}>
|
<Button as={ClientLink} href={`/projects/`} locale={locale} color="primary" radius="full" className="px-0">
|
||||||
<Button color="primary" radius="full" className="px-0">
|
{t('demo')}
|
||||||
{t('demo')}
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<NextUiLink isExternal href="https://kimatata.github.io/unittcms/docs/getstarted/selfhost" aria-label="docs">
|
<Button
|
||||||
<Button
|
showAnchorIcon
|
||||||
color="primary"
|
as={NextUiLink}
|
||||||
variant="bordered"
|
isExternal
|
||||||
radius="full"
|
href="https://kimatata.github.io/unittcms/docs/getstarted/selfhost"
|
||||||
className="ms-2"
|
aria-label="docs"
|
||||||
endContent={<MoveUpRight size={12} />}
|
color="primary"
|
||||||
>
|
variant="bordered"
|
||||||
{t('get_started')}
|
radius="full"
|
||||||
</Button>
|
className="ms-2"
|
||||||
</NextUiLink>
|
>
|
||||||
|
{t('get_started')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
<NextUiLink size="sm" isExternal href="https://github.com/kimatata/unittcms" aria-label="Github">
|
<Button
|
||||||
<Button
|
showAnchorIcon
|
||||||
color="primary"
|
as={NextUiLink}
|
||||||
variant="bordered"
|
isExternal
|
||||||
radius="full"
|
href="https://github.com/kimatata/unittcms"
|
||||||
className="ms-2"
|
aria-label="Github"
|
||||||
endContent={<MoveUpRight size={12} />}
|
color="primary"
|
||||||
>
|
variant="bordered"
|
||||||
GitHub
|
radius="full"
|
||||||
</Button>
|
className="ms-2"
|
||||||
</NextUiLink>
|
>
|
||||||
|
GitHub
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -86,11 +86,16 @@ export default function AccountPage({ messages, locale }: Props) {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<span className="text-default-500 me-2">{messages.notOwnAnyProjects}</span>
|
<span className="text-default-500 me-2">{messages.notOwnAnyProjects}</span>
|
||||||
<Link href={`/projects/`} locale={locale} className={NextUiLinkClasses}>
|
<Button
|
||||||
<Button variant="flat" size="sm" endContent={<ArrowRight size={12} />}>
|
as={Link}
|
||||||
{messages.findProjects}
|
href={`/projects/`}
|
||||||
</Button>
|
locale={locale}
|
||||||
</Link>
|
variant="flat"
|
||||||
|
size="sm"
|
||||||
|
endContent={<ArrowRight size={12} />}
|
||||||
|
>
|
||||||
|
{messages.findProjects}
|
||||||
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -97,11 +97,16 @@ export default function AuthPage({ isSignup, messages, locale }: Props) {
|
|||||||
<Card className="w-[480px] mt-16">
|
<Card className="w-[480px] mt-16">
|
||||||
<CardHeader className="px-4 pt-4 pb-0 flex justify-between">
|
<CardHeader className="px-4 pt-4 pb-0 flex justify-between">
|
||||||
<h4 className="font-bold text-large">{messages.title}</h4>
|
<h4 className="font-bold text-large">{messages.title}</h4>
|
||||||
<Link href={isSignup ? '/account/signin' : '/account/signup'} locale={locale}>
|
<Button
|
||||||
<Button color="primary" variant="light" endContent={<ChevronRight size={16} />}>
|
as={Link}
|
||||||
{messages.linkTitle}
|
href={isSignup ? '/account/signin' : '/account/signup'}
|
||||||
</Button>
|
locale={locale}
|
||||||
</Link>
|
color="primary"
|
||||||
|
variant="light"
|
||||||
|
endContent={<ChevronRight size={16} />}
|
||||||
|
>
|
||||||
|
{messages.linkTitle}
|
||||||
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody className="overflow-visible px-4 pt-0 pb-4">
|
<CardBody className="overflow-visible px-4 pt-0 pb-4">
|
||||||
<form>
|
<form>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useState, useEffect, useContext } from 'react';
|
import { useState, useEffect, useContext } from 'react';
|
||||||
import { TokenContext } from '@/utils/TokenProvider';
|
|
||||||
import TestCaseTable from './TestCaseTable';
|
import TestCaseTable from './TestCaseTable';
|
||||||
import { fetchCases, createCase, deleteCases } from '@/utils/caseControl';
|
import CaseDialog from './CaseDialog';
|
||||||
|
import { TokenContext } from '@/utils/TokenProvider';
|
||||||
|
import { fetchCases, createCase, deleteCases, exportCases } from '@/utils/caseControl';
|
||||||
import { CaseType, CasesMessages } from '@/types/case';
|
import { CaseType, CasesMessages } from '@/types/case';
|
||||||
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog';
|
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog';
|
||||||
import CaseDialog from './CaseDialog';
|
|
||||||
import { PriorityMessages } from '@/types/priority';
|
import { PriorityMessages } from '@/types/priority';
|
||||||
import { LocaleCodeType } from '@/types/locale';
|
import { LocaleCodeType } from '@/types/locale';
|
||||||
|
|
||||||
@@ -74,6 +74,10 @@ export default function CasesPane({ projectId, folderId, messages, priorityMessa
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onExportCases = async (type: string) => {
|
||||||
|
await exportCases(context.token.access_token, Number(folderId), type);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TestCaseTable
|
<TestCaseTable
|
||||||
@@ -83,6 +87,7 @@ export default function CasesPane({ projectId, folderId, messages, priorityMessa
|
|||||||
onCreateCase={() => setIsCaseDialogOpen(true)}
|
onCreateCase={() => setIsCaseDialogOpen(true)}
|
||||||
onDeleteCase={onDeleteCase}
|
onDeleteCase={onDeleteCase}
|
||||||
onDeleteCases={onDeleteCases}
|
onDeleteCases={onDeleteCases}
|
||||||
|
onExportCases={onExportCases}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
priorityMessages={priorityMessages}
|
priorityMessages={priorityMessages}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ import {
|
|||||||
DropdownItem,
|
DropdownItem,
|
||||||
Selection,
|
Selection,
|
||||||
SortDescriptor,
|
SortDescriptor,
|
||||||
|
ButtonGroup,
|
||||||
} from '@heroui/react';
|
} from '@heroui/react';
|
||||||
import { Link, NextUiLinkClasses } from '@/src/i18n/routing';
|
import { Plus, MoreVertical, Trash, Download, FileDown, ChevronDown, FileJson, FileSpreadsheet } from 'lucide-react';
|
||||||
import { Plus, MoreVertical, Trash } from 'lucide-react';
|
import { Link } from '@/src/i18n/routing';
|
||||||
import { CaseType, CasesMessages } from '@/types/case';
|
import { CaseType, CasesMessages } from '@/types/case';
|
||||||
import { PriorityMessages } from '@/types/priority';
|
import { PriorityMessages } from '@/types/priority';
|
||||||
import TestCasePriority from '@/components/TestCasePriority';
|
import TestCasePriority from '@/components/TestCasePriority';
|
||||||
@@ -28,6 +29,7 @@ type Props = {
|
|||||||
onCreateCase: () => void;
|
onCreateCase: () => void;
|
||||||
onDeleteCase: (caseId: number) => void;
|
onDeleteCase: (caseId: number) => void;
|
||||||
onDeleteCases: (caseIds: number[]) => void;
|
onDeleteCases: (caseIds: number[]) => void;
|
||||||
|
onExportCases: (type: string) => void;
|
||||||
messages: CasesMessages;
|
messages: CasesMessages;
|
||||||
priorityMessages: PriorityMessages;
|
priorityMessages: PriorityMessages;
|
||||||
locale: LocaleCodeType;
|
locale: LocaleCodeType;
|
||||||
@@ -40,6 +42,7 @@ export default function TestCaseTable({
|
|||||||
onCreateCase,
|
onCreateCase,
|
||||||
onDeleteCase,
|
onDeleteCase,
|
||||||
onDeleteCases,
|
onDeleteCases,
|
||||||
|
onExportCases,
|
||||||
messages,
|
messages,
|
||||||
priorityMessages,
|
priorityMessages,
|
||||||
locale,
|
locale,
|
||||||
@@ -56,6 +59,7 @@ export default function TestCaseTable({
|
|||||||
column: 'id',
|
column: 'id',
|
||||||
direction: 'ascending',
|
direction: 'ascending',
|
||||||
});
|
});
|
||||||
|
const [exportType, setExportType] = useState(new Set(['json']));
|
||||||
|
|
||||||
const sortedItems = useMemo(() => {
|
const sortedItems = useMemo(() => {
|
||||||
if (cases.length === 0) {
|
if (cases.length === 0) {
|
||||||
@@ -82,14 +86,15 @@ export default function TestCaseTable({
|
|||||||
return <span>{cellValue as number}</span>;
|
return <span>{cellValue as number}</span>;
|
||||||
case 'title':
|
case 'title':
|
||||||
return (
|
return (
|
||||||
<Button size="sm" variant="light" className="data-[hover=true]:bg-transparent">
|
<Button
|
||||||
<Link
|
size="sm"
|
||||||
href={`/projects/${projectId}/folders/${testCase.folderId}/cases/${testCase.id}`}
|
as={Link}
|
||||||
locale={locale}
|
href={`/projects/${projectId}/folders/${testCase.folderId}/cases/${testCase.id}`}
|
||||||
className={NextUiLinkClasses}
|
locale={locale}
|
||||||
>
|
variant="light"
|
||||||
{cellValue as string}
|
className="data-[hover=true]:bg-transparent"
|
||||||
</Link>
|
>
|
||||||
|
{cellValue as string}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
case 'priority':
|
case 'priority':
|
||||||
@@ -130,6 +135,10 @@ export default function TestCaseTable({
|
|||||||
setSelectedKeys(new Set([]));
|
setSelectedKeys(new Set([]));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleExportTypeChange = (keys: Selection) => {
|
||||||
|
setExportType(new Set(Array.from(keys as Set<string>)));
|
||||||
|
};
|
||||||
|
|
||||||
const classNames = useMemo(
|
const classNames = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
wrapper: ['max-w-3xl'],
|
wrapper: ['max-w-3xl'],
|
||||||
@@ -153,6 +162,7 @@ export default function TestCaseTable({
|
|||||||
<>
|
<>
|
||||||
<div className="border-b-1 dark:border-neutral-700 w-full p-3 flex items-center justify-between">
|
<div className="border-b-1 dark:border-neutral-700 w-full p-3 flex items-center justify-between">
|
||||||
<h3 className="font-bold">{messages.testCaseList}</h3>
|
<h3 className="font-bold">{messages.testCaseList}</h3>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{((selectedKeys !== 'all' && selectedKeys.size > 0) || selectedKeys === 'all') && (
|
{((selectedKeys !== 'all' && selectedKeys.size > 0) || selectedKeys === 'all') && (
|
||||||
<Button
|
<Button
|
||||||
@@ -166,6 +176,38 @@ export default function TestCaseTable({
|
|||||||
{messages.delete}
|
{messages.delete}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
<ButtonGroup className="me-2">
|
||||||
|
<Button
|
||||||
|
startContent={<FileDown size={16} />}
|
||||||
|
size="sm"
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
onPress={() => onExportCases(Array.from(exportType)[0])}
|
||||||
|
>
|
||||||
|
{messages.export} {exportType}
|
||||||
|
</Button>
|
||||||
|
<Dropdown placement="bottom-end">
|
||||||
|
<DropdownTrigger>
|
||||||
|
<Button isIconOnly size="sm">
|
||||||
|
<ChevronDown size={16} />
|
||||||
|
</Button>
|
||||||
|
</DropdownTrigger>
|
||||||
|
<DropdownMenu
|
||||||
|
disallowEmptySelection
|
||||||
|
aria-label="Export options"
|
||||||
|
className="max-w-[300px]"
|
||||||
|
selectedKeys={exportType}
|
||||||
|
selectionMode="single"
|
||||||
|
onSelectionChange={handleExportTypeChange}
|
||||||
|
>
|
||||||
|
<DropdownItem key="json" startContent={<FileJson size={16} />}>
|
||||||
|
json
|
||||||
|
</DropdownItem>
|
||||||
|
<DropdownItem key="csv" startContent={<FileSpreadsheet size={16} />}>
|
||||||
|
csv
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
</ButtonGroup>
|
||||||
<Button
|
<Button
|
||||||
startContent={<Plus size={16} />}
|
startContent={<Plus size={16} />}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useState, useEffect, useContext, ChangeEvent, DragEvent } from 'react';
|
import { useState, useEffect, useContext, ChangeEvent, DragEvent } from 'react';
|
||||||
import { Input, Textarea, Select, SelectItem, Button, Divider, Tooltip, addToast } from '@heroui/react';
|
import { Input, Textarea, Select, SelectItem, Button, Divider, Tooltip, addToast, Badge } from '@heroui/react';
|
||||||
import { useRouter } from '@/src/i18n/routing';
|
import { useRouter } from '@/src/i18n/routing';
|
||||||
import { Save, Plus, ArrowLeft, Circle } from 'lucide-react';
|
import { Save, Plus, ArrowLeft, Circle } from 'lucide-react';
|
||||||
import { priorities, testTypes, templates } from '@/config/selection';
|
import { priorities, testTypes, templates } from '@/config/selection';
|
||||||
@@ -54,7 +54,7 @@ export default function CaseEditor({
|
|||||||
}: Props) {
|
}: Props) {
|
||||||
const tokenContext = useContext(TokenContext);
|
const tokenContext = useContext(TokenContext);
|
||||||
const [testCase, setTestCase] = useState<CaseType>(defaultTestCase);
|
const [testCase, setTestCase] = useState<CaseType>(defaultTestCase);
|
||||||
const [isTitleInvalid, setIsTitleInvalid] = useState<boolean>(false);
|
const [isTitleInvalid] = useState<boolean>(false);
|
||||||
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
||||||
const [plusCount, setPlusCount] = useState<number>(0);
|
const [plusCount, setPlusCount] = useState<number>(0);
|
||||||
const [isDirty, setIsDirty] = useState(false);
|
const [isDirty, setIsDirty] = useState(false);
|
||||||
@@ -245,9 +245,12 @@ export default function CaseEditor({
|
|||||||
<h3 className="font-bold ms-2">{testCase.title}</h3>
|
<h3 className="font-bold ms-2">{testCase.title}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{isDirty && <Circle size={8} color="#525252" fill="#525252" className="me-1" />}
|
|
||||||
<Button
|
<Button
|
||||||
startContent={<Save size={16} />}
|
startContent={
|
||||||
|
<Badge isInvisible={!isDirty} color="danger" size="sm" content="" shape="circle">
|
||||||
|
<Save size={16} />
|
||||||
|
</Badge>
|
||||||
|
}
|
||||||
size="sm"
|
size="sm"
|
||||||
isDisabled={!tokenContext.isProjectDeveloper(Number(projectId))}
|
isDisabled={!tokenContext.isProjectDeveloper(Number(projectId))}
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { PriorityMessages } from '@/types/priority';
|
|
||||||
import CasesPane from './CasesPane';
|
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import CasesPane from './CasesPane';
|
||||||
|
import { PriorityMessages } from '@/types/priority';
|
||||||
import { LocaleCodeType } from '@/types/locale';
|
import { LocaleCodeType } from '@/types/locale';
|
||||||
|
|
||||||
export async function generateMetadata({ params: { locale } }: { params: { locale: LocaleCodeType } }) {
|
export async function generateMetadata({ params: { locale } }: { params: { locale: LocaleCodeType } }) {
|
||||||
@@ -25,6 +25,7 @@ export default function Page({ params }: { params: { projectId: string; folderId
|
|||||||
close: t('close'),
|
close: t('close'),
|
||||||
areYouSure: t('are_you_sure'),
|
areYouSure: t('are_you_sure'),
|
||||||
newTestCase: t('new_test_case'),
|
newTestCase: t('new_test_case'),
|
||||||
|
export: t('export'),
|
||||||
status: t('status'),
|
status: t('status'),
|
||||||
noCasesFound: t('no_cases_found'),
|
noCasesFound: t('no_cases_found'),
|
||||||
caseTitle: t('case_title'),
|
caseTitle: t('case_title'),
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { useEffect, useState, useContext } from 'react';
|
|||||||
import { Button } from '@heroui/react';
|
import { Button } from '@heroui/react';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import RunsTable from './RunsTable';
|
import RunsTable from './RunsTable';
|
||||||
|
import RunDialog from './RunDialog';
|
||||||
import { fetchRuns, createRun, updateRun, deleteRun } from './runsControl';
|
import { fetchRuns, createRun, updateRun, deleteRun } from './runsControl';
|
||||||
import { RunType, RunsMessages } from '@/types/run';
|
import { RunType, RunsMessages } from '@/types/run';
|
||||||
import RunDialog from './RunDialog';
|
|
||||||
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog';
|
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog';
|
||||||
import { TokenContext } from '@/utils/TokenProvider';
|
import { TokenContext } from '@/utils/TokenProvider';
|
||||||
import { LocaleCodeType } from '@/types/locale';
|
import { LocaleCodeType } from '@/types/locale';
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React from 'react';
|
|
||||||
import { useState, useEffect, useContext } from 'react';
|
import { useState, useEffect, useContext } from 'react';
|
||||||
import { useRouter } from '@/src/i18n/routing';
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Input,
|
Input,
|
||||||
@@ -18,14 +16,23 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
addToast,
|
addToast,
|
||||||
|
ButtonGroup,
|
||||||
|
Badge,
|
||||||
} from '@heroui/react';
|
} from '@heroui/react';
|
||||||
import { Save, Circle, ArrowLeft, Folder, ChevronDown, CopyPlus, CopyMinus, RotateCw } from 'lucide-react';
|
import {
|
||||||
import RunProgressChart from './RunPregressDonutChart';
|
Save,
|
||||||
import TestCaseSelector from './TestCaseSelector';
|
ArrowLeft,
|
||||||
import { testRunStatus } from '@/config/selection';
|
Folder,
|
||||||
import { RunType, RunStatusCountType, RunMessages } from '@/types/run';
|
ChevronDown,
|
||||||
import { CaseType } from '@/types/case';
|
CopyPlus,
|
||||||
import { FolderType } from '@/types/folder';
|
CopyMinus,
|
||||||
|
RotateCw,
|
||||||
|
FileDown,
|
||||||
|
FileSpreadsheet,
|
||||||
|
FileCode,
|
||||||
|
FileJson,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useTheme } from 'next-themes';
|
||||||
import {
|
import {
|
||||||
fetchRun,
|
fetchRun,
|
||||||
updateRun,
|
updateRun,
|
||||||
@@ -33,10 +40,17 @@ import {
|
|||||||
fetchProjectCases,
|
fetchProjectCases,
|
||||||
includeExcludeTestCases,
|
includeExcludeTestCases,
|
||||||
changeStatus,
|
changeStatus,
|
||||||
|
exportRun,
|
||||||
} from '../runsControl';
|
} from '../runsControl';
|
||||||
import { fetchFolders } from '../../folders/foldersControl';
|
import { fetchFolders } from '../../folders/foldersControl';
|
||||||
|
import RunProgressChart from './RunPregressDonutChart';
|
||||||
|
import TestCaseSelector from './TestCaseSelector';
|
||||||
|
import { useRouter } from '@/src/i18n/routing';
|
||||||
|
import { testRunStatus } from '@/config/selection';
|
||||||
|
import { RunType, RunStatusCountType, RunMessages } from '@/types/run';
|
||||||
|
import { CaseType } from '@/types/case';
|
||||||
|
import { FolderType } from '@/types/folder';
|
||||||
import { TokenContext } from '@/utils/TokenProvider';
|
import { TokenContext } from '@/utils/TokenProvider';
|
||||||
import { useTheme } from 'next-themes';
|
|
||||||
import { useFormGuard } from '@/utils/formGuard';
|
import { useFormGuard } from '@/utils/formGuard';
|
||||||
import { PriorityMessages } from '@/types/priority';
|
import { PriorityMessages } from '@/types/priority';
|
||||||
import { RunStatusMessages, TestRunCaseStatusMessages } from '@/types/status';
|
import { RunStatusMessages, TestRunCaseStatusMessages } from '@/types/status';
|
||||||
@@ -75,7 +89,7 @@ export default function RunEditor({
|
|||||||
locale,
|
locale,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const tokenContext = useContext(TokenContext);
|
const tokenContext = useContext(TokenContext);
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const [testRun, setTestRun] = useState<RunType>(defaultTestRun);
|
const [testRun, setTestRun] = useState<RunType>(defaultTestRun);
|
||||||
const [folders, setFolders] = useState<FolderType[]>([]);
|
const [folders, setFolders] = useState<FolderType[]>([]);
|
||||||
const [runStatusCounts, setRunStatusCounts] = useState<RunStatusCountType[]>([]);
|
const [runStatusCounts, setRunStatusCounts] = useState<RunStatusCountType[]>([]);
|
||||||
@@ -83,9 +97,10 @@ export default function RunEditor({
|
|||||||
const [selectedFolder, setSelectedFolder] = useState<FolderType | null>(null);
|
const [selectedFolder, setSelectedFolder] = useState<FolderType | null>(null);
|
||||||
const [testCases, setTestCases] = useState<CaseType[]>([]);
|
const [testCases, setTestCases] = useState<CaseType[]>([]);
|
||||||
const [filteredTestCases, setFilteredTestCases] = useState<CaseType[]>([]);
|
const [filteredTestCases, setFilteredTestCases] = useState<CaseType[]>([]);
|
||||||
const [isNameInvalid, setIsNameInvalid] = useState<boolean>(false);
|
const [isNameInvalid] = useState<boolean>(false);
|
||||||
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
||||||
const [isDirty, setIsDirty] = useState(false);
|
const [isDirty, setIsDirty] = useState(false);
|
||||||
|
const [exportType, setExportType] = useState(new Set(['xml']));
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
useFormGuard(isDirty, messages.areYouSureLeave);
|
useFormGuard(isDirty, messages.areYouSureLeave);
|
||||||
|
|
||||||
@@ -184,6 +199,10 @@ export default function RunEditor({
|
|||||||
const baseClass = '';
|
const baseClass = '';
|
||||||
const selectedClass = `${baseClass} bg-neutral-200 dark:bg-neutral-700`;
|
const selectedClass = `${baseClass} bg-neutral-200 dark:bg-neutral-700`;
|
||||||
|
|
||||||
|
const handleExportTypeChange = (keys: Selection) => {
|
||||||
|
setExportType(new Set(Array.from(keys as Set<string>)));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="border-b-1 dark:border-neutral-700 w-full p-3 flex items-center justify-between">
|
<div className="border-b-1 dark:border-neutral-700 w-full p-3 flex items-center justify-between">
|
||||||
@@ -201,9 +220,47 @@ export default function RunEditor({
|
|||||||
<h3 className="font-bold ms-2">{testRun.name}</h3>
|
<h3 className="font-bold ms-2">{testRun.name}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{isDirty && <Circle size={8} color="#525252" fill="#525252" className="me-1" />}
|
<ButtonGroup className="me-2">
|
||||||
|
<Button
|
||||||
|
startContent={<FileDown size={16} />}
|
||||||
|
size="sm"
|
||||||
|
isDisabled={!tokenContext.isProjectReporter(Number(projectId))}
|
||||||
|
onPress={() => exportRun(tokenContext.token.access_token, Number(testRun.id), Array.from(exportType)[0])}
|
||||||
|
>
|
||||||
|
{messages.export} {exportType}
|
||||||
|
</Button>
|
||||||
|
<Dropdown placement="bottom-end">
|
||||||
|
<DropdownTrigger>
|
||||||
|
<Button isIconOnly size="sm">
|
||||||
|
<ChevronDown size={16} />
|
||||||
|
</Button>
|
||||||
|
</DropdownTrigger>
|
||||||
|
<DropdownMenu
|
||||||
|
disallowEmptySelection
|
||||||
|
aria-label="Export options"
|
||||||
|
className="max-w-[300px]"
|
||||||
|
selectedKeys={exportType}
|
||||||
|
selectionMode="single"
|
||||||
|
onSelectionChange={handleExportTypeChange}
|
||||||
|
>
|
||||||
|
<DropdownItem key="xml" startContent={<FileCode size={16} />}>
|
||||||
|
xml
|
||||||
|
</DropdownItem>
|
||||||
|
<DropdownItem key="json" startContent={<FileJson size={16} />}>
|
||||||
|
json
|
||||||
|
</DropdownItem>
|
||||||
|
<DropdownItem key="csv" startContent={<FileSpreadsheet size={16} />}>
|
||||||
|
csv
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
</ButtonGroup>
|
||||||
<Button
|
<Button
|
||||||
startContent={<Save size={16} />}
|
startContent={
|
||||||
|
<Badge isInvisible={!isDirty} color="danger" size="sm" content="" shape="circle">
|
||||||
|
<Save size={16} />
|
||||||
|
</Badge>
|
||||||
|
}
|
||||||
size="sm"
|
size="sm"
|
||||||
isDisabled={!tokenContext.isProjectReporter(Number(projectId))}
|
isDisabled={!tokenContext.isProjectReporter(Number(projectId))}
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export default function Page({ params }: { params: { projectId: string; runId: s
|
|||||||
updating: t('updating'),
|
updating: t('updating'),
|
||||||
update: t('update'),
|
update: t('update'),
|
||||||
updatedTestRun: t('updated_test_run'),
|
updatedTestRun: t('updated_test_run'),
|
||||||
|
export: t('export'),
|
||||||
progress: t('progress'),
|
progress: t('progress'),
|
||||||
refresh: t('refresh'),
|
refresh: t('refresh'),
|
||||||
id: t('id'),
|
id: t('id'),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { LocaleCodeType } from '@/types/locale';
|
|
||||||
import RunsPage from './RunsPage';
|
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import RunsPage from './RunsPage';
|
||||||
|
import { LocaleCodeType } from '@/types/locale';
|
||||||
|
|
||||||
export async function generateMetadata({ params: { locale } }: { params: { locale: LocaleCodeType } }) {
|
export async function generateMetadata({ params: { locale } }: { params: { locale: LocaleCodeType } }) {
|
||||||
const t = await getTranslations({ locale, namespace: 'Runs' });
|
const t = await getTranslations({ locale, namespace: 'Runs' });
|
||||||
|
|||||||
@@ -129,6 +129,39 @@ async function deleteRun(jwt: string, runId: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function exportRun(jwt: string, runId: number, type: string) {
|
||||||
|
if (type !== 'xml' && type !== 'json' && type !== 'csv') {
|
||||||
|
console.error('export type error. type:', type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = `${apiServer}/runs/download/${runId}?type=${type}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwt}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
const objectUrl = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = objectUrl;
|
||||||
|
a.download = `run_${runId}.${type}`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
window.URL.revokeObjectURL(objectUrl);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching data:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchRunCases(jwt: string, runId: number) {
|
async function fetchRunCases(jwt: string, runId: number) {
|
||||||
const url = `${apiServer}/runcases?runId=${runId}`;
|
const url = `${apiServer}/runcases?runId=${runId}`;
|
||||||
|
|
||||||
@@ -310,6 +343,7 @@ export {
|
|||||||
createRun,
|
createRun,
|
||||||
updateRun,
|
updateRun,
|
||||||
deleteRun,
|
deleteRun,
|
||||||
|
exportRun,
|
||||||
fetchRunCases,
|
fetchRunCases,
|
||||||
changeStatus,
|
changeStatus,
|
||||||
includeExcludeTestCases,
|
includeExcludeTestCases,
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ type CasesMessages = {
|
|||||||
areYouSure: string;
|
areYouSure: string;
|
||||||
delete: string;
|
delete: string;
|
||||||
newTestCase: string;
|
newTestCase: string;
|
||||||
|
export: string;
|
||||||
status: string;
|
status: string;
|
||||||
noCasesFound: string;
|
noCasesFound: string;
|
||||||
caseTitle: string;
|
caseTitle: string;
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ type RunMessages = {
|
|||||||
updating: string;
|
updating: string;
|
||||||
update: string;
|
update: string;
|
||||||
updatedTestRun: string;
|
updatedTestRun: string;
|
||||||
|
export: string;
|
||||||
progress: string;
|
progress: string;
|
||||||
refresh: string;
|
refresh: string;
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -132,4 +132,37 @@ async function deleteCases(jwt: string, deleteCaseIds: number[], projectId: numb
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { fetchCase, fetchCases, updateCase, createCase, deleteCases };
|
async function exportCases(jwt: string, folderId: number, type: string) {
|
||||||
|
if (type !== 'json' && type !== 'csv') {
|
||||||
|
console.error('export type error. type:', type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = `${apiServer}/cases/download?folderId=${folderId}&type=${type}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwt}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
const objectUrl = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = objectUrl;
|
||||||
|
a.download = `folder_${folderId}.${type}`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
window.URL.revokeObjectURL(objectUrl);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching data:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { fetchCase, fetchCases, updateCase, createCase, deleteCases, exportCases };
|
||||||
|
|||||||
Reference in New Issue
Block a user