11import { describe , it , expect } from "vitest" ;
22import { formatForecast } from "../src/lib/format-forecast" ;
3- import type { ForecastProduct } from "../src/services/avalanche-ca" ;
3+ import type { ForecastProduct , AvalancheProblem } from "../src/services/avalanche-ca" ;
4+
5+ type ProblemInput = {
6+ name : string ;
7+ aspects ?: string [ ] ;
8+ } ;
9+
10+ function makeProblem ( input : string | ProblemInput ) : AvalancheProblem {
11+ if ( typeof input === "string" ) {
12+ return { type : { value : input . toLowerCase ( ) , display : input } } ;
13+ }
14+ return {
15+ type : { value : input . name . toLowerCase ( ) , display : input . name } ,
16+ data : input . aspects
17+ ? { aspects : input . aspects . map ( ( a ) => ( { value : a , display : a } ) ) }
18+ : undefined ,
19+ } ;
20+ }
421
522function makeForecast (
623 overrides : Partial < {
@@ -9,7 +26,7 @@ function makeForecast(
926 alp : string ;
1027 tln : string ;
1128 btl : string ;
12- problems : string [ ] ;
29+ problems : Array < string | ProblemInput > ;
1330 } > = { }
1431) : ForecastProduct {
1532 const {
@@ -18,7 +35,10 @@ function makeForecast(
1835 alp = "considerable" ,
1936 tln = "moderate" ,
2037 btl = "low" ,
21- problems = [ "Wind slab" , "Storm slab" ] ,
38+ problems = [
39+ { name : "Wind slab" , aspects : [ "n" , "ne" , "e" ] } ,
40+ { name : "Storm slab" , aspects : [ "n" , "ne" , "e" , "se" , "s" , "sw" , "w" , "nw" ] } ,
41+ ] ,
2242 } = overrides ;
2343
2444 return {
@@ -33,79 +53,101 @@ function makeForecast(
3353 {
3454 date : { value : dateIssued , display : "Monday" } ,
3555 ratings : {
36- alp : {
37- display : "Alpine" ,
38- rating : { value : alp as any , display : "" } ,
39- } ,
40- tln : {
41- display : "Treeline" ,
42- rating : { value : tln as any , display : "" } ,
43- } ,
44- btl : {
45- display : "Below Treeline" ,
46- rating : { value : btl as any , display : "" } ,
47- } ,
56+ alp : { display : "Alpine" , rating : { value : alp as any , display : "" } } ,
57+ tln : { display : "Treeline" , rating : { value : tln as any , display : "" } } ,
58+ btl : { display : "Below Treeline" , rating : { value : btl as any , display : "" } } ,
4859 } ,
4960 } ,
5061 ] ,
51- problems : problems . map ( ( p ) => ( { type : { value : p . toLowerCase ( ) , display : p } } ) ) ,
62+ problems : problems . map ( makeProblem ) ,
5263 } ,
5364 } ;
5465}
5566
5667describe ( "formatForecast" , ( ) => {
57- it ( "formats a standard forecast within 160 chars" , ( ) => {
68+ it ( "formats a forecast with aspects within 160 chars" , ( ) => {
5869 const result = formatForecast ( makeForecast ( ) ) ;
59- expect ( result ) . toBe (
60- "Kananaskis\nFeb 23\nAlp:Considerable TL:Moderate BT:Low\nWind slab, Storm slab\navalanche.ca"
61- ) ;
70+ expect ( result ) . toContain ( "Wind slab (N,NE,E)" ) ;
71+ expect ( result ) . toContain ( "Storm slab (All aspects)" ) ;
6272 expect ( result . length ) . toBeLessThanOrEqual ( 160 ) ;
6373 } ) ;
6474
6575 it ( "never exceeds 160 characters" , ( ) => {
6676 const result = formatForecast (
6777 makeForecast ( {
68- title :
69- "Badshot-Battle-Central Selkirk-Esplanade-Goat-Gold-Jordan-North Monashee-North Selkirk-Retallack-West Purcell" ,
70- problems : [ "Storm slab" , "Persistent slab" , "Wind slab" , "Deep persistent slab" ] ,
78+ title : "Badshot-Battle-Central Selkirk-Esplanade-Goat-Gold-Jordan-North Monashee" ,
79+ problems : [
80+ { name : "Storm slab" , aspects : [ "n" , "ne" , "e" , "se" ] } ,
81+ { name : "Persistent slab" , aspects : [ "n" , "ne" , "nw" ] } ,
82+ { name : "Wind slab" , aspects : [ "e" , "se" , "s" ] } ,
83+ ] ,
7184 } )
7285 ) ;
7386 expect ( result . length ) . toBeLessThanOrEqual ( 160 ) ;
7487 } ) ;
7588
76- it ( "truncates long region names with ellipsis" , ( ) => {
89+ it ( "truncates region name to 25 chars with ellipsis" , ( ) => {
7790 const result = formatForecast (
7891 makeForecast ( {
79- title :
80- "Badshot-Battle-Central Selkirk-Esplanade-Goat-Gold-Jordan-North Monashee-North Selkirk-Retallack-West Purcell" ,
92+ title : "Badshot-Battle-Central Selkirk-Esplanade" ,
93+ problems : [ ] ,
8194 } )
8295 ) ;
83- expect ( result ) . toContain ( " \u2026") ;
96+ expect ( result . startsWith ( "Badshot-Battle-Central S \u2026") ) . toBe ( true ) ;
8497 expect ( result . length ) . toBeLessThanOrEqual ( 160 ) ;
8598 } ) ;
8699
100+ it ( "does not truncate short region names" , ( ) => {
101+ const result = formatForecast ( makeForecast ( { title : "Kananaskis" } ) ) ;
102+ expect ( result ) . toContain ( "Kananaskis\n" ) ;
103+ } ) ;
104+
87105 it ( "handles no problems" , ( ) => {
88106 const result = formatForecast ( makeForecast ( { problems : [ ] } ) ) ;
89107 expect ( result ) . toBe (
90108 "Kananaskis\nFeb 23\nAlp:Considerable TL:Moderate BT:Low\navalanche.ca"
91109 ) ;
92- expect ( result . length ) . toBeLessThanOrEqual ( 160 ) ;
93110 } ) ;
94111
95- it ( "truncates problem list to fit" , ( ) => {
112+ it ( "handles problems without aspects" , ( ) => {
113+ const result = formatForecast ( makeForecast ( { problems : [ "Wind slab" , "Storm slab" ] } ) ) ;
114+ expect ( result ) . toContain ( "Wind slab, Storm slab" ) ;
115+ } ) ;
116+
117+ it ( "shows 'All aspects' when all 8 directions present" , ( ) => {
118+ const result = formatForecast (
119+ makeForecast ( {
120+ problems : [
121+ { name : "Storm slab" , aspects : [ "n" , "ne" , "e" , "se" , "s" , "sw" , "w" , "nw" ] } ,
122+ ] ,
123+ } )
124+ ) ;
125+ expect ( result ) . toContain ( "Storm slab (All aspects)" ) ;
126+ } ) ;
127+
128+ it ( "sorts aspects in compass order" , ( ) => {
129+ const result = formatForecast (
130+ makeForecast ( {
131+ problems : [
132+ { name : "Wind slab" , aspects : [ "sw" , "n" , "e" ] } ,
133+ ] ,
134+ } )
135+ ) ;
136+ expect ( result ) . toContain ( "Wind slab (N,E,SW)" ) ;
137+ } ) ;
138+
139+ it ( "truncates problem list to fit within 160 chars" , ( ) => {
96140 const result = formatForecast (
97141 makeForecast ( {
98- title : "A Fairly Long Region Name Here" ,
99142 problems : [
100- "Storm slab" ,
101- "Persistent slab" ,
102- "Wind slab" ,
103- "Deep persistent slab" ,
143+ { name : "Storm slab" , aspects : [ "n" , "ne" , "e" , "se" ] } ,
144+ { name : "Persistent slab" , aspects : [ "n" , "ne" , "nw" ] } ,
145+ { name : "Wind slab" , aspects : [ "e" , "se" , "s" ] } ,
146+ { name : "Deep persistent slab" , aspects : [ "n" , "ne" ] } ,
104147 ] ,
105148 } )
106149 ) ;
107150 expect ( result . length ) . toBeLessThanOrEqual ( 160 ) ;
108- // Should include at least the first problem
109151 expect ( result ) . toContain ( "Storm slab" ) ;
110152 } ) ;
111153
@@ -121,7 +163,7 @@ describe("formatForecast", () => {
121163
122164 for ( const [ value , display ] of cases ) {
123165 const result = formatForecast (
124- makeForecast ( { alp : value , tln : value , btl : value } )
166+ makeForecast ( { alp : value , tln : value , btl : value , problems : [ ] } )
125167 ) ;
126168 expect ( result ) . toContain ( `Alp:${ display } ` ) ;
127169 expect ( result ) . toContain ( `TL:${ display } ` ) ;
@@ -131,7 +173,7 @@ describe("formatForecast", () => {
131173
132174 it ( "formats date correctly" , ( ) => {
133175 const result = formatForecast (
134- makeForecast ( { dateIssued : "2026-12-05T00:00:00Z" } )
176+ makeForecast ( { dateIssued : "2026-12-05T00:00:00Z" , problems : [ ] } )
135177 ) ;
136178 expect ( result ) . toContain ( "Dec 5" ) ;
137179 } ) ;
@@ -142,8 +184,7 @@ describe("formatForecast", () => {
142184 } ) ;
143185
144186 it ( "uses first day danger ratings" , ( ) => {
145- const forecast = makeForecast ( { alp : "high" , tln : "considerable" , btl : "moderate" } ) ;
146- // Add a second day
187+ const forecast = makeForecast ( { alp : "high" , tln : "considerable" , btl : "moderate" , problems : [ ] } ) ;
147188 forecast . report . dangerRatings . push ( {
148189 date : { value : "2026-02-25T00:00:00Z" , display : "Tuesday" } ,
149190 ratings : {
0 commit comments