baaa80aeaf9ec5c662183577e53791f64b2ef6b1
[portal/nonrtric-controlpanel.git] / webapp-frontend / src / app / policy / policy-instance / policy-instance.component.spec.ts
1 /*-
2  * ========================LICENSE_START=================================
3  * O-RAN-SC
4  * %%
5  * Copyright (C) 2020 Nordix Foundation
6  * %%
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ========================LICENSE_END===================================
19  */
20
21 import { HarnessLoader } from "@angular/cdk/testing";
22 import { TestbedHarnessEnvironment } from "@angular/cdk/testing/testbed";
23 import { HttpResponse } from "@angular/common/http";
24 import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
25 import { ComponentFixture, TestBed } from "@angular/core/testing";
26 import { FormsModule, ReactiveFormsModule } from "@angular/forms";
27 import { MatButtonHarness } from "@angular/material/button/testing";
28 import { MatDialog } from "@angular/material/dialog";
29 import { MatIconModule } from "@angular/material/icon";
30 import { MatInputHarness } from "@angular/material/input/testing";
31 import { MatSortModule } from "@angular/material/sort";
32 import { MatSortHarness } from "@angular/material/sort/testing";
33 import { MatTableModule } from "@angular/material/table";
34 import { MatTableHarness } from "@angular/material/table/testing";
35 import { By } from "@angular/platform-browser";
36 import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
37 import {
38   PolicyInstance,
39   PolicyInstances,
40   PolicyStatus,
41   PolicyTypeSchema,
42 } from "@interfaces/policy.types";
43 import { PolicyService } from "@services/policy/policy.service";
44 import { ConfirmDialogService } from "@services/ui/confirm-dialog.service";
45 import { NotificationService } from "@services/ui/notification.service";
46 import { UiService } from "@services/ui/ui.service";
47 import { ToastrModule } from "ngx-toastr";
48 import { Observable, of } from "rxjs";
49 import { PolicyInstanceDialogComponent } from "../policy-instance-dialog/policy-instance-dialog.component";
50 import { PolicyInstanceComponent } from "./policy-instance.component";
51
52 const lastModifiedTime = "2021-01-26T13:15:11.895297Z";
53 describe("PolicyInstanceComponent", () => {
54   let hostComponent: PolicyInstanceComponentHostComponent;
55   let componentUnderTest: PolicyInstanceComponent;
56   let hostFixture: ComponentFixture<PolicyInstanceComponentHostComponent>;
57   let loader: HarnessLoader;
58   let policyServiceSpy: jasmine.SpyObj<PolicyService>;
59   let dialogSpy: jasmine.SpyObj<MatDialog>;
60   let notificationServiceSpy: jasmine.SpyObj<NotificationService>;
61   let confirmServiceSpy: jasmine.SpyObj<ConfirmDialogService>;
62
63   const policyInstances = {
64     policy_ids: ["policy1", "policy2"],
65   } as PolicyInstances;
66   const policyTypeSchema = JSON.parse(
67     '{"title": "1", "description": "Type 1 policy type"}'
68   );
69   const policy1 = {
70     policy_id: "policy1",
71     policy_data: "{}",
72     ric_id: "1",
73     service_id: "service",
74     lastModified: "Now",
75   } as PolicyInstance;
76   const policy2 = {
77     policy_id: "policy2",
78     policy_data: "{}",
79     ric_id: "2",
80     service_id: "service",
81     lastModified: "Now",
82   } as PolicyInstance;
83   const policy1Status = {
84     last_modified: lastModifiedTime,
85   } as PolicyStatus;
86   const policy2Status = {
87     last_modified: lastModifiedTime,
88   } as PolicyStatus;
89
90   const policyIdToInstanceMap = {
91     policy1: policy1,
92     policy2: policy2,
93   };
94   const policyIdToStatusMap = {
95     policy1: policy1Status,
96     policy2: policy2Status,
97   };
98
99   @Component({
100     selector: "policy-instance-compnent-host-component",
101     template:
102       "<nrcp-policy-instance [policyTypeSchema]=policyType></nrcp-policy-instance>",
103   })
104   class PolicyInstanceComponentHostComponent {
105     policyType = {
106       id: "type1",
107       name: "1",
108       schemaObject: policyTypeSchema,
109     } as PolicyTypeSchema;
110   }
111
112   beforeEach(async () => {
113     policyServiceSpy = jasmine.createSpyObj("PolicyService", [
114       "getPolicyInstancesByType",
115       "getPolicyInstance",
116       "getPolicyStatus",
117       "deletePolicy",
118     ]);
119
120     dialogSpy = jasmine.createSpyObj("MatDialog", ["open"]);
121     notificationServiceSpy = jasmine.createSpyObj("NotificationService", [
122       "success",
123       "warn",
124     ]);
125     confirmServiceSpy = jasmine.createSpyObj("ConfirmDialogService", [
126       "openConfirmDialog",
127     ]);
128
129     TestBed.configureTestingModule({
130       imports: [
131         BrowserAnimationsModule,
132         FormsModule,
133         MatIconModule,
134         MatSortModule,
135         MatTableModule,
136         ReactiveFormsModule,
137         ToastrModule.forRoot(),
138       ],
139       schemas: [CUSTOM_ELEMENTS_SCHEMA],
140       declarations: [
141         PolicyInstanceComponent,
142         PolicyInstanceComponentHostComponent,
143       ],
144       providers: [
145         { provide: PolicyService, useValue: policyServiceSpy },
146         { provide: MatDialog, useValue: dialogSpy },
147         { provide: NotificationService, useValue: notificationServiceSpy },
148         { provide: ConfirmDialogService, useValue: confirmServiceSpy },
149         UiService,
150       ],
151     });
152   });
153
154   describe("content and dialogs", () => {
155     beforeEach(() => {
156       policyServiceSpy.getPolicyInstancesByType.and.returnValue(
157         of(policyInstances)
158       );
159       policyServiceSpy.getPolicyInstance.and.callFake(function (
160         policyId: string
161       ) {
162         return of(policyIdToInstanceMap[policyId]);
163       });
164       policyServiceSpy.getPolicyStatus.and.callFake(function (
165         policyId: string
166       ) {
167         return of(policyIdToStatusMap[policyId]);
168       });
169
170       compileAndGetComponents();
171     });
172
173     it("should create", () => {
174       expect(hostComponent).toBeTruthy();
175
176       expect(componentUnderTest).toBeTruthy();
177     });
178
179     it("should set correct dark mode from UIService", () => {
180       const uiService: UiService = TestBed.inject(UiService);
181       expect(componentUnderTest.darkMode).toBeTruthy();
182
183       uiService.darkModeState.next(false);
184       hostFixture.detectChanges();
185       expect(componentUnderTest.darkMode).toBeFalsy();
186     });
187
188     it("should contain number of instances heading and value, create and refresh buttons, and policies table", async () => {
189       const instancesHeading = hostFixture.debugElement.nativeElement.querySelector(
190         "div"
191       );
192       expect(instancesHeading.innerText).toContain("Number of instances: 2");
193
194       const createButton: MatButtonHarness = await loader.getHarness(
195         MatButtonHarness.with({ selector: "#createButton" })
196       );
197       expect(createButton).toBeTruthy();
198       const createIcon = hostFixture.debugElement.nativeElement.querySelector(
199         "#createIcon"
200       );
201       expect(createIcon.innerText).toContain("add_box");
202
203       const refreshButton: MatButtonHarness = await loader.getHarness(
204         MatButtonHarness.with({ selector: "#refreshButton" })
205       );
206       expect(refreshButton).toBeTruthy();
207       const refreshIcon = hostFixture.debugElement.nativeElement.querySelector(
208         "#refreshIcon"
209       );
210       expect(refreshIcon.innerText).toContain("refresh");
211
212       const policiesTable = await loader.getHarness(
213         MatTableHarness.with({ selector: "#policiesTable" })
214       );
215       expect(policiesTable).toBeTruthy();
216     });
217
218     it("should open dialog to create policy and refresh policies after successful creation", async () => {
219       const dialogRefSpy = setupDialogRefSpy();
220       dialogSpy.open.and.returnValue(dialogRefSpy);
221
222       spyOn(componentUnderTest, "getPolicyInstances");
223
224       const createButton: MatButtonHarness = await loader.getHarness(
225         MatButtonHarness.with({ selector: "#createButton" })
226       );
227       await createButton.click();
228
229       expect(dialogSpy.open).toHaveBeenCalledWith(
230         PolicyInstanceDialogComponent,
231         {
232           maxWidth: "1200px",
233           maxHeight: "900px",
234           width: "900px",
235           role: "dialog",
236           disableClose: false,
237           panelClass: "dark-theme",
238           data: {
239             createSchema: policyTypeSchema,
240             instanceId: null,
241             instanceJson: null,
242             name: "1",
243             ric: null,
244           },
245         }
246       );
247       expect(componentUnderTest.getPolicyInstances).toHaveBeenCalled();
248     });
249
250     it("should open dialog to edit policy and refresh policies after successful update", async () => {
251       const dialogRefSpy = setupDialogRefSpy();
252       dialogSpy.open.and.returnValue(dialogRefSpy);
253
254       spyOn(componentUnderTest, "getPolicyInstances");
255
256       const editButton: MatButtonHarness = await loader.getHarness(
257         MatButtonHarness.with({ selector: "#policy1EditButton" })
258       );
259       await editButton.click();
260
261       expect(dialogSpy.open).toHaveBeenCalledWith(
262         PolicyInstanceDialogComponent,
263         {
264           maxWidth: "1200px",
265           maxHeight: "900px",
266           width: "900px",
267           role: "dialog",
268           disableClose: false,
269           panelClass: "dark-theme",
270           data: {
271             createSchema: policyTypeSchema,
272             instanceId: "policy1",
273             instanceJson: "{}",
274             name: "1",
275             ric: "1",
276           },
277         }
278       );
279       expect(componentUnderTest.getPolicyInstances).toHaveBeenCalled();
280     });
281
282     it("should open dialog to edit policy and not refresh policies when dialog closed wihtout submit", async () => {
283       const dialogRefSpy = setupDialogRefSpy(false);
284       dialogSpy.open.and.returnValue(dialogRefSpy);
285
286       spyOn(componentUnderTest, "getPolicyInstances");
287
288       const editButton: MatButtonHarness = await loader.getHarness(
289         MatButtonHarness.with({ selector: "#policy1EditButton" })
290       );
291       await editButton.click();
292
293       expect(componentUnderTest.getPolicyInstances).not.toHaveBeenCalled();
294     });
295
296     it("should open instance dialog when clicking in any policy cell in table", async () => {
297       spyOn(componentUnderTest, "modifyInstance");
298
299       const policiesTable = await loader.getHarness(
300         MatTableHarness.with({ selector: "#policiesTable" })
301       );
302       const firstRow = (await policiesTable.getRows())[0];
303       const idCell = (await firstRow.getCells())[0];
304       (await idCell.host()).click();
305       const ownerCell = (await firstRow.getCells())[1];
306       (await ownerCell.host()).click();
307       const serviceCell = (await firstRow.getCells())[2];
308       (await serviceCell.host()).click();
309       const lastModifiedCell = (await firstRow.getCells())[3];
310       (await lastModifiedCell.host()).click();
311
312       // Totally unnecessary call just to make the bloody framework count the number of calls to the spy correctly!
313       await policiesTable.getRows();
314
315       expect(componentUnderTest.modifyInstance).toHaveBeenCalledTimes(4);
316     });
317
318     it("should open dialog asking for delete and delete when ok response and refresh table afterwards", async () => {
319       const dialogRefSpy = setupDialogRefSpy();
320       confirmServiceSpy.openConfirmDialog.and.returnValue(dialogRefSpy);
321       const createResponse = { status: 204 } as HttpResponse<Object>;
322       policyServiceSpy.deletePolicy.and.returnValue(of(createResponse));
323
324       spyOn(componentUnderTest, "getPolicyInstances");
325       const deleteButton: MatButtonHarness = await loader.getHarness(
326         MatButtonHarness.with({ selector: "#policy1DeleteButton" })
327       );
328       await deleteButton.click();
329
330       expect(confirmServiceSpy.openConfirmDialog).toHaveBeenCalledWith(
331         "Delete Policy",
332         "Are you sure you want to delete this policy instance?"
333       );
334       expect(policyServiceSpy.deletePolicy).toHaveBeenCalledWith("policy1");
335       expect(notificationServiceSpy.success).toHaveBeenCalledWith(
336         "Delete succeeded!"
337       );
338       expect(componentUnderTest.getPolicyInstances).toHaveBeenCalled();
339     });
340
341     it("should open dialog asking for delete and not delete whith Cancel as response", async () => {
342       const dialogRefSpy = setupDialogRefSpy(false);
343       confirmServiceSpy.openConfirmDialog.and.returnValue(dialogRefSpy);
344
345       const deleteButton: MatButtonHarness = await loader.getHarness(
346         MatButtonHarness.with({ selector: "#policy1DeleteButton" })
347       );
348       await deleteButton.click();
349
350       expect(policyServiceSpy.deletePolicy).not.toHaveBeenCalled();
351     });
352
353     it("should refresh table", async () => {
354       spyOn(componentUnderTest, "getPolicyInstances");
355
356       const refreshButton: MatButtonHarness = await loader.getHarness(
357         MatButtonHarness.with({ selector: "#refreshButton" })
358       );
359       await refreshButton.click();
360
361       expect(componentUnderTest.getPolicyInstances).toHaveBeenCalled();
362     });
363   });
364
365   describe("no instances", () => {
366     beforeEach(() => {
367       policyServiceSpy.getPolicyInstancesByType.and.returnValue(
368         of({
369           policy_ids: [],
370         } as PolicyInstances)
371       );
372
373       compileAndGetComponents();
374     });
375
376     it("should display message of no instances", async () => {
377       const policiesTable = await loader.getHarness(
378         MatTableHarness.with({ selector: "#policiesTable" })
379       );
380       const footerRows = await policiesTable.getFooterRows();
381       const footerRow = footerRows[0];
382       const footerRowHost = await footerRow.host();
383
384       expect(await footerRowHost.hasClass("display-none")).toBeFalsy();
385       const footerTexts = await footerRow.getCellTextByColumnName();
386       expect(footerTexts["noRecordsFound"]).toEqual("No records found.");
387     });
388   });
389
390   describe("#policiesTable", () => {
391     const expectedPolicy1Row = {
392       instanceId: "policy1",
393       ric: "1",
394       service: "service",
395       lastModified: toLocalTime(lastModifiedTime),
396       action: "editdelete",
397     };
398
399     beforeEach(() => {
400       policyServiceSpy.getPolicyInstancesByType.and.returnValue(
401         of(policyInstances)
402       );
403       policyServiceSpy.getPolicyInstance.and.callFake(function (
404         policyId: string
405       ) {
406         return of(policyIdToInstanceMap[policyId]);
407       });
408       policyServiceSpy.getPolicyStatus.and.callFake(function (
409         policyId: string
410       ) {
411         return of(policyIdToStatusMap[policyId]);
412       });
413
414       compileAndGetComponents();
415     });
416
417     it("should contain correct headings", async () => {
418       const policiesTable = await loader.getHarness(
419         MatTableHarness.with({ selector: "#policiesTable" })
420       );
421       const headerRow = (await policiesTable.getHeaderRows())[0];
422       const headers = await headerRow.getCellTextByColumnName();
423
424       expect(headers).toEqual({
425         instanceId: "Instance",
426         ric: "Target",
427         service: "Owner",
428         lastModified: "Last modified",
429         action: "Action",
430       });
431     });
432
433     it("should contain data after initialization", async () => {
434       const expectedJobRows = [
435         expectedPolicy1Row,
436         {
437           instanceId: "policy2",
438           ric: "2",
439           service: "service",
440           lastModified: toLocalTime(lastModifiedTime),
441           action: "editdelete",
442         },
443       ];
444       const policiesTable = await loader.getHarness(
445         MatTableHarness.with({ selector: "#policiesTable" })
446       );
447       const policyRows = await policiesTable.getRows();
448       expect(policyRows.length).toEqual(2);
449       policyRows.forEach((row) => {
450         row.getCellTextByColumnName().then((values) => {
451           expect(expectedJobRows).toContain(jasmine.objectContaining(values));
452         });
453       });
454
455       // No message about no entries
456       const footerRows = await policiesTable.getFooterRows();
457       const footerRow = await footerRows[0];
458       const footerRowHost = await footerRow.host();
459
460       expect(await footerRowHost.hasClass("display-none")).toBeTruthy();
461     });
462
463     it("should have filtering for all four policy data headings", async () => {
464       const policiesTable = await loader.getHarness(
465         MatTableHarness.with({ selector: "#policiesTable" })
466       );
467
468       const idFilterInput = await loader.getHarness(
469         MatInputHarness.with({ selector: "#policyInstanceIdFilter" })
470       );
471       await idFilterInput.setValue("1");
472       const policyRows = await policiesTable.getRows();
473       expect(policyRows.length).toEqual(1);
474       expect(await policyRows[0].getCellTextByColumnName()).toEqual(
475         expectedPolicy1Row
476       );
477
478       const targetFilterInput = await loader.getHarness(
479         MatInputHarness.with({ selector: "#policyInstanceTargetFilter" })
480       );
481       expect(targetFilterInput).toBeTruthy();
482
483       const ownerFilterInput = await loader.getHarness(
484         MatInputHarness.with({ selector: "#policyInstanceOwnerFilter" })
485       );
486       expect(ownerFilterInput).toBeTruthy();
487
488       const lastModifiedFilterInput = await loader.getHarness(
489         MatInputHarness.with({ selector: "#policyInstanceLastModifiedFilter" })
490       );
491       expect(lastModifiedFilterInput).toBeTruthy();
492     });
493
494     it("should not sort when click in filter inputs", async () => {
495       spyOn(componentUnderTest, "stopSort").and.callThrough();
496
497       const idFilterInputDiv = hostFixture.debugElement.nativeElement.querySelector(
498         "#idSortStop"
499       );
500       idFilterInputDiv.click();
501
502       const targetFilterInputDiv = hostFixture.debugElement.nativeElement.querySelector(
503         "#targetSortStop"
504       );
505       targetFilterInputDiv.click();
506
507       const ownerFilterInputDiv = hostFixture.debugElement.nativeElement.querySelector(
508         "#ownerSortStop"
509       );
510       ownerFilterInputDiv.click();
511
512       const lastModifiedFilterInputDiv = hostFixture.debugElement.nativeElement.querySelector(
513         "#lastModifiedSortStop"
514       );
515       lastModifiedFilterInputDiv.click();
516
517       expect(componentUnderTest.stopSort).toHaveBeenCalledTimes(4);
518
519       const eventSpy = jasmine.createSpyObj("any", ["stopPropagation"]);
520       componentUnderTest.stopSort(eventSpy);
521       expect(eventSpy.stopPropagation).toHaveBeenCalled();
522     });
523
524     describe("#sorting", () => {
525       it("should verify sort functionality on the table", async () => {
526         const sort = await loader.getHarness(MatSortHarness);
527         const headers = await sort.getSortHeaders({ sortDirection: "" });
528         expect(headers.length).toBe(4);
529
530         await headers[0].click();
531         expect(await headers[0].isActive()).toBe(true);
532         expect(await headers[0].getSortDirection()).toBe("asc");
533
534         await headers[0].click();
535         expect(await headers[0].getSortDirection()).toBe("desc");
536       });
537
538       it("should sort table asc and desc by first header", async () => {
539         const sort = await loader.getHarness(MatSortHarness);
540         const policyTable = await loader.getHarness(
541           MatTableHarness.with({ selector: "#policiesTable" })
542         );
543         const firstHeader = (await sort.getSortHeaders())[0];
544         expect(await firstHeader.getSortDirection()).toBe("");
545
546         await firstHeader.click();
547         expect(await firstHeader.getSortDirection()).toBe("asc");
548         let policyRows = await policyTable.getRows();
549         expect(await policyRows[0].getCellTextByColumnName()).toEqual(
550           expectedPolicy1Row
551         );
552
553         await firstHeader.click();
554         expect(await firstHeader.getSortDirection()).toBe("desc");
555         policyRows = await policyTable.getRows();
556         expect(
557           await policyRows[policyRows.length - 1].getCellTextByColumnName()
558         ).toEqual(expectedPolicy1Row);
559       });
560
561       it("should sort table asc and desc by second header", async () => {
562         const sort = await loader.getHarness(MatSortHarness);
563         const jobsTable = await loader.getHarness(
564           MatTableHarness.with({ selector: "#policiesTable" })
565         );
566         const firstHeader = (await sort.getSortHeaders())[1];
567         expect(await firstHeader.getSortDirection()).toBe("");
568
569         await firstHeader.click();
570         expect(await firstHeader.getSortDirection()).toBe("asc");
571         let policyRows = await jobsTable.getRows();
572         policyRows = await jobsTable.getRows();
573         expect(await policyRows[0].getCellTextByColumnName()).toEqual(
574           expectedPolicy1Row
575         );
576
577         await firstHeader.click();
578         expect(await firstHeader.getSortDirection()).toBe("desc");
579         policyRows = await jobsTable.getRows();
580         expect(
581           await policyRows[policyRows.length - 1].getCellTextByColumnName()
582         ).toEqual(expectedPolicy1Row);
583       });
584
585       it("should sort table asc and desc by third header", async () => {
586         const sort = await loader.getHarness(MatSortHarness);
587         const jobsTable = await loader.getHarness(
588           MatTableHarness.with({ selector: "#policiesTable" })
589         );
590         const firstHeader = (await sort.getSortHeaders())[2];
591         expect(await firstHeader.getSortDirection()).toBe("");
592
593         await firstHeader.click();
594         expect(await firstHeader.getSortDirection()).toBe("asc");
595         let policyRows = await jobsTable.getRows();
596         policyRows = await jobsTable.getRows();
597         expect(await policyRows[0].getCellTextByColumnName()).toEqual(
598           expectedPolicy1Row
599         );
600
601         await firstHeader.click();
602         expect(await firstHeader.getSortDirection()).toBe("desc");
603         policyRows = await jobsTable.getRows();
604         expect(
605           await policyRows[policyRows.length - 1].getCellTextByColumnName()
606         ).toEqual(expectedPolicy1Row);
607       });
608
609       it("should sort table asc and desc by fourth header", async () => {
610         const sort = await loader.getHarness(MatSortHarness);
611         const jobsTable = await loader.getHarness(
612           MatTableHarness.with({ selector: "#policiesTable" })
613         );
614         const firstHeader = (await sort.getSortHeaders())[3];
615         expect(await firstHeader.getSortDirection()).toBe("");
616
617         await firstHeader.click();
618         expect(await firstHeader.getSortDirection()).toBe("asc");
619         let policyRows = await jobsTable.getRows();
620         policyRows = await jobsTable.getRows();
621         expect(await policyRows[0].getCellTextByColumnName()).toEqual(
622           expectedPolicy1Row
623         );
624
625         await firstHeader.click();
626         expect(await firstHeader.getSortDirection()).toBe("desc");
627         policyRows = await jobsTable.getRows();
628         expect(
629           await policyRows[policyRows.length - 1].getCellTextByColumnName()
630         ).toEqual(expectedPolicy1Row);
631       });
632     });
633   });
634
635   function compileAndGetComponents() {
636     TestBed.compileComponents();
637
638     hostFixture = TestBed.createComponent(PolicyInstanceComponentHostComponent);
639     hostComponent = hostFixture.componentInstance;
640     componentUnderTest = hostFixture.debugElement.query(
641       By.directive(PolicyInstanceComponent)
642     ).componentInstance;
643     hostFixture.detectChanges();
644     loader = TestbedHarnessEnvironment.loader(hostFixture);
645     return { hostFixture, hostComponent, componentUnderTest, loader };
646   }
647 });
648
649 function setupDialogRefSpy(returnValue: boolean = true) {
650   const afterClosedObservable = new Observable((observer) => {
651     observer.next(returnValue);
652   });
653
654   const dialogRefSpy = jasmine.createSpyObj("MatDialogRef", ["afterClosed"]);
655   dialogRefSpy.afterClosed.and.returnValue(afterClosedObservable);
656   return dialogRefSpy;
657 }
658
659 function toLocalTime(utcTime: string): string {
660   const date = new Date(utcTime);
661   const toutc = date.toUTCString();
662   return new Date(toutc + " UTC").toLocaleString();
663 }