]>
Commit | Line | Data |
---|---|---|
9f95a23c TL |
1 | import { HttpClientTestingModule } from '@angular/common/http/testing'; |
2 | import { Type } from '@angular/core'; | |
3 | import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; | |
4 | import { Validators } from '@angular/forms'; | |
5 | import { RouterTestingModule } from '@angular/router/testing'; | |
6 | ||
e306af50 | 7 | import { TreeComponent, TreeModule, TREE_ACTIONS } from 'angular-tree-component'; |
9f95a23c TL |
8 | import { NgBootstrapFormValidationModule } from 'ng-bootstrap-form-validation'; |
9 | import { BsModalRef, BsModalService, ModalModule } from 'ngx-bootstrap/modal'; | |
10 | import { ToastrModule } from 'ngx-toastr'; | |
11 | import { Observable, of } from 'rxjs'; | |
12 | ||
13 | import { | |
14 | configureTestBed, | |
15 | i18nProviders, | |
16 | modalServiceShow, | |
17 | PermissionHelper | |
18 | } from '../../../../testing/unit-test-helper'; | |
19 | import { CephfsService } from '../../../shared/api/cephfs.service'; | |
20 | import { ConfirmationModalComponent } from '../../../shared/components/confirmation-modal/confirmation-modal.component'; | |
21 | import { FormModalComponent } from '../../../shared/components/form-modal/form-modal.component'; | |
22 | import { NotificationType } from '../../../shared/enum/notification-type.enum'; | |
23 | import { CdValidators } from '../../../shared/forms/cd-validators'; | |
24 | import { CdTableAction } from '../../../shared/models/cd-table-action'; | |
25 | import { CdTableSelection } from '../../../shared/models/cd-table-selection'; | |
26 | import { | |
27 | CephfsDir, | |
28 | CephfsQuotas, | |
29 | CephfsSnapshot | |
30 | } from '../../../shared/models/cephfs-directory-models'; | |
31 | import { NotificationService } from '../../../shared/services/notification.service'; | |
32 | import { SharedModule } from '../../../shared/shared.module'; | |
33 | import { CephfsDirectoriesComponent } from './cephfs-directories.component'; | |
34 | ||
35 | describe('CephfsDirectoriesComponent', () => { | |
36 | let component: CephfsDirectoriesComponent; | |
37 | let fixture: ComponentFixture<CephfsDirectoriesComponent>; | |
38 | let cephfsService: CephfsService; | |
39 | let noAsyncUpdate: boolean; | |
40 | let lsDirSpy: jasmine.Spy; | |
41 | let modalShowSpy: jasmine.Spy; | |
42 | let notificationShowSpy: jasmine.Spy; | |
43 | let minValidator: jasmine.Spy; | |
44 | let maxValidator: jasmine.Spy; | |
45 | let minBinaryValidator: jasmine.Spy; | |
46 | let maxBinaryValidator: jasmine.Spy; | |
47 | let modal: any; | |
48 | ||
49 | // Get's private attributes or functions | |
50 | const get = { | |
51 | nodeIds: (): { [path: string]: CephfsDir } => component['nodeIds'], | |
52 | dirs: (): CephfsDir[] => component['dirs'], | |
53 | requestedPaths: (): string[] => component['requestedPaths'] | |
54 | }; | |
55 | ||
56 | // Object contains mock data that will be reset before each test. | |
57 | let mockData: { | |
58 | nodes: any; | |
59 | parent: any; | |
60 | createdSnaps: CephfsSnapshot[] | any[]; | |
61 | deletedSnaps: CephfsSnapshot[] | any[]; | |
62 | updatedQuotas: { [path: string]: CephfsQuotas }; | |
63 | createdDirs: CephfsDir[]; | |
64 | }; | |
65 | ||
66 | // Object contains mock functions | |
67 | const mockLib = { | |
68 | quotas: (max_bytes: number, max_files: number): CephfsQuotas => ({ max_bytes, max_files }), | |
69 | snapshots: (dirPath: string, howMany: number): CephfsSnapshot[] => { | |
70 | const name = 'someSnapshot'; | |
71 | const snapshots = []; | |
72 | const oneDay = 3600 * 24 * 1000; | |
73 | for (let i = 0; i < howMany; i++) { | |
74 | const snapName = `${name}${i + 1}`; | |
75 | const path = `${dirPath}/.snap/${snapName}`; | |
76 | const created = new Date(+new Date() - oneDay * i).toString(); | |
77 | snapshots.push({ name: snapName, path, created }); | |
78 | } | |
79 | return snapshots; | |
80 | }, | |
81 | dir: (parentPath: string, name: string, modifier: number): CephfsDir => { | |
82 | const dirPath = `${parentPath === '/' ? '' : parentPath}/${name}`; | |
83 | let snapshots = mockLib.snapshots(parentPath, modifier); | |
84 | const extraSnapshots = mockData.createdSnaps.filter((s) => s.path === dirPath); | |
85 | if (extraSnapshots.length > 0) { | |
86 | snapshots = snapshots.concat(extraSnapshots); | |
87 | } | |
88 | const deletedSnapshots = mockData.deletedSnaps | |
89 | .filter((s) => s.path === dirPath) | |
90 | .map((s) => s.name); | |
91 | if (deletedSnapshots.length > 0) { | |
92 | snapshots = snapshots.filter((s) => !deletedSnapshots.includes(s.name)); | |
93 | } | |
94 | return { | |
95 | name, | |
96 | path: dirPath, | |
97 | parent: parentPath, | |
98 | quotas: Object.assign( | |
99 | mockLib.quotas(1024 * modifier, 10 * modifier), | |
100 | mockData.updatedQuotas[dirPath] || {} | |
101 | ), | |
102 | snapshots: snapshots | |
103 | }; | |
104 | }, | |
105 | // Only used inside other mocks | |
106 | lsSingleDir: (path = ''): CephfsDir[] => { | |
107 | const customDirs = mockData.createdDirs.filter((d) => d.parent === path); | |
108 | const isCustomDir = mockData.createdDirs.some((d) => d.path === path); | |
109 | if (isCustomDir || path.includes('b')) { | |
110 | // 'b' has no sub directories | |
111 | return customDirs; | |
112 | } | |
113 | return customDirs.concat([ | |
114 | // Directories are not sorted! | |
115 | mockLib.dir(path, 'c', 3), | |
116 | mockLib.dir(path, 'a', 1), | |
117 | mockLib.dir(path, 'b', 2) | |
118 | ]); | |
119 | }, | |
120 | lsDir: (_id: number, path = ''): Observable<CephfsDir[]> => { | |
121 | // will return 2 levels deep | |
122 | let data = mockLib.lsSingleDir(path); | |
123 | const paths = data.map((dir) => dir.path); | |
124 | paths.forEach((pathL2) => { | |
125 | data = data.concat(mockLib.lsSingleDir(pathL2)); | |
126 | }); | |
127 | if (path === '' || path === '/') { | |
128 | // Adds root directory on ls of '/' to the directories list. | |
129 | const root = mockLib.dir(path, '/', 1); | |
130 | root.path = '/'; | |
131 | root.parent = undefined; | |
132 | root.quotas = undefined; | |
133 | data = [root].concat(data); | |
134 | } | |
135 | return of(data); | |
136 | }, | |
137 | mkSnapshot: (_id: any, path: string, name: string): Observable<string> => { | |
138 | mockData.createdSnaps.push({ | |
139 | name, | |
140 | path, | |
141 | created: new Date().toString() | |
142 | }); | |
143 | return of(name); | |
144 | }, | |
145 | rmSnapshot: (_id: any, path: string, name: string): Observable<string> => { | |
146 | mockData.deletedSnaps.push({ | |
147 | name, | |
148 | path, | |
149 | created: new Date().toString() | |
150 | }); | |
151 | return of(name); | |
152 | }, | |
153 | updateQuota: (_id: any, path: string, updated: CephfsQuotas): Observable<string> => { | |
154 | mockData.updatedQuotas[path] = Object.assign(mockData.updatedQuotas[path] || {}, updated); | |
155 | return of('Response'); | |
156 | }, | |
157 | modalShow: (comp: Type<any>, init: any): any => { | |
158 | modal = modalServiceShow(comp, init); | |
159 | return modal.ref; | |
160 | }, | |
161 | getNodeById: (path: string) => { | |
162 | return mockLib.useNode(path); | |
163 | }, | |
164 | updateNodes: (path: string) => { | |
165 | const p: Promise<any[]> = component.treeOptions.getChildren({ id: path }); | |
166 | return noAsyncUpdate ? () => p : mockLib.asyncNodeUpdate(p); | |
167 | }, | |
168 | asyncNodeUpdate: fakeAsync((p: Promise<any[]>) => { | |
169 | p.then((nodes) => { | |
170 | mockData.nodes = mockData.nodes.concat(nodes); | |
171 | }); | |
172 | tick(); | |
173 | }), | |
174 | changeId: (id: number) => { | |
175 | // For some reason this spy has to be renewed after usage | |
176 | spyOn(global, 'setTimeout').and.callFake((fn) => fn()); | |
177 | component.id = id; | |
178 | component.ngOnChanges(); | |
179 | mockData.nodes = component.nodes.concat(mockData.nodes); | |
180 | }, | |
181 | selectNode: (path: string) => { | |
182 | component.treeOptions.actionMapping.mouse.click(undefined, mockLib.useNode(path), undefined); | |
183 | }, | |
184 | // Creates TreeNode with parents until root | |
185 | useNode: (path: string): { id: string; parent: any; data: any; loadNodeChildren: Function } => { | |
186 | const parentPath = path.split('/'); | |
187 | parentPath.pop(); | |
188 | const parentIsRoot = parentPath.length === 1; | |
189 | const parent = parentIsRoot ? { id: '/' } : mockLib.useNode(parentPath.join('/')); | |
190 | return { | |
191 | id: path, | |
192 | parent, | |
193 | data: {}, | |
194 | loadNodeChildren: () => mockLib.updateNodes(path) | |
195 | }; | |
196 | }, | |
197 | treeActions: { | |
198 | toggleActive: (_a: any, node: any, _b: any) => { | |
199 | return mockLib.updateNodes(node.id); | |
200 | } | |
201 | }, | |
202 | mkDir: (path: string, name: string, maxFiles: number, maxBytes: number) => { | |
203 | const dir = mockLib.dir(path, name, 3); | |
204 | dir.quotas.max_bytes = maxBytes * 1024; | |
205 | dir.quotas.max_files = maxFiles; | |
206 | mockData.createdDirs.push(dir); | |
207 | // Below is needed for quota tests only where 4 dirs are mocked | |
208 | get.nodeIds()[dir.path] = dir; | |
209 | mockData.nodes.push({ id: dir.path }); | |
210 | }, | |
211 | createSnapshotThroughModal: (name: string) => { | |
212 | component.createSnapshot(); | |
213 | modal.component.onSubmitForm({ name }); | |
214 | }, | |
215 | deleteSnapshotsThroughModal: (snapshots: CephfsSnapshot[]) => { | |
216 | component.snapshot.selection.selected = snapshots; | |
217 | component.deleteSnapshotModal(); | |
218 | modal.component.callSubmitAction(); | |
219 | }, | |
220 | updateQuotaThroughModal: (attribute: string, value: number) => { | |
221 | component.quota.selection.selected = component.settings.filter( | |
222 | (q) => q.quotaKey === attribute | |
223 | ); | |
224 | component.updateQuotaModal(); | |
225 | modal.component.onSubmitForm({ [attribute]: value }); | |
226 | }, | |
227 | unsetQuotaThroughModal: (attribute: string) => { | |
228 | component.quota.selection.selected = component.settings.filter( | |
229 | (q) => q.quotaKey === attribute | |
230 | ); | |
231 | component.unsetQuotaModal(); | |
232 | modal.component.onSubmit(); | |
233 | }, | |
234 | setFourQuotaDirs: (quotas: number[][]) => { | |
235 | expect(quotas.length).toBe(4); // Make sure this function is used correctly | |
236 | let path = ''; | |
237 | quotas.forEach((quota, index) => { | |
238 | index += 1; | |
239 | mockLib.mkDir(path === '' ? '/' : path, index.toString(), quota[0], quota[1]); | |
240 | path += '/' + index; | |
241 | }); | |
242 | mockData.parent = { | |
243 | value: '3', | |
244 | id: '/1/2/3', | |
245 | parent: { | |
246 | value: '2', | |
247 | id: '/1/2', | |
248 | parent: { | |
249 | value: '1', | |
250 | id: '/1', | |
251 | parent: { value: '/', id: '/' } | |
252 | } | |
253 | } | |
254 | }; | |
255 | mockLib.selectNode('/1/2/3/4'); | |
256 | } | |
257 | }; | |
258 | ||
259 | // Expects that are used frequently | |
260 | const assert = { | |
261 | dirLength: (n: number) => expect(get.dirs().length).toBe(n), | |
262 | nodeLength: (n: number) => expect(mockData.nodes.length).toBe(n), | |
263 | lsDirCalledTimes: (n: number) => expect(lsDirSpy).toHaveBeenCalledTimes(n), | |
264 | lsDirHasBeenCalledWith: (id: number, paths: string[]) => { | |
265 | paths.forEach((path) => expect(lsDirSpy).toHaveBeenCalledWith(id, path)); | |
266 | assert.lsDirCalledTimes(paths.length); | |
267 | }, | |
268 | requestedPaths: (expected: string[]) => expect(get.requestedPaths()).toEqual(expected), | |
269 | snapshotsByName: (snaps: string[]) => | |
270 | expect(component.selectedDir.snapshots.map((s) => s.name)).toEqual(snaps), | |
271 | dirQuotas: (bytes: number, files: number) => { | |
272 | expect(component.selectedDir.quotas).toEqual({ max_bytes: bytes, max_files: files }); | |
273 | }, | |
274 | noQuota: (key: 'bytes' | 'files') => { | |
275 | assert.quotaRow(key, '', 0, ''); | |
276 | }, | |
277 | quotaIsNotInherited: (key: 'bytes' | 'files', shownValue: any, nextMaximum: number) => { | |
278 | const dir = component.selectedDir; | |
279 | const path = dir.path; | |
280 | assert.quotaRow(key, shownValue, nextMaximum, path); | |
281 | }, | |
282 | quotaIsInherited: (key: 'bytes' | 'files', shownValue: any, path: string) => { | |
283 | const isBytes = key === 'bytes'; | |
284 | const nextMaximum = get.nodeIds()[path].quotas[isBytes ? 'max_bytes' : 'max_files']; | |
285 | assert.quotaRow(key, shownValue, nextMaximum, path); | |
286 | }, | |
287 | quotaRow: ( | |
288 | key: 'bytes' | 'files', | |
289 | shownValue: number | string, | |
290 | nextTreeMaximum: number, | |
291 | originPath: string | |
292 | ) => { | |
293 | const isBytes = key === 'bytes'; | |
294 | expect(component.settings[isBytes ? 1 : 0]).toEqual({ | |
295 | row: { | |
296 | name: `Max ${isBytes ? 'size' : key}`, | |
297 | value: shownValue, | |
298 | originPath | |
299 | }, | |
300 | quotaKey: `max_${key}`, | |
301 | dirValue: expect.any(Number), | |
302 | nextTreeMaximum: { | |
303 | value: nextTreeMaximum, | |
304 | path: expect.any(String) | |
305 | } | |
306 | }); | |
307 | }, | |
308 | quotaUnsetModalTexts: (titleText: string, message: string, notificationMsg: string) => { | |
309 | expect(modalShowSpy).toHaveBeenCalledWith(ConfirmationModalComponent, { | |
310 | initialState: expect.objectContaining({ | |
311 | titleText, | |
312 | description: message, | |
313 | buttonText: 'Unset' | |
314 | }) | |
315 | }); | |
316 | expect(notificationShowSpy).toHaveBeenCalledWith(NotificationType.success, notificationMsg); | |
317 | }, | |
318 | quotaUpdateModalTexts: (titleText: string, message: string, notificationMsg: string) => { | |
319 | expect(modalShowSpy).toHaveBeenCalledWith(FormModalComponent, { | |
320 | initialState: expect.objectContaining({ | |
321 | titleText, | |
322 | message, | |
323 | submitButtonText: 'Save' | |
324 | }) | |
325 | }); | |
326 | expect(notificationShowSpy).toHaveBeenCalledWith(NotificationType.success, notificationMsg); | |
327 | }, | |
328 | quotaUpdateModalField: ( | |
329 | type: string, | |
330 | label: string, | |
331 | key: string, | |
332 | value: number, | |
333 | max: number, | |
334 | errors?: { [key: string]: string } | |
335 | ) => { | |
336 | expect(modalShowSpy).toHaveBeenCalledWith(FormModalComponent, { | |
337 | initialState: expect.objectContaining({ | |
338 | fields: [ | |
339 | { | |
340 | type, | |
341 | label, | |
342 | errors, | |
343 | name: key, | |
344 | value, | |
345 | validators: expect.anything(), | |
346 | required: true | |
347 | } | |
348 | ] | |
349 | }) | |
350 | }); | |
351 | if (type === 'binary') { | |
352 | expect(minBinaryValidator).toHaveBeenCalledWith(0); | |
353 | expect(maxBinaryValidator).toHaveBeenCalledWith(max); | |
354 | } else { | |
355 | expect(minValidator).toHaveBeenCalledWith(0); | |
356 | expect(maxValidator).toHaveBeenCalledWith(max); | |
357 | } | |
358 | } | |
359 | }; | |
360 | ||
361 | configureTestBed({ | |
362 | imports: [ | |
363 | HttpClientTestingModule, | |
364 | SharedModule, | |
365 | RouterTestingModule, | |
366 | TreeModule.forRoot(), | |
367 | NgBootstrapFormValidationModule.forRoot(), | |
368 | ToastrModule.forRoot(), | |
369 | ModalModule.forRoot() | |
370 | ], | |
371 | declarations: [CephfsDirectoriesComponent], | |
372 | providers: [i18nProviders, BsModalRef] | |
373 | }); | |
374 | ||
375 | beforeEach(() => { | |
376 | noAsyncUpdate = false; | |
377 | mockData = { | |
378 | nodes: [], | |
379 | parent: undefined, | |
380 | createdSnaps: [], | |
381 | deletedSnaps: [], | |
382 | createdDirs: [], | |
383 | updatedQuotas: {} | |
384 | }; | |
385 | ||
386 | cephfsService = TestBed.get(CephfsService); | |
387 | lsDirSpy = spyOn(cephfsService, 'lsDir').and.callFake(mockLib.lsDir); | |
388 | spyOn(cephfsService, 'mkSnapshot').and.callFake(mockLib.mkSnapshot); | |
389 | spyOn(cephfsService, 'rmSnapshot').and.callFake(mockLib.rmSnapshot); | |
390 | spyOn(cephfsService, 'updateQuota').and.callFake(mockLib.updateQuota); | |
391 | ||
392 | modalShowSpy = spyOn(TestBed.get(BsModalService), 'show').and.callFake(mockLib.modalShow); | |
393 | notificationShowSpy = spyOn(TestBed.get(NotificationService), 'show').and.stub(); | |
394 | ||
395 | fixture = TestBed.createComponent(CephfsDirectoriesComponent); | |
396 | component = fixture.componentInstance; | |
397 | fixture.detectChanges(); | |
398 | ||
399 | spyOn(TREE_ACTIONS, 'TOGGLE_ACTIVE').and.callFake(mockLib.treeActions.toggleActive); | |
400 | ||
401 | component.treeComponent = { | |
402 | sizeChanged: () => null, | |
403 | treeModel: { getNodeById: mockLib.getNodeById, update: () => null } | |
404 | } as TreeComponent; | |
405 | }); | |
406 | ||
407 | it('should create', () => { | |
408 | expect(component).toBeTruthy(); | |
409 | }); | |
410 | ||
411 | describe('mock self test', () => { | |
412 | it('tests snapshots mock', () => { | |
413 | expect(mockLib.snapshots('/a', 1).map((s) => ({ name: s.name, path: s.path }))).toEqual([ | |
414 | { | |
415 | name: 'someSnapshot1', | |
416 | path: '/a/.snap/someSnapshot1' | |
417 | } | |
418 | ]); | |
419 | expect(mockLib.snapshots('/a/b', 3).map((s) => ({ name: s.name, path: s.path }))).toEqual([ | |
420 | { | |
421 | name: 'someSnapshot1', | |
422 | path: '/a/b/.snap/someSnapshot1' | |
423 | }, | |
424 | { | |
425 | name: 'someSnapshot2', | |
426 | path: '/a/b/.snap/someSnapshot2' | |
427 | }, | |
428 | { | |
429 | name: 'someSnapshot3', | |
430 | path: '/a/b/.snap/someSnapshot3' | |
431 | } | |
432 | ]); | |
433 | }); | |
434 | ||
435 | it('tests dir mock', () => { | |
436 | const path = '/a/b/c'; | |
801d1391 TL |
437 | mockData.createdSnaps = [ |
438 | { path, name: 's1' }, | |
439 | { path, name: 's2' } | |
440 | ]; | |
441 | mockData.deletedSnaps = [ | |
442 | { path, name: 'someSnapshot2' }, | |
443 | { path, name: 's2' } | |
444 | ]; | |
9f95a23c TL |
445 | const dir = mockLib.dir('/a/b', 'c', 2); |
446 | expect(dir.path).toBe('/a/b/c'); | |
447 | expect(dir.parent).toBe('/a/b'); | |
448 | expect(dir.quotas).toEqual({ max_bytes: 2048, max_files: 20 }); | |
449 | expect(dir.snapshots.map((s) => s.name)).toEqual(['someSnapshot1', 's1']); | |
450 | }); | |
451 | ||
452 | it('tests lsdir mock', () => { | |
453 | let dirs: CephfsDir[] = []; | |
454 | mockLib.lsDir(2, '/a').subscribe((x) => (dirs = x)); | |
455 | expect(dirs.map((d) => d.path)).toEqual([ | |
456 | '/a/c', | |
457 | '/a/a', | |
458 | '/a/b', | |
459 | '/a/c/c', | |
460 | '/a/c/a', | |
461 | '/a/c/b', | |
462 | '/a/a/c', | |
463 | '/a/a/a', | |
464 | '/a/a/b' | |
465 | ]); | |
466 | }); | |
467 | ||
468 | describe('test quota update mock', () => { | |
469 | const PATH = '/a'; | |
470 | const ID = 2; | |
471 | ||
472 | const updateQuota = (quotas: CephfsQuotas) => mockLib.updateQuota(ID, PATH, quotas); | |
473 | ||
474 | const expectMockUpdate = (max_bytes?: number, max_files?: number) => | |
475 | expect(mockData.updatedQuotas[PATH]).toEqual({ | |
476 | max_bytes, | |
477 | max_files | |
478 | }); | |
479 | ||
480 | const expectLsUpdate = (max_bytes?: number, max_files?: number) => { | |
481 | let dir: CephfsDir; | |
482 | mockLib.lsDir(ID, '/').subscribe((dirs) => (dir = dirs.find((d) => d.path === PATH))); | |
483 | expect(dir.quotas).toEqual({ | |
484 | max_bytes, | |
485 | max_files | |
486 | }); | |
487 | }; | |
488 | ||
489 | it('tests to set quotas', () => { | |
490 | expectLsUpdate(1024, 10); | |
491 | ||
492 | updateQuota({ max_bytes: 512 }); | |
493 | expectMockUpdate(512); | |
494 | expectLsUpdate(512, 10); | |
495 | ||
496 | updateQuota({ max_files: 100 }); | |
497 | expectMockUpdate(512, 100); | |
498 | expectLsUpdate(512, 100); | |
499 | }); | |
500 | ||
501 | it('tests to unset quotas', () => { | |
502 | updateQuota({ max_files: 0 }); | |
503 | expectMockUpdate(undefined, 0); | |
504 | expectLsUpdate(1024, 0); | |
505 | ||
506 | updateQuota({ max_bytes: 0 }); | |
507 | expectMockUpdate(0, 0); | |
508 | expectLsUpdate(0, 0); | |
509 | }); | |
510 | }); | |
511 | }); | |
512 | ||
513 | it('calls lsDir only if an id exits', () => { | |
514 | assert.lsDirCalledTimes(0); | |
515 | ||
516 | mockLib.changeId(1); | |
517 | assert.lsDirCalledTimes(1); | |
518 | expect(lsDirSpy).toHaveBeenCalledWith(1, '/'); | |
519 | ||
520 | mockLib.changeId(2); | |
521 | assert.lsDirCalledTimes(2); | |
522 | expect(lsDirSpy).toHaveBeenCalledWith(2, '/'); | |
523 | }); | |
524 | ||
525 | describe('listing sub directories', () => { | |
526 | beforeEach(() => { | |
527 | mockLib.changeId(1); | |
528 | /** | |
529 | * Tree looks like this: | |
530 | * v / | |
531 | * > a | |
532 | * * b | |
533 | * > c | |
534 | * */ | |
535 | }); | |
536 | ||
537 | it('expands first level', () => { | |
538 | // Tree will only show '*' if nor 'loadChildren' or 'children' are defined | |
539 | expect( | |
540 | mockData.nodes.map((node: any) => ({ | |
541 | [node.id]: node.hasChildren || node.isExpanded || Boolean(node.children) | |
542 | })) | |
543 | ).toEqual([{ '/': true }, { '/a': true }, { '/b': false }, { '/c': true }]); | |
544 | }); | |
545 | ||
546 | it('resets all dynamic content on id change', () => { | |
547 | mockLib.selectNode('/a'); | |
548 | /** | |
549 | * Tree looks like this: | |
550 | * v / | |
551 | * v a <- Selected | |
552 | * > a | |
553 | * * b | |
554 | * > c | |
555 | * * b | |
556 | * > c | |
557 | * */ | |
558 | assert.requestedPaths(['/', '/a']); | |
559 | assert.nodeLength(7); | |
560 | assert.dirLength(16); | |
561 | expect(component.selectedDir).toBeDefined(); | |
562 | ||
563 | mockLib.changeId(undefined); | |
564 | assert.dirLength(0); | |
565 | assert.requestedPaths([]); | |
566 | expect(component.selectedDir).not.toBeDefined(); | |
567 | }); | |
568 | ||
569 | it('should select a node and show the directory contents', () => { | |
570 | mockLib.selectNode('/a'); | |
571 | const dir = get.dirs().find((d) => d.path === '/a'); | |
572 | expect(component.selectedDir).toEqual(dir); | |
573 | assert.quotaIsNotInherited('files', 10, 0); | |
574 | assert.quotaIsNotInherited('bytes', '1 KiB', 0); | |
575 | }); | |
576 | ||
577 | it('should extend the list by subdirectories when expanding', () => { | |
578 | mockLib.selectNode('/a'); | |
579 | mockLib.selectNode('/a/c'); | |
580 | /** | |
581 | * Tree looks like this: | |
582 | * v / | |
583 | * v a | |
584 | * > a | |
585 | * * b | |
586 | * v c <- Selected | |
587 | * > a | |
588 | * * b | |
589 | * > c | |
590 | * * b | |
591 | * > c | |
592 | * */ | |
593 | assert.lsDirCalledTimes(3); | |
594 | assert.requestedPaths(['/', '/a', '/a/c']); | |
595 | assert.dirLength(22); | |
596 | assert.nodeLength(10); | |
597 | }); | |
598 | ||
599 | it('should update the tree after each selection', () => { | |
600 | const spy = spyOn(component.treeComponent, 'sizeChanged').and.callThrough(); | |
601 | expect(spy).toHaveBeenCalledTimes(0); | |
602 | mockLib.selectNode('/a'); | |
603 | expect(spy).toHaveBeenCalledTimes(1); | |
604 | mockLib.selectNode('/a/c'); | |
605 | expect(spy).toHaveBeenCalledTimes(2); | |
606 | }); | |
607 | ||
608 | it('should select parent by path', () => { | |
609 | mockLib.selectNode('/a'); | |
610 | mockLib.selectNode('/a/c'); | |
611 | mockLib.selectNode('/a/c/a'); | |
612 | component.selectOrigin('/a'); | |
613 | expect(component.selectedDir.path).toBe('/a'); | |
614 | }); | |
615 | ||
616 | it('should refresh directories with no sub directories as they could have some now', () => { | |
617 | mockLib.selectNode('/b'); | |
618 | /** | |
619 | * Tree looks like this: | |
620 | * v / | |
621 | * > a | |
622 | * * b <- Selected | |
623 | * > c | |
624 | * */ | |
625 | assert.lsDirCalledTimes(2); | |
626 | assert.requestedPaths(['/', '/b']); | |
627 | assert.nodeLength(4); | |
628 | }); | |
629 | ||
630 | describe('used quotas', () => { | |
631 | it('should use no quota if none is set', () => { | |
801d1391 TL |
632 | mockLib.setFourQuotaDirs([ |
633 | [0, 0], | |
634 | [0, 0], | |
635 | [0, 0], | |
636 | [0, 0] | |
637 | ]); | |
9f95a23c TL |
638 | assert.noQuota('files'); |
639 | assert.noQuota('bytes'); | |
640 | assert.dirQuotas(0, 0); | |
641 | }); | |
642 | ||
643 | it('should use quota from upper parents', () => { | |
801d1391 TL |
644 | mockLib.setFourQuotaDirs([ |
645 | [100, 0], | |
646 | [0, 8], | |
647 | [0, 0], | |
648 | [0, 0] | |
649 | ]); | |
9f95a23c TL |
650 | assert.quotaIsInherited('files', 100, '/1'); |
651 | assert.quotaIsInherited('bytes', '8 KiB', '/1/2'); | |
652 | assert.dirQuotas(0, 0); | |
653 | }); | |
654 | ||
655 | it('should use quota from the parent with the lowest value (deep inheritance)', () => { | |
801d1391 TL |
656 | mockLib.setFourQuotaDirs([ |
657 | [200, 1], | |
658 | [100, 4], | |
659 | [400, 3], | |
660 | [300, 2] | |
661 | ]); | |
9f95a23c TL |
662 | assert.quotaIsInherited('files', 100, '/1/2'); |
663 | assert.quotaIsInherited('bytes', '1 KiB', '/1'); | |
664 | assert.dirQuotas(2048, 300); | |
665 | }); | |
666 | ||
667 | it('should use current value', () => { | |
801d1391 TL |
668 | mockLib.setFourQuotaDirs([ |
669 | [200, 2], | |
670 | [300, 4], | |
671 | [400, 3], | |
672 | [100, 1] | |
673 | ]); | |
9f95a23c TL |
674 | assert.quotaIsNotInherited('files', 100, 200); |
675 | assert.quotaIsNotInherited('bytes', '1 KiB', 2048); | |
676 | assert.dirQuotas(1024, 100); | |
677 | }); | |
678 | }); | |
679 | }); | |
680 | ||
681 | describe('snapshots', () => { | |
682 | beforeEach(() => { | |
683 | mockLib.changeId(1); | |
684 | mockLib.selectNode('/a'); | |
685 | }); | |
686 | ||
687 | it('should create a snapshot', () => { | |
688 | mockLib.createSnapshotThroughModal('newSnap'); | |
689 | expect(cephfsService.mkSnapshot).toHaveBeenCalledWith(1, '/a', 'newSnap'); | |
690 | assert.snapshotsByName(['someSnapshot1', 'newSnap']); | |
691 | }); | |
692 | ||
693 | it('should delete a snapshot', () => { | |
694 | mockLib.createSnapshotThroughModal('deleteMe'); | |
695 | mockLib.deleteSnapshotsThroughModal([component.selectedDir.snapshots[1]]); | |
696 | assert.snapshotsByName(['someSnapshot1']); | |
697 | }); | |
698 | ||
699 | it('should delete all snapshots', () => { | |
700 | mockLib.createSnapshotThroughModal('deleteAll'); | |
701 | mockLib.deleteSnapshotsThroughModal(component.selectedDir.snapshots); | |
702 | assert.snapshotsByName([]); | |
703 | }); | |
704 | }); | |
705 | ||
706 | it('should test all snapshot table actions combinations', () => { | |
707 | const permissionHelper: PermissionHelper = new PermissionHelper(component.permission); | |
708 | const tableActions = permissionHelper.setPermissionsAndGetActions( | |
709 | component.snapshot.tableActions | |
710 | ); | |
711 | ||
712 | expect(tableActions).toEqual({ | |
713 | 'create,update,delete': { | |
714 | actions: ['Create', 'Delete'], | |
715 | primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Create' } | |
716 | }, | |
717 | 'create,update': { | |
718 | actions: ['Create'], | |
719 | primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' } | |
720 | }, | |
721 | 'create,delete': { | |
722 | actions: ['Create', 'Delete'], | |
723 | primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Create' } | |
724 | }, | |
725 | create: { | |
726 | actions: ['Create'], | |
727 | primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' } | |
728 | }, | |
729 | 'update,delete': { | |
730 | actions: ['Delete'], | |
731 | primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' } | |
732 | }, | |
733 | update: { | |
734 | actions: [], | |
735 | primary: { multiple: '', executing: '', single: '', no: '' } | |
736 | }, | |
737 | delete: { | |
738 | actions: ['Delete'], | |
739 | primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' } | |
740 | }, | |
741 | 'no-permissions': { | |
742 | actions: [], | |
743 | primary: { multiple: '', executing: '', single: '', no: '' } | |
744 | } | |
745 | }); | |
746 | }); | |
747 | ||
748 | describe('quotas', () => { | |
749 | beforeEach(() => { | |
750 | // Spies | |
751 | minValidator = spyOn(Validators, 'min').and.callThrough(); | |
752 | maxValidator = spyOn(Validators, 'max').and.callThrough(); | |
753 | minBinaryValidator = spyOn(CdValidators, 'binaryMin').and.callThrough(); | |
754 | maxBinaryValidator = spyOn(CdValidators, 'binaryMax').and.callThrough(); | |
755 | // Select /a/c/b | |
756 | mockLib.changeId(1); | |
757 | mockLib.selectNode('/a'); | |
758 | mockLib.selectNode('/a/c'); | |
759 | mockLib.selectNode('/a/c/b'); | |
760 | // Quotas after selection | |
761 | assert.quotaIsInherited('files', 10, '/a'); | |
762 | assert.quotaIsInherited('bytes', '1 KiB', '/a'); | |
763 | assert.dirQuotas(2048, 20); | |
764 | }); | |
765 | ||
766 | describe('update modal', () => { | |
767 | describe('max_files', () => { | |
768 | beforeEach(() => { | |
769 | mockLib.updateQuotaThroughModal('max_files', 5); | |
770 | }); | |
771 | ||
772 | it('should update max_files correctly', () => { | |
773 | expect(cephfsService.updateQuota).toHaveBeenCalledWith(1, '/a/c/b', { max_files: 5 }); | |
774 | assert.quotaIsNotInherited('files', 5, 10); | |
775 | }); | |
776 | ||
777 | it('uses the correct form field', () => { | |
778 | assert.quotaUpdateModalField('number', 'Max files', 'max_files', 20, 10, { | |
779 | min: 'Value has to be at least 0 or more', | |
780 | max: 'Value has to be at most 10 or less' | |
781 | }); | |
782 | }); | |
783 | ||
784 | it('shows the right texts', () => { | |
785 | assert.quotaUpdateModalTexts( | |
786 | "Update CephFS files quota for '/a/c/b'", | |
787 | "The inherited files quota 10 from '/a' is the maximum value to be used.", | |
788 | "Updated CephFS files quota for '/a/c/b'" | |
789 | ); | |
790 | }); | |
791 | }); | |
792 | ||
793 | describe('max_bytes', () => { | |
794 | beforeEach(() => { | |
795 | mockLib.updateQuotaThroughModal('max_bytes', 512); | |
796 | }); | |
797 | ||
798 | it('should update max_files correctly', () => { | |
799 | expect(cephfsService.updateQuota).toHaveBeenCalledWith(1, '/a/c/b', { max_bytes: 512 }); | |
800 | assert.quotaIsNotInherited('bytes', '512 B', 1024); | |
801 | }); | |
802 | ||
803 | it('uses the correct form field', () => { | |
804 | mockLib.updateQuotaThroughModal('max_bytes', 512); | |
805 | assert.quotaUpdateModalField('binary', 'Max size', 'max_bytes', 2048, 1024); | |
806 | }); | |
807 | ||
808 | it('shows the right texts', () => { | |
809 | assert.quotaUpdateModalTexts( | |
810 | "Update CephFS size quota for '/a/c/b'", | |
811 | "The inherited size quota 1 KiB from '/a' is the maximum value to be used.", | |
812 | "Updated CephFS size quota for '/a/c/b'" | |
813 | ); | |
814 | }); | |
815 | }); | |
816 | ||
817 | describe('action behaviour', () => { | |
818 | it('opens with next maximum as maximum if directory holds the current maximum', () => { | |
819 | mockLib.updateQuotaThroughModal('max_bytes', 512); | |
820 | mockLib.updateQuotaThroughModal('max_bytes', 888); | |
821 | assert.quotaUpdateModalField('binary', 'Max size', 'max_bytes', 512, 1024); | |
822 | }); | |
823 | ||
824 | it("uses 'Set' action instead of 'Update' if the quota is not set (0)", () => { | |
825 | mockLib.updateQuotaThroughModal('max_bytes', 0); | |
826 | mockLib.updateQuotaThroughModal('max_bytes', 200); | |
827 | assert.quotaUpdateModalTexts( | |
828 | "Set CephFS size quota for '/a/c/b'", | |
829 | "The inherited size quota 1 KiB from '/a' is the maximum value to be used.", | |
830 | "Set CephFS size quota for '/a/c/b'" | |
831 | ); | |
832 | }); | |
833 | }); | |
834 | }); | |
835 | ||
836 | describe('unset modal', () => { | |
837 | describe('max_files', () => { | |
838 | beforeEach(() => { | |
839 | mockLib.updateQuotaThroughModal('max_files', 5); // Sets usable quota | |
840 | mockLib.unsetQuotaThroughModal('max_files'); | |
841 | }); | |
842 | ||
843 | it('should unset max_files correctly', () => { | |
844 | expect(cephfsService.updateQuota).toHaveBeenCalledWith(1, '/a/c/b', { max_files: 0 }); | |
845 | assert.dirQuotas(2048, 0); | |
846 | }); | |
847 | ||
848 | it('shows the right texts', () => { | |
849 | assert.quotaUnsetModalTexts( | |
850 | "Unset CephFS files quota for '/a/c/b'", | |
851 | "Unset files quota 5 from '/a/c/b' in order to inherit files quota 10 from '/a'.", | |
852 | "Unset CephFS files quota for '/a/c/b'" | |
853 | ); | |
854 | }); | |
855 | }); | |
856 | ||
857 | describe('max_bytes', () => { | |
858 | beforeEach(() => { | |
859 | mockLib.updateQuotaThroughModal('max_bytes', 512); // Sets usable quota | |
860 | mockLib.unsetQuotaThroughModal('max_bytes'); | |
861 | }); | |
862 | ||
863 | it('should unset max_files correctly', () => { | |
864 | expect(cephfsService.updateQuota).toHaveBeenCalledWith(1, '/a/c/b', { max_bytes: 0 }); | |
865 | assert.dirQuotas(0, 20); | |
866 | }); | |
867 | ||
868 | it('shows the right texts', () => { | |
869 | assert.quotaUnsetModalTexts( | |
870 | "Unset CephFS size quota for '/a/c/b'", | |
871 | "Unset size quota 512 B from '/a/c/b' in order to inherit size quota 1 KiB from '/a'.", | |
872 | "Unset CephFS size quota for '/a/c/b'" | |
873 | ); | |
874 | }); | |
875 | }); | |
876 | ||
877 | describe('action behaviour', () => { | |
878 | it('uses different Text if no quota is inherited', () => { | |
879 | mockLib.selectNode('/a'); | |
880 | mockLib.unsetQuotaThroughModal('max_bytes'); | |
881 | assert.quotaUnsetModalTexts( | |
882 | "Unset CephFS size quota for '/a'", | |
883 | "Unset size quota 1 KiB from '/a' in order to have no quota on the directory.", | |
884 | "Unset CephFS size quota for '/a'" | |
885 | ); | |
886 | }); | |
887 | ||
888 | it('uses different Text if quota is already inherited', () => { | |
889 | mockLib.unsetQuotaThroughModal('max_bytes'); | |
890 | assert.quotaUnsetModalTexts( | |
891 | "Unset CephFS size quota for '/a/c/b'", | |
892 | "Unset size quota 2 KiB from '/a/c/b' which isn't used because of the inheritance " + | |
893 | "of size quota 1 KiB from '/a'.", | |
894 | "Unset CephFS size quota for '/a/c/b'" | |
895 | ); | |
896 | }); | |
897 | }); | |
898 | }); | |
899 | }); | |
900 | ||
901 | describe('table actions', () => { | |
902 | let actions: CdTableAction[]; | |
903 | ||
904 | const empty = (): CdTableSelection => new CdTableSelection(); | |
905 | ||
906 | const select = (value: number): CdTableSelection => { | |
907 | const selection = new CdTableSelection(); | |
908 | selection.selected = [{ dirValue: value }]; | |
909 | return selection; | |
910 | }; | |
911 | ||
912 | beforeEach(() => { | |
913 | actions = component.quota.tableActions; | |
914 | }); | |
915 | ||
916 | it("shows 'Set' for empty and not set quotas", () => { | |
917 | const isSetVisible = actions[0].visible; | |
918 | expect(isSetVisible(empty())).toBe(true); | |
919 | expect(isSetVisible(select(0))).toBe(true); | |
920 | expect(isSetVisible(select(1))).toBe(false); | |
921 | }); | |
922 | ||
923 | it("shows 'Update' for set quotas only", () => { | |
924 | const isUpdateVisible = actions[1].visible; | |
925 | expect(isUpdateVisible(empty())).toBeFalsy(); | |
926 | expect(isUpdateVisible(select(0))).toBe(false); | |
927 | expect(isUpdateVisible(select(1))).toBe(true); | |
928 | }); | |
929 | ||
930 | it("only enables 'Unset' for set quotas only", () => { | |
931 | const isUnsetDisabled = actions[2].disable; | |
932 | expect(isUnsetDisabled(empty())).toBe(true); | |
933 | expect(isUnsetDisabled(select(0))).toBe(true); | |
934 | expect(isUnsetDisabled(select(1))).toBe(false); | |
935 | }); | |
936 | ||
937 | it('should test all quota table actions permission combinations', () => { | |
938 | const permissionHelper: PermissionHelper = new PermissionHelper(component.permission); | |
939 | const tableActions = permissionHelper.setPermissionsAndGetActions( | |
940 | component.quota.tableActions | |
941 | ); | |
942 | ||
943 | expect(tableActions).toEqual({ | |
944 | 'create,update,delete': { | |
945 | actions: ['Set', 'Update', 'Unset'], | |
946 | primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' } | |
947 | }, | |
948 | 'create,update': { | |
949 | actions: ['Set', 'Update', 'Unset'], | |
950 | primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' } | |
951 | }, | |
952 | 'create,delete': { | |
953 | actions: [], | |
954 | primary: { multiple: '', executing: '', single: '', no: '' } | |
955 | }, | |
956 | create: { | |
957 | actions: [], | |
958 | primary: { multiple: '', executing: '', single: '', no: '' } | |
959 | }, | |
960 | 'update,delete': { | |
961 | actions: ['Set', 'Update', 'Unset'], | |
962 | primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' } | |
963 | }, | |
964 | update: { | |
965 | actions: ['Set', 'Update', 'Unset'], | |
966 | primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' } | |
967 | }, | |
968 | delete: { | |
969 | actions: [], | |
970 | primary: { multiple: '', executing: '', single: '', no: '' } | |
971 | }, | |
972 | 'no-permissions': { | |
973 | actions: [], | |
974 | primary: { multiple: '', executing: '', single: '', no: '' } | |
975 | } | |
976 | }); | |
977 | }); | |
978 | }); | |
979 | ||
980 | describe('reload all', () => { | |
981 | const calledPaths = ['/', '/a', '/a/c', '/a/c/a', '/a/c/a/b']; | |
982 | ||
983 | const dirsByPath = (): string[] => get.dirs().map((d) => d.path); | |
984 | ||
985 | beforeEach(() => { | |
986 | mockLib.changeId(1); | |
987 | mockLib.selectNode('/a'); | |
988 | mockLib.selectNode('/a/c'); | |
989 | mockLib.selectNode('/a/c/a'); | |
990 | mockLib.selectNode('/a/c/a/b'); | |
991 | }); | |
992 | ||
993 | it('should reload all requested paths', () => { | |
994 | assert.lsDirHasBeenCalledWith(1, calledPaths); | |
995 | lsDirSpy.calls.reset(); | |
996 | assert.lsDirHasBeenCalledWith(1, []); | |
997 | component.refreshAllDirectories(); | |
998 | assert.lsDirHasBeenCalledWith(1, calledPaths); | |
999 | }); | |
1000 | ||
1001 | it('should reload all requested paths if not selected anything', () => { | |
1002 | lsDirSpy.calls.reset(); | |
1003 | mockLib.changeId(2); | |
1004 | assert.lsDirHasBeenCalledWith(2, ['/']); | |
1005 | lsDirSpy.calls.reset(); | |
1006 | component.refreshAllDirectories(); | |
1007 | assert.lsDirHasBeenCalledWith(2, ['/']); | |
1008 | }); | |
1009 | ||
1010 | it('should add new directories', () => { | |
1011 | // Create two new directories in preparation | |
1012 | const dirsBeforeRefresh = dirsByPath(); | |
1013 | expect(dirsBeforeRefresh.includes('/a/c/has_dir_now')).toBe(false); | |
1014 | mockLib.mkDir('/a/c', 'has_dir_now', 0, 0); | |
1015 | mockLib.mkDir('/a/c/a/b', 'has_dir_now_too', 0, 0); | |
1016 | // Now the new directories will be fetched | |
1017 | component.refreshAllDirectories(); | |
1018 | const dirsAfterRefresh = dirsByPath(); | |
1019 | expect(dirsAfterRefresh.length - dirsBeforeRefresh.length).toBe(2); | |
1020 | expect(dirsAfterRefresh.includes('/a/c/has_dir_now')).toBe(true); | |
1021 | expect(dirsAfterRefresh.includes('/a/c/a/b/has_dir_now_too')).toBe(true); | |
1022 | }); | |
1023 | ||
1024 | it('should remove deleted directories', () => { | |
1025 | // Create one new directory and refresh in order to have it added to the directories list | |
1026 | mockLib.mkDir('/a/c', 'will_be_removed_shortly', 0, 0); | |
1027 | component.refreshAllDirectories(); | |
1028 | const dirsBeforeRefresh = dirsByPath(); | |
1029 | expect(dirsBeforeRefresh.includes('/a/c/will_be_removed_shortly')).toBe(true); | |
1030 | mockData.createdDirs = []; // Mocks the deletion of the directory | |
1031 | // Now the deleted directory will be missing on refresh | |
1032 | component.refreshAllDirectories(); | |
1033 | const dirsAfterRefresh = dirsByPath(); | |
1034 | expect(dirsAfterRefresh.length - dirsBeforeRefresh.length).toBe(-1); | |
1035 | expect(dirsAfterRefresh.includes('/a/c/will_be_removed_shortly')).toBe(false); | |
1036 | }); | |
1037 | ||
1038 | describe('loading indicator', () => { | |
1039 | beforeEach(() => { | |
1040 | noAsyncUpdate = true; | |
1041 | }); | |
1042 | ||
1043 | it('should have set loading indicator to false after refreshing all dirs', fakeAsync(() => { | |
1044 | component.refreshAllDirectories(); | |
1045 | expect(component.loadingIndicator).toBe(true); | |
1046 | tick(3000); // To resolve all promises | |
1047 | expect(component.loadingIndicator).toBe(false); | |
1048 | })); | |
1049 | ||
1050 | it('should only update the tree once and not on every call', fakeAsync(() => { | |
1051 | const spy = spyOn(component.treeComponent, 'sizeChanged').and.callThrough(); | |
1052 | component.refreshAllDirectories(); | |
1053 | expect(spy).toHaveBeenCalledTimes(0); | |
1054 | tick(3000); // To resolve all promises | |
1055 | // Called during the interval and at the end of timeout | |
1056 | expect(spy).toHaveBeenCalledTimes(2); | |
1057 | })); | |
1058 | ||
1059 | it('should have set all loaded dirs as attribute names of "indicators"', () => { | |
1060 | noAsyncUpdate = false; | |
1061 | component.refreshAllDirectories(); | |
1062 | expect(Object.keys(component.loading).sort()).toEqual(calledPaths); | |
1063 | }); | |
1064 | ||
1065 | it('should set an indicator to true during load', () => { | |
1066 | lsDirSpy.and.callFake(() => Observable.create((): null => null)); | |
1067 | component.refreshAllDirectories(); | |
1068 | expect(Object.values(component.loading).every((b) => b)).toBe(true); | |
1069 | expect(component.loadingIndicator).toBe(true); | |
1070 | }); | |
1071 | }); | |
1072 | }); | |
1073 | }); |