Skip to content

Commit 7685f94

Browse files
committed
test(tokens): added new unit tests
1 parent 3644ea9 commit 7685f94

5 files changed

Lines changed: 249 additions & 113 deletions

File tree

jest.config.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,8 @@ module.exports = {
8989
'<rootDir>/src/app/features/project/project.component.ts',
9090
'<rootDir>/src/app/features/registries/',
9191
'<rootDir>/src/app/features/settings/addons/',
92-
'<rootDir>/src/app/features/settings/settings-container.component.ts',
9392
'<rootDir>/src/app/features/settings/tokens/mappers/',
9493
'<rootDir>/src/app/features/settings/tokens/store/',
95-
'<rootDir>/src/app/features/settings/tokens/pages/tokens-list/',
9694
'<rootDir>/src/app/shared/components/file-menu/',
9795
'<rootDir>/src/app/shared/components/files-tree/',
9896
'<rootDir>/src/app/shared/components/line-chart/',

src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.spec.ts

Lines changed: 137 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Store } from '@ngxs/store';
22

3-
import { TranslatePipe } from '@ngx-translate/core';
4-
import { MockPipe, MockProvider } from 'ng-mocks';
3+
import { TranslateService } from '@ngx-translate/core';
4+
import { MockProvider } from 'ng-mocks';
55

66
import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog';
77

@@ -12,14 +12,17 @@ import { ReactiveFormsModule } from '@angular/forms';
1212
import { ActivatedRoute, Router } from '@angular/router';
1313

1414
import { TokenCreatedDialogComponent } from '@osf/features/settings/tokens/components';
15+
import { InputLimits } from '@osf/shared/constants';
1516
import { MOCK_STORE, TranslateServiceMock } from '@shared/mocks';
1617
import { ToastService } from '@shared/services';
1718

1819
import { TokenFormControls, TokenModel } from '../../models';
19-
import { TokensSelectors } from '../../store';
20+
import { CreateToken, TokensSelectors } from '../../store';
2021

2122
import { TokenAddEditFormComponent } from './token-add-edit-form.component';
2223

