diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 569608d..6e92b31 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -61,8 +61,8 @@ export const routes: Routes = [ }, { path: 'decisions/:decisionId/edit', - redirectTo: 'decisions', - pathMatch: 'full' + loadComponent: () => import('./components/decision-form/decision-form.component').then(m => m.DecisionFormComponent), + canActivate: [authGuard] }, { path: '', redirectTo: 'decisions', pathMatch: 'full' } ] diff --git a/src/app/components/decision-form/decision-form.component.spec.ts b/src/app/components/decision-form/decision-form.component.spec.ts new file mode 100644 index 0000000..bc22feb --- /dev/null +++ b/src/app/components/decision-form/decision-form.component.spec.ts @@ -0,0 +1,74 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DecisionFormComponent } from './decision-form.component'; +import { DecisionService } from '../../services/decision.service'; +import { of } from 'rxjs'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ActivatedRoute, convertToParamMap } from '@angular/router'; +import { ReactiveFormsModule } from '@angular/forms'; + +describe('DecisionFormComponent', () => { + let component: DecisionFormComponent; + let fixture: ComponentFixture; + let mockDecisionService: any; + + beforeEach(async () => { + mockDecisionService = { + getDecision: jasmine.createSpy('getDecision').and.returnValue(of({ + id: '1', + title: 'Loaded Decision', + description: 'Loaded Description', + status: 'OPEN', + workspaceId: '10' + })), + updateDecision: jasmine.createSpy('updateDecision').and.returnValue(of({ + id: '1', + title: 'Updated Decision', + status: 'CLOSED' + })), + createDecision: jasmine.createSpy('createDecision').and.returnValue(of({})) + }; + + await TestBed.configureTestingModule({ + imports: [DecisionFormComponent, RouterTestingModule, ReactiveFormsModule], + providers: [ + { provide: DecisionService, useValue: mockDecisionService }, + { + provide: ActivatedRoute, + useValue: { + paramMap: of(convertToParamMap({ decisionId: '1' })), + snapshot: { paramMap: convertToParamMap({ id: '10' }) }, + pathFromRoot: [] + } + } + ] + }).compileComponents(); + + fixture = TestBed.createComponent(DecisionFormComponent); + component = fixture.componentInstance; + // Mock resolveWorkspaceId to return '10' + spyOn(component, 'resolveWorkspaceId').and.returnValue('10'); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should enter edit mode when decisionId is present', () => { + expect(component.isEditMode).toBeTrue(); + expect(component.decisionId).toBe('1'); + expect(mockDecisionService.getDecision).toHaveBeenCalledWith('10', '1'); + }); + + it('should populate form with decision data', () => { + expect(component.decisionForm.value.title).toBe('Loaded Decision'); + expect(component.decisionForm.value.description).toBe('Loaded Description'); + expect(component.decisionForm.value.status).toBe('OPEN'); + }); + + it('should call updateDecision on submit in edit mode', () => { + component.decisionForm.patchValue({ title: 'Updated Title' }); + component.onSubmit(); + expect(mockDecisionService.updateDecision).toHaveBeenCalled(); + }); +}); diff --git a/src/app/components/decision-form/decision-form.component.ts b/src/app/components/decision-form/decision-form.component.ts index fa1f20d..74da894 100644 --- a/src/app/components/decision-form/decision-form.component.ts +++ b/src/app/components/decision-form/decision-form.component.ts @@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'; import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; -import { Subject, Subscription, takeUntil, timeout } from 'rxjs'; +import { finalize, Subject, Subscription, takeUntil, timeout } from 'rxjs'; import { Decision } from '../../models/decision.model'; import { DecisionService } from '../../services/decision.service'; @@ -251,10 +251,12 @@ export class DecisionFormComponent implements OnInit, OnDestroy { this.loadSubscription = this.decisionService.getDecision(this.workspaceId, id).pipe( timeout(this.requestTimeoutMs), takeUntil(this.destroy$), + finalize(() => { + this.isLoading = false; + this.clearLoadGuardTimer(); + }) ).subscribe({ next: (decision) => { - this.clearActiveLoad(); - this.isLoading = false; if (!decision) { this.submitError = 'Decision not found.'; return; @@ -268,8 +270,6 @@ export class DecisionFormComponent implements OnInit, OnDestroy { }); }, error: (error) => { - this.clearActiveLoad(); - this.isLoading = false; this.submitError = this.resolveSubmitError(error, 'Unable to load decision. Please try again.'); } }); @@ -283,13 +283,17 @@ export class DecisionFormComponent implements OnInit, OnDestroy { } private resolveWorkspaceId(): string | null { - for (const route of this.route.pathFromRoot) { - const id = route.snapshot.paramMap.get('id'); + // Try to get from route hierarchy first + let currentRoute: ActivatedRoute | null = this.route; + while (currentRoute) { + const id = currentRoute.snapshot.paramMap.get('id'); if (id) { return id; } + currentRoute = currentRoute.parent; } + // Fallback to URL parsing if route hierarchy fails const urlMatch = this.router.url.match(/\/workspaces\/([^/]+)/); if (urlMatch?.[1]) { return urlMatch[1]; diff --git a/src/app/components/decision-list/decision-list.component.html b/src/app/components/decision-list/decision-list.component.html index 14f4d26..687fb9c 100644 --- a/src/app/components/decision-list/decision-list.component.html +++ b/src/app/components/decision-list/decision-list.component.html @@ -33,6 +33,7 @@

{{ decision.title }}

+ Edit
diff --git a/src/app/components/decision-list/decision-list.component.spec.ts b/src/app/components/decision-list/decision-list.component.spec.ts new file mode 100644 index 0000000..f4e20f3 --- /dev/null +++ b/src/app/components/decision-list/decision-list.component.spec.ts @@ -0,0 +1,73 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DecisionListComponent } from './decision-list.component'; +import { DecisionService } from '../../services/decision.service'; +import { of } from 'rxjs'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ActivatedRoute } from '@angular/router'; +import { Decision } from '../../models/decision.model'; +import { By } from '@angular/platform-browser'; + +describe('DecisionListComponent', () => { + let component: DecisionListComponent; + let fixture: ComponentFixture; + let mockDecisionService: any; + + const mockDecisions: Decision[] = [ + { + id: '1', + workspaceId: '10', + userId: '3', + title: 'Test Decision', + description: 'Test Description', + status: 'OPEN', + createdAt: new Date(), + updatedAt: new Date(), + isDeleted: false + } + ]; + + beforeEach(async () => { + mockDecisionService = { + getDecisions: jasmine.createSpy('getDecisions').and.returnValue(of(mockDecisions)), + deleteDecision: jasmine.createSpy('deleteDecision').and.returnValue(of(void 0)) + }; + + await TestBed.configureTestingModule({ + imports: [DecisionListComponent, RouterTestingModule], + providers: [ + { provide: DecisionService, useValue: mockDecisionService }, + { + provide: ActivatedRoute, + useValue: { + pathFromRoot: [ + { snapshot: { paramMap: { get: () => '10' } } } + ] + } + } + ] + }).compileComponents(); + + fixture = TestBed.createComponent(DecisionListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should render an edit button for each decision', () => { + const compiled = fixture.nativeElement as HTMLElement; + const editButtons = compiled.querySelectorAll('.btn-secondary'); + expect(editButtons.length).toBe(1); + expect(editButtons[0].textContent).toContain('Edit'); + }); + + it('should have correct edit link', () => { + const editButton = fixture.debugElement.query(By.css('.btn-secondary')); + expect(editButton).toBeTruthy(); + // Since it's a relative link [decision.id, 'edit'], we check if the attribute is present or just trust the binding + const link = editButton.nativeElement as HTMLAnchorElement; + expect(link.textContent).toContain('Edit'); + }); +}); diff --git a/src/app/components/workspace/workspace-details/workspace-details.ts b/src/app/components/workspace/workspace-details/workspace-details.ts index 4da2451..0160bc5 100644 --- a/src/app/components/workspace/workspace-details/workspace-details.ts +++ b/src/app/components/workspace/workspace-details/workspace-details.ts @@ -21,9 +21,11 @@ export class WorkspaceDetailsComponent implements OnInit { ) { } ngOnInit(): void { + let currentId: string | null = null; this.route.paramMap.subscribe(params => { const id = params.get('id'); - if (id) { + if (id && id !== currentId) { + currentId = id; this.workspace$ = this.workspaceService.getWorkspace(id); } });