@testcollab/cli 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -85,6 +85,20 @@ tc report \
85
85
  | `--result-file <path>` | Yes | Path to the result file |
86
86
  | `--api-key <key>` | No | TestCollab API key (or set `TESTCOLLAB_TOKEN` env var) |
87
87
  | `--api-url <url>` | No | API base URL override (default: `https://api.testcollab.io`). Use `https://api-eu.testcollab.io` for EU region. |
88
+ | `--skip-missing` | No | Mark test cases in the test plan but not in the result file as **skipped** |
89
+
90
+ #### `--skip-missing`
91
+
92
+ By default, test cases in the test plan that don't appear in the result file are left untouched. When `--skip-missing` is passed, these unmatched cases are automatically marked as **skipped**. This is useful when your result file only contains the tests that actually ran, and you want the full test plan status to reflect that anything not executed was skipped.
93
+
94
+ ```bash
95
+ tc report \
96
+ --project 123 \
97
+ --test-plan-id 555 \
98
+ --format junit \
99
+ --result-file ./results.xml \
100
+ --skip-missing
101
+ ```
88
102
 
89
103
  #### Mapping test cases
90
104
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testcollab/cli",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "Command-line interface for TestCollab operations",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -775,6 +775,18 @@ class TcApiClient {
775
775
  return false;
776
776
  }
777
777
  }
778
+
779
+ async bulkSkipTestCases(data) {
780
+ try {
781
+ const result = await this.request('/testplantestcases/bulkAction', {
782
+ method: 'POST',
783
+ body: data
784
+ });
785
+ return result;
786
+ } catch {
787
+ return null;
788
+ }
789
+ }
778
790
  }
779
791
 
