diff --git a/libs/api/event/feature/src/queries/get-event-statistics.handler.ts b/libs/api/event/feature/src/queries/get-event-statistics.handler.ts index 09fb4736..379196c9 100644 --- a/libs/api/event/feature/src/queries/get-event-statistics.handler.ts +++ b/libs/api/event/feature/src/queries/get-event-statistics.handler.ts @@ -27,6 +27,8 @@ export class GetEventStatisticsHandler let turnover_rate = 0; let average_attendance_time = 0; let max_attendance_time = 0; + const attendance_over_time_data: number[] = []; + const attendance_over_time_labels: Date[] = []; if (events.length == 0) { return { @@ -36,6 +38,8 @@ export class GetEventStatisticsHandler turnover_rate: turnover_rate, average_attendance_time: average_attendance_time, max_attendance_time: max_attendance_time, + attendance_over_time_data: attendance_over_time_data, + attendance_over_time_labels: attendance_over_time_labels, }; } @@ -104,6 +108,13 @@ export class GetEventStatisticsHandler average_attendance = total_unique_devices / devicesOverTime.size ? total_unique_devices / devicesOverTime.size : 0; average_attendance_time = total_attendance_time / deviceTimeRange.size ? total_attendance_time / deviceTimeRange.size : 0; + // set attendance over time from devicesOverTime + + for (const [key, value] of devicesOverTime.entries()) { + attendance_over_time_data.push(value.size); + attendance_over_time_labels.push(key); + } + //compute statistics end return { @@ -113,6 +124,8 @@ export class GetEventStatisticsHandler turnover_rate: turnover_rate, average_attendance_time: average_attendance_time, max_attendance_time: max_attendance_time, + attendance_over_time_data: attendance_over_time_data, + attendance_over_time_labels: attendance_over_time_labels, }; } } diff --git a/libs/api/event/util/src/responses/get-event-statistics.response.ts b/libs/api/event/util/src/responses/get-event-statistics.response.ts index 16526308..9ac4731d 100644 --- a/libs/api/event/util/src/responses/get-event-statistics.response.ts +++ b/libs/api/event/util/src/responses/get-event-statistics.response.ts @@ -6,4 +6,6 @@ export interface IGetEventStatisticsResponse { turnover_rate: number | undefined |null, average_attendance_time: number | undefined |null, max_attendance_time: number | undefined |null, + attendance_over_time_data: number[] | undefined |null, + attendance_over_time_labels: Date[] | undefined |null, } \ No newline at end of file diff --git a/libs/app/components/src/lib/compare-page/compare-page.component.html b/libs/app/components/src/lib/compare-page/compare-page.component.html index 8871b3dc..403b5b52 100644 --- a/libs/app/components/src/lib/compare-page/compare-page.component.html +++ b/libs/app/components/src/lib/compare-page/compare-page.component.html @@ -55,8 +55,8 @@ -
- +
+
-
+
-
-
Event Statistics
+
+
{{getFirstEventStats().name}}
+
Event Statistics
+
{{getSecondEventStats().name}}
@@ -158,6 +160,17 @@
+
+
Attendance Over Time
+
+
+ +
+
+ +
+
+
diff --git a/libs/app/components/src/lib/compare-page/compare-page.component.ts b/libs/app/components/src/lib/compare-page/compare-page.component.ts index ca8595ff..b7e0f7cb 100644 --- a/libs/app/components/src/lib/compare-page/compare-page.component.ts +++ b/libs/app/components/src/lib/compare-page/compare-page.component.ts @@ -1,50 +1,85 @@ -import { Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular/core'; +import { + Component, + ElementRef, + HostListener, + OnInit, + ViewChild, +} from '@angular/core'; import { AppApiService } from '@event-participation-trends/app/api'; import { ActivatedRoute, Router } from '@angular/router'; -import { IEvent, IGetEventStatisticsResponse, IPosition } from '@event-participation-trends/api/event/util'; +import { + IEvent, + IGetEventStatisticsResponse, + IPosition, +} from '@event-participation-trends/api/event/util'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { NgIconsModule, provideIcons } from '@ng-icons/core'; import { HeatmapContainerComponent } from '../heatmap-container/heatmap-container.component'; -import { matCheckCircleOutline } from "@ng-icons/material-icons/outline"; -import { matRadioButtonUnchecked, matSearch, matFilterCenterFocus, matZoomIn, matZoomOut } from "@ng-icons/material-icons/baseline"; -import { heroAdjustmentsHorizontal } from "@ng-icons/heroicons/outline"; -import { heroInboxSolid } from '@ng-icons/heroicons/solid'; +import { matCheckCircleOutline } from '@ng-icons/material-icons/outline'; +import { + matRadioButtonUnchecked, + matSearch, + matFilterCenterFocus, + matZoomIn, + matZoomOut, +} from '@ng-icons/material-icons/baseline'; +import { heroAdjustmentsHorizontal } from '@ng-icons/heroicons/outline'; +import { heroInboxSolid } from '@ng-icons/heroicons/solid'; +import Chart, { ChartConfiguration } from 'chart.js/auto'; +import 'chartjs-plugin-datalabels'; class Stats { - id = 0 - total_attendance = 0 - average_attendance = 0 - peak_attendance = 0 - turnover_rate = 0 - average_attendance_time = 0 - max_attendance_time = 0 + id = 0; + name = ''; + total_attendance = 0; + average_attendance = 0; + peak_attendance = 0; + turnover_rate = 0; + average_attendance_time = 0; + max_attendance_time = 0; + attendance_over_time_data: number[] = []; + attendance_over_time_labels: Date[] = []; } @Component({ selector: 'event-participation-trends-compare-page', standalone: true, imports: [ - CommonModule, + CommonModule, FormsModule, NgIconsModule, - HeatmapContainerComponent + HeatmapContainerComponent, ], templateUrl: './compare-page.component.html', styleUrls: ['./compare-page.component.css'], providers: [ - provideIcons({matCheckCircleOutline, matRadioButtonUnchecked, heroAdjustmentsHorizontal, matSearch, matFilterCenterFocus, matZoomIn, matZoomOut, heroInboxSolid}) - ] + provideIcons({ + matCheckCircleOutline, + matRadioButtonUnchecked, + heroAdjustmentsHorizontal, + matSearch, + matFilterCenterFocus, + matZoomIn, + matZoomOut, + heroInboxSolid, + }), + ], }) -export class ComparePageComponent implements OnInit{ +export class ComparePageComponent implements OnInit { + @ViewChild('firstAttendanceOverTime') + firstAttendanceOverTime!: ElementRef; + @ViewChild('secondAttendanceOverTime') + secondAttendanceOverTime!: ElementRef; + public id = ''; - public event : any | null = null; + public event: any | null = null; public show = false; public loading = true; public categories: string[] = []; public events: IEvent[] = []; - public eventList: {event: IEvent, selected: boolean}[] = []; + public eventList: { event: IEvent; selected: boolean }[] = []; public show_search = true; public role = 'viewer'; public search = ''; @@ -53,6 +88,9 @@ export class ComparePageComponent implements OnInit{ public largeScreen = false; public hintText = 'Use the tab on the right to select events.'; + firstAttendanceOverTimeChart: Chart | null = null; + secondAttendanceOverTimeChart: Chart | null = null; + selectedCategory = 'Show All'; eventsSelected = 0; showDropDown = false; @@ -60,9 +98,23 @@ export class ComparePageComponent implements OnInit{ eventStats: Stats[] = []; parentContainer: HTMLDivElement | null = null; - - constructor(private readonly appApiService: AppApiService, private readonly route: ActivatedRoute, private readonly router: Router) {} + chartColors = { + 'ept-deep-grey': '#101010', + 'ept-bumble-yellow': '#facc15', + 'ept-off-white': '#F5F5F5', + 'ept-blue-grey': '#B1B8D4', + 'ept-navy-blue': '#22242A', + 'ept-light-blue': '#57D3DD', + 'ept-light-green': '#4ade80', + 'ept-light-red': '#ef4444', + }; + + constructor( + private readonly appApiService: AppApiService, + private readonly route: ActivatedRoute, + private readonly router: Router + ) {} async ngOnInit() { // retrieve categories from API @@ -76,9 +128,8 @@ export class ComparePageComponent implements OnInit{ this.events = await this.appApiService.getAllEvents(); for (const event of this.events) { - this.eventList.push({event, selected: false}); + this.eventList.push({ event, selected: false }); } - } else if (this.role === 'manager') { //get managed categories this.categories = await this.appApiService.getManagedEventCategories(); @@ -87,7 +138,7 @@ export class ComparePageComponent implements OnInit{ this.events = await this.appApiService.getManagedEvents(); for (const event of this.events) { - this.eventList.push({event, selected: false}); + this.eventList.push({ event, selected: false }); } } else { this.router.navigate(['/home']); @@ -96,16 +147,16 @@ export class ComparePageComponent implements OnInit{ // test if window size is less than 1100px if (window.innerWidth < 1100) { this.showSidePanel = false; - this.hintText = 'Use the button in the top right corner to select events.'; - } - else { + this.hintText = + 'Use the button in the top right corner to select events.'; + } else { this.showSidePanel = true; this.hintText = 'Use the tab on the right to select events.'; - } + } if (window.innerWidth > 1024) { this.largeScreen = true; - } else { + } else { this.largeScreen = false; } @@ -113,7 +164,9 @@ export class ComparePageComponent implements OnInit{ setTimeout(() => { this.show = true; - this.parentContainer = document.getElementById('parentContainer') as HTMLDivElement; + this.parentContainer = document.getElementById( + 'parentContainer' + ) as HTMLDivElement; }, 200); } @@ -136,7 +189,9 @@ export class ComparePageComponent implements OnInit{ isSelectedEvent(event: IEvent): boolean { const index = this.eventList.findIndex((item) => { const sameName = item.event.Name === event.Name; - const sameStartAndEndDate = item.event.StartDate === event.StartDate && item.event.EndDate === event.EndDate; + const sameStartAndEndDate = + item.event.StartDate === event.StartDate && + item.event.EndDate === event.EndDate; const sameCategory = item.event.Category === event.Category; return sameName && sameStartAndEndDate && sameCategory; @@ -155,7 +210,9 @@ export class ComparePageComponent implements OnInit{ async selectEvent(event: IEvent): Promise { const index = this.eventList.findIndex((item) => { const sameName = item.event.Name === event.Name; - const sameStartAndEndDate = item.event.StartDate === event.StartDate && item.event.EndDate === event.EndDate; + const sameStartAndEndDate = + item.event.StartDate === event.StartDate && + item.event.EndDate === event.EndDate; const sameCategory = item.event.Category === event.Category; return sameName && sameStartAndEndDate && sameCategory; @@ -171,28 +228,31 @@ export class ComparePageComponent implements OnInit{ this.eventsSelected++; this.selectedEvents.push(event); - const response = await this.appApiService.getEventStatistics({eventId: (event as any)._id}); + const response = await this.appApiService.getEventStatistics({ + eventId: (event as any)._id, + }); const stats = { id: (event as any)._id, + name: event.Name!, total_attendance: response.total_attendance!, average_attendance: response.average_attendance!, peak_attendance: response.peak_attendance!, turnover_rate: response.turnover_rate!, average_attendance_time: response.average_attendance!, max_attendance_time: response.max_attendance_time!, + attendance_over_time_data: response.attendance_over_time_data!, + attendance_over_time_labels: response.attendance_over_time_labels!, }; - console.log(stats); - console.log(this.eventStats); - this.eventStats.push(stats); - } - else { + this.renderCharts(); + } else { this.eventsSelected--; const eventIndex = this.selectedEvents.findIndex((item) => { const sameName = item.Name === event.Name; - const sameStartAndEndDate = item.StartDate === event.StartDate && item.EndDate === event.EndDate; + const sameStartAndEndDate = + item.StartDate === event.StartDate && item.EndDate === event.EndDate; const sameCategory = item.Category === event.Category; return sameName && sameStartAndEndDate && sameCategory; @@ -203,6 +263,7 @@ export class ComparePageComponent implements OnInit{ return item.id === (event as any)._id; }); this.eventStats.splice(statsIndex, 1); + this.renderCharts(); } this.showDropDown = false; @@ -215,15 +276,22 @@ export class ComparePageComponent implements OnInit{ if (!search) return text; const pattern = new RegExp(search, 'gi'); - return text.replace(pattern, match => `${match}`); + return text.replace( + pattern, + (match) => + `${match}` + ); } - getEventCategories() : string[] { + getEventCategories(): string[] { const categoryList: string[] = []; this.events.forEach((event) => { if (event && event.Name && event.Category) { - if (event.Name.toLowerCase().includes(this.search.toLowerCase()) && !categoryList.includes(event.Category)) { + if ( + event.Name.toLowerCase().includes(this.search.toLowerCase()) && + !categoryList.includes(event.Category) + ) { categoryList.push(event.Category); } } @@ -232,20 +300,20 @@ export class ComparePageComponent implements OnInit{ return categoryList; } - getEvents() : IEvent[] { + getEvents(): IEvent[] { const eventList = this.events; - if (this.selectedCategory == "Show All") { + if (this.selectedCategory == 'Show All') { return eventList.filter((event) => { return event.Name ? event.Name.toLowerCase().includes(this.search.toLowerCase()) : false; }); - } - else { + } else { return eventList.filter((event) => { return event.Name - ? event.Name.toLowerCase().includes(this.search.toLowerCase()) && event.Category == this.selectedCategory + ? event.Name.toLowerCase().includes(this.search.toLowerCase()) && + event.Category == this.selectedCategory : false; }); } @@ -259,12 +327,13 @@ export class ComparePageComponent implements OnInit{ this.hintText = 'Use the tab on the right to select events.'; } else { this.showSidePanel = false; - this.hintText = 'Use the button in the top right corner to select events.'; - } + this.hintText = + 'Use the button in the top right corner to select events.'; + } if (event.target.innerWidth > 1024) { this.largeScreen = true; - } else { + } else { this.largeScreen = false; } } @@ -289,12 +358,15 @@ export class ComparePageComponent implements OnInit{ if (this.eventStats.length === 0) { return { id: 0, + name: '', total_attendance: 0, average_attendance: 0, peak_attendance: 0, turnover_rate: 0, average_attendance_time: 0, max_attendance_time: 0, + attendance_over_time_data: [], + attendance_over_time_labels: [], }; } @@ -305,15 +377,243 @@ export class ComparePageComponent implements OnInit{ if (this.eventStats.length <= 1) { return { id: 0, + name: '', total_attendance: 0, average_attendance: 0, peak_attendance: 0, turnover_rate: 0, average_attendance_time: 0, max_attendance_time: 0, + attendance_over_time_data: [], + attendance_over_time_labels: [], }; } return this.eventStats[1]; } + + renderCharts() { + { + // First Attendance Over Time + + if (this.firstAttendanceOverTimeChart) { + this.firstAttendanceOverTimeChart.destroy(); + } + + if (this.secondAttendanceOverTimeChart) { + this.secondAttendanceOverTimeChart.destroy(); + } + + if (this.eventStats.length === 0) { + return; + } + const ctx: CanvasRenderingContext2D | null = + this.firstAttendanceOverTime.nativeElement?.getContext('2d'); + + let gradientStroke = null; + if (ctx) { + gradientStroke = ctx.createLinearGradient(0, 230, 0, 50); + + gradientStroke.addColorStop(1, 'rgba(72,72,176,0.2)'); + gradientStroke.addColorStop(0.2, 'rgba(72,72,176,0.0)'); + gradientStroke.addColorStop(0, 'rgba(119,52,169,0)'); //purple colors + } + + const labels = this.getFirstEventStats().attendance_over_time_labels.map( + (date) => { + return date.toLocaleString(); + } + ); + + const data = this.getFirstEventStats().attendance_over_time_data; + + const config: ChartConfiguration = { + type: 'line', + data: { + labels: labels, + datasets: [ + { + data: data, + fill: true, + backgroundColor: gradientStroke ? gradientStroke : 'white', + borderColor: this.chartColors['ept-bumble-yellow'], + borderWidth: 1, + borderDash: [], + borderDashOffset: 0.0, + pointBackgroundColor: this.chartColors['ept-bumble-yellow'], + pointBorderColor: 'rgba(255,255,255,0)', + pointHoverBackgroundColor: this.chartColors['ept-bumble-yellow'], + pointBorderWidth: 1, + pointHoverRadius: 1, + pointHoverBorderWidth: 1, + pointRadius: 1, + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + tooltip: { + enabled: false, + }, + legend: { + display: false, + }, + title: { + display: true, + text: this.getFirstEventStats().name, + color: this.chartColors['ept-off-white'], // Set the title text color to white + }, + }, + scales: { + x: { + display: true, + grid: { + color: 'rgba(255, 255, 255, 0.1)', // Adjust the color of the x-axis grid lines + }, + ticks: { + color: this.chartColors['ept-blue-grey'], // Adjust the color of the x-axis labels + }, + }, + y: { + display: true, + beginAtZero: true, + grid: { + color: 'rgba(255, 255, 255, 0.1)', // Adjust the color of the y-axis grid lines + }, + ticks: { + color: this.chartColors['ept-blue-grey'], // Adjust the color of the y-axis labels + }, + }, + }, + elements: { + line: { + tension: 0.3, // Adjust the tension of the line for a smoother curve + borderWidth: 1, + }, + }, + }, + }; + const firstAttendanceOverTimeCanvas = + this.firstAttendanceOverTime.nativeElement; + + const firstAttendanceOverTimeCtx = + firstAttendanceOverTimeCanvas.getContext('2d', { + willReadFrequently: true, + }); + this.firstAttendanceOverTimeChart = new Chart( + firstAttendanceOverTimeCtx!, + config + ); + } + + { + // Second Attendance Over Time + + if (this.eventStats.length <= 1) { + return; + } + + const ctx: CanvasRenderingContext2D | null = + this.secondAttendanceOverTime.nativeElement?.getContext('2d'); + + let gradientStroke = null; + if (ctx) { + gradientStroke = ctx.createLinearGradient(0, 230, 0, 50); + + gradientStroke.addColorStop(1, 'rgba(72,72,176,0.2)'); + gradientStroke.addColorStop(0.2, 'rgba(72,72,176,0.0)'); + gradientStroke.addColorStop(0, 'rgba(119,52,169,0)'); //purple colors + } + + const labels = this.getSecondEventStats().attendance_over_time_labels.map( + (date) => { + return date.toLocaleString(); + } + ); + + const data = this.getSecondEventStats().attendance_over_time_data; + + const config: ChartConfiguration = { + type: 'line', + data: { + labels: labels, + datasets: [ + { + data: data, + fill: true, + backgroundColor: gradientStroke ? gradientStroke : 'white', + borderColor: this.chartColors['ept-bumble-yellow'], + borderWidth: 1, + borderDash: [], + borderDashOffset: 0.0, + pointBackgroundColor: this.chartColors['ept-bumble-yellow'], + pointBorderColor: 'rgba(255,255,255,0)', + pointHoverBackgroundColor: this.chartColors['ept-bumble-yellow'], + pointBorderWidth: 1, + pointHoverRadius: 1, + pointHoverBorderWidth: 1, + pointRadius: 1, + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + tooltip: { + enabled: false, + }, + legend: { + display: false, + }, + title: { + display: true, + text: this.getSecondEventStats().name, + color: this.chartColors['ept-off-white'], // Set the title text color to white + }, + }, + scales: { + x: { + display: true, + grid: { + color: 'rgba(255, 255, 255, 0.1)', // Adjust the color of the x-axis grid lines + }, + ticks: { + color: this.chartColors['ept-blue-grey'], // Adjust the color of the x-axis labels + }, + }, + y: { + display: true, + beginAtZero: true, + grid: { + color: 'rgba(255, 255, 255, 0.1)', // Adjust the color of the y-axis grid lines + }, + ticks: { + color: this.chartColors['ept-blue-grey'], // Adjust the color of the y-axis labels + }, + }, + }, + elements: { + line: { + tension: 0.3, // Adjust the tension of the line for a smoother curve + borderWidth: 1, + }, + }, + }, + }; + const secondAttendanceOverTimeCanvas = + this.secondAttendanceOverTime.nativeElement; + + const secondAttendanceOverTimeCtx = + secondAttendanceOverTimeCanvas.getContext('2d', { + willReadFrequently: true, + }); + this.secondAttendanceOverTimeChart = new Chart( + secondAttendanceOverTimeCtx!, + config + ); + } + } } diff --git a/libs/app/components/src/lib/heatmap-container/heatmap-container.component.ts b/libs/app/components/src/lib/heatmap-container/heatmap-container.component.ts index b3c938b8..79ca9bb7 100644 --- a/libs/app/components/src/lib/heatmap-container/heatmap-container.component.ts +++ b/libs/app/components/src/lib/heatmap-container/heatmap-container.component.ts @@ -78,7 +78,7 @@ export class HeatmapContainerComponent implements OnInit{ async ngOnInit() { // check if the event has device positions const startDate = new Date(this.containerEvent.StartDate); - startDate.setDate(startDate.getDate() - 1); // for test Event: Demo 3 + // startDate.setDate(startDate.getDate() - 1); // for test Event: Demo 3 const endDate = new Date(this.containerEvent.EndDate); this.startDate = startDate; @@ -122,6 +122,8 @@ export class HeatmapContainerComponent implements OnInit{ this.heatmap = new HeatMap({ container: document.getElementById('view-'+this.containerEvent._id+'')!, maxOpacity: .6, + width: 1000, + height: 1000, radius: 50, blur: 0.90, gradient: { diff --git a/libs/app/components/src/lib/link-sensor-modal/link-sensor-modal.component.spec.ts b/libs/app/components/src/lib/link-sensor-modal/link-sensor-modal.component.spec.ts index 1173e2b0..af1ab461 100644 --- a/libs/app/components/src/lib/link-sensor-modal/link-sensor-modal.component.spec.ts +++ b/libs/app/components/src/lib/link-sensor-modal/link-sensor-modal.component.spec.ts @@ -1,21 +1,124 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, tick, fakeAsync, flush } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { LinkSensorModalComponent } from './link-sensor-modal.component'; +import { ReactiveFormsModule, FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { NgIconsModule, provideIcons } from '@ng-icons/core'; +import { matClose } from '@ng-icons/material-icons/baseline'; +import { AppApiService } from '@event-participation-trends/app/api'; +import Konva from 'konva'; describe('LinkSensorModalComponent', () => { let component: LinkSensorModalComponent; let fixture: ComponentFixture; + let appApiService: AppApiService; + let httpTestingController: HttpTestingController; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [LinkSensorModalComponent], + imports: [LinkSensorModalComponent, ReactiveFormsModule, NgIconsModule, HttpClientTestingModule], + providers: [AppApiService, provideIcons({matClose})] }).compileComponents(); fixture = TestBed.createComponent(LinkSensorModalComponent); component = fixture.componentInstance; + + component.macAddressForm = new FormBuilder().group({ + macAddressBlock1: ['', [Validators.required, Validators.pattern('^[0-9a-fA-F]{2}$')]], + macAddressBlock2: ['', [Validators.required, Validators.pattern('^[0-9a-fA-F]{2}$')]], + macAddressBlock3: ['', [Validators.required, Validators.pattern('^[0-9a-fA-F]{2}$')]], + macAddressBlock4: ['', [Validators.required, Validators.pattern('^[0-9a-fA-F]{2}$')]], + macAddressBlock5: ['', [Validators.required, Validators.pattern('^[0-9a-fA-F]{2}$')]], + macAddressBlock6: ['', [Validators.required, Validators.pattern('^[0-9a-fA-F]{2}$')]], + }); + + component.customId = 'sensor-1'; // Example customId value + component.macAddrFromQR = 'AA:BB:CC:DD:EE:FF'; // Example QR_MAC_ADDRESS value + component.macAddressBlocks = ['AA', 'BB', 'CC', 'DD', 'EE', 'FF']; // Example macAddressBlocks value + fixture.detectChanges(); + + // Inject the http service and test controller for each test + appApiService = TestBed.inject(AppApiService); + httpTestingController = TestBed.inject(HttpTestingController); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should have a valid macAddressForm', () => { + const form: FormGroup = component.macAddressForm; + expect(form.valid).toBeFalsy(); + + // Set values for the form fields + form.setValue({ + macAddressBlock1: '12', + macAddressBlock2: '34', + macAddressBlock3: '56', + macAddressBlock4: '78', + macAddressBlock5: '9A', + macAddressBlock6: 'BC', + }); + + expect(form.valid).toBeTruthy(); + }); + + it('should reset macAddressForm and emit closeModalEvent when closeModal is called', () => { + // Mock the necessary properties and methods + (component.macAddressForm as any) = { + reset: jest.fn(), + }; + + component.canLinkSensorWithMacAddress = true; + (component.closeModalEvent as any) = { + emit: jest.fn(), + }; + + // Call the function to be tested + component.closeModal(); + + // Expectations + expect(component.macAddressForm.reset).toHaveBeenCalled(); + expect(component.canLinkSensorWithMacAddress).toBe(false); + expect(component.closeModalEvent.emit).toHaveBeenCalledWith(true); + }); + + it('should update linked sensors and show linking toast on success', fakeAsync(() => { + const eventSensorMac = 'aa:bb:cc:dd:ee:ff'; + component.macAddressBlocks = ['aa', 'bb', 'cc', 'dd', 'ee', 'ff']; + + jest.spyOn(component, 'showLinkingToast').mockImplementation(() => { + component.showToastLinking = false; + component.showToastSuccess = true; + component.toastMessage = 'Sensor ' + component.customId + ' linked successfully!'; + component.toastType = 'success'; + + setTimeout(() => { + component.showToastSuccess = false; + document.querySelector('#linkSensorModal')?.classList.add('visible'); + component.closeModal(); + }, 800); + }); + component.updateLinkedSensors(); + + //Expect a call to this URL + const request = httpTestingController.expectOne(`/api/sensorlinking/${eventSensorMac}`); + + //Assert that the request is a POST. + expect(request.request.method).toEqual('POST'); + + //Respond with the data + request.flush({success: true}); + + //Call tick whic actually processes te response + tick(); + + //Run our tests + expect(component.showLinkingToast).toHaveBeenCalledWith(true); + + //Finish test + httpTestingController.verify(); + flush(); + })); + });