Skip to content

Commit a6c8ed5

Browse files
Merge pull request presidio-oss#390 from hayagreevan-presidio/feat-export-test-case
feat: export testcases as xlsx
2 parents 91dbb14 + a0b00af commit a6c8ed5

File tree

8 files changed

+346
-32
lines changed

8 files changed

+346
-32
lines changed

.changeset/dark-bars-clap.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"specif-ai": patch
3+
---
4+
5+
Export feature for Testcases

ui/src/app/constants/export.constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const SPREADSHEET_HEADER_ROW = {
1111
[REQUIREMENT_TYPE.NFR]: ['Id', 'Title', 'Description'],
1212
[REQUIREMENT_TYPE.UIR]: ['Id', 'Title', 'Description'],
1313
[REQUIREMENT_TYPE.BP]: ['Id', 'Title', 'Description'],
14-
[REQUIREMENT_TYPE.TC]: ['Id', 'Title', 'Description'], // FIXME: Update with actual fields
14+
[REQUIREMENT_TYPE.TC]: ['Id','User Story Id', 'PRD ID' ,'Title', 'Description', 'PreConditions', 'PostConditions','Priority','Type', 'Steps'],
1515
[REQUIREMENT_TYPE.SI]: ['Id', 'Title', 'Description'], // FIXME: Update with actual fields
1616
[REQUIREMENT_TYPE.US]: ['Id', 'Parent Id', 'Name', 'Description'],
1717
[REQUIREMENT_TYPE.TASK]: ['Id', 'Parent Id', 'Title', 'Acceptance Criteria'],

ui/src/app/pages/test-cases/test-case-home/test-case-home.component.html

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,26 @@
22
class="bg-white border rounded-lg p-5 flex flex-col h-full lg:col-span-2 w-full"
33
>
44
<div class="mb-4">
5-
<div class="flex items-center mt-2 justify-between">
6-
<div class="flex flex-col gap-2 min-w-0">
7-
<h1
8-
class="text-lg font-bold text-secondary-800 truncate max-w-full pr-8 flex items-center"
9-
>
10-
Test Cases
11-
<ng-icon
12-
name="heroInformationCircle"
13-
(click)="electronService.openExternalUrl(docUrl)"
14-
class="cursor-pointer ml-1 hover:text-secondary-500"
15-
></ng-icon>
16-
</h1>
5+
<div class="flex items-center mt-2 w-full">
6+
<div class="flex flex-col gap-2 min-w-0 w-full">
7+
<div class="flex items-center justify-between w-full">
8+
<div class="flex items-center flex-grow">
9+
<h1 class="text-lg font-bold text-secondary-800 truncate max-w-full pr-8">
10+
Test Cases
11+
<ng-icon
12+
name="heroInformationCircle"
13+
(click)="electronService.openExternalUrl(docUrl)"
14+
class="cursor-pointer ml-1 hover:text-secondary-500"
15+
></ng-icon>
16+
</h1>
17+
</div>
18+
<div class="flex-shrink-0">
19+
<app-export-dropdown
20+
[disabled]="getTotalTestCases() === 0"
21+
[groupedOptions]="getExportOptions()"
22+
/>
23+
</div>
24+
</div>
1725

1826
<div class="flex items-center">
1927
<h2 class="text-md font-semibold text-secondary-600">User Stories</h2>

ui/src/app/pages/test-cases/test-case-home/test-case-home.component.ts

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ import { SummaryCardComponent } from "../../../components/summary-card/summary-c
2828
import { FormsModule } from '@angular/forms';
2929
import { joinPaths } from 'src/app/utils/path.utils';
3030
import { ButtonComponent } from "../../../components/core/button/button.component";
31+
import { EXPORT_FILE_FORMATS, ExportFileFormat } from 'src/app/constants/export.constants';
32+
import { DropdownOptionGroup, ExportDropdownComponent } from 'src/app/export-dropdown/export-dropdown.component';
33+
import { TestCaseExportStrategy } from '../../../services/export/strategies/test-case-export.strategy';
3134

3235
interface IPrdInfo {
3336
id: string;
@@ -61,7 +64,8 @@ interface SummaryCardData {
6164
AppSelectComponent,
6265
FormsModule,
6366
ButtonComponent,
64-
NgIcon
67+
NgIcon,
68+
ExportDropdownComponent
6569
],
6670
providers: [
6771
provideIcons({
@@ -130,6 +134,7 @@ export class TestCaseHomeComponent implements OnInit, OnDestroy {
130134
private toast: ToasterService,
131135
private appSystemService: AppSystemService,
132136
private route: ActivatedRoute,
137+
private testCaseExportStrategy: TestCaseExportStrategy,
133138
) {
134139
}
135140

@@ -423,4 +428,107 @@ export class TestCaseHomeComponent implements OnInit, OnDestroy {
423428
this.destroy$.next();
424429
this.destroy$.complete();
425430
}
431+
432+
exportOptions: DropdownOptionGroup[] = [
433+
{
434+
groupName: 'Export',
435+
options: [
436+
{
437+
label: 'Download',
438+
callback: () => {
439+
this.exportTestCases(EXPORT_FILE_FORMATS.EXCEL);
440+
},
441+
icon: 'heroDocumentText',
442+
additionalInfo: 'Excel (.xlsx)',
443+
isTimestamp: false,
444+
},
445+
],
446+
},
447+
];
448+
449+
getExportOptions(): DropdownOptionGroup[] {
450+
return this.exportOptions;
451+
}
452+
async exportTestCases(format: string) {
453+
if (this.userStories.length === 0) {
454+
this.toast.showError('No user stories found');
455+
return;
456+
}
457+
458+
try {
459+
const allTestCaseContents: any[] = [];
460+
const skippedUserStories: string[] = [];
461+
462+
for (const userStory of this.userStories) {
463+
try {
464+
const testCasePath = joinPaths(this.currentProject, REQUIREMENT_TYPE.TC, userStory.id);
465+
let files: string[] = [];
466+
467+
if(await this.appSystemService.fileExists(testCasePath)){
468+
files = await this.appSystemService.getFolders(testCasePath, FILTER_STRINGS.BASE, false);
469+
}
470+
else{
471+
skippedUserStories.push(userStory.id);
472+
continue;
473+
}
474+
475+
if (files && files.length > 0) {
476+
const testCaseContents = await Promise.all(
477+
files.map(async (fileName: string) => {
478+
const fullPath = joinPaths(this.currentProject, REQUIREMENT_TYPE.TC, userStory.id, fileName);
479+
480+
try {
481+
const content = await this.appSystemService.readFile(fullPath);
482+
const parsedContent = JSON.parse(content);
483+
484+
parsedContent.us_id = userStory.id;
485+
parsedContent.us_name = userStory.name;
486+
parsedContent.prd_id = userStory.prdId || '';
487+
488+
return {
489+
folderName: REQUIREMENT_TYPE.TC,
490+
fileName: fullPath,
491+
content: parsedContent
492+
};
493+
} catch (fileError) {
494+
this.logger.warn(`Error reading test case file: ${fullPath}`, fileError);
495+
return null;
496+
}
497+
})
498+
);
499+
500+
const validTestCaseContents = testCaseContents.filter(tc => tc !== null);
501+
allTestCaseContents.push(...validTestCaseContents);
502+
}
503+
} catch (userStoryError) {
504+
this.logger.warn(`Error processing user story: ${userStory.id}`, userStoryError);
505+
skippedUserStories.push(userStory.id);
506+
}
507+
}
508+
509+
if (allTestCaseContents.length === 0) {
510+
this.toast.showError('No test cases found to export');
511+
return;
512+
}
513+
514+
if (skippedUserStories.length > 0) {
515+
this.toast.showWarning(`No test cases found for user stories: ${skippedUserStories.join(', ')}`);
516+
}
517+
518+
const exportResult = await this.testCaseExportStrategy.export(
519+
allTestCaseContents,
520+
{
521+
format: format as ExportFileFormat,
522+
projectName: this.currentProject
523+
}
524+
);
525+
526+
if (!exportResult.success) {
527+
this.toast.showError('Failed to export test cases');
528+
}
529+
} catch (error) {
530+
this.logger.error('Error exporting test cases:', error);
531+
this.toast.showError('Failed to export test cases');
532+
}
533+
}
426534
}

ui/src/app/pages/test-cases/test-case-list/test-case-list.component.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ <h2 class="text-md font-semibold text-secondary-600">Test Cases</h2>
4747
/>
4848
</div>
4949
<!-- TODO export feature -->
50-
<!-- <app-export-dropdown
50+
<app-export-dropdown
5151
[disabled]="testCases.length === 0"
52-
[options]="exportOptions"
53-
/> -->
52+
[groupedOptions]="getExportOptions()"
53+
/>
5454
</div>
5555
</div>
5656

ui/src/app/pages/test-cases/test-case-list/test-case-list.component.ts

Lines changed: 75 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { Router, ActivatedRoute } from '@angular/router';
1111
import { NGXLogger } from 'ngx-logger';
1212
import { Store } from '@ngxs/store';
1313
import { ProjectsState } from '../../../store/projects/projects.state';
14-
import { ArchiveFile, ReadFile } from '../../../store/projects/projects.actions';
14+
import { ArchiveFile, ExportRequirementData, ReadFile, BulkReadFiles, ClearBRDPRDState } from '../../../store/projects/projects.actions';
1515
import { UserStoriesState } from '../../../store/user-stories/user-stories.state';
1616
import { SetSelectedUserStory } from '../../../store/user-stories/user-stories.actions';
1717
import { RequirementIdService } from '../../../services/requirement-id.service';
@@ -30,13 +30,15 @@ import {
3030
FILTER_STRINGS,
3131
REQUIREMENT_TYPE,
3232
TOASTER_MESSAGES,
33-
SPECIFAI_REQ_DOCS
33+
SPECIFAI_REQ_DOCS,
34+
FOLDER_REQUIREMENT_TYPE_MAP,
35+
FOLDER
3436
} from '../../../constants/app.constants';
3537
import { SearchInputComponent } from '../../../components/core/search-input/search-input.component';
3638
import { BehaviorSubject, combineLatest, firstValueFrom, map, Subject, take, takeUntil } from 'rxjs';
3739
import { AppSelectComponent, SelectOption } from '../../../components/core/app-select/app-select.component';
3840
import { TestCaseContextModalComponent } from 'src/app/components/test-case-context-modal/test-case-context-modal.component';
39-
import { ExportDropdownComponent } from 'src/app/export-dropdown/export-dropdown.component';
41+
import { DropdownOptionGroup, ExportDropdownComponent } from 'src/app/export-dropdown/export-dropdown.component';
4042
import {
4143
WorkflowType,
4244
WorkflowProgressEvent,
@@ -60,6 +62,7 @@ import { WorkflowProgressService } from 'src/app/services/workflow-progress/work
6062
import { heroArrowRight } from '@ng-icons/heroicons/outline';
6163
import { NgIcon, provideIcons } from '@ng-icons/core';
6264
import { RequirementTypeEnum } from 'src/app/model/enum/requirement-type.enum';
65+
import { EXPORT_FILE_FORMATS, ExportFileFormat } from 'src/app/constants/export.constants';
6366

6467
@Component({
6568
selector: 'app-test-case-list',
@@ -936,20 +939,77 @@ export class TestCaseListComponent implements OnInit, OnDestroy {
936939
}
937940
});
938941
}
942+
943+
exportOptions: DropdownOptionGroup[] = [
944+
{
945+
groupName: 'Export',
946+
options: [
947+
{
948+
label: 'Download',
949+
callback: () => this.exportTestCases(EXPORT_FILE_FORMATS.EXCEL),
950+
icon: 'heroDocumentText',
951+
additionalInfo: 'Excel (.xlsx)',
952+
isTimestamp: false,
953+
},
954+
],
955+
},
956+
];
957+
getExportOptions = () => {
958+
return this.exportOptions;
959+
}
960+
async exportTestCases(format: string) {
961+
if (!this.userStoryId) {
962+
this.toast.showError('No user story selected');
963+
return;
964+
}
939965

940-
exportOptions = [
941-
{
942-
label: 'Copy JSON to Clipboard',
943-
callback: () => this.exportTestCases('json'),
944-
},
945-
{
946-
label: 'Download as Excel (.xlsx)',
947-
callback: () => this.exportTestCases('xlsx'),
948-
},
949-
];
966+
try {
967+
const testCasePath = joinPaths(this.currentProject, REQUIREMENT_TYPE.TC, this.userStoryId);
968+
const files = await this.appSystemService.getFolders(testCasePath, FILTER_STRINGS.BASE, false);
969+
970+
if (!files || files.length === 0) {
971+
this.toast.showError('No test cases found to export');
972+
return;
973+
}
974+
975+
const testCaseContents = await Promise.all(
976+
files.map(async (fileName: string) => {
977+
const content = await this.appSystemService.readFile(
978+
joinPaths(this.currentProject, REQUIREMENT_TYPE.TC, this.userStoryId, fileName)
979+
);
980+
const parsedContent = JSON.parse(content);
981+
982+
parsedContent.us_id = this.navigation.userStoryInfo.id || this.userStoryId;
983+
parsedContent.prd_id = this.navigation.prdInfo.prdId || '';
984+
985+
return {
986+
folderName: REQUIREMENT_TYPE.TC,
987+
fileName,
988+
content: parsedContent
989+
};
990+
})
991+
);
992+
993+
await firstValueFrom(this.store.dispatch(new ClearBRDPRDState()));
950994

951-
exportTestCases(format: string) {
952-
// Implementation for exporting test cases
995+
const state = this.store.snapshot();
996+
await this.store.reset({
997+
...state,
998+
projects: {
999+
...state.projects,
1000+
selectedFileContents: testCaseContents
1001+
}
1002+
});
1003+
1004+
await firstValueFrom(this.store.dispatch(
1005+
new ExportRequirementData(REQUIREMENT_TYPE.TC, {
1006+
type: format as ExportFileFormat,
1007+
}),
1008+
));
1009+
} catch (error) {
1010+
this.logger.error('Error exporting test cases:', error);
1011+
this.toast.showError('Failed to export test cases');
1012+
}
9531013
}
9541014

9551015
setupWorkflowProgressListener() {

ui/src/app/services/export/requirement-export-strategy.manager.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { UIRExportStrategy } from './strategies/uir-export.strategy';
1111
import { AppSystemService } from '../app-system/app-system.service';
1212
import { ClipboardService } from '../clipboard.service';
1313
import { UserStoriesExportStrategy } from './strategies/user-stories-export.strategy';
14+
import { TestCaseExportStrategy } from './strategies/test-case-export.strategy';
1415

1516
@Injectable({
1617
providedIn: 'root',
@@ -70,6 +71,13 @@ export class RequirementExportStrategyManager {
7071
this.clipboardService,
7172
);
7273
}
74+
case REQUIREMENT_TYPE.TC: {
75+
return new TestCaseExportStrategy(
76+
this.exportService,
77+
this.logger,
78+
this.clipboardService,
79+
);
80+
}
7381
default: {
7482
throw new Error(`Export is not supported for ${requirementType}`);
7583
}

0 commit comments

Comments
 (0)