24+
import { OSFTestingStoreModule } from '@testing/osf.testing.module';
25+
2326
describe('TokenAddEditFormComponent', () => {
2427
let component: TokenAddEditFormComponent;
2528
let fixture: ComponentFixture<TokenAddEditFormComponent>;
@@ -32,9 +35,7 @@ describe('TokenAddEditFormComponent', () => {
3235
const MOCK_TOKEN: TokenModel = {
3336
id: '1',
3437
name: 'Test Token',
35-
tokenId: 'token1',
3638
scopes: ['read', 'write'],
37-
ownerId: 'user1',
3839
};
3940

4041
const MOCK_SCOPES = [
@@ -78,7 +79,7 @@ describe('TokenAddEditFormComponent', () => {
7879
};
7980

8081
await TestBed.configureTestingModule({
81-
imports: [TokenAddEditFormComponent, MockPipe(TranslatePipe), ReactiveFormsModule],
82+
imports: [TokenAddEditFormComponent, ReactiveFormsModule, OSFTestingStoreModule],
8283
providers: [
8384
TranslateServiceMock,
8485
MockProvider(Store, MOCK_STORE),
@@ -107,6 +108,34 @@ describe('TokenAddEditFormComponent', () => {
107108
expect(component.tokenForm.get(TokenFormControls.Scopes)?.value).toEqual(MOCK_TOKEN.scopes);
108109
});
109110

111+
it('should call patchValue with initial values on ngOnInit', () => {
112+
fixture.componentRef.setInput('initialValues', MOCK_TOKEN);
113+
const patchSpy = jest.spyOn(component.tokenForm, 'patchValue');
114+
115+
component.ngOnInit();
116+
117+
expect(patchSpy).toHaveBeenCalledWith(
118+
expect.objectContaining({
119+
[TokenFormControls.TokenName]: MOCK_TOKEN.name,
120+
[TokenFormControls.Scopes]: MOCK_TOKEN.scopes,
121+
})
122+
);
123+
});
124+
125+
it('should not patch form when initialValues are not provided', () => {
126+
fixture.componentRef.setInput('initialValues', null);
127+
128+
component.tokenForm.patchValue({
129+
[TokenFormControls.TokenName]: 'Existing Name',
130+
[TokenFormControls.Scopes]: ['read'],
131+
});
132+
133+
component.ngOnInit();
134+
135+
expect(component.tokenForm.get(TokenFormControls.TokenName)?.value).toBe('Existing Name');
136+
expect(component.tokenForm.get(TokenFormControls.Scopes)?.value).toEqual(['read']);
137+
});
138+
110139
it('should not submit when form is invalid', () => {
111140
component.tokenForm.patchValue({
112141
[TokenFormControls.TokenName]: '',
@@ -115,11 +144,13 @@ describe('TokenAddEditFormComponent', () => {
115144

116145
const markAllAsTouchedSpy = jest.spyOn(component.tokenForm, 'markAllAsTouched');
117146
const markAsDirtySpy = jest.spyOn(component.tokenForm.get(TokenFormControls.TokenName)!, 'markAsDirty');
147+
const markScopesAsDirtySpy = jest.spyOn(component.tokenForm.get(TokenFormControls.Scopes)!, 'markAsDirty');
118148

119149
component.handleSubmitForm();
120150

121151
expect(markAllAsTouchedSpy).toHaveBeenCalled();
122152
expect(markAsDirtySpy).toHaveBeenCalled();
153+
expect(markScopesAsDirtySpy).toHaveBeenCalled();
123154
expect(MOCK_STORE.dispatch).not.toHaveBeenCalled();
124155
});
125156

@@ -145,32 +176,60 @@ describe('TokenAddEditFormComponent', () => {
145176
expect(MOCK_STORE.dispatch).not.toHaveBeenCalled();
146177
});
147178

148-
it('should create token when not in edit mode', () => {
149-
fixture.componentRef.setInput('isEditMode', false);
179+
it('should early-return when tokenName is falsy even if form is valid', () => {
180+
const tokenNameControl = component.tokenForm.get(TokenFormControls.TokenName)!;
181+
const scopesControl = component.tokenForm.get(TokenFormControls.Scopes)!;
182+
183+
tokenNameControl.clearValidators();
184+
scopesControl.clearValidators();
185+
tokenNameControl.updateValueAndValidity();
186+
scopesControl.updateValueAndValidity();
187+
188+
component.tokenForm.patchValue({
189+
[TokenFormControls.TokenName]: undefined as unknown as string,
190+
[TokenFormControls.Scopes]: ['read'],
191+
});
192+
193+
expect(component.tokenForm.valid).toBe(true);
194+
195+
component.handleSubmitForm();
196+
197+
expect(MOCK_STORE.dispatch).not.toHaveBeenCalled();
198+
});
199+
200+
it('should early-return when scopes is falsy even if form is valid', () => {
201+
const tokenNameControl = component.tokenForm.get(TokenFormControls.TokenName)!;
202+
const scopesControl = component.tokenForm.get(TokenFormControls.Scopes)!;
203+
204+
tokenNameControl.clearValidators();
205+
scopesControl.clearValidators();
206+
tokenNameControl.updateValueAndValidity();
207+
scopesControl.updateValueAndValidity();
208+
150209
component.tokenForm.patchValue({
151210
[TokenFormControls.TokenName]: 'Test Token',
152-
[TokenFormControls.Scopes]: ['read', 'write'],
211+
[TokenFormControls.Scopes]: undefined as unknown as string[],
153212
});
154213

155-
MOCK_STORE.dispatch.mockReturnValue(of(undefined));
214+
expect(component.tokenForm.valid).toBe(true);
156215

157216
component.handleSubmitForm();
158217

159-
expect(MOCK_STORE.dispatch).toHaveBeenCalled();
218+
expect(MOCK_STORE.dispatch).not.toHaveBeenCalled();
160219
});
161220

162-
it('should update token when in edit mode', () => {
163-
fixture.componentRef.setInput('isEditMode', true);
221+
it('should create token when not in edit mode', () => {
222+
fixture.componentRef.setInput('isEditMode', false);
164223
component.tokenForm.patchValue({
165-
[TokenFormControls.TokenName]: 'Updated Token',
166-
[TokenFormControls.Scopes]: ['read', 'write', 'delete'],
224+
[TokenFormControls.TokenName]: 'Test Token',
225+
[TokenFormControls.Scopes]: ['read', 'write'],
167226
});
168227

169228
MOCK_STORE.dispatch.mockReturnValue(of(undefined));
170229

171230
component.handleSubmitForm();
172231

173-
expect(MOCK_STORE.dispatch).toHaveBeenCalled();
232+
expect(MOCK_STORE.dispatch).toHaveBeenCalledWith(new CreateToken('Test Token', ['read', 'write']));
174233
});
175234

176235
it('should show success toast and close dialog after creating token', () => {
@@ -188,46 +247,35 @@ describe('TokenAddEditFormComponent', () => {
188247
expect(dialogRef.close).toHaveBeenCalled();
189248
});
190249

191-
it('should show success toast and navigate after updating token', () => {
192-
fixture.componentRef.setInput('isEditMode', true);
250+
it('should open created dialog with new token name and value after create', () => {
251+
fixture.componentRef.setInput('isEditMode', false);
193252
component.tokenForm.patchValue({
194-
[TokenFormControls.TokenName]: 'Updated Token',
253+
[TokenFormControls.TokenName]: 'Test Token',
195254
[TokenFormControls.Scopes]: ['read', 'write'],
196255
});
197256

257+
const showDialogSpy = jest.spyOn(component, 'showTokenCreatedDialog');
258+
198259
MOCK_STORE.dispatch.mockReturnValue(of(undefined));
199260

200261
component.handleSubmitForm();
201262

202-
expect(toastService.showSuccess).toHaveBeenCalledWith('settings.tokens.toastMessage.successEdit');
203-
expect(router.navigate).toHaveBeenCalledWith(['settings/tokens']);
263+
expect(showDialogSpy).toHaveBeenCalledWith(MOCK_TOKEN.name, MOCK_TOKEN.id);
204264
});
205265

206-
it('should show token created dialog with new token data after successful creation', () => {
207-
fixture.componentRef.setInput('isEditMode', false);
266+
it('should show success toast and navigate after updating token', () => {
267+
fixture.componentRef.setInput('isEditMode', true);
208268
component.tokenForm.patchValue({
209-
[TokenFormControls.TokenName]: 'Test Token',
269+
[TokenFormControls.TokenName]: 'Updated Token',
210270
[TokenFormControls.Scopes]: ['read', 'write'],
211271
});
212272

213273
MOCK_STORE.dispatch.mockReturnValue(of(undefined));
214274

215275
component.handleSubmitForm();
216276

217-
expect(dialogService.open).toHaveBeenCalledWith(
218-
TokenCreatedDialogComponent,
219-
expect.objectContaining({
220-
width: '500px',
221-
header: 'settings.tokens.createdDialog.title',
222-
closeOnEscape: true,
223-
modal: true,
224-
closable: true,
225-
data: {
226-
tokenName: MOCK_TOKEN.name,
227-
tokenValue: MOCK_TOKEN.tokenId,
228-
},
229-
})
230-
);
277+
expect(toastService.showSuccess).toHaveBeenCalledWith('settings.tokens.toastMessage.successEdit');
278+
expect(router.navigate).toHaveBeenCalledWith(['settings/tokens']);
231279
});
232280

233281
it('should open dialog with correct configuration', () => {
@@ -252,6 +300,31 @@ describe('TokenAddEditFormComponent', () => {
252300
);
253301
});
254302

303+
it('should use TranslateService.instant for dialog header', () => {
304+
const translate = TestBed.inject(TranslateService) as unknown as { instant: jest.Mock };
305+
component.showTokenCreatedDialog('Name', 'Value');
306+
expect(translate.instant).toHaveBeenCalledWith('settings.tokens.createdDialog.title');
307+
});
308+
309+
it('should read tokens via selectSignal after create', () => {
310+
fixture.componentRef.setInput('isEditMode', false);
311+
component.tokenForm.patchValue({
312+
[TokenFormControls.TokenName]: 'Test Token',
313+
[TokenFormControls.Scopes]: ['read'],
314+
});
315+
316+
const selectSpy = jest.spyOn(MOCK_STORE, 'selectSignal');
317+
MOCK_STORE.dispatch.mockReturnValue(of(undefined));
318+
319+
component.handleSubmitForm();
320+
321+
expect(selectSpy).toHaveBeenCalledWith(TokensSelectors.getTokens);
322+
});
323+
324+
it('should expose the same inputLimits as InputLimits.fullName', () => {
325+
expect(component.inputLimits).toBe(InputLimits.fullName);
326+
});
327+
255328
it('should require token name', () => {
256329
const tokenNameControl = component.tokenForm.get(TokenFormControls.TokenName);
257330
expect(tokenNameControl?.hasError('required')).toBe(true);
@@ -274,4 +347,30 @@ describe('TokenAddEditFormComponent', () => {
274347
it('should have correct input limits for token name', () => {
275348
expect(component.inputLimits).toBeDefined();
276349
});
350+
351+
it('should expose tokenId from route params', () => {
352+
expect(component.tokenId()).toBe(MOCK_TOKEN.id);
353+
});
354+
355+
it('should expose scopes from store via tokenScopes signal', () => {
356+
expect(component.tokenScopes()).toEqual(MOCK_SCOPES);
357+
});
358+
359+
it('should disable form when isLoading is true', () => {
360+
(MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => {
361+
if (selector === TokensSelectors.getScopes) return () => MOCK_SCOPES;
362+
if (selector === TokensSelectors.isTokensLoading) return () => true;
363+
if (selector === TokensSelectors.getTokens) return () => MOCK_TOKENS;
364+
if (selector === TokensSelectors.getTokenById) {
365+
return () => (id: string) => MOCK_TOKENS.find((token) => token.id === id);
366+
}
367+
return () => null;
368+
});
369+
370+
const loadingFixture = TestBed.createComponent(TokenAddEditFormComponent);
371+
const loadingComponent = loadingFixture.componentInstance;
372+
loadingFixture.detectChanges();
373+
374+
expect(loadingComponent.tokenForm.disabled).toBe(true);
375+
});
277376
});
Lines changed: 6 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,26 @@
1-
import { MockProvider } from 'ng-mocks';
1+
import { MockComponent, MockProvider } from 'ng-mocks';
22

3-
import { DialogService, DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
3+
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
44

55
import { ComponentFixture, TestBed } from '@angular/core/testing';
6-
import { By } from '@angular/platform-browser';
7-
import { ActivatedRoute, Router } from '@angular/router';
86

9-
import { TranslateServiceMock } from '@shared/mocks';
10-
import { ToastService } from '@shared/services';
7+
import { CopyButtonComponent } from '@shared/components';
118

129
import { TokenCreatedDialogComponent } from './token-created-dialog.component';
1310

11+
import { OSFTestingModule } from '@testing/osf.testing.module';
12+
1413
describe('TokenCreatedDialogComponent', () => {
1514
let component: TokenCreatedDialogComponent;
1615
let fixture: ComponentFixture<TokenCreatedDialogComponent>;
17-
let dialogService: Partial<DialogService>;
18-
let dialogRef: Partial<DynamicDialogRef>;
19-
let activatedRoute: Partial<ActivatedRoute>;
20-
let router: Partial<Router>;
21-
let toastService: Partial<ToastService>;
2216

2317
const mockTokenName = 'Test Token';
2418
const mockTokenValue = 'test-token-value';
2519

2620
beforeEach(async () => {
27-
dialogService = {
28-
open: jest.fn(),
29-
};
30-
31-
dialogRef = {
32-
close: jest.fn(),
33-
};
34-
35-
router = {
36-
navigate: jest.fn(),
37-
};
38-
39-
toastService = {
40-
showSuccess: jest.fn(),
41-
showError: jest.fn(),
42-
};
43-
4421
await TestBed.configureTestingModule({
45-
imports: [TokenCreatedDialogComponent],
22+
imports: [TokenCreatedDialogComponent, OSFTestingModule, MockComponent(CopyButtonComponent)],
4623
providers: [
47-
TranslateServiceMock,
48-
MockProvider(ToastService),
4924
MockProvider(DynamicDialogRef, { close: jest.fn() }),
5025
MockProvider(DynamicDialogConfig, {
5126
data: {
@@ -64,20 +39,4 @@ describe('TokenCreatedDialogComponent', () => {
6439
it('should create', () => {
6540
expect(component).toBeTruthy();
6641
});
67-
68-
it('should initialize with token data from config', () => {
69-
expect(component.tokenName()).toBe(mockTokenName);
70-
expect(component.tokenId()).toBe(mockTokenValue);
71-
});
72-
73-
it('should display token name and value in the template', () => {
74-
const tokenInput = fixture.debugElement.query(By.css('input')).nativeElement;
75-
expect(tokenInput.value).toBe(mockTokenValue);
76-
});
77-
78-
it('should set input selection range to 0 after render', () => {
79-
const input = fixture.debugElement.query(By.css('input')).nativeElement;
80-
expect(input.selectionStart).toBe(0);
81-
expect(input.selectionEnd).toBe(0);
82-
});
8342
});

0 commit comments

Comments
 (0)