|
1 | | -import { ComponentFixture, TestBed } from '@angular/core/testing'; |
| 1 | +import { ComponentFixture, TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; |
2 | 2 | import { Router } from '@angular/router'; |
3 | | -import { of } from 'rxjs'; |
| 3 | +import { Subject, of, throwError } from 'rxjs'; |
4 | 4 | import { CreateTenderModalComponent } from './create-tender-modal.component'; |
5 | 5 | import { QuoteService } from 'src/app/features/quotes/services/quote.service'; |
6 | 6 | import { NotificationService } from 'src/app/services/notification.service'; |
7 | 7 | import { LocalStorageService } from 'src/app/services/local-storage.service'; |
8 | | -import { ProviderService } from 'src/app/services/provider.service'; |
| 8 | +import { Provider, ProviderService } from 'src/app/services/provider.service'; |
9 | 9 | import { ApiServiceService } from 'src/app/services/product-service.service'; |
10 | 10 | import { AccountServiceService } from 'src/app/services/account-service.service'; |
11 | 11 |
|
12 | 12 | describe('CreateTenderModalComponent', () => { |
13 | 13 | let fixture: ComponentFixture<CreateTenderModalComponent>; |
14 | 14 | let component: CreateTenderModalComponent; |
| 15 | + let quoteService: jasmine.SpyObj<QuoteService>; |
| 16 | + let notificationService: jasmine.SpyObj<NotificationService>; |
| 17 | + let providerService: jasmine.SpyObj<ProviderService>; |
15 | 18 |
|
16 | 19 | beforeEach(async () => { |
| 20 | + quoteService = jasmine.createSpyObj<QuoteService>('QuoteService', [ |
| 21 | + 'addAttachmentToQuote', |
| 22 | + 'createTenderingQuote', |
| 23 | + 'deleteQuote', |
| 24 | + 'getTenderingQuotesByUser', |
| 25 | + 'updateQuoteDate', |
| 26 | + ]); |
| 27 | + notificationService = jasmine.createSpyObj<NotificationService>('NotificationService', ['showError', 'showSuccess']); |
| 28 | + providerService = jasmine.createSpyObj<ProviderService>('ProviderService', [ |
| 29 | + 'getProviderCountryOptions', |
| 30 | + 'getProvidersForTender', |
| 31 | + 'getProvidersForTenderNew', |
| 32 | + ]); |
| 33 | + |
| 34 | + quoteService.getTenderingQuotesByUser.and.returnValue(of([])); |
| 35 | + providerService.getProviderCountryOptions.and.returnValue(of([])); |
| 36 | + providerService.getProvidersForTenderNew.and.returnValue(of([])); |
| 37 | + providerService.getProvidersForTender.and.returnValue(of([])); |
| 38 | + |
17 | 39 | await TestBed.configureTestingModule({ |
18 | 40 | imports: [CreateTenderModalComponent], |
19 | 41 | providers: [ |
20 | | - { provide: QuoteService, useValue: {} }, |
21 | | - { provide: NotificationService, useValue: { showError: jasmine.createSpy('showError'), showSuccess: jasmine.createSpy('showSuccess') } }, |
| 42 | + { provide: QuoteService, useValue: quoteService }, |
| 43 | + { provide: NotificationService, useValue: notificationService }, |
22 | 44 | { provide: LocalStorageService, useValue: { getObject: () => ({}) } }, |
23 | | - { provide: ProviderService, useValue: { getProviderCountryOptions: () => of([]) } }, |
24 | | - { provide: ApiServiceService, useValue: {} }, |
25 | | - { provide: AccountServiceService, useValue: {} }, |
| 45 | + { provide: ProviderService, useValue: providerService }, |
| 46 | + { provide: ApiServiceService, useValue: { getDefaultCategories: () => Promise.resolve([]), getCategoriesByParentId: () => Promise.resolve([]) } }, |
| 47 | + { provide: AccountServiceService, useValue: { getOrgInfo: () => Promise.resolve({ tradingName: 'Known Provider' }) } }, |
26 | 48 | { provide: Router, useValue: { navigate: jasmine.createSpy('navigate') } }, |
27 | 49 | ], |
28 | 50 | }).compileComponents(); |
@@ -104,4 +126,100 @@ describe('CreateTenderModalComponent', () => { |
104 | 126 | expect(selectedRow.className).toContain('bg-[#EBF0F7]'); |
105 | 127 | expect(selectedRow.className).toContain('border-l-[#1f4fbf]'); |
106 | 128 | }); |
| 129 | + |
| 130 | + it('shows the uploaded PDF in the provider step summary immediately after step 2 is saved', fakeAsync(() => { |
| 131 | + const selectedFile = new File(['pdf'], 'Tender-request.pdf', { type: 'application/pdf' }); |
| 132 | + quoteService.updateQuoteDate.and.returnValue(of({ id: 'coordinator-1' } as any)); |
| 133 | + quoteService.addAttachmentToQuote.and.returnValue(of({ |
| 134 | + id: 'coordinator-1', |
| 135 | + quoteItem: [{ |
| 136 | + attachment: [{ |
| 137 | + name: 'Tender-request.pdf', |
| 138 | + mimeType: 'application/pdf', |
| 139 | + content: 'cGRm', |
| 140 | + size: { amount: 3 }, |
| 141 | + }], |
| 142 | + }], |
| 143 | + } as any)); |
| 144 | + |
| 145 | + component.isOpen = true; |
| 146 | + component.customerId = 'customer-1'; |
| 147 | + component.createdQuoteId = 'coordinator-1'; |
| 148 | + component.tenderTitle = 'Tender with attachment'; |
| 149 | + component.requestedCompletionDate = '2026-06-01'; |
| 150 | + component.expectedCompletionDate = '2026-06-10'; |
| 151 | + component.selectedPdfFile = selectedFile; |
| 152 | + |
| 153 | + component.proceedToProviderSelection(); |
| 154 | + flushMicrotasks(); |
| 155 | + fixture.detectChanges(); |
| 156 | + |
| 157 | + const summary = fixture.nativeElement.querySelector('[aria-label="Tender setup summary"]'); |
| 158 | + |
| 159 | + expect(component.tenderCreationStep).toBe(3); |
| 160 | + expect(component.pdfAttachmentSet).toBeTrue(); |
| 161 | + expect(component.existingAttachment?.name).toBe('Tender-request.pdf'); |
| 162 | + expect(summary.textContent).toContain('Tender-request.pdf'); |
| 163 | + })); |
| 164 | + |
| 165 | + it('reloads provider candidates on filter changes without entering the invite-saving state', () => { |
| 166 | + const providers$ = new Subject<Provider[]>(); |
| 167 | + providerService.getProvidersForTenderNew.and.returnValue(providers$); |
| 168 | + component.isOpen = true; |
| 169 | + component.customerId = 'customer-1'; |
| 170 | + component.currentUserId = 'customer-1'; |
| 171 | + component.createdQuoteId = 'coordinator-1'; |
| 172 | + component.tenderCreationStep = 3; |
| 173 | + |
| 174 | + component.emitFilters(); |
| 175 | + |
| 176 | + expect(component.tenderLoading).toBeFalse(); |
| 177 | + expect(component.providerInviteSaving).toBeFalse(); |
| 178 | + expect(quoteService.createTenderingQuote).not.toHaveBeenCalled(); |
| 179 | + |
| 180 | + providers$.next([]); |
| 181 | + providers$.complete(); |
| 182 | + }); |
| 183 | + |
| 184 | + it('removes saved providers from the candidate list after manual invite', fakeAsync(() => { |
| 185 | + const selectedProvider = { id: 'provider-1', tradingName: 'Provider One' } as Provider; |
| 186 | + const otherProvider = { id: 'provider-2', tradingName: 'Provider Two' } as Provider; |
| 187 | + spyOn(component.tenderUpdated, 'emit'); |
| 188 | + quoteService.createTenderingQuote.and.returnValue(of({ id: 'quote-1' } as any)); |
| 189 | + component.currentUserId = 'customer-1'; |
| 190 | + component.createdQuoteId = 'coordinator-1'; |
| 191 | + component.tenderProviders = [selectedProvider, otherProvider]; |
| 192 | + component._safeInvitedList = [selectedProvider]; |
| 193 | + component.selectedProviders = new Set(['provider-1']); |
| 194 | + component.availableProviders = [ |
| 195 | + { provider: selectedProvider, selected: true }, |
| 196 | + { provider: otherProvider, selected: false }, |
| 197 | + ]; |
| 198 | + |
| 199 | + component.saveProvidersList(); |
| 200 | + flushMicrotasks(); |
| 201 | + |
| 202 | + expect(component.invitedProviders.map(invited => invited.provider.id)).toEqual(['provider-1']); |
| 203 | + expect(component.selectedProviders.size).toBe(0); |
| 204 | + expect(component.availableProviders.map(candidate => candidate.provider.id)).toEqual(['provider-2']); |
| 205 | + expect(component.tenderUpdated.emit).toHaveBeenCalled(); |
| 206 | + })); |
| 207 | + |
| 208 | + it('treats a gateway timeout while removing an invited provider as a completed delete and refreshes candidates', () => { |
| 209 | + const provider = { id: 'provider-1', tradingName: 'Provider One' } as Provider; |
| 210 | + quoteService.deleteQuote.and.returnValue(throwError(() => ({ status: 504, statusText: 'OK' }))); |
| 211 | + component.currentUserId = 'customer-1'; |
| 212 | + component.createdQuoteId = 'coordinator-1'; |
| 213 | + component.invitedProviders = [{ provider, quoteId: 'quote-1' }]; |
| 214 | + component.tenderProviders = [provider]; |
| 215 | + component.availableProviders = []; |
| 216 | + |
| 217 | + component.removeInvitedProvider('quote-1', 'provider-1'); |
| 218 | + component.genericConfirmCallback?.(); |
| 219 | + |
| 220 | + expect(component.invitedProviders).toEqual([]); |
| 221 | + expect(component.availableProviders.map(candidate => candidate.provider.id)).toEqual(['provider-1']); |
| 222 | + expect(notificationService.showSuccess).toHaveBeenCalledWith('Provider invitation removed successfully'); |
| 223 | + expect(notificationService.showError).not.toHaveBeenCalled(); |
| 224 | + }); |
107 | 225 | }); |
0 commit comments