780
792
  function findMatchingExecutedCase(casesAssigned, runRecord, hasConfig, configId) {
@@ -834,7 +846,8 @@ async function uploadUsingReporterFlow({
834
846
  apiUrl,
835
847
  hasConfig,
836
848
  resultsToUpload,
837
- unresolvedIds
849
+ unresolvedIds,
850
+ skipMissing = false
838
851
  }) {
839
852
  const tcApiInstance = new TcApiClient({
840
853
  accessToken: apiKey,
@@ -883,9 +896,11 @@ async function uploadUsingReporterFlow({
883
896
 
884
897
  const unmatchedCaseIds = new Set();
885
898
  const unmatchedConfigIds = new Set();
899
+ const matchedExecCaseIds = new Set();
886
900
  let matched = 0;
887
901
  let updated = 0;
888
902
  let errors = 0;
903
+ let skippedMissing = 0;
889
904
 
890
905
  const configIds = Object.keys(resultsToUpload);
891
906
 
@@ -916,6 +931,7 @@ async function uploadUsingReporterFlow({
916
931
  }
917
932
 
918
933
  matched += 1;
934
+ matchedExecCaseIds.add(execCase.id);
919
935
 
920
936
  const updatePayload = buildUpdatePayload({
921
937
  execCase,
@@ -955,10 +971,67 @@ async function uploadUsingReporterFlow({
955
971
  }
956
972
  }
957
973
 
974
+ // --skip-missing: mark all assigned cases not present in the result file as skipped
975
+ if (skipMissing) {
976
+ const missingCases = casesAssigned.filter(
977
+ (assignedCase) => assignedCase && assignedCase.id && !matchedExecCaseIds.has(assignedCase.id)
978
+ );
979
+
980
+ if (missingCases.length) {
981
+ console.log(`\n⏭️ --skip-missing: marking ${missingCases.length} unmatched test case(s) as skipped...`);
982
+
983
+ // Group missing cases by config ID — the bulkAction endpoint requires
984
+ // test_plan_config when configs exist, so we need one call per config
985
+ const missingByConfig = {};
986
+ for (const c of missingCases) {
987
+ const tptc = c.test_plan_test_case;
988
+ const testCaseId = tptc && typeof tptc === 'object' ? tptc.test_case : null;
989
+ if (testCaseId === null || testCaseId === undefined) {
990
+ continue;
991
+ }
992
+ const configId = c.test_plan_config && typeof c.test_plan_config === 'object'
993
+ ? String(c.test_plan_config.id)
994
+ : c.test_plan_config ? String(c.test_plan_config) : '0';
995
+ if (!missingByConfig[configId]) {
996
+ missingByConfig[configId] = [];
997
+ }
998
+ missingByConfig[configId].push(testCaseId);
999
+ }
1000
+
1001
+ for (const [configId, testCaseIds] of Object.entries(missingByConfig)) {
1002
+ if (!testCaseIds.length) {
1003
+ continue;
1004
+ }
1005
+
1006
+ const bulkPayload = {
1007
+ actionType: 'skip',
1008
+ testcases: testCaseIds,
1009
+ project: projectId,
1010
+ testplan: testPlanId
1011
+ };
1012
+
1013
+ if (configId && configId !== '0') {
1014
+ bulkPayload.test_plan_config = Number(configId);
1015
+ }
1016
+
1017
+ const bulkResult = await tcApiInstance.bulkSkipTestCases(bulkPayload);
1018
+
1019
+ if (bulkResult && bulkResult.status === true) {
1020
+ skippedMissing += bulkResult.affected || testCaseIds.length;
1021
+ } else {
1022
+ const errMsg = (bulkResult && bulkResult.message) || 'Unknown error';
1023
+ console.warn(`⚠️ Bulk skip failed for config ${configId}: ${errMsg}`);
1024
+ errors += testCaseIds.length;
1025
+ }
1026
+ }
1027
+ }
1028
+ }
1029
+
958
1030
  return {
959
1031
  matched,
960
1032
  updated,
961
1033
  errors,
1034
+ skippedMissing,
962
1035
  unresolvedIds: unique(unresolvedIds || []),
963
1036
  unmatchedCaseIds: unique([...unmatchedCaseIds]),
964
1037
  unmatchedConfigIds: unique([...unmatchedConfigIds])
@@ -1001,6 +1074,9 @@ function validateRequiredOptions({ apiKey, project, testPlanId }) {
1001
1074
  function logUploadSummary(formatLabel, summary) {
1002
1075
  console.log(`✅ ${formatLabel} report processed (${summary.matched || 0} matched, ${summary.updated || 0} updated)`);
1003
1076
 
1077
+ if (summary.skippedMissing) {
1078
+ console.log(`⏭️ ${summary.skippedMissing} test case(s) not in result file marked as skipped`);
1079
+ }
1004
1080
  if (summary.unresolvedIds?.length) {
1005
1081
  console.warn(`⚠️ ${summary.unresolvedIds.length} testcase(s) missing TestCollab ID`);
1006
1082
  }
@@ -1029,7 +1105,8 @@ export async function report(options) {
1029
1105
  testPlanId,
1030
1106
  format,
1031
1107
  resultFile,
1032
- apiUrl
1108
+ apiUrl,
1109
+ skipMissing
1033
1110
  } = options;
1034
1111
 
1035
1112
  // Resolve API key: --api-key flag takes precedence, then TESTCOLLAB_TOKEN env var
@@ -1076,7 +1153,8 @@ export async function report(options) {
1076
1153
  apiUrl,
1077
1154
  hasConfig: parsedReport.hasConfig,
1078
1155
  resultsToUpload: parsedReport.resultsToUpload,
1079
- unresolvedIds: parsedReport.unresolvedIds
1156
+ unresolvedIds: parsedReport.unresolvedIds,
1157
+ skipMissing: Boolean(skipMissing)
1080
1158
  });
1081
1159
 
1082
1160
  logUploadSummary('JUnit', summary);
@@ -1105,7 +1183,8 @@ export async function report(options) {
1105
1183
  apiUrl,
1106
1184
  hasConfig: parsedReport.hasConfig,
1107
1185
  resultsToUpload: parsedReport.resultsToUpload,
1108
- unresolvedIds: parsedReport.unresolvedIds
1186
+ unresolvedIds: parsedReport.unresolvedIds,
1187
+ skipMissing: Boolean(skipMissing)
1109
1188
  });
1110
1189
 
1111
1190
  logUploadSummary('Mochawesome', summary);
package/src/index.js CHANGED
@@ -51,6 +51,7 @@ program
51
51
  .requiredOption('--format <type>', 'Result format: mochawesome or junit')
52
52
  .requiredOption('--result-file <path>', 'Path to test result file')
53
53
  .option('--api-url <url>', 'TestCollab API base URL override', 'https://api.testcollab.io')
54
+ .option('--skip-missing', 'Mark test cases in the test plan but not in the result file as skipped', false)
54
55
  .action(report);
55
56
 
56
57
  // Add specgen command