View raw

1 use Mojo::Base -strict; 2 3 use Test2::V0; 4 5 use FindBin; 6 use lib "${FindBin::Bin}/../lib/"; 7 use lib "${FindBin::Bin}/lib/"; 8 use Dashboard::Test qw(test_app test_empty_app); 9 10 my $t = test_app(); 11 12 subtest 'insights page renders with all sections' => sub { 13 $t->get_ok('/insights') 14 ->status_is(200) 15 ->text_is( 'title', 'FAPG DAQ Insights' ) 16 ->element_exists('.navbar-nav a.nav-link.active[href="/insights"]') 17 ->element_exists_not('[data-home-masthead]') 18 ->element_exists_not('[data-home-koan-card]') 19 ->element_exists( 20 '[data-mobile-navigation] a.active[href="/insights"][aria-current="page"]' 21 ) 22 ->element_exists( 23 'main section[data-controls-panel][aria-labelledby="insights-controls-title"]' 24 ) 25 ->text_is( '#insights-controls-title', 'Insights controls' ) 26 ->element_exists( 27 '#insights-controls .graph-control[data-default-timeframe="day"]') 28 ->element_exists('[data-default-timeframe="day"]'); 29 }; 30 31 subtest 'insights page has all chart canvases' => sub { 32 $t->get_ok('/insights') 33 ->status_is(200) 34 ->element_exists('canvas#chart-stability') 35 ->element_exists('canvas#chart-derivatives') 36 ->element_exists('canvas#chart-diurnal') 37 ->element_exists('canvas#chart-overlay-do-orp') 38 ->element_exists('canvas#chart-ph-do-lag'); 39 }; 40 41 subtest 'diurnal API returns structured response' => sub { 42 $t->get_ok('/api/insights/diurnal/do') 43 ->status_is(200) 44 ->json_has('/probe') 45 ->json_is( '/probe' => 'do' ) 46 ->json_has('/hours') 47 ->json_has('/traces') 48 ->json_has('/mean'); 49 }; 50 51 subtest 'diurnal API with invalid probe' => sub { 52 $t->get_ok('/api/insights/diurnal/nope') 53 ->status_is(404) 54 ->json_has('/error'); 55 }; 56 57 subtest 'diurnal API accepts days param' => sub { 58 $t->get_ok('/api/insights/diurnal/ph?days=3') 59 ->status_is(200) 60 ->json_is( '/days' => 3 ) 61 ->json_is( '/probe' => 'ph' ); 62 }; 63 64 subtest 'derivatives API returns all probes' => sub { 65 $t->get_ok('/api/insights/derivatives') 66 ->status_is(200) 67 ->json_has('/probes') 68 ->json_has('/probes/ph') 69 ->json_has('/probes/do') 70 ->json_has('/probes/orp') 71 ->json_has('/probes/ec') 72 ->json_is( '/hours' => 48 ); 73 }; 74 75 subtest 'derivatives API accepts hours param' => sub { 76 $t->get_ok('/api/insights/derivatives?hours=24') 77 ->status_is(200) 78 ->json_is( '/hours' => 24 ); 79 }; 80 81 subtest 'stability API returns points array' => sub { 82 $t->get_ok('/api/insights/stability') 83 ->status_is(200) 84 ->json_has('/points') 85 ->json_is( '/hours' => 168 ) 86 ->json_is( '/window' => 6 ); 87 }; 88 89 subtest 'stability API accepts hours param' => sub { 90 $t->get_ok('/api/insights/stability?hours=48') 91 ->status_is(200) 92 ->json_is( '/hours' => 48 ); 93 }; 94 95 subtest 'empty database insights page' => sub { 96 my $empty = test_empty_app(); 97 $empty->get_ok('/insights') 98 ->status_is(200) 99 ->element_exists('canvas#chart-stability'); 100 }; 101 102 subtest 'insights APIs with no recent data' => sub { 103 104 # test_app has data from 2026-07-05, outside default time windows 105 # so these return empty/minimal results gracefully 106 $t->get_ok('/api/insights/stability?hours=1') 107 ->status_is(200) 108 ->json_is( '/points' => [] ); 109 $t->get_ok('/api/insights/derivatives?hours=1') 110 ->status_is(200) 111 ->json_has('/probes'); 112 $t->get_ok('/api/insights/diurnal/do?days=1') 113 ->status_is(200) 114 ->json_is( '/traces' => [] ); 115 }; 116 117 subtest 'insights APIs with matching data' => sub { 118 119 # Seed recent hourly rollups to exercise computation paths 120 my $now = time; 121 122 for my $offset ( 1 .. 12 ) { 123 my $epoch = ( int( $now / 3600 ) - $offset ) * 3600; 124 for my $probe (qw(ph do orp ec)) { 125 my %base = ( ph => 7.0, do => 8.0, orp => 350, ec => 800 ); 126 my $val = $base{$probe} + ( $offset * 0.1 ); 127 $t->app->sqlite->db->query( 128 q{INSERT OR IGNORE INTO reading_rollups 129 (probe, bucket_seconds, bucket_epoch, sample_count, 130 value_total, value_min, value_max, 131 first_sample_epoch, last_sample_epoch, max_gap_seconds) 132 VALUES (?, 3600, ?, 10, ?, ?, ?, ?, ?, 0)}, 133 $probe, $epoch, $val * 10, $val - 0.1, $val + 0.1, 134 $epoch, $epoch + 3000, 135 ); 136 } 137 } 138 139 $t->get_ok('/api/insights/stability?hours=24') 140 ->status_is(200) 141 ->json_has('/points'); 142 143 $t->get_ok('/api/insights/derivatives?hours=24') 144 ->status_is(200) 145 ->json_has('/probes/ph'); 146 147 my $derivs = $t->tx->res->json->{probes}; 148 ok @{ $derivs->{ph} } > 0, 'derivatives returns rate points for ph'; 149 150 $t->get_ok('/api/insights/diurnal/do?days=1') 151 ->status_is(200) 152 ->json_has('/traces'); 153 154 my $diurnal = $t->tx->res->json; 155 ok @{ $diurnal->{traces} } > 0, 'diurnal returns traces with recent data'; 156 }; 157 158 done_testing; 159