2026-06-14 04:38:48 +00:00
package reporttemplate
import (
"strings"
"testing"
)
func TestTemplateLookup ( t * testing . T ) {
source , err := Template ( "hourly" )
if err != nil {
t . Fatalf ( "Template() error = %v" , err )
}
2026-06-16 20:53:27 -05:00
for _ , want := range [ ] string { "# {{ .Report.Title }}" , "**Updated:**" , ` {{ template "alert_digest" . }} ` , "## Current Conditions" , "## Hourly Forecast" , "## Forecast Discussion" } {
2026-06-14 04:38:48 +00:00
if ! strings . Contains ( source , want ) {
t . Fatalf ( "template missing %q:\n%s" , want , source )
}
}
}
2026-06-14 23:36:47 +00:00
func TestTomorrowTemplateLookup ( t * testing . T ) {
source , err := Template ( "tomorrow" )
if err != nil {
t . Fatalf ( "Template() error = %v" , err )
}
2026-06-16 20:53:27 -05:00
for _ , want := range [ ] string { "# {{ .Report.Title }}" , "**Forecast date:**" , ` {{ template "alert_digest" . }} ` , ` {{ template "daypart_forecast" . }} ` , ` {{ template "precipitation_timing" . }} ` , "## Forecast Discussion" } {
2026-06-14 23:36:47 +00:00
if ! strings . Contains ( source , want ) {
t . Fatalf ( "template missing %q:\n%s" , want , source )
}
}
}
2026-06-15 16:23:13 +00:00
func TestDailyTemplateLookup ( t * testing . T ) {
source , err := Template ( "daily" )
if err != nil {
t . Fatalf ( "Template() error = %v" , err )
}
2026-06-16 20:53:27 -05:00
for _ , want := range [ ] string { "# {{ .Report.Title }}" , "**Forecast date:**" , ` {{ template "alert_digest" . }} ` , ` {{ template "daypart_forecast" . }} ` , ` {{ template "precipitation_timing" . }} ` , "## Forecast Discussion" } {
2026-06-15 16:23:13 +00:00
if ! strings . Contains ( source , want ) {
t . Fatalf ( "template missing %q:\n%s" , want , source )
}
}
}
2026-06-15 14:45:49 +00:00
func TestTodayTemplateLookup ( t * testing . T ) {
source , err := Template ( "today" )
if err != nil {
t . Fatalf ( "Template() error = %v" , err )
}
2026-06-16 20:53:27 -05:00
for _ , want := range [ ] string { "# {{ .Report.Title }}" , "**Forecast date:**" , ` {{ template "alert_digest" . }} ` , "## Current Conditions" , ` {{ template "today_daypart_forecast" . }} ` , ` {{ template "precipitation_timing" . }} ` , "## Forecast Discussion" } {
2026-06-15 14:45:49 +00:00
if ! strings . Contains ( source , want ) {
t . Fatalf ( "template missing %q:\n%s" , want , source )
}
}
2026-06-15 11:01:03 -05:00
if strings . Contains ( source , "## Planning Notes" ) {
t . Fatalf ( "template includes Planning Notes section:\n%s" , source )
}
2026-06-15 14:45:49 +00:00
}
2026-06-14 04:38:48 +00:00
func TestRenderHourly ( t * testing . T ) {
rendered , err := Render ( "hourly" , testRenderContext {
2026-06-14 08:57:53 -05:00
Report : testReportContext {
Title : "Hourly Report" ,
LocationName : "Brentwood" ,
ValidPeriodLabel : "May 29, 8:30 AM to 2:30 PM" ,
2026-06-14 12:54:32 -05:00
GeneratedAtLabel : "Saturday, June 14, 2026 at 9:14 AM" ,
2026-06-14 08:57:53 -05:00
} ,
2026-06-14 04:38:48 +00:00
GeneratedText : testGeneratedText {
2026-06-14 12:54:32 -05:00
Summary : "Storm chances increase through late morning." ,
ForecastDiscussion : "A front will keep the region unsettled." ,
PrecipitationTiming : "A cold front is moving into the region." ,
2026-06-14 04:38:48 +00:00
} ,
2026-06-14 08:57:53 -05:00
Modules : testModules {
CurrentConditions : & testCurrentConditions {
ConditionText : "Partly cloudy" ,
2026-06-14 12:54:32 -05:00
ConditionTextLower : "partly cloudy" ,
2026-06-14 17:40:19 -05:00
TemperatureF : intPtr ( 74 ) ,
ApparentTemperatureF : intPtr ( 76 ) ,
RelativeHumidityPercent : intPtr ( 71 ) ,
2026-06-14 08:57:53 -05:00
WindDirection : "S" ,
2026-06-14 12:54:32 -05:00
WindDirectionText : "south" ,
2026-06-14 17:40:19 -05:00
WindSpeedMph : intPtr ( 8 ) ,
2026-06-14 08:57:53 -05:00
} ,
HourlyForecast : & testHourlyForecast {
Periods : [ ] testHourlyPeriod {
2026-06-14 12:54:32 -05:00
{ HourLabel : "9:00 AM" , TextDescription : "Cloudy" , TextDescriptionLower : "cloudy" , TemperatureF : floatPtr ( 74 ) , ProbabilityOfPrecipitationPercent : floatPtr ( 19 ) , WindDirection : "S" , WindSpeedMph : floatPtr ( 8 ) } ,
{ HourLabel : "10:00 AM" , TextDescription : "Showers" , TextDescriptionLower : "showers" , TemperatureF : floatPtr ( 75 ) , ProbabilityOfPrecipitationPercent : floatPtr ( 70 ) , MentionPrecipitation : true , WindDirection : "S" , WindSpeedMph : floatPtr ( 10 ) } ,
2026-06-14 08:57:53 -05:00
} ,
} ,
PrecipTiming : & testPrecipTiming {
MaxPopPercent : intPtr ( 70 ) ,
MaxPopTime : "10 AM" ,
PrecipitationWindows : [ ] testPrecipWindow {
2026-06-16 19:20:26 -05:00
{ PeriodBegins : "10 AM" , PeriodBeginsHourLabel : "10:00 AM" , PeriodEnds : "12 PM" , PeriodEndsHourLabel : "12:00 PM" , MaxPopPercent : intPtr ( 70 ) , MaxPopTime : "10 AM" , MaxPopHourLabel : "10:00 AM" , ExpectationPhrase : "Expect showers." } ,
2026-06-14 08:57:53 -05:00
} ,
} ,
AlertDigest : & testAlertDigest {
2026-06-16 21:09:17 -05:00
Relevant : [ ] testAlert { { Event : "Flood Watch" , Headline : "Flood Watch until 2:30 PM" , Severity : "Moderate" , PeriodBegins : "May 29 at 10:00 AM" , PeriodEnds : "May 29 at 2:30 PM" , Instruction : "Avoid low-water crossings." } } ,
2026-06-14 08:57:53 -05:00
} ,
SPCConvectiveOutlooks : & testSPCOutlooks {
2026-06-16 21:42:20 -05:00
Outlooks : [ ] testSPCOutlook { { LabelText : "Slight Risk" , PeriodBegins : "8 AM" , PeriodEnds : "2 PM" } } ,
RiskDigest : [ ] testSPCRiskDigest { { LabelText : "Slight Risk" , RiskLabel : "Slight risk" , PeriodBegins : "May 29 at 8:00 AM" , PeriodEnds : "May 29 at 2:00 PM" } } ,
2026-06-14 08:57:53 -05:00
} ,
SPCConvectiveDiscussion : & testSPCDiscussion {
Discussions : [ ] testSPCDiscussionRecord { { Summary : "Strong storms may develop late morning." } } ,
} ,
AreaForecastDiscussion : & testForecastDiscussion {
KeyMessages : [ ] string { "Storms are most likely late morning." } ,
ShortTerm : "Short-term discussion favors increasing rain coverage." ,
} ,
WeatherStory : & testWeatherStory {
Available : true ,
Title : "Morning storms" ,
Description : "Morning storms remain the main story." ,
} ,
2026-06-14 04:38:48 +00:00
} ,
} )
if err != nil {
t . Fatalf ( "Render() error = %v" , err )
}
text := string ( rendered )
for _ , want := range [ ] string {
"# Hourly Report" ,
2026-06-14 12:54:32 -05:00
"**Updated:** Saturday, June 14, 2026 at 9:14 AM" ,
2026-06-14 04:38:48 +00:00
"Storm chances increase through late morning." ,
2026-06-14 12:54:32 -05:00
"Currently, it is 74°F and partly cloudy. It feels like 76°F, with a relative humidity of 71% and winds from the south at 8 mph." ,
2026-06-16 21:42:20 -05:00
"## Alert Digest" ,
2026-07-02 11:05:31 -05:00
"- **Flood Watch**: Flood Watch in effect from May 29 at 10:00 AM to May 29 at 2:30 PM." ,
2026-06-14 12:54:32 -05:00
"- **9:00 AM:** 74°F and cloudy." ,
"- **10:00 AM:** 75°F and showers. Probability of precipitation is 70%." ,
2026-06-16 19:20:26 -05:00
"- **10:00 AM** to **12:00 PM**: Expect showers. The peak precipitation chance is 70% at 10:00 AM." ,
2026-06-14 12:54:32 -05:00
"A cold front is moving into the region." ,
"A front will keep the region unsettled." ,
2026-06-14 04:38:48 +00:00
} {
if ! strings . Contains ( text , want ) {
t . Fatalf ( "rendered template missing %q:\n%s" , want , text )
}
}
2026-08-01 01:25:57 +00:00
if strings . Contains ( text , "19%" ) || strings . Contains ( text , "wind S" ) {
2026-06-14 12:54:32 -05:00
t . Fatalf ( "rendered template included omitted details:\n%s" , text )
}
2026-07-02 11:05:31 -05:00
for _ , unwanted := range [ ] string { "Avoid low-water crossings." , "Slight risk for severe thunderstorms" } {
if strings . Contains ( text , unwanted ) {
t . Fatalf ( "rendered template included %q:\n%s" , unwanted , text )
}
}
2026-06-14 05:24:25 +00:00
assertOrderedText ( t , text , [ ] string {
"# Hourly Report" ,
2026-06-16 21:42:20 -05:00
"## Alert Digest" ,
2026-06-16 20:53:27 -05:00
"## Current Conditions" ,
2026-06-14 05:24:25 +00:00
"## Hourly Forecast" ,
"## Precipitation Timing" ,
"## Forecast Discussion" ,
} )
2026-06-14 04:38:48 +00:00
}
2026-06-14 23:36:47 +00:00
func TestRenderTomorrow ( t * testing . T ) {
rendered , err := Render ( "tomorrow" , testTomorrowRenderContext {
Report : testTomorrowReportContext {
Title : "Monday's Weather" ,
ForecastDateLabel : "Monday, June 15, 2026" ,
GeneratedAtLabel : "Sunday, June 14, 2026 at 9:14 AM" ,
} ,
GeneratedText : testTomorrowGeneratedText {
Summary : "Tomorrow starts dry before showers return later in the day." ,
ForecastDiscussion : [ ] string {
"Clouds increase after sunrise." ,
"Rain chances peak during the afternoon." ,
} ,
PrecipitationTiming : "A few showers may linger into early evening." ,
} ,
Modules : testTomorrowModules {
Dayparts : [ ] testTomorrowDaypart {
2026-06-14 23:26:45 -05:00
{
Key : "overnight" ,
Summary : testDaypartSummary {
DisplayName : "Overnight" ,
TemperatureTrend : "falling" ,
TemperatureStartPhraseF : "mid 60s" ,
TemperatureEndPhraseF : "upper 50s" ,
DominantConditionDisplay : "Partly cloudy" ,
DominantConditionLower : "partly cloudy" ,
} ,
} ,
2026-06-14 23:36:47 +00:00
{
Key : "morning" ,
Summary : testDaypartSummary {
2026-06-14 23:26:45 -05:00
DisplayName : "Morning" ,
TemperatureTrend : "rising" ,
TemperatureStartPhraseF : "upper 50s" ,
TemperatureEndPhraseF : "upper 60s" ,
DominantConditionDisplay : "Sunny" ,
DominantConditionLower : "sunny" ,
2026-06-14 23:36:47 +00:00
} ,
} ,
{
Key : "afternoon" ,
Summary : testDaypartSummary {
2026-06-14 23:26:45 -05:00
DisplayName : "Afternoon" ,
TemperatureTrend : "steady" ,
TemperatureSteadyPhraseF : "upper 70s" ,
DominantConditionDisplay : "Sunny" ,
DominantConditionLower : "sunny" ,
MaxPopPercent : intPtr ( 70 ) ,
MaxPopTimeLabel : "3:00 PM" ,
MentionPrecipitation : true ,
2026-06-14 23:36:47 +00:00
} ,
} ,
} ,
PrecipTiming : & testPrecipTiming {
PrecipitationWindows : [ ] testPrecipWindow {
2026-06-16 19:10:11 -05:00
{ PeriodBeginsHourLabel : "3:00 PM" , PeriodEndsHourLabel : "6:00 PM" , MaxPopPercent : intPtr ( 70 ) , MaxPopHourLabel : "3:00 PM" , ExpectationPhrase : "Expect showers." } ,
2026-06-14 23:36:47 +00:00
} ,
} ,
2026-06-16 20:53:27 -05:00
AlertDigest : & testAlertDigest {
2026-06-16 21:09:17 -05:00
Relevant : [ ] testAlert { { Event : "Wind Advisory" , Headline : "Wind Advisory until 8:00 PM" , Severity : "Moderate" , PeriodBegins : "June 15 at 1:00 PM" , PeriodEnds : "June 15 at 8:00 PM" , Instruction : "Secure outdoor objects." } } ,
2026-06-16 20:53:27 -05:00
} ,
2026-06-16 21:42:20 -05:00
SPCConvectiveOutlooks : & testSPCOutlooks {
RiskDigest : [ ] testSPCRiskDigest { { LabelText : "Slight Risk" , RiskLabel : "Slight risk" , PeriodBegins : "June 15 at 7:00 AM" , PeriodEnds : "June 16 at 7:00 AM" } } ,
} ,
2026-06-14 23:36:47 +00:00
} ,
} )
if err != nil {
t . Fatalf ( "Render() error = %v" , err )
}
text := string ( rendered )
for _ , want := range [ ] string {
"# Monday's Weather" ,
"**Forecast date:** Monday, June 15, 2026" ,
"**Updated:** Sunday, June 14, 2026 at 9:14 AM" ,
"Tomorrow starts dry before showers return later in the day." ,
2026-06-16 21:42:20 -05:00
"## Alert Digest" ,
2026-07-02 11:05:31 -05:00
"- **Wind Advisory**: Wind Advisory in effect from June 15 at 1:00 PM to June 15 at 8:00 PM." ,
2026-06-14 23:26:45 -05:00
"- **Overnight:** Partly cloudy, with temperatures falling from the mid 60s to the upper 50s." ,
"- **Morning:** Sunny, with temperatures rising from the upper 50s to the upper 60s." ,
"- **Afternoon:** Sunny, with temperatures in the upper 70s. Chance of precipitation is 70%." ,
2026-06-16 19:10:11 -05:00
"- **3:00 PM** to **6:00 PM**: Expect showers. The peak precipitation chance is 70% at 3:00 PM." ,
2026-06-14 23:36:47 +00:00
"A few showers may linger into early evening." ,
"Clouds increase after sunrise." ,
"Rain chances peak during the afternoon." ,
} {
if ! strings . Contains ( text , want ) {
t . Fatalf ( "rendered template missing %q:\n%s" , want , text )
}
}
2026-06-14 23:26:45 -05:00
for _ , unwanted := range [ ] string {
"upper 50s.\n\n- **Morning:**" ,
"upper 60s.\n\n- **Afternoon:**" ,
2026-07-02 11:05:31 -05:00
"Secure outdoor objects." ,
"Slight risk for severe thunderstorms" ,
2026-06-14 23:26:45 -05:00
} {
if strings . Contains ( text , unwanted ) {
t . Fatalf ( "rendered template includes blank lines between daypart bullets:\n%s" , text )
}
}
2026-06-14 23:36:47 +00:00
assertOrderedText ( t , text , [ ] string {
"# Monday's Weather" ,
2026-06-16 20:53:27 -05:00
"Tomorrow starts dry before showers return later in the day." ,
2026-06-16 21:42:20 -05:00
"## Alert Digest" ,
2026-06-14 23:36:47 +00:00
"## Daypart Forecast" ,
2026-06-14 23:26:45 -05:00
"- **Overnight:**" ,
2026-06-14 23:36:47 +00:00
"- **Morning:**" ,
"- **Afternoon:**" ,
"## Precipitation Timing" ,
"## Forecast Discussion" ,
"Clouds increase after sunrise." ,
"Rain chances peak during the afternoon." ,
} )
}
2026-06-15 16:23:13 +00:00
func TestRenderDaily ( t * testing . T ) {
rendered , err := Render ( "daily" , testDailyRenderContext {
Report : testDailyReportContext {
Title : "Monday's Weather" ,
ForecastDateLabel : "Monday, June 15, 2026" ,
GeneratedAtLabel : "Sunday, June 14, 2026 at 9:14 AM" ,
} ,
GeneratedText : testDailyGeneratedText {
Summary : "The selected day starts dry before showers return later in the day." ,
ForecastDiscussion : [ ] string {
"Clouds increase after sunrise." ,
"Rain chances peak during the afternoon." ,
} ,
PrecipitationTiming : "A few showers may linger into early evening." ,
} ,
Modules : testDailyModules {
Dayparts : [ ] testDailyDaypart {
{
Key : "morning" ,
Summary : testDaypartSummary {
DisplayName : "Morning" ,
TemperatureTrend : "rising" ,
TemperatureStartPhraseF : "upper 50s" ,
TemperatureEndPhraseF : "upper 60s" ,
DominantConditionDisplay : "Sunny" ,
DominantConditionLower : "sunny" ,
} ,
} ,
{
Key : "afternoon" ,
Summary : testDaypartSummary {
DisplayName : "Afternoon" ,
TemperatureTrend : "steady" ,
TemperatureSteadyPhraseF : "upper 70s" ,
DominantConditionDisplay : "Showers" ,
DominantConditionLower : "showers" ,
MaxPopPercent : intPtr ( 70 ) ,
MaxPopTimeLabel : "3:00 PM" ,
MentionPrecipitation : true ,
} ,
} ,
} ,
PrecipTiming : & testPrecipTiming {
PrecipitationWindows : [ ] testPrecipWindow {
2026-06-16 19:10:11 -05:00
{ PeriodBeginsHourLabel : "3:00 PM" , PeriodEndsHourLabel : "6:00 PM" , MaxPopPercent : intPtr ( 70 ) , MaxPopHourLabel : "3:00 PM" , ExpectationPhrase : "Expect showers." } ,
2026-06-15 16:23:13 +00:00
} ,
} ,
2026-06-16 20:53:27 -05:00
AlertDigest : & testAlertDigest {
2026-06-16 21:09:17 -05:00
Relevant : [ ] testAlert { { Event : "Flood Watch" , Headline : "Flood Watch until 6:00 PM" , Severity : "Moderate" , PeriodBegins : "June 15 at 3:00 PM" , PeriodEnds : "June 15 at 6:00 PM" , Description : "Monitor creek levels." } } ,
2026-06-16 20:53:27 -05:00
} ,
2026-06-16 21:42:20 -05:00
SPCConvectiveOutlooks : & testSPCOutlooks {
RiskDigest : [ ] testSPCRiskDigest { { LabelText : "Enhanced Risk" , RiskLabel : "Enhanced risk" , PeriodBegins : "June 15 at 7:00 AM" , PeriodEnds : "June 16 at 7:00 AM" } } ,
} ,
2026-06-15 16:23:13 +00:00
} ,
} )
if err != nil {
t . Fatalf ( "Render() error = %v" , err )
}
text := string ( rendered )
for _ , want := range [ ] string {
"# Monday's Weather" ,
"**Forecast date:** Monday, June 15, 2026" ,
"**Updated:** Sunday, June 14, 2026 at 9:14 AM" ,
"The selected day starts dry before showers return later in the day." ,
2026-06-16 21:42:20 -05:00
"## Alert Digest" ,
2026-07-02 11:05:31 -05:00
"- **Flood Watch**: Flood Watch in effect from June 15 at 3:00 PM to June 15 at 6:00 PM." ,
2026-06-16 21:42:20 -05:00
"- **SPC Convective Outlook**: Enhanced risk for severe thunderstorms in effect from June 15 at 7:00 AM to June 16 at 7:00 AM." ,
2026-06-15 16:23:13 +00:00
"- **Morning:** Sunny, with temperatures rising from the upper 50s to the upper 60s." ,
"- **Afternoon:** Showers, with temperatures in the upper 70s. Chance of precipitation is 70%." ,
2026-06-16 19:10:11 -05:00
"- **3:00 PM** to **6:00 PM**: Expect showers. The peak precipitation chance is 70% at 3:00 PM." ,
2026-06-15 16:23:13 +00:00
"A few showers may linger into early evening." ,
"Clouds increase after sunrise." ,
"Rain chances peak during the afternoon." ,
} {
if ! strings . Contains ( text , want ) {
t . Fatalf ( "rendered template missing %q:\n%s" , want , text )
}
}
2026-07-02 11:05:31 -05:00
if strings . Contains ( text , "Monitor creek levels." ) {
t . Fatalf ( "rendered template included alert description:\n%s" , text )
}
2026-06-15 16:23:13 +00:00
assertOrderedText ( t , text , [ ] string {
"# Monday's Weather" ,
2026-06-16 20:53:27 -05:00
"The selected day starts dry before showers return later in the day." ,
2026-06-16 21:42:20 -05:00
"## Alert Digest" ,
2026-06-15 16:23:13 +00:00
"## Daypart Forecast" ,
"- **Morning:**" ,
"- **Afternoon:**" ,
"## Precipitation Timing" ,
"## Forecast Discussion" ,
} )
}
2026-06-15 14:45:49 +00:00
func TestRenderToday ( t * testing . T ) {
rendered , err := Render ( "today" , testTodayRenderContext {
Report : testTodayReportContext {
Title : "Today's Weather" ,
ForecastDateLabel : "Monday, June 15, 2026" ,
GeneratedAtLabel : "Monday, June 15, 2026 at 7:14 AM" ,
} ,
GeneratedText : testTomorrowGeneratedText {
Summary : "Today starts dry before showers return later in the day." ,
ForecastDiscussion : [ ] string {
"Clouds increase after sunrise." ,
"Rain chances peak during the afternoon." ,
} ,
PrecipitationTiming : "A few showers may linger into early evening." ,
} ,
Modules : testTodayModules {
CurrentConditions : & testCurrentConditions {
2026-06-15 11:01:03 -05:00
ConditionTextLower : "clear" ,
TemperatureF : intPtr ( 58 ) ,
ApparentTemperatureF : intPtr ( 57 ) ,
RelativeHumidityPercent : intPtr ( 61 ) ,
WindDirectionText : "northwest" ,
WindSpeedMph : intPtr ( 9 ) ,
2026-06-15 14:45:49 +00:00
} ,
Dayparts : [ ] testTomorrowDaypart {
{
Key : "morning" ,
Summary : testDaypartSummary {
DisplayName : "Morning" ,
TemperatureTrend : "rising" ,
TemperatureStartPhraseF : "upper 50s" ,
TemperatureEndPhraseF : "upper 60s" ,
DominantConditionDisplay : "Sunny" ,
DominantConditionLower : "sunny" ,
} ,
} ,
{
Key : "afternoon" ,
Summary : testDaypartSummary {
DisplayName : "Afternoon" ,
TemperatureTrend : "steady" ,
TemperatureSteadyPhraseF : "upper 70s" ,
DominantConditionDisplay : "Showers" ,
DominantConditionLower : "showers" ,
MaxPopPercent : intPtr ( 70 ) ,
MaxPopTimeLabel : "3:00 PM" ,
MentionPrecipitation : true ,
} ,
} ,
2026-06-15 11:01:03 -05:00
{
Key : "evening" ,
Summary : testDaypartSummary {
DisplayName : "Evening" ,
} ,
} ,
2026-06-15 14:45:49 +00:00
} ,
PrecipTiming : & testPrecipTiming {
PrecipitationWindows : [ ] testPrecipWindow {
2026-06-16 19:10:11 -05:00
{ PeriodBeginsHourLabel : "3:00 PM" , PeriodEndsHourLabel : "6:00 PM" , MaxPopPercent : intPtr ( 70 ) , MaxPopHourLabel : "3:00 PM" , ExpectationPhrase : "Expect showers." } ,
2026-06-15 14:45:49 +00:00
} ,
} ,
2026-06-16 20:53:27 -05:00
AlertDigest : & testAlertDigest {
2026-06-16 21:09:17 -05:00
Relevant : [ ] testAlert { { Event : "Wind Advisory" , Headline : "Wind Advisory until 8:00 PM" , Severity : "Moderate" , PeriodBegins : "June 15 at 1:00 PM" , PeriodEnds : "June 15 at 8:00 PM" , Instruction : "Secure outdoor objects." } } ,
2026-06-16 20:53:27 -05:00
} ,
2026-06-16 21:42:20 -05:00
SPCConvectiveOutlooks : & testSPCOutlooks {
RiskDigest : [ ] testSPCRiskDigest { { LabelText : "Slight Risk" , RiskLabel : "Slight risk" , PeriodBegins : "June 15 at 7:00 AM" , PeriodEnds : "June 16 at 7:00 AM" } } ,
} ,
2026-06-15 14:45:49 +00:00
TodayPlanning : & testTodayPlanning {
MorningReadiness : [ ] string { "Morning weather looks routine." } ,
LateDayChangeWatch : [ ] string { "Watch late-day shower timing." } ,
} ,
} ,
} )
if err != nil {
t . Fatalf ( "Render() error = %v" , err )
}
text := string ( rendered )
for _ , want := range [ ] string {
"# Today's Weather" ,
"**Forecast date:** Monday, June 15, 2026" ,
"**Updated:** Monday, June 15, 2026 at 7:14 AM" ,
"Today starts dry before showers return later in the day." ,
2026-06-16 21:42:20 -05:00
"## Alert Digest" ,
2026-07-02 11:05:31 -05:00
"- **Wind Advisory**: Wind Advisory in effect from June 15 at 1:00 PM to June 15 at 8:00 PM." ,
2026-06-15 11:01:03 -05:00
"Currently, it is 58°F and clear. It feels like 57°F, with a relative humidity of 61% and winds from the northwest at 9 mph." ,
2026-06-15 14:45:49 +00:00
"- **Morning:** Sunny, with temperatures rising from the upper 50s to the upper 60s." ,
"- **Afternoon:** Showers, with temperatures in the upper 70s. Chance of precipitation is 70%." ,
2026-06-16 19:10:11 -05:00
"- **3:00 PM** to **6:00 PM**: Expect showers. The peak precipitation chance is 70% at 3:00 PM." ,
2026-06-15 14:45:49 +00:00
"A few showers may linger into early evening." ,
"Clouds increase after sunrise." ,
"Rain chances peak during the afternoon." ,
} {
if ! strings . Contains ( text , want ) {
t . Fatalf ( "rendered template missing %q:\n%s" , want , text )
}
}
assertOrderedText ( t , text , [ ] string {
"# Today's Weather" ,
2026-06-16 20:53:27 -05:00
"Today starts dry before showers return later in the day." ,
2026-06-16 21:42:20 -05:00
"## Alert Digest" ,
2026-06-15 14:45:49 +00:00
"## Current Conditions" ,
"## Daypart Forecast" ,
"- **Morning:**" ,
"- **Afternoon:**" ,
"## Precipitation Timing" ,
"## Forecast Discussion" ,
} )
2026-07-02 11:05:31 -05:00
for _ , unwanted := range [ ] string { "## Planning Notes" , "Morning weather looks routine." , "Watch late-day shower timing." , "- **Evening:**" , "Forecast details are limited" , "Secure outdoor objects." , "Slight risk for severe thunderstorms" } {
2026-06-15 11:01:03 -05:00
if strings . Contains ( text , unwanted ) {
t . Fatalf ( "rendered template included %q:\n%s" , unwanted , text )
}
}
2026-06-15 14:45:49 +00:00
}
2026-06-15 20:38:56 +00:00
func TestRenderDaypartTemplatesUseRichHelperFields ( t * testing . T ) {
tests := [ ] struct {
name string
template string
context any
want [ ] string
} {
{
name : "daily" ,
template : "daily" ,
context : testDailyRenderContext {
Report : testDailyReportContext { Title : "Daily" , ForecastDateLabel : "Monday, June 15, 2026" } ,
GeneratedText : testDailyGeneratedText { Summary : "Daily summary." } ,
Modules : testDailyModules { Dayparts : [ ] testDailyDaypart {
{ Key : "morning" , Summary : testDaypartSummary {
DisplayName : "Morning" ,
DominantConditionDisplay : "Showers" ,
TemperaturePhraseF : "upper 60s" ,
} } ,
} } ,
} ,
want : [ ] string { "- **Morning:** Showers, with temperatures in the upper 60s." } ,
} ,
{
name : "today" ,
template : "today" ,
context : testTodayRenderContext {
Report : testTodayReportContext { Title : "Today" , ForecastDateLabel : "Monday, June 15, 2026" } ,
GeneratedText : testTomorrowGeneratedText { Summary : "Today summary." } ,
Modules : testTodayModules { Dayparts : [ ] testTomorrowDaypart {
{ Key : "afternoon" , Summary : testDaypartSummary {
DisplayName : "Afternoon" ,
DominantConditionDisplay : "Storms" ,
TemperaturePeakPhraseF : "low 80s" ,
TemperatureTrend : "peaking" ,
MaxPopPercent : intPtr ( 70 ) ,
MentionPrecipitation : true ,
} } ,
} } ,
} ,
want : [ ] string { "- **Afternoon:** Storms, with temperatures peaking in the low 80s. Chance of precipitation is 70%." } ,
} ,
{
name : "tomorrow" ,
template : "tomorrow" ,
context : testTomorrowRenderContext {
Report : testTomorrowReportContext { Title : "Tomorrow" , ForecastDateLabel : "Tuesday, June 16, 2026" } ,
GeneratedText : testTomorrowGeneratedText { Summary : "Tomorrow summary." } ,
Modules : testTomorrowModules { Dayparts : [ ] testTomorrowDaypart {
{ Key : "morning" , Summary : testDaypartSummary {
DisplayName : "Morning" ,
DominantConditionDisplay : "Clear" ,
TemperatureStartPhraseF : "upper 50s" ,
TemperatureEndPhraseF : "upper 60s" ,
TemperatureTrend : "rising" ,
} } ,
} } ,
} ,
want : [ ] string { "- **Morning:** Clear, with temperatures rising from the upper 50s to the upper 60s." } ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
rendered , err := Render ( tt . template , tt . context )
if err != nil {
t . Fatalf ( "Render() error = %v" , err )
}
text := string ( rendered )
for _ , want := range tt . want {
if ! strings . Contains ( text , want ) {
t . Fatalf ( "rendered template missing %q:\n%s" , want , text )
}
}
2026-06-16 21:42:20 -05:00
if strings . Contains ( text , "## Alert Digest" ) {
t . Fatalf ( "rendered template includes empty Alerts and Risk Products section:\n%s" , text )
2026-06-16 20:53:27 -05:00
}
if strings . Contains ( text , "\n\n\n" ) {
t . Fatalf ( "rendered template includes excess blank lines:\n%s" , text )
}
2026-06-15 20:38:56 +00:00
} )
}
}
2026-08-13 02:11:25 +00:00
func TestRenderTodayUsesDaypartFallbackWhenRowsAreNotDisplayable ( t * testing . T ) {
rendered , err := Render ( "today" , testTodayRenderContext {
Report : testTodayReportContext { Title : "Today" , ForecastDateLabel : "Monday, June 15, 2026" } ,
GeneratedText : testTomorrowGeneratedText { Summary : "Today summary." } ,
Modules : testTodayModules { Dayparts : [ ] testTomorrowDaypart {
{ Key : "morning" , Summary : testDaypartSummary { DisplayName : "Morning" } } ,
} } ,
} )
if err != nil {
t . Fatalf ( "Render() error = %v" , err )
}
text := string ( rendered )
if ! strings . Contains ( text , "## Daypart Forecast\n- No daypart forecast details are available." ) {
t . Fatalf ( "rendered template missing daypart fallback:\n%s" , text )
}
if strings . Contains ( text , "- **Morning:**" ) {
t . Fatalf ( "rendered template included an unavailable daypart row:\n%s" , text )
}
}
2026-06-14 23:36:47 +00:00
func TestRenderTomorrowOmitsPrecipitationTimingWithoutWindows ( t * testing . T ) {
rendered , err := Render ( "tomorrow" , testTomorrowRenderContext {
Report : testTomorrowReportContext {
Title : "Monday's Weather" ,
ForecastDateLabel : "Monday, June 15, 2026" ,
GeneratedAtLabel : "Sunday, June 14, 2026 at 9:14 AM" ,
} ,
GeneratedText : testTomorrowGeneratedText {
Summary : "Dry weather is expected tomorrow." ,
ForecastDiscussion : [ ] string { "High pressure keeps rain chances low." } ,
PrecipitationTiming : "Any stray shower chance is too low to highlight." ,
} ,
Modules : testTomorrowModules {
Dayparts : [ ] testTomorrowDaypart {
{
Key : "morning" ,
Summary : testDaypartSummary {
2026-06-14 23:26:45 -05:00
DisplayName : "Morning" ,
TemperatureTrend : "steady" ,
TemperatureSteadyPhraseF : "low 60s" ,
DominantConditionDisplay : "Clear" ,
DominantConditionLower : "clear" ,
2026-06-14 23:36:47 +00:00
} ,
} ,
} ,
PrecipTiming : & testPrecipTiming { } ,
} ,
} )
if err != nil {
t . Fatalf ( "Render() error = %v" , err )
}
text := string ( rendered )
for _ , unwanted := range [ ] string { "## Precipitation Timing" , "Any stray shower chance is too low to highlight." } {
if strings . Contains ( text , unwanted ) {
t . Fatalf ( "dry render includes %q:\n%s" , unwanted , text )
}
}
}
2026-07-02 11:05:31 -05:00
func TestAlertDigestOmitsBelowThresholdSPCRiskOnlySection ( t * testing . T ) {
rendered , err := Render ( "hourly" , testRenderContext {
Report : testReportContext { Title : "Hourly Report" } ,
GeneratedText : testGeneratedText {
Summary : "Storm chances remain low." ,
ForecastDiscussion : "Only isolated severe storms are possible." ,
} ,
Modules : testModules {
CurrentConditions : & testCurrentConditions { } ,
HourlyForecast : & testHourlyForecast { } ,
SPCConvectiveOutlooks : & testSPCOutlooks {
RiskDigest : [ ] testSPCRiskDigest { { LabelText : "Slight Risk" , RiskLabel : "Slight risk" , PeriodBegins : "June 15 at 7:00 AM" , PeriodEnds : "June 16 at 7:00 AM" } } ,
} ,
} ,
} )
if err != nil {
t . Fatalf ( "Render() error = %v" , err )
}
text := string ( rendered )
for _ , unwanted := range [ ] string { "## Alert Digest" , "SPC Convective Outlook" , "Slight risk for severe thunderstorms" } {
if strings . Contains ( text , unwanted ) {
t . Fatalf ( "rendered template included %q for below-threshold SPC-only digest:\n%s" , unwanted , text )
}
}
}
func TestAlertDigestRendersEnhancedOrHigherSPCRiskWithoutAlerts ( t * testing . T ) {
tests := [ ] struct {
label string
risk string
} {
{ label : "Enhanced Risk" , risk : "Enhanced risk" } ,
{ label : "Moderate Risk" , risk : "Moderate risk" } ,
{ label : "High Risk" , risk : "High risk" } ,
}
for _ , tt := range tests {
t . Run ( tt . label , func ( t * testing . T ) {
rendered , err := Render ( "hourly" , testRenderContext {
Report : testReportContext { Title : "Hourly Report" } ,
GeneratedText : testGeneratedText {
Summary : "Severe storms are possible." ,
ForecastDiscussion : "SPC outlooks highlight the risk." ,
} ,
Modules : testModules {
CurrentConditions : & testCurrentConditions { } ,
HourlyForecast : & testHourlyForecast { } ,
SPCConvectiveOutlooks : & testSPCOutlooks {
RiskDigest : [ ] testSPCRiskDigest { { LabelText : tt . label , RiskLabel : tt . risk , PeriodBegins : "June 15 at 7:00 AM" , PeriodEnds : "June 16 at 7:00 AM" } } ,
} ,
} ,
} )
if err != nil {
t . Fatalf ( "Render() error = %v" , err )
}
text := string ( rendered )
for _ , want := range [ ] string {
"## Alert Digest" ,
"- **SPC Convective Outlook**: " + tt . risk + " for severe thunderstorms in effect from June 15 at 7:00 AM to June 16 at 7:00 AM." ,
} {
if ! strings . Contains ( text , want ) {
t . Fatalf ( "rendered template missing %q:\n%s" , want , text )
}
}
} )
}
}
2026-06-14 12:54:32 -05:00
func TestRenderHourlyOmitsConditionalSectionsForClearWeather ( t * testing . T ) {
rendered , err := Render ( "hourly" , testRenderContext {
Report : testReportContext {
Title : "Hourly Report" ,
GeneratedAtLabel : "Saturday, June 14, 2026 at 9:14 AM" ,
} ,
GeneratedText : testGeneratedText {
Summary : "Dry weather is expected through the next several hours." ,
ForecastDiscussion : "Quiet conditions should persist through midday." ,
} ,
Modules : testModules {
CurrentConditions : & testCurrentConditions {
ConditionTextLower : "cloudy" ,
2026-06-14 17:40:19 -05:00
TemperatureF : intPtr ( 72 ) ,
2026-06-14 12:54:32 -05:00
} ,
HourlyForecast : & testHourlyForecast {
Periods : [ ] testHourlyPeriod {
{ HourLabel : "9:00 AM" , TextDescriptionLower : "mostly cloudy" , TemperatureF : floatPtr ( 71 ) , ProbabilityOfPrecipitationPercent : floatPtr ( 10 ) } ,
} ,
} ,
AlertDigest : & testAlertDigest { } ,
PrecipTiming : & testPrecipTiming {
MaxPopPercent : intPtr ( 10 ) ,
} ,
} ,
} )
if err != nil {
t . Fatalf ( "Render() error = %v" , err )
}
text := string ( rendered )
2026-06-16 21:42:20 -05:00
for _ , unwanted := range [ ] string { "## Alert Digest" , "## Precipitation Timing" , "Probability of precipitation is 10%" , "wind" } {
2026-06-14 12:54:32 -05:00
if strings . Contains ( text , unwanted ) {
t . Fatalf ( "clear render includes %q:\n%s" , unwanted , text )
}
}
2026-06-16 20:44:28 -05:00
if strings . Contains ( text , "\n\n\n" ) {
t . Fatalf ( "clear render includes excess blank lines:\n%s" , text )
}
for _ , want := range [ ] string {
"Currently, it is 72°F and cloudy.\n\n## Hourly Forecast" ,
"- **9:00 AM:** 71°F and mostly cloudy.\n\n## Forecast Discussion" ,
} {
if ! strings . Contains ( text , want ) {
t . Fatalf ( "clear render missing spacing %q:\n%s" , want , text )
}
}
2026-06-14 12:54:32 -05:00
}
2026-06-16 19:20:26 -05:00
func TestRenderHourlyUsesSharedOpenEndedPrecipitationTiming ( t * testing . T ) {
rendered , err := Render ( "hourly" , testRenderContext {
Report : testReportContext {
Title : "Hourly Report" ,
GeneratedAtLabel : "Saturday, June 14, 2026 at 9:14 AM" ,
} ,
GeneratedText : testGeneratedText {
Summary : "Rain chances increase through midday." ,
ForecastDiscussion : "Showers may continue beyond the report period." ,
PrecipitationTiming : "Plan for wet roads through the end of the period." ,
} ,
Modules : testModules {
CurrentConditions : & testCurrentConditions { } ,
HourlyForecast : & testHourlyForecast { } ,
AlertDigest : & testAlertDigest { } ,
PrecipTiming : & testPrecipTiming {
PrecipitationWindows : [ ] testPrecipWindow {
{
PeriodBeginsHourLabel : "8:00 AM" ,
MaxPopPercent : intPtr ( 60 ) ,
MaxPopHourLabel : "10:00 AM" ,
ExpectationPhrase : "Showers likely." ,
} ,
} ,
} ,
} ,
} )
if err != nil {
t . Fatalf ( "Render() error = %v" , err )
}
text := string ( rendered )
for _ , want := range [ ] string {
"## Precipitation Timing" ,
"- **Starting at 8:00 AM**: Showers likely. The peak precipitation chance is 60% at 10:00 AM." ,
"Plan for wet roads through the end of the period." ,
} {
if ! strings . Contains ( text , want ) {
t . Fatalf ( "rendered template missing %q:\n%s" , want , text )
}
}
}
2026-08-13 02:17:11 +00:00
func TestRenderTreatsGeneratedProseAsPlainText ( t * testing . T ) {
const summary = "Summary paragraph.\n\n## Fabricated Alert\n- Fabricated warning\n1. Fabricated list\n> Fabricated quote"
const discussion = "Discussion paragraph.\n\n<script>unsafe</script>\n[unsafe](javascript:alert(1))\n```code```\n**emphasis**\n indented code\x00"
const timing = "Timing paragraph.\n\n## Fabricated Timing"
tests := [ ] struct {
name string
template string
context any
} {
{
name : "daily" ,
template : "daily" ,
context : testDailyRenderContext {
Report : testDailyReportContext { Title : "Daily" } ,
GeneratedText : testDailyGeneratedText { Summary : summary , ForecastDiscussion : [ ] string { discussion } , PrecipitationTiming : timing } ,
Modules : testDailyModules { PrecipTiming : testPrecipitationTimingForGeneratedProseTest ( ) } ,
} ,
} ,
{
name : "today" ,
template : "today" ,
context : testTodayRenderContext {
Report : testTodayReportContext { Title : "Today" } ,
GeneratedText : testTomorrowGeneratedText { Summary : summary , ForecastDiscussion : [ ] string { discussion } , PrecipitationTiming : timing } ,
Modules : testTodayModules { PrecipTiming : testPrecipitationTimingForGeneratedProseTest ( ) } ,
} ,
} ,
{
name : "tomorrow" ,
template : "tomorrow" ,
context : testTomorrowRenderContext {
Report : testTomorrowReportContext { Title : "Tomorrow" } ,
GeneratedText : testTomorrowGeneratedText { Summary : summary , ForecastDiscussion : [ ] string { discussion } , PrecipitationTiming : timing } ,
Modules : testTomorrowModules { PrecipTiming : testPrecipitationTimingForGeneratedProseTest ( ) } ,
} ,
} ,
{
name : "hourly" ,
template : "hourly" ,
context : testRenderContext {
Report : testReportContext { Title : "Hourly" } ,
GeneratedText : testGeneratedText { Summary : summary , ForecastDiscussion : discussion , PrecipitationTiming : timing } ,
Modules : testModules { PrecipTiming : testPrecipitationTimingForGeneratedProseTest ( ) } ,
} ,
} ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
rendered , err := Render ( test . template , test . context )
if err != nil {
t . Fatalf ( "Render() error = %v" , err )
}
text := string ( rendered )
for _ , want := range [ ] string {
"Summary paragraph.\n\n\\## Fabricated Alert\n\\- Fabricated warning\n1\\. Fabricated list\n\\> Fabricated quote" ,
"Discussion paragraph.\n\n\\<script\\>unsafe\\</script\\>\n\\[unsafe\\]\\(javascript:alert\\(1\\)\\)\n\\`\\`\\`code\\`\\`\\`\n\\*\\*emphasis\\*\\*\nindented code " ,
"Timing paragraph.\n\n\\## Fabricated Timing" ,
} {
if ! strings . Contains ( text , want ) {
t . Fatalf ( "rendered template missing escaped prose %q:\n%s" , want , text )
}
}
for _ , unwanted := range [ ] string {
"\n## Fabricated Alert" ,
"\n- Fabricated warning" ,
"\n1. Fabricated list" ,
"\n> Fabricated quote" ,
"<script>unsafe</script>" ,
"[unsafe](javascript:alert(1))" ,
"```code```" ,
"**emphasis**" ,
"\n indented code" ,
"\n## Fabricated Timing" ,
} {
if strings . Contains ( text , unwanted ) {
t . Fatalf ( "rendered template preserved Markdown structure %q:\n%s" , unwanted , text )
}
}
} )
}
}
func testPrecipitationTimingForGeneratedProseTest ( ) * testPrecipTiming {
return & testPrecipTiming { PrecipitationWindows : [ ] testPrecipWindow { { } } }
}
2026-06-14 04:38:48 +00:00
func TestUnknownAssetsReturnActionableErrors ( t * testing . T ) {
2026-06-15 16:23:13 +00:00
if _ , err := Template ( "missing" ) ; err == nil || ! strings . Contains ( err . Error ( ) , ` unknown report template "missing" ` ) {
2026-06-14 04:38:48 +00:00
t . Fatalf ( "Template() error = %v, want unknown template" , err )
}
2026-06-15 16:23:13 +00:00
if _ , err := Render ( "missing" , testRenderContext { } ) ; err == nil || ! strings . Contains ( err . Error ( ) , ` unknown report template "missing" ` ) {
2026-06-14 04:38:48 +00:00
t . Fatalf ( "Render() error = %v, want unknown template" , err )
}
}
func TestRenderFailsForMissingContextFields ( t * testing . T ) {
2026-06-14 08:57:53 -05:00
_ , err := Render ( "hourly" , map [ string ] any { "Report" : map [ string ] any { "Title" : "Hourly Report" } } )
2026-06-14 04:38:48 +00:00
if err == nil {
t . Fatal ( "Render() error = nil, want missing field error" )
}
if ! strings . Contains ( err . Error ( ) , ` render report template "hourly" ` ) {
t . Fatalf ( "Render() error = %v, want render context" , err )
}
}
type testRenderContext struct {
2026-06-14 08:57:53 -05:00
Report testReportContext
GeneratedText testGeneratedText
Modules testModules
}
2026-06-14 23:36:47 +00:00
type testTomorrowRenderContext struct {
Report testTomorrowReportContext
GeneratedText testTomorrowGeneratedText
Modules testTomorrowModules
}
2026-06-15 14:45:49 +00:00
type testTodayRenderContext struct {
Report testTodayReportContext
GeneratedText testTomorrowGeneratedText
Modules testTodayModules
}
2026-06-15 16:23:13 +00:00
type testDailyRenderContext struct {
Report testDailyReportContext
GeneratedText testDailyGeneratedText
Modules testDailyModules
}
2026-06-14 08:57:53 -05:00
type testReportContext struct {
Title string
LocationName string
ValidPeriodLabel string
GeneratedAtLabel string
2026-06-14 04:38:48 +00:00
}
type testGeneratedText struct {
2026-06-14 12:54:32 -05:00
Summary string
ForecastDiscussion string
PrecipitationTiming string
2026-06-14 04:38:48 +00:00
}
2026-06-14 23:36:47 +00:00
type testTomorrowReportContext struct {
Title string
ForecastDateLabel string
GeneratedAtLabel string
}
2026-06-15 14:45:49 +00:00
type testTodayReportContext struct {
Title string
ForecastDateLabel string
GeneratedAtLabel string
}
2026-06-15 16:23:13 +00:00
type testDailyReportContext struct {
Title string
ForecastDateLabel string
GeneratedAtLabel string
}
2026-06-14 23:36:47 +00:00
type testTomorrowGeneratedText struct {
Summary string
ForecastDiscussion [ ] string
PrecipitationTiming string
}
2026-06-15 16:23:13 +00:00
type testDailyGeneratedText struct {
Summary string
ForecastDiscussion [ ] string
PrecipitationTiming string
}
2026-06-14 08:57:53 -05:00
type testModules struct {
CurrentConditions * testCurrentConditions
HourlyForecast * testHourlyForecast
PrecipTiming * testPrecipTiming
AlertDigest * testAlertDigest
SPCConvectiveOutlooks * testSPCOutlooks
AreaForecastDiscussion * testForecastDiscussion
SPCConvectiveDiscussion * testSPCDiscussion
WeatherStory * testWeatherStory
}
2026-06-14 23:36:47 +00:00
type testTomorrowModules struct {
2026-06-16 21:42:20 -05:00
Dayparts [ ] testTomorrowDaypart
PrecipTiming * testPrecipTiming
AlertDigest * testAlertDigest
SPCConvectiveOutlooks * testSPCOutlooks
2026-06-14 23:36:47 +00:00
}
2026-06-15 14:45:49 +00:00
type testTodayModules struct {
2026-06-16 21:42:20 -05:00
CurrentConditions * testCurrentConditions
Dayparts [ ] testTomorrowDaypart
PrecipTiming * testPrecipTiming
AlertDigest * testAlertDigest
SPCConvectiveOutlooks * testSPCOutlooks
TodayPlanning * testTodayPlanning
2026-06-15 14:45:49 +00:00
}
2026-08-13 02:11:25 +00:00
func ( modules testTodayModules ) HasDaypartDetails ( ) bool {
for _ , daypart := range modules . Dayparts {
if daypart . Summary . DominantConditionDisplay != "" || daypart . Summary . DominantCondition != "" {
return true
}
}
return false
}
2026-06-15 16:23:13 +00:00
type testDailyModules struct {
2026-06-16 21:42:20 -05:00
Dayparts [ ] testDailyDaypart
PrecipTiming * testPrecipTiming
AlertDigest * testAlertDigest
SPCConvectiveOutlooks * testSPCOutlooks
2026-06-15 16:23:13 +00:00
}
2026-06-15 14:45:49 +00:00
type testTodayPlanning struct {
MorningReadiness [ ] string
CommuteSchoolWorkdayConcerns [ ] string
OutdoorPlanning [ ] string
LateDayChangeWatch [ ] string
}
2026-06-14 23:36:47 +00:00
type testTomorrowDaypart struct {
Key string
Summary testDaypartSummary
}
2026-06-15 16:23:13 +00:00
type testDailyDaypart struct {
Key string
Summary testDaypartSummary
}
2026-06-14 23:36:47 +00:00
type testDaypartSummary struct {
2026-06-14 23:26:45 -05:00
DisplayName string
TempRangeF string
TemperaturePhraseF string
TemperatureTrend string
TemperatureStartPhraseF string
TemperatureEndPhraseF string
TemperaturePeakPhraseF string
TemperatureSteadyPhraseF string
MaxPopPercent * int
MaxPopTime string
MaxPopTimeLabel string
MentionPrecipitation bool
DominantCondition string
DominantConditionLower string
DominantConditionDisplay string
2026-06-14 23:36:47 +00:00
}
2026-06-14 08:57:53 -05:00
type testCurrentConditions struct {
ConditionText string
2026-06-14 12:54:32 -05:00
ConditionTextLower string
2026-06-14 17:40:19 -05:00
TemperatureF * int
ApparentTemperatureF * int
RelativeHumidityPercent * int
WindSpeedMph * int
2026-06-14 08:57:53 -05:00
WindDirection string
2026-06-14 12:54:32 -05:00
WindDirectionText string
2026-06-14 08:57:53 -05:00
}
type testHourlyForecast struct {
Periods [ ] testHourlyPeriod
}
type testHourlyPeriod struct {
2026-06-14 12:54:32 -05:00
HourLabel string
2026-06-14 08:57:53 -05:00
PeriodBegins string
Name string
TextDescription string
2026-06-14 12:54:32 -05:00
TextDescriptionLower string
2026-06-14 08:57:53 -05:00
TemperatureF * float64
WindSpeedMph * float64
WindGustMph * float64
WindDirection string
ProbabilityOfPrecipitationPercent * float64
2026-06-14 12:54:32 -05:00
MentionPrecipitation bool
2026-06-14 08:57:53 -05:00
}
type testPrecipTiming struct {
MaxPopPercent * int
MaxPopTime string
PrecipitationWindows [ ] testPrecipWindow
ThunderMentioned bool
}
type testPrecipWindow struct {
2026-06-14 12:54:32 -05:00
PeriodBegins string
PeriodBeginsHourLabel string
PeriodEnds string
PeriodEndsHourLabel string
MaxPopPercent * int
MaxPopTime string
MaxPopHourLabel string
2026-06-16 19:10:11 -05:00
PrecipitationType string
ExpectationPhrase string
2026-06-14 08:57:53 -05:00
}
type testAlertDigest struct {
Missing bool
Relevant [ ] testAlert
}
type testAlert struct {
2026-06-16 21:09:17 -05:00
Event string
Headline string
Severity string
PeriodBegins string
PeriodEnds string
Instruction string
Description string
2026-06-14 08:57:53 -05:00
}
type testSPCOutlooks struct {
2026-06-16 21:42:20 -05:00
Outlooks [ ] testSPCOutlook
RiskDigest [ ] testSPCRiskDigest
2026-06-14 08:57:53 -05:00
}
type testSPCOutlook struct {
Label string
LabelText string
OutlookType string
PeriodBegins string
PeriodEnds string
2026-06-14 04:38:48 +00:00
}
2026-06-16 21:42:20 -05:00
type testSPCRiskDigest struct {
LabelText string
RiskLabel string
PeriodBegins string
PeriodEnds string
}
2026-06-14 04:38:48 +00:00
type testForecastDiscussion struct {
KeyMessages [ ] string
ShortTerm string
}
2026-06-14 05:24:25 +00:00
2026-06-14 08:57:53 -05:00
type testSPCDiscussion struct {
Discussions [ ] testSPCDiscussionRecord
}
type testSPCDiscussionRecord struct {
Headline string
Summary string
Discussion string
}
type testWeatherStory struct {
Available bool
Title string
Description string
}
func floatPtr ( value float64 ) * float64 {
return & value
}
func intPtr ( value int ) * int {
return & value
}
2026-06-14 05:24:25 +00:00
func assertOrderedText ( t * testing . T , text string , ordered [ ] string ) {
t . Helper ( )
previousIndex := - 1
for _ , want := range ordered {
index := strings . Index ( text , want )
if index < 0 {
t . Fatalf ( "text missing %q:\n%s" , want , text )
}
if index <= previousIndex {
t . Fatalf ( "%q appears out of order in:\n%s" , want , text )
}
previousIndex = index
}
}