import { ClosedQuestionStatus , Heatmap } from '@koh/common' ; import { CacheModule } from '@nestjs/common' ; import { Test , TestingModule } from '@nestjs/testing' ; import { QuestionModel } from 'question/question.entity' ; import { Connection } from 'typeorm' ; import { TestTypeOrmModule } from '../../test/util/testUtils' ; import { HeatmapService } from './heatmap.service' ; import { OfficeHourModel } from './office-hour.entity' ; describe ( 'HeatmapService' , ( ) => { let service ; let conn ; beforeAll ( async ( ) => { const module = await Test . createTestingModule ( { imports : [ TestTypeOrmModule , CacheModule . register ( ) ] , providers : [ HeatmapService ] , } ) . compile ( ) ; service = module . get < HeatmapService > ( HeatmapService ) ; conn = module . get < Connection > ( Connection ) ; } ) ; afterAll ( async ( ) => { await conn . close ( ) ; } ) ; describe ( 'generateHeatmap with replay' , ( ) => { function questionsFromDates ( questionTimes ) { return questionTimes . map ( ( [ createdAt , helpedAt ] ) => QuestionModel . create ( { createdAt : new Date ( createdAt ) , helpedAt : new Date ( helpedAt ) , text : 'pls help' , status : ClosedQuestionStatus . Resolved , } ) ) ; } function officehoursFromDates ( hoursTimes ) { return hoursTimes . map ( ( [ start , end ] ) => OfficeHourModel . create ( { startTime : new Date ( start ) , endTime : new Date ( end ) , } ) ) ; } const OCT8 = ( start , end ) : [ string , string ] => [ `2020-10-08T${ start }:00-0400` , `2020-10-08T${ end }:00-0400` , ] ; const OCT4 = ( start , end ) : [ string , string ] => [ `2020-10-04T${ start }:00-0400` , `2020-10-04T${ end }:00-0400` , ] ; const OCT11 = ( start , end ) : [ string , string ] => [ `2020-10-11T${ start }:00-0400` , `2020-10-11T${ end }:00-0400` , ] ; const OCT18 = ( start , end ) : [ string , string ] => [ `2020-10-18T${ start }:00-0400` , `2020-10-18T${ end }:00-0400` , ] ; const BUCKET_SIZE = 60 ; const SAMPLES_PER_BUCKET = 4 ; function heatmapFromDates ( questionTimes , hoursTimes ) { return service . _generateHeatMapWithReplay ( questionsFromDates ( questionTimes ) , officehoursFromDates ( hoursTimes ) , 'America/New_York' , BUCKET_SIZE , SAMPLES_PER_BUCKET ) ; } const emptyHeatmap = ( ) => [ ... Array ( 24 * 7 ) ] . map ( ( ) => - 1 ) ; const sparseHeatmap = ( records ) => Object . assign ( emptyHeatmap ( ) , records ) ; it ( 'returns heatmap for no questions and no hours' , ( ) => { const heatmap = heatmapFromDates ( [ ] , [ ] ) ; const expected = emptyHeatmap ( ) ; expect ( heatmap ) . toEqual ( expected ) ; } ) ; it ( 'returns heatmap for no questions during office hours' , ( ) => { const heatmap = heatmapFromDates ( [ ] , [ OCT4 ( '03:00' , '04:00' ) ] ) ; const expected = sparseHeatmap ( { 3 : 0 } ) ; expect ( heatmap ) . toEqual ( expected ) ; } ) ; it ( 'returns heatmap for just one question' , ( ) => { const heatmap = heatmapFromDates ( [ OCT4 ( '03:01' , '03:16' ) ] , [ OCT4 ( '03:00' , '04:00' ) ] ) ; const expected = sparseHeatmap ( { 3 : 1 / 4 } ) ; expect ( heatmap ) . toEqual ( expected ) ; } ) ; it ( 'returns heatmap when questions overlap within 1 bucket' , ( ) => { const heatmap = heatmapFromDates ( [ OCT4 ( '02:59' , '03:10' ) , OCT4 ( '03:05' , '03:21' ) , OCT4 ( '03:10' , '03:31' ) , OCT4 ( '03:48' , '03:58' ) , ] , [ OCT4 ( '03:00' , '04:00' ) ] ) ; /**
             *   Timepoint | Wait time   | Question in front of you
             *   -----------------------------------------------------
             *     2:45    | 0 min       |
             *     3:00    | 10 minutes  |  Q1
             *     3:15    | 16 mintues  |  Q3
             *     3:30    | 1 minutes   |  Q3
             *     3:45    | 0 minutes   |
             *
             *  Average timepoint wait = 6.75
             */ const expected = sparseHeatmap ( { 3 : 6.75 , } ) ; expect ( heatmap ) . toEqual ( expected ) ; } ) ; it ( 'returns heatmap when questions overlap spanning multiple buckets' , ( ) => { const heatmap = heatmapFromDates ( [ OCT4 ( '03:29' , '04:10' ) , OCT4 ( '04:01' , '04:21' ) , ] , [ OCT4 ( '03:00' , '05:00' ) ] ) ; /**
             *   Timepoint | Wait time   | Question in front of you
             *   -----------------------------------------------------
             *     3:00    |  0 minutes  |
             *     3:15    |  0 mintues  |
             *     3:30    | 40 minutes  |  Q1
             *     3:45    | 25 minutes  |  Q1
             *     4:00    | 10 minutes  |  Q1
             *     4:15    |  6 minutes  |  Q2
             *
             *  3-4 bucket avg: 16.25
             *  4-5 bucket avg: 4
             */ const expected = sparseHeatmap ( { 3 : 16.25 , 4 : 4 , } ) ; expect ( heatmap ) . toEqual ( expected ) ; } ) ; it ( 'returns heatmap when questions overlap spanning multiple buckets and a question is skipped' , ( ) => { const heatmap = heatmapFromDates ( [ OCT4 ( '03:29' , '04:10' ) , OCT4 ( '04:01' , '04:21' ) , OCT4 ( '04:05' , '04:16' ) , ] , [ OCT4 ( '03:00' , '05:00' ) ] ) ; /**
             *   Timepoint | Wait time   | Question in front of you
             *   -----------------------------------------------------
             *     3:00    |  0 minutes  |
             *     3:15    |  0 mintues  |
             *     3:30    | 40 minutes  |  Q1
             *     3:45    | 25 minutes  |  Q1
             *     4:00    | 10 minutes  |  Q1
             *     4:15    |  1 minutes  |  Q3
             *
             *  3-4 bucket avg: 16.25
             *  4-5 bucket avg: 2.75
             */ const expected = sparseHeatmap ( { 3 : 16.25 , 4 : 2.75 , } ) ; expect ( heatmap ) . toEqual ( expected ) ; } ) ; it ( 'returns heatmap when large gap between questions' , ( ) => { const heatmap = heatmapFromDates ( [ OCT4 ( '03:10' , '03:30' ) , OCT4 ( '07:01' , '07:21' ) , ] , [ OCT4 ( '03:00' , '04:00' ) , OCT4 ( '07:00' , '08:00' ) ] ) ; /**
             *   Timepoint | Wait time   | Question in front of you
             *   -----------------------------------------------------
             *     3:00    |  0 minutes  |
             *     3:15    | 15 mintues  |  Q1
             *     3:30    |  0 minutes  |
             *     3:45    |  0 minutes  |
             *
             *     7:00    |  0 minutes  |
             *     7:15    |  6 minutes  |  Q2
             *
             *  3-4 bucket avg: 15/4
             *  7-8 bucket avg: 6/4
             */ const expected = sparseHeatmap ( { 3 : 15 / 4 , 7 : 6 / 4 , } ) ; expect ( heatmap ) . toEqual ( expected ) ; } ) ; it ( 'returns heatmap where a question is asked outside of scheduled office hours' , ( ) => { const heatmap = heatmapFromDates ( [ OCT4 ( '03:10' , '03:30' ) ] , [ OCT4 ( '07:00' , '08:00' ) ] ) ; const expected = sparseHeatmap ( { 7 : 0 , } ) ; expect ( heatmap ) . toEqual ( expected ) ; } ) ; it ( 'returns heatmap when questions are later in the week' , ( ) => { const heatmap = heatmapFromDates ( [ OCT4 ( '03:29' , '04:10' ) , OCT4 ( '04:01' , '04:21' ) , OCT8 ( '03:29' , '04:14' ) , OCT8 ( '04:01' , '04:21' ) , ] , [ OCT4 ( '03:00' , '05:00' ) , OCT8 ( '03:00' , '05:00' ) ] ) ; const expected = sparseHeatmap ( { 3 : 16.25 , 4 : 4 , [ 24 * 4 + 3 ] : 73 / 4 , [ 24 * 4 + 4 ] : 5 , } ) ; expect ( heatmap ) . toEqual ( expected ) ; } ) ; it ( 'returns heatmap when questions are across multiple weeks' , ( ) => { const heatmap = heatmapFromDates ( [ OCT4 ( '03:29' , '04:10' ) , OCT4 ( '04:01' , '04:21' ) , OCT11 ( '03:21' , '04:25' ) , OCT11 ( '04:22' , '04:55' ) , ] , [ OCT4 ( '03:00' , '05:00' ) , OCT11 ( '03:00' , '05:00' ) ] ) ; /**
             *   Timepoint | Wait time   | Question in front of you
             *   -----------------------------------------------------
             *     3:00    |  0 minutes  |
             *     3:15    |  0 mintues  |
             *     3:30    | 40 minutes  |  Q1
             *     3:45    | 25 minutes  |  Q1
             *     4:00    | 10 minutes  |  Q1
             *     4:15    |  6 minutes  |  Q2
             *
             *  3-4 bucket avg: 16.25
             *  4-5 bucket avg: 4
             *
             *
             *    Timepoint | Wait time   | Question in front of you
             *   -----------------------------------------------------
             *     3:00    |  0 minutes  |
             *     3:15    |  0 mintues  |
             *     3:30    | 55 minutes  |  Q1
             *     3:45    | 40 minutes  |  Q1
             *     4:00    | 25 minutes  |  Q1
             *     4:15    | 10 minutes  |  Q2
             *     4:30    | 25 minutes  |
             *     4:45    | 10 minutes  |  Q2
             *     5:00    |  0 minutes  |  Q2
             *
             *  3-4 bucket avg: 23.75
             *  4-5 bucket avg: 17.5
             *
             * ------OVERALL-----
             * 3-4 bucket avg: 20
             * 4-5 bucket avg: 10.75
             */ const expected = sparseHeatmap ( { 3 : 20 , 4 : 10.75 , } ) ; expect ( heatmap ) . toEqual ( expected ) ; } ) ; it ( 'returns heatmap when questions are across multiple weeks but no office hours during one of the weeks' , ( ) => { const heatmap = heatmapFromDates ( [ OCT4 ( '03:29' , '04:10' ) , OCT18 ( '03:21' , '04:25' ) ] , [ OCT4 ( '03:00' , '04:00' ) , OCT18 ( '03:00' , '04:00' ) ] ) ; /**
             *   Timepoint | Wait time   | Question in front of you
             *   -----------------------------------------------------
             *     3:00    |  0 minutes  |
             *     3:15    |  0 mintues  |
             *     3:30    | 40 minutes  |  Q1
             *     3:45    | 25 minutes  |  Q1
             *
             *  3-4 bucket avg: 16.25
             *
             *    Timepoint | Wait time   | Question in front of you
             *   -----------------------------------------------------
             *     3:00    |  0 minutes  |
             *     3:15    |  0 mintues  |
             *     3:30    | 55 minutes  |  Q1
             *     3:45    | 40 minutes  |  Q1
             *
             *  3-4 bucket avg: 23.75
             *
             * ------OVERALL-----
             * 3-4 bucket avg: 20
             */ const expected = sparseHeatmap ( { 3 : 20 , } ) ; expect ( heatmap ) . toEqual ( expected ) ; } ) ; it ( 'returns heatmap during a week with daylight savings fall back' , ( ) => { const heatmap = heatmapFromDates ( [ [ `2020-10-25T08:04:00.000Z` , `2020-10-25T08:32:00.000Z` ] , [ `2020-11-01T09:04:00.000Z` , `2020-11-01T09:32:00.000Z` ] , ] , [ [ `2020-10-25T08:00:00.000Z` , `2020-10-25T09:00:00.000Z` ] , [ `2020-11-01T09:00:00.000Z` , `2020-11-01T10:00:00.000Z` ] , ] ) ; /**
             * For both weeks wait time is 19/4
             */ const expected = sparseHeatmap ( { 4 : 19 / 4 , } ) ; expect ( heatmap ) . toEqual ( expected ) ; } ) ; it ( 'returns heatmap during a week with daylight savings, but the course is not in a DST timezone' , ( ) => { const heatmap = service . _generateHeatMapWithReplay ( questionsFromDates ( [ [ `2020-10-25T04:04:00-1000` , `2020-10-25T04:32:00-1000` ] , [ `2020-11-01T05:04:00-1000` , `2020-11-01T05:32:00-1000` ] , ] ) , officehoursFromDates ( [ [ `2020-10-25T04:00:00-1000` , `2020-10-25T05:00:00-1000` ] , [ `2020-11-01T05:00:00-1000` , `2020-11-01T06:00:00-1000` ] , ] ) , 'Pacific/Honolulu' , BUCKET_SIZE , SAMPLES_PER_BUCKET ) ; /**
             * For both weeks wait time is 19/4
             */ const expected = sparseHeatmap ( { 4 : 19 / 4 , 5 : 19 / 4 , } ) ; expect ( heatmap ) . toEqual ( expected ) ; } ) ; it ( 'returns heatmap during a week with daylight savings spring forward' , ( ) => { const heatmap = heatmapFromDates ( [ [ `2020-03-07T08:04:00.000Z` , `2020-03-07T08:32:00.000Z` ] , [ `2020-03-14T07:04:00.000Z` , `2020-03-14T07:32:00.000Z` ] , ] , [ [ `2020-03-07T08:00:00.000Z` , `2020-03-07T09:00:00.000Z` ] , [ `2020-03-14T07:00:00.000Z` , `2020-03-14T08:00:00.000Z` ] , ] ) ; /**
             * For both weeks wait time is 19/4
             */ const expected = sparseHeatmap ( { [ 24 * 6 + 3 ] : 19 / 4 , } ) ; expect ( heatmap ) . toEqual ( expected ) ; } ) ; it ( 'returns heatmap during fall back with question crossing boundary' , ( ) => { const heatmap = heatmapFromDates ( [ [ `2020-11-01T05:44:00.000Z` , `2020-11-01T06:12:00.000Z` ] , ] , [ [ `2020-11-01T05:00:00.000Z` , `2020-11-01T07:00:00.000Z` ] ] ) ; /**
             *   Timepoint | Wait time   | Question in front of you
             *   -----------------------------------------------------
             *     1:45    | 27 minutes  | Q1
             *
             *     1:00    | 12 mintues  | Q1
             *
             * we have to divide by 8 because this hour happened twice
             **/ const expected = sparseHeatmap ( { 1 : 39 / 8 , } ) ; expect ( heatmap ) . toEqual ( expected ) ; } ) ; it ( 'returns heatmap during spring forward with question crossing boundary' , ( ) => { const heatmap = heatmapFromDates ( [ [ `2020-03-08T06:44:00.000Z` , `2020-03-08T07:12:00.000Z` ] , ] , [ [ `2020-03-08T06:00:00.000Z` , `2020-03-08T08:00:00.000Z` ] ] ) ; /**
             *   Timepoint | Wait time   | Question in front of you
             *   -----------------------------------------------------
             *     1:45    | 27 minutes  | Q1
             *
             *     3:00    | 12 mintues  | Q1
             *
             *
             *   We don't bucket for 2AM at all.
             **/ const expected = sparseHeatmap ( { 1 : 27 / 4 , 2 : 0 , 3 : 12 / 4 , } ) ; expect ( heatmap ) . toEqual ( expected ) ; } ) ; it ( 'returns heatmap when a question crosses the midnight boundary' , ( ) => { const heatmap = heatmapFromDates ( [ [ `2020-10-11T05:00:00.000Z` , `2020-10-12T02:00:00.000Z` ] ] , [ OCT4 ( '03:00' , '04:00' ) ] ) ; const expected = sparseHeatmap ( { 3 : 0 , } ) ; expect ( heatmap ) . toEqual ( expected ) ; } ) ; it ( 'works when the bucketsize and sample interval are different' , ( ) => { const heatmap = service . _generateHeatMapWithReplay ( questionsFromDates ( [ OCT4 ( '03:01' , '03:11' ) , OCT8 ( '05:12' , '05:22' ) ] ) , officehoursFromDates ( [ OCT4 ( '03:00' , '04:00' ) , OCT8 ( '05:00' , '06:00' ) ] ) , 'America/New_York' , 24 * 60 , 1 ) ; expect ( heatmap ) . toEqual ( [ 0 , - 1 , - 1 , - 1 , 0 , - 1 , - 1 ] ) ; } ) ; } ) ; } ) ;