fix correct verdict colours, simplify progress page and chart

- Swap the verdict colours: progress is green, regress oxblood; stagnation stays brass. - Progress page drops the search. It now renders one chart per exercise that has at least two merged observations; the rest are omitted, with a message when none qualify. - Chart is a plain stroked line -- no markers, no area fill -- with labelled axes: kg on the left, reps on the right, first and last dates on the x-axis. - Mobile logbook rows are a single-line flex row instead of a grid: the name flexes and truncates, and date, duration, and the three-dot menu always sit together on one line. The import pill hides below 26rem; the menu still marks the row as imported.

Commit
53d752b0f1a0b58880a166de23e2635fb3c8df80
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
lib/presenter/pages_progress.ml
index 7bf4a9f9..e87fc5c9 100644..100644
@@ -2,41 +2,6 @@
2 2 open Page_kit
3 3 open Page_shell
4 4
5 Removed: (* The exercise search: a native text input backed by a datalist, submitting a
6 Removed: GET so the page reloads with the chosen exercise's chart. No script needed.
7 Removed: The datalist maps a visible name to its id via a hidden convention: the
8 Removed: input carries the id as its value once chosen from the list. To keep it
9 Removed: no-JS, the list options use the exercise name and the form submits the name,
10 Removed: which the handler resolves. *)
11 Removed: let search_form ~(exercises : (string * string) list) ~selected_id =
12 Removed: let selected_name =
13 Removed: match selected_id with
14 Removed: | None -> ""
15 Removed: | Some id -> (
16 Removed: match List.assoc_opt id exercises with Some name -> name | None -> "")
17 Removed: in
18 Removed: tag "form"
19 Removed: [ action Routes.progress; class_ "progress-search" ]
20 Removed: [
21 Removed: tag "label" [ Dream_html.string_attr "for" "exercise" ] [ txt "Exercise" ];
22 Removed: void "input"
23 Removed: [
24 Removed: id "exercise";
25 Removed: name "exercise";
26 Removed: Dream_html.string_attr "list" "exercise-options";
27 Removed: Dream_html.string_attr "value" "%s" selected_name;
28 Removed: Dream_html.string_attr "placeholder" "Search exercises…";
29 Removed: Dream_html.string_attr "autocomplete" "off";
30 Removed: ];
31 Removed: tag "datalist"
32 Removed: [ id "exercise-options" ]
33 Removed: (List.map
34 Removed: (fun (_, name) ->
35 Removed: tag "option" [ Dream_html.string_attr "value" "%s" name ] [])
36 Removed: exercises);
37 Removed: tag "button" [ type_ "submit" ] [ txt "View" ];
38 Removed: ]
39 Removed:
40 5 let legend () =
41 6 tag "div"
42 7 [ class_ "chart-legend" ]
@@ -47,31 +12,27 @@
47 12 tag "span" [ class_ "chart-legend-item reps" ] [ txt "Reps, right axis" ];
48 13 ]
49 14
15 Added: let chart_section (chart : View_model.Progress_page.chart) =
16 Added: tag "section"
17 Added: [ class_ "progress-chart-section" ]
18 Added: [
19 Added: tag "h2" [] [ txt "%s" chart.exercise_name ];
20 Added: legend ();
21 Added: Svg_chart.render chart;
22 Added: ]
23 Added:
50 24 let progress request ?(logging = false) ~viewer
51 25 (vm : View_model.Progress_page.t) =
52 26 html_page ~viewer ~request ~active:"logbook" ~logging "Progress"
53 Removed: [
54 Removed: tag "h1" [] [ txt "Progress" ];
55 Removed: search_form ~exercises:vm.exercises ~selected_id:vm.selected_id;
56 Removed: (match vm.chart with
57 Removed: | Some chart ->
58 Removed: tag "section"
59 Removed: [ class_ "progress-chart-section" ]
60 Removed: [
61 Removed: tag "h2" [] [ txt "%s" chart.exercise_name ];
62 Removed: legend ();
63 Removed: Svg_chart.render chart.points;
64 Removed: ]
65 Removed: | None ->
66 Removed: if vm.insufficient then
67 Removed: tag "p"
68 Removed: [ class_ "eyebrow" ]
69 Removed: [
70 Removed: txt
71 Removed: "Not enough history to chart yet — log or import at least \
72 Removed: two performances of this exercise.";
73 Removed: ]
74 Removed: else
75 Removed: tag "p" []
76 Removed: [ txt "Search for an exercise to see its progress chart." ]);
77 Removed: ]
27 Added: (tag "h1" [] [ txt "Progress" ]
28 Added: ::
29 Added: (if vm.charts = [] then
30 Added: [
31 Added: tag "p" []
32 Added: [
33 Added: txt
34 Added: "No exercise has enough history to chart yet. Log or import at \
35 Added: least two performances of an exercise to see its progress.";
36 Added: ];
37 Added: ]
38 Added: else List.map chart_section vm.charts))
lib/presenter/svg_chart.ml
index 9839a6f3..ac8364c5 100644..100644
@@ -10,10 +10,8 @@
10 10 open Dream_html
11 11
12 12 let svg = std_tag "svg"
13 Removed: let g = std_tag "g"
14 13 let polyline = std_tag "polyline"
15 14 let line = std_tag "line"
16 Removed: let circle = std_tag "circle"
17 15 let text = std_tag "text"
18 16 let sattr = string_attr
19 17 let fattr = float_attr
@@ -53,29 +51,15 @@
53 51 |> List.map (fun (x, y) -> Printf.sprintf "%.1f,%.1f" x y)
54 52 |> String.concat " "
55 53
54 Added: (* A plain stroked line: no markers, no area fill. *)
56 55 let series ~css_class coords =
57 Removed: let markers =
58 Removed: List.map
59 Removed: (fun (x, y) ->
60 Removed: circle
61 Removed: [
62 Removed: sattr "class" "%s" css_class;
63 Removed: fattr "cx" x;
64 Removed: fattr "cy" y;
65 Removed: fattr "r" 3.;
66 Removed: ]
67 Removed: [])
68 Removed: coords
69 Removed: in
70 Removed: g []
71 Removed: (polyline
72 Removed: [
73 Removed: sattr "class" "%s" css_class;
74 Removed: sattr "fill" "none";
75 Removed: sattr "points" "%s" (points_attr coords);
76 Removed: ]
77 Removed: []
78 Removed: :: markers)
56 Added: polyline
57 Added: [
58 Added: sattr "class" "%s" css_class;
59 Added: sattr "fill" "none";
60 Added: sattr "points" "%s" (points_attr coords);
61 Added: ]
62 Added: []
79 63
80 64 let axis_line ~x1 ~y1 ~x2 ~y2 =
81 65 line
@@ -100,7 +84,8 @@
100 84
101 85 (* Render the chart for a series of (weight, reps) points in time order. The
102 86 caller guarantees at least two points. *)
103 Removed: let render (points : View_model.Progress_page.point list) =
87 Added: let render (chart : View_model.Progress_page.chart) =
88 Added: let points = chart.points in
104 89 let n = List.length points in
105 90 let weights =
106 91 List.map (fun (p : View_model.Progress_page.point) -> p.weight) points
@@ -129,32 +114,52 @@
129 114 sattr "class" "progress-chart";
130 115 sattr "viewBox" "0 0 %g %g" width height;
131 116 sattr "role" "img";
132 Removed: sattr "aria-label"
133 Removed: "Line chart of weight and reps over time for the selected exercise";
117 Added: sattr "aria-label" "Line chart of weight and reps over time for %s"
118 Added: chart.exercise_name;
134 119 sattr "preserveAspectRatio" "xMidYMid meet";
135 120 ]
136 Removed: [
137 Removed: (* Axes: left (weight), right (reps), and a baseline. *)
138 Removed: axis_line ~x1:pad_left ~y1:pad_top ~x2:pad_left ~y2:(pad_top +. plot_h);
139 Removed: axis_line ~x1:(pad_left +. plot_w) ~y1:pad_top ~x2:(pad_left +. plot_w)
140 Removed: ~y2:(pad_top +. plot_h);
141 Removed: axis_line ~x1:pad_left ~y1:(pad_top +. plot_h) ~x2:(pad_left +. plot_w)
142 Removed: ~y2:(pad_top +. plot_h);
143 Removed: (* Left axis bounds (weight, kg). *)
144 Removed: axis_label ~x:(pad_left -. 6.) ~y:(pad_top +. 4.) ~anchor:"end"
145 Removed: ~css_class:"chart-tick weight" (Printf.sprintf "%g" w_hi);
146 Removed: axis_label ~x:(pad_left -. 6.) ~y:(pad_top +. plot_h) ~anchor:"end"
147 Removed: ~css_class:"chart-tick weight" (Printf.sprintf "%g" w_lo);
148 Removed: (* Right axis bounds (reps). *)
149 Removed: axis_label
150 Removed: ~x:(pad_left +. plot_w +. 6.)
151 Removed: ~y:(pad_top +. 4.) ~anchor:"start" ~css_class:"chart-tick reps"
152 Removed: (Printf.sprintf "%g" r_hi);
153 Removed: axis_label
154 Removed: ~x:(pad_left +. plot_w +. 6.)
155 Removed: ~y:(pad_top +. plot_h) ~anchor:"start" ~css_class:"chart-tick reps"
156 Removed: (Printf.sprintf "%g" r_lo);
157 Removed: (* The two series. *)
158 Removed: series ~css_class:"chart-series weight" weight_coords;
159 Removed: series ~css_class:"chart-series reps" rep_coords;
160 Removed: ]
121 Added: (let first_date =
122 Added: match points with p :: _ -> p.View_model.Progress_page.date | [] -> ""
123 Added: in
124 Added: let last_date =
125 Added: match List.rev points with
126 Added: | p :: _ -> p.View_model.Progress_page.date
127 Added: | [] -> ""
128 Added: in
129 Added: [
130 Added: (* Axes: left (weight), right (reps), and a baseline. *)
131 Added: axis_line ~x1:pad_left ~y1:pad_top ~x2:pad_left ~y2:(pad_top +. plot_h);
132 Added: axis_line ~x1:(pad_left +. plot_w) ~y1:pad_top ~x2:(pad_left +. plot_w)
133 Added: ~y2:(pad_top +. plot_h);
134 Added: axis_line ~x1:pad_left ~y1:(pad_top +. plot_h) ~x2:(pad_left +. plot_w)
135 Added: ~y2:(pad_top +. plot_h);
136 Added: (* Axis titles. *)
137 Added: axis_label ~x:pad_left ~y:(pad_top -. 10.) ~anchor:"middle"
138 Added: ~css_class:"chart-axis-title weight" "kg";
139 Added: axis_label ~x:(pad_left +. plot_w) ~y:(pad_top -. 10.) ~anchor:"middle"
140 Added: ~css_class:"chart-axis-title reps" "reps";
141 Added: (* Left axis bounds (weight, kg). *)
142 Added: axis_label ~x:(pad_left -. 6.) ~y:(pad_top +. 4.) ~anchor:"end"
143 Added: ~css_class:"chart-tick weight" (Printf.sprintf "%g" w_hi);
144 Added: axis_label ~x:(pad_left -. 6.) ~y:(pad_top +. plot_h) ~anchor:"end"
145 Added: ~css_class:"chart-tick weight" (Printf.sprintf "%g" w_lo);
146 Added: (* Right axis bounds (reps). *)
147 Added: axis_label
148 Added: ~x:(pad_left +. plot_w +. 6.)
149 Added: ~y:(pad_top +. 4.) ~anchor:"start" ~css_class:"chart-tick reps"
150 Added: (Printf.sprintf "%g" r_hi);
151 Added: axis_label
152 Added: ~x:(pad_left +. plot_w +. 6.)
153 Added: ~y:(pad_top +. plot_h) ~anchor:"start" ~css_class:"chart-tick reps"
154 Added: (Printf.sprintf "%g" r_lo);
155 Added: (* X-axis: first and last dates. *)
156 Added: axis_label ~x:pad_left
157 Added: ~y:(pad_top +. plot_h +. 20.)
158 Added: ~anchor:"start" ~css_class:"chart-tick date" first_date;
159 Added: axis_label ~x:(pad_left +. plot_w)
160 Added: ~y:(pad_top +. plot_h +. 20.)
161 Added: ~anchor:"end" ~css_class:"chart-tick date" last_date;
162 Added: (* The two series. *)
163 Added: series ~css_class:"chart-series weight" weight_coords;
164 Added: series ~css_class:"chart-series reps" rep_coords;
165 Added: ])
lib/presenter/view_model.ml
index 5f604550..676305d6 100644..100644
@@ -118,8 +118,8 @@
118 118 form, since its option set is fixed UI vocabulary, not domain data. *)
119 119 end
120 120
121 Removed: (** A per-exercise progress verdict, for colour coding. Progress is oxblood,
122 Removed: regress green, stagnation brass. *)
121 Added: (** A per-exercise progress verdict, for colour coding. Progress is green,
122 Added: regress oxblood, stagnation brass. *)
123 123 type verdict = Progressed | Regressed | Stagnant
124 124
125 125 module Logbook_change = struct
@@ -188,28 +188,19 @@
188 188 weight : float;
189 189 reps : int;
190 190 }
191 Removed: (** One observation on the chart: its load and reps at a point in time. *)
191 Added: (** One observation on a chart: its load and reps at a point in time. *)
192 192
193 193 type chart = {
194 194 exercise_name : string;
195 Removed: points : point list; (** Oldest first. *)
195 Added: points : point list; (** Oldest first, at least two. *)
196 196 }
197 Removed: (** The selected exercise's merged native and imported history, ready to plot
198 Removed: as a dual-axis line chart: weight on the left axis, reps on the right. *)
197 Added: (** One exercise's merged native and imported history, ready to plot as a
198 Added: dual-axis line chart: weight on the left axis, reps on the right. *)
199 199
200 Removed: type t = {
201 Removed: exercises : (string * string) list;
202 Removed: (** Every catalog exercise as [(id, name)], for the search datalist. *)
203 Removed: selected_id : string option; (** The chosen exercise id, if any. *)
204 Removed: chart : chart option;
205 Removed: (** [Some] once an exercise with at least two observations is selected.
206 Removed: [None] before selection or with too little data to draw a line. *)
207 Removed: insufficient : bool;
208 Removed: (** True when an exercise was selected but has fewer than two
209 Removed: observations, so no line can be drawn. *)
210 Removed: }
211 Removed: (** The progress page: a no-JS exercise search that reloads with the chosen
212 Removed: exercise's chart. *)
200 Added: type t = { charts : chart list }
201 Added: (** The progress page: one chart per exercise that has at least two
202 Added: observations. Exercises with too little history to draw a line are
203 Added: omitted. *)
213 204 end
214 205
215 206 module Import_warning = struct
lib/web/assets/hito.css
index 87640a47..e77222a8 100644..100644
@@ -552,14 +552,14 @@
552 552 white-space: nowrap;
553 553 }
554 554 .logbook-table tbody { display: block; }
555 Added: /* One line per workout on mobile. A flex row is simpler than a grid here: the
556 Added: name flexes and truncates, and everything else stays its natural width, so
557 Added: date, duration, and the three-dot menu always sit together on one line. */
555 558 .logbook-table tbody > tr:not(.logbook-detail-row) {
556 Removed: display: grid;
557 Removed: grid-template-columns: auto minmax(0, 1fr) auto auto;
558 Removed: grid-template-areas:
559 Removed: "date workout duration actions"
560 Removed: "progress progress progress progress";
561 Removed: gap: 0.25rem 0.5rem;
559 Added: display: flex;
562 560 align-items: center;
561 Added: gap: 0.5rem;
562 Added: padding: 0.6rem 0.75rem;
563 563 border: 1px solid var(--rule-strong);
564 564 border-radius: var(--radius);
565 565 background: var(--paper-raised);
@@ -567,62 +567,40 @@
567 567 overflow: visible;
568 568 }
569 569 .logbook-table td {
570 Removed: display: block;
571 570 padding: 0;
572 571 border: none;
573 572 text-align: left;
574 573 }
575 Removed: /* One line per workout, the same cell order and style at every width:
576 Removed: monospaced date, sans workout name (with an import pill), monospaced
577 Removed: duration, then the row menu. A native record's progress toggle drops to a
578 Removed: second line on mobile; an import carries no progress cell, so its row stays a
579 Removed: single line. */
574 Added: /* Date: monospaced, fixed width. */
580 575 .logbook-table tbody > tr:not(.logbook-detail-row) > td:nth-child(1) {
581 Removed: grid-area: date;
582 Removed: padding: 0.7rem 0 0.7rem 0.85rem;
576 Added: flex: 0 0 auto;
583 577 font-family: var(--mono);
584 Removed: line-height: 1.25;
585 578 white-space: nowrap;
586 579 }
580 Added: /* Name: sans, takes the slack and truncates when the row is too narrow. The
581 Added: import pill lives here and is hidden on a very narrow screen. */
587 582 .logbook-table tbody > tr:not(.logbook-detail-row) > td:nth-child(2) {
588 Removed: grid-area: workout;
583 Added: flex: 1 1 auto;
589 584 min-width: 0;
590 Removed: padding: 0.7rem 0;
591 585 font-family: var(--sans);
592 586 font-weight: 400;
593 Removed: line-height: 1.25;
594 587 overflow: hidden;
595 588 text-overflow: ellipsis;
596 589 white-space: nowrap;
597 590 }
591 Added: /* Duration: monospaced, natural width, muted. */
598 592 .logbook-table tbody > tr:not(.logbook-detail-row) > td:nth-child(3) {
599 Removed: grid-area: duration;
600 Removed: padding: 0.7rem 0;
593 Added: flex: 0 0 auto;
601 594 font-family: var(--mono);
602 595 color: var(--muted-ink);
603 596 white-space: nowrap;
604 597 }
598 Added: /* The three-dot menu always sits beside the duration at the row's end. */
605 599 .logbook-table tbody > tr:not(.logbook-detail-row) > td:nth-child(4) {
606 Removed: grid-area: progress;
607 Removed: padding: 0 0 0.75rem 0.85rem;
608 Removed: text-align: left;
609 Removed: font-family: var(--sans);
610 Removed: }
611 Removed: .logbook-table tbody > tr:not(.logbook-detail-row) > td:nth-child(4):empty {
612 Removed: display: none;
613 Removed: }
614 Removed: .logbook-table tbody > tr:not(.logbook-detail-row) > td:nth-child(5) {
615 Removed: grid-area: actions;
616 Removed: padding: 0 0.35rem 0 0;
600 Added: flex: 0 0 auto;
601 Added: margin-left: auto;
617 602 text-align: right;
618 603 }
619 Removed:
620 Removed: /* On the ledger grid this is a control, not a fixed-width table column. */
621 Removed: .logbook-table tbody > tr:not(.logbook-detail-row) > td.logbook-actions {
622 Removed: width: auto;
623 Removed: justify-self: end;
624 Removed: align-self: start;
625 Removed: }
626 604 .logbook-duration,
627 605 .logbook-progress {
628 606 white-space: nowrap;
@@ -775,10 +753,10 @@
775 753 font: 700 var(--font-size-small)/1 var(--sans);
776 754 white-space: nowrap;
777 755 }
778 Removed: /* Per-exercise progress verdict colours in the row menu: progress oxblood,
779 Removed: regress green, stagnation brass. The colour lands on that line's delta. */
780 Removed: .verdict-progress .change-delta { color: var(--oxblood-dark); }
781 Removed: .verdict-regress .change-delta { color: var(--evergreen); }
756 Added: /* Per-exercise progress verdict colours in the row menu: progress green,
757 Added: regress oxblood, stagnation brass. The colour lands on that line's delta. */
758 Added: .verdict-progress .change-delta { color: var(--evergreen); }
759 Added: .verdict-regress .change-delta { color: var(--oxblood-dark); }
782 760 .verdict-stagnant .change-delta { color: var(--brass); }
783 761 .verdict-none .change-delta { color: var(--muted-ink); }
784 762
@@ -1371,6 +1349,9 @@
1371 1349
1372 1350 @media (max-width: 26rem) {
1373 1351 .button-group { grid-template-columns: 1fr; }
1352 Added: /* Too narrow for the pill beside a truncated name; the row stays one line.
1353 Added: The three-dot menu still marks the workout as imported. */
1354 Added: .logbook-import-pill { display: none; }
1374 1355 }
1375 1356
1376 1357 @media (prefers-reduced-motion: reduce) {
lib/web/handlers_logbook.ml
index 2152b3e4..fc9caafb 100644..100644
@@ -346,6 +346,10 @@
346 346 name; resolve it to a catalog entry, then plot its merged native and
347 347 imported history. A chart needs at least two observations to draw a line;
348 348 fewer reports as insufficient. *)
349 Added: (* The progress page. One dual-axis line chart per exercise that has at least
350 Added: two observations in the merged native and imported history; exercises with
351 Added: too little to draw a line are omitted. No search — the page shows every
352 Added: chartable exercise. *)
349 353 let progress t trainee request =
350 354 let viewer =
351 355 {
@@ -353,71 +357,37 @@
353 357 Trainee.username_to_string trainee.Trainee.username;
354 358 }
355 359 in
356 Removed: let exercises =
357 Removed: List.map
358 Removed: (fun e -> ((Exercise.id e :> string), Exercise.name e))
359 Removed: Exercise.catalog
360 Added: let point_of o : View_model.Progress_page.point =
361 Added: let effort = Evidence.Observation.effort o in
362 Added: let at =
363 Added: Recovery.timestamp_to_unix_seconds (Evidence.Observation.performed_at o)
364 Added: in
365 Added: {
366 Added: date = date_of_unix at;
367 Added: at_unix = at;
368 Added: weight = Evidence.Stimulus.Effort.load effort;
369 Added: reps = Evidence.Stimulus.Effort.reps effort;
370 Added: }
360 371 in
361 Removed: (* The search field submits the exercise name; match it back to a catalog
362 Removed: entry. *)
363 Removed: let selected =
364 Removed: match Dream.query request "exercise" with
365 Removed: | None | Some "" -> None
366 Removed: | Some query ->
367 Removed: List.find_opt
368 Removed: (fun e -> String.equal (Exercise.name e) (String.trim query))
369 Removed: Exercise.catalog
370 Removed: in
371 372 Service.in_progress t.service trainee.Trainee.id >>= fun in_progress ->
372 373 let logging = Option.is_some in_progress in
373 Removed: match selected with
374 Removed: | None ->
375 Removed: html
376 Removed: (Pages.progress request ~logging ~viewer
377 Removed: {
378 Removed: View_model.Progress_page.exercises;
379 Removed: selected_id = None;
380 Removed: chart = None;
381 Removed: insufficient = false;
382 Removed: })
383 Removed: | Some exercise ->
374 Added: (* One history fetch per catalog exercise, kept only when it can draw. *)
375 Added: Lwt_list.filter_map_s
376 Added: (fun exercise ->
384 377 Service.exercise_history t.service trainee.Trainee.id exercise
385 Removed: >>= fun history ->
386 Removed: let points =
387 Removed: List.map
388 Removed: (fun o ->
389 Removed: let effort = Evidence.Observation.effort o in
390 Removed: let at =
391 Removed: Recovery.timestamp_to_unix_seconds
392 Removed: (Evidence.Observation.performed_at o)
393 Removed: in
394 Removed: {
395 Removed: View_model.Progress_page.date = date_of_unix at;
396 Removed: at_unix = at;
397 Removed: weight = Evidence.Stimulus.Effort.load effort;
398 Removed: reps = Evidence.Stimulus.Effort.reps effort;
399 Removed: })
400 Removed: history
401 Removed: in
402 Removed: let chart, insufficient =
403 Removed: if List.length points >= 2 then
404 Removed: ( Some
405 Removed: {
406 Removed: View_model.Progress_page.exercise_name =
407 Removed: Exercise.name exercise;
408 Removed: points;
409 Removed: },
410 Removed: false )
411 Removed: else (None, true)
412 Removed: in
413 Removed: html
414 Removed: (Pages.progress request ~logging ~viewer
415 Removed: {
416 Removed: View_model.Progress_page.exercises;
417 Removed: selected_id = Some (Exercise.id exercise :> string);
418 Removed: chart;
419 Removed: insufficient;
420 Removed: })
378 Added: >|= fun history ->
379 Added: if List.length history >= 2 then
380 Added: Some
381 Added: {
382 Added: View_model.Progress_page.exercise_name = Exercise.name exercise;
383 Added: points = List.map point_of history;
384 Added: }
385 Added: else None)
386 Added: Exercise.catalog
387 Added: >>= fun charts ->
388 Added: html
389 Added: (Pages.progress request ~logging ~viewer
390 Added: { View_model.Progress_page.charts })
421 391
422 392 let routes t =
423 393 [
test/web/logbook.ml
index 37031c6a..2e1adc86 100644..100644
@@ -658,14 +658,21 @@
658 658 (Printf.sprintf "/profile/import/%s/promote" bid)
659 659 [ ("dream.csrf", t) ]
660 660 in
661 Removed: (* The search submits the exercise name; the page returns its chart. *)
662 Removed: let page = body (get c "/progress?exercise=Squats") in
661 Added: (* The progress page renders a chart for every chartable exercise. *)
662 Added: let page = body (get c "/progress") in
663 663 Alcotest.(check bool)
664 664 "the SVG chart is rendered" true
665 665 (Option.is_some (index_from ~needle:"progress-chart" page 0));
666 666 Alcotest.(check bool)
667 667 "the chart names the exercise" true
668 Removed: (Option.is_some (index_from ~needle:"Squats" page 0)) );
668 Added: (Option.is_some (index_from ~needle:"Squats" page 0));
669 Added: (* Axes are labelled: kg on the left, reps on the right. *)
670 Added: Alcotest.(check bool)
671 Added: "the weight axis is labelled kg" true
672 Added: (Option.is_some (index_from ~needle:"chart-axis-title" page 0));
673 Added: Alcotest.(check bool)
674 Added: "the reps axis is labelled" true
675 Added: (Option.is_some (index_from ~needle:">reps<" page 0)) );
669 676 ( "the progress page reports too little history to chart",
670 677 `Quick,
671 678 fun () ->
@@ -718,13 +725,14 @@
718 725 (Printf.sprintf "/profile/import/%s/workouts/%s/promote" bid wid)
719 726 [ ("dream.csrf", t) ]
720 727 in
721 Removed: let page = body (get c "/progress?exercise=Squats") in
728 Added: let page = body (get c "/progress") in
722 729 Alcotest.(check bool)
723 730 "no chart is drawn for one point" false
724 731 (Option.is_some (index_from ~needle:"progress-chart" page 0));
725 732 Alcotest.(check bool)
726 733 "an insufficient-history message is shown" true
727 Removed: (Option.is_some (index_from ~needle:"Not enough history" page 0))
734 Added: (Option.is_some
735 Added: (index_from ~needle:"No exercise has enough history" page 0))
728 736 );
729 737 ] );
730 738 ]