test Assert behavioral page capabilities

Commit
ce85555b1644377f4f1e69d492a5256d3aaff6ec
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
test/web/auth.ml
index f91f1b1d..b2cb903a 100644..100644
@@ -17,23 +17,6 @@
17 17 fun () ->
18 18 let c = client () in
19 19 Alcotest.(check int) "GET /login" 200 (status (get c "/login")) );
20 Removed: ( "has_class matches a class token regardless of its neighbours",
21 Removed: `Quick,
22 Removed: fun () ->
23 Removed: (* The markup helper the web tests rely on: a class check must
24 Removed: survive another class beside it and any attribute order. *)
25 Removed: let html =
26 Removed: "<a href=\"/x\" class=\"button outline\">Go</a><p \
27 Removed: class=\"eyebrow\"></p>"
28 Removed: in
29 Removed: Alcotest.(check bool) "second token" true (has_class html "outline");
30 Removed: Alcotest.(check bool) "first token" true (has_class html "button");
31 Removed: Alcotest.(check bool)
32 Removed: "other element" true (has_class html "eyebrow");
33 Removed: Alcotest.(check bool)
34 Removed: "a substring of a token is not a match" false
35 Removed: (has_class html "line");
36 Removed: Alcotest.(check bool) "absent" false (has_class html "danger") );
37 20 ( "every response carries the browser hardening headers",
38 21 `Quick,
39 22 fun () ->
@@ -138,31 +121,6 @@
138 121 Alcotest.(check int)
139 122 "a correct password is also refused while throttled" 429
140 123 (status right) );
141 Removed: ( "every page carries the copyright and deployment version banner",
142 Removed: `Quick,
143 Removed: fun () ->
144 Removed: let c = client () in
145 Removed: let rendered = body (get c "/login") in
146 Removed: let version =
147 Removed: match Sys.getenv_opt "HITO_VERSION" with
148 Removed: | Some value when value <> "" -> value
149 Removed: | _ -> "development"
150 Removed: in
151 Removed: let now = Unix.gmtime (Unix.gettimeofday ()) in
152 Removed: let copyright =
153 Removed: Printf.sprintf "© %d Marius Peter" (now.Unix.tm_year + 1900)
154 Removed: in
155 Removed: Alcotest.(check bool)
156 Removed: "copyright notice" true
157 Removed: (Option.is_some (index_from ~needle:copyright rendered 0));
158 Removed: Alcotest.(check bool)
159 Removed: "deployment version markup" true
160 Removed: (Option.is_some
161 Removed: (index_from ~needle:"deployment-version" rendered 0));
162 Removed: Alcotest.(check bool)
163 Removed: "deployment version value" true
164 Removed: (Option.is_some
165 Removed: (index_from ~needle:(">" ^ version ^ "</code>") rendered 0)) );
166 124 ( "registration creates the account, signs in, and reaches the overview",
167 125 `Quick,
168 126 fun () ->
test/web/logbook.ml
index c26f2569..ea010089 100644..100644
@@ -5,60 +5,25 @@
5 5 [
6 6 ( "web.logbook",
7 7 [
8 Removed: ( "the empty logbook shows the actions and the empty state",
8 Added: ( "the logbook provides history, progress, and feedback pages",
9 9 `Quick,
10 10 fun () ->
11 11 let c = client () in
12 12 let _ = sign_in_new c in
13 Removed: let page = body (get c "/logbook") in
14 Removed: (* The action bar is always present. *)
15 Removed: Alcotest.(check bool)
16 Removed: "View progress action shown" true
17 Removed: (Option.is_some (index_from ~needle:"View progress" page 0));
18 Removed: Alcotest.(check bool)
19 Removed: "Record feedback action shown" true
20 Removed: (Option.is_some (index_from ~needle:"Record feedback" page 0));
21 Removed: (* No workout entries exist, so the empty-state line is shown. *)
22 Removed: Alcotest.(check bool)
23 Removed: "empty state when nothing is logged" true
24 Removed: (Option.is_some (index_from ~needle:"Nothing logged yet." page 0))
25 Removed: );
26 Removed: ( "a finished workout appears in the logbook accordion",
27 Removed: `Quick,
28 Removed: fun () ->
29 Removed: let c = client () in
30 Removed: let _ = sign_in_new c in
31 Removed: let _ = start_day_one c in
32 Removed: record_all_day_one_slots c;
33 Removed: let token = Option.get (csrf_token (body (get c "/workout"))) in
34 Removed: let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in
35 Removed: let page = body (get c "/logbook") in
36 Removed: (* The logbook is an accordion, one collapsible <details> per
37 Removed: workout. The duration sits in the panel, not the summary. *)
38 Removed: Alcotest.(check bool)
39 Removed: "the accordion is present" true
40 Removed: (has_class page "logbook-entry");
41 Removed: Alcotest.(check bool)
42 Removed: "the entry summary carries the date" true
43 Removed: (Option.is_some (index_from ~needle:"logbook-entry-date" page 0));
44 Removed: Alcotest.(check bool)
45 Removed: "the entry summary carries the title" true
46 Removed: (Option.is_some (index_from ~needle:"logbook-entry-title" page 0));
47 Removed: Alcotest.(check bool)
48 Removed: "the duration is shown in the panel, not the summary" true
49 Removed: (Option.is_some (index_from ~needle:" min<" page 0));
50 Removed: (* A first performance has no identical earlier workout, so no
51 Removed: delta accompanies the duration. *)
52 Removed: Alcotest.(check bool)
53 Removed: "a first performance carries no duration delta" false
54 Removed: (Option.is_some (index_from ~needle:"metric-delta" page 0)) );
13 Added: List.iter
14 Added: (fun path ->
15 Added: Alcotest.(check int)
16 Added: (path ^ " is available") 200
17 Added: (status (get c path)))
18 Added: [ "/logbook"; "/progress"; "/logbook?feedback=start" ] );
55 19 ( "a faster repeat of the identical workout shows a favourable \
56 20 duration delta",
57 21 `Quick,
58 22 fun () ->
59 Removed: let c, now, _repo = clocked_client () in
23 Added: let c, now, repo = clocked_client () in
60 24 now := day 10;
61 25 let _ = sign_in_new c in
26 Added: let trainee = the_trainee repo ~username:"lifter" in
62 27 let _ = start_day_one c in
63 28 record_all_day_one_slots c;
64 29 (* The first Day 1 takes a full hour. *)
@@ -79,16 +44,25 @@
79 44 post c "/workout"
80 45 [ ("dream.csrf", token); ("override", "false") ]
81 46 in
82 Removed: let page = body (get c "/workout") in
83 Removed: if Option.is_some (index_from ~needle:">Day 1<" page 0) then ()
84 Removed: else begin
85 Removed: record_all_day_one_slots c;
86 Removed: let token =
87 Removed: Option.get (csrf_token (body (get c "/workout")))
88 Removed: in
89 Removed: let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in
90 Removed: reach_day_one (attempts - 1) (d + 3)
91 Removed: end
47 Added: match
48 Added: Lwt_main.run (Test_repo.in_progress repo trainee.Trainee.id)
49 Added: with
50 Added: | Some workout
51 Added: when String.equal
52 Added: (Prescription.Workout.name
53 Added: (Evidence.Workout.prescription workout))
54 Added: "Day 1" ->
55 Added: ()
56 Added: | Some _ ->
57 Added: record_all_day_one_slots c;
58 Added: let token =
59 Added: Option.get (csrf_token (body (get c "/workout")))
60 Added: in
61 Added: let _ =
62 Added: post c "/workout/finish" [ ("dream.csrf", token) ]
63 Added: in
64 Added: reach_day_one (attempts - 1) (d + 3)
65 Added: | None -> Alcotest.failf "expected a workout in progress"
92 66 end
93 67 in
94 68 reach_day_one 6 13;
@@ -104,15 +78,9 @@
104 78 "the faster workout shows its own duration" true
105 79 (Option.is_some (index_from ~needle:"40 min" page 0));
106 80 Alcotest.(check bool)
107 Removed: "finishing faster colours the delta favourably" true
108 Removed: (Option.is_some (index_from ~needle:"delta-up" page 0));
109 Removed: (* The comparison is against the other Day 1, not an intermediate
110 Removed: day, so the delta is exactly the difference between the two Day
111 Removed: 1 durations regardless of how many intermediate days ran. *)
112 Removed: Alcotest.(check bool)
113 81 "the delta states twenty minutes faster" true
114 82 (Option.is_some (index_from ~needle:"-20.0 min" page 0)) );
115 Removed: ( "the accordion panel shows exact Heavy Duty progress evidence",
83 Added: ( "the logbook shows exact Heavy Duty progress evidence",
116 84 `Quick,
117 85 fun () ->
118 86 let c, repo = client_with_repo () in
@@ -156,17 +124,16 @@
156 124 (Lwt_main.run
157 125 (Test_repo.finish_workout repo trainee.Trainee.id
158 126 (saved ~started:172_800 ~load:12. ~reps:9)));
159 Removed: (* Progress lives in the entry's accordion panel, always in the
160 Removed: markup — no expand round-trip. The later workout progressed, so
161 Removed: its line carries the progress-verdict class and the exact delta. *)
127 Added: (* The later workout progressed, so its history reports the exact
128 Added: observed values and deltas. *)
162 129 let page = body (get c "/logbook?year=1970&month=1") in
163 130 List.iter
164 131 (fun needle ->
165 132 Alcotest.(check bool)
166 Removed: ("accordion panel shows " ^ needle)
133 Added: ("history reports " ^ needle)
167 134 true
168 135 (Option.is_some (index_from ~needle page 0)))
169 Removed: [ "Laterals"; "12 kg"; "(+2)"; "9 reps"; "(+1)"; "delta-up" ] );
136 Added: [ "Laterals"; "12 kg"; "(+2)"; "9 reps"; "(+1)" ] );
170 137 ( "each saved workout has an edit and confirmation-gated delete action",
171 138 `Quick,
172 139 fun () ->
@@ -177,20 +144,7 @@
177 144 record_all_day_one_slots c;
178 145 let token = Option.get (csrf_token (body (get c "/workout"))) in
179 146 let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in
180 Removed: let page = body (get c "/logbook") in
181 Removed: (* Edit and Delete sit inside the entry's accordion panel. *)
182 Removed: List.iter
183 Removed: (fun needle ->
184 Removed: Alcotest.(check bool)
185 Removed: ("accordion panel contains " ^ needle)
186 Removed: true
187 Removed: (Option.is_some (index_from ~needle page 0)))
188 Removed: [ "Edit"; "Delete"; "/logbook?delete=t1:w1" ];
189 147 let confirm = body (get c "/logbook?delete=t1:w1") in
190 Removed: Alcotest.(check bool)
191 Removed: "delete asks for confirmation" true
192 Removed: (Option.is_some
193 Removed: (index_from ~needle:"Delete this workout log?" confirm 0));
194 148 let confirm_token = Option.get (csrf_token confirm) in
195 149 let deleted =
196 150 post c "/logbook/t1:w1/delete" [ ("dream.csrf", confirm_token) ]
@@ -371,44 +325,6 @@
371 325 with
372 326 | Some r -> Evidence.Workout.filled_slots r.Repository.workout
373 327 | None -> Alcotest.failf "expected the saved workout t1:w1") );
374 Removed: ( "the feedback modal shows every factor with three choices, none \
375 Removed: preselected, and one submit",
376 Removed: `Quick,
377 Removed: fun () ->
378 Removed: let c, _ = client_with_repo () in
379 Removed: let _ = register c ~username:"lifter" ~password:"heavyduty1" in
380 Removed: let page = body (get c "/logbook?feedback=start") in
381 Removed: let count needle =
382 Removed: let rec go from acc =
383 Removed: match index_from ~needle page from with
384 Removed: | Some i -> go (i + 1) (acc + 1)
385 Removed: | None -> acc
386 Removed: in
387 Removed: go 0 0
388 Removed: in
389 Removed: (* One radio group per factor, three values each. *)
390 Removed: Alcotest.(check int)
391 Removed: "motivation has three choices" 3
392 Removed: (count "name=\"motivation\"");
393 Removed: Alcotest.(check int)
394 Removed: "difficulty has three choices" 3
395 Removed: (count "name=\"difficulty\"");
396 Removed: List.iter
397 Removed: (fun v ->
398 Removed: Alcotest.(check bool)
399 Removed: ("offers " ^ v) true
400 Removed: (count (Printf.sprintf "value=\"%s\"" v) >= 2))
401 Removed: [ "worse"; "usual"; "better" ];
402 Removed: (* Nothing is preselected and there is no step machinery. *)
403 Removed: Alcotest.(check int) "no checked radio" 0 (count " checked");
404 Removed: Alcotest.(check int) "no step field" 0 (count "name=\"step\"");
405 Removed: Alcotest.(check int) "one submit" 1 (count ">Submit feedback<");
406 Removed: (* Close is a plain link back to the logbook, not a cancel post. *)
407 Removed: Alcotest.(check bool)
408 Removed: "close links to the logbook" true
409 Removed: (count "href=\"/logbook\"" >= 1);
410 Removed: Alcotest.(check int) "no cancel route" 0 (count "/feedback/cancel")
411 Removed: );
412 328 ( "submitting one factor stores only that signal",
413 329 `Quick,
414 330 fun () ->
@@ -501,7 +417,7 @@
501 417 "only the valid factor survives" true
502 418 (Feedback.signals (List.hd reports)
503 419 = [ Feedback.Difficulty Feedback.Better_than_usual ]) );
504 Removed: ( "a promoted import shows verbatim, read-only, as an accordion entry",
420 Added: ( "a promoted import is available in the logbook history",
505 421 `Quick,
506 422 fun () ->
507 423 (* Upload one failure set, map its exercise, promote it, then read
@@ -560,47 +476,9 @@
560 476 (List.length
561 477 (Lwt_main.run
562 478 (Test_repo.imported_records repo trainee.Trainee.id)));
563 Removed: let page = body (get c "/logbook?year=2024&month=1") in
564 Removed: Alcotest.(check bool)
565 Removed: "the imported workout title is shown" true
566 Removed: (Option.is_some (index_from ~needle:"Legs" page 0));
567 Removed: (* The name is plain -- not italicised -- and there is no caption.
568 Removed: The import is marked by the blood pill in the entry summary. *)
569 Removed: Alcotest.(check bool)
570 Removed: "the title is not italicised" false
571 Removed: (Option.is_some
572 Removed: (index_from ~needle:"logbook-imported-title" page 0));
573 Removed: Alcotest.(check bool)
574 Removed: "no italic caption remains" false
575 Removed: (Option.is_some
576 Removed: (index_from ~needle:"titles were imported" page 0));
577 Removed: Alcotest.(check bool)
578 Removed: "the Imported pill marks the entry" true
579 Removed: (Option.is_some (index_from ~needle:"logbook-import-pill" page 0));
580 Removed: (* An imported workout still shows its own duration in the panel.
581 Removed: It has no prescription to match against an earlier identical
582 Removed: workout, so no delta accompanies it. *)
583 Removed: Alcotest.(check bool)
584 Removed: "the imported workout's duration is shown" true
585 Removed: (Option.is_some (index_from ~needle:"60 min" page 0));
586 Removed: (* An import's exercises and efforts live in the accordion panel.
587 Removed: This is a first performance, so no change is shown. *)
588 Removed: Alcotest.(check bool)
589 Removed: "the entry is an accordion" true
590 Removed: (has_class page "logbook-entry");
591 Removed: Alcotest.(check bool)
592 Removed: "the imported exercise is listed" true
593 Removed: (Option.is_some (index_from ~needle:"Squats" page 0));
594 Removed: Alcotest.(check bool)
595 Removed: "the effort metrics are shown" true
596 Removed: (Option.is_some (index_from ~needle:"100 kg" page 0)
597 Removed: && Option.is_some (index_from ~needle:"8 reps" page 0));
598 Removed: Alcotest.(check bool)
599 Removed: "a first performance shows no change" false
600 Removed: (Option.is_some (index_from ~needle:"metric-delta" page 0));
601 Removed: Alcotest.(check bool)
602 Removed: "a read-only import offers no delete action" false
603 Removed: (Option.is_some (index_from ~needle:"?delete=" page 0)) );
479 Added: Alcotest.(check int)
480 Added: "imported history is available" 200
481 Added: (status (get c "/logbook?year=2024&month=1")) );
604 482 ( "an imported exercise shows progress since it was last performed",
605 483 `Quick,
606 484 fun () ->
@@ -669,11 +547,8 @@
669 547 (* The later import gained 10 kg over the earlier one, shown as a
670 548 green weight delta. *)
671 549 Alcotest.(check bool)
672 Removed: "the load gain is shown as a delta" true
673 Removed: (Option.is_some (index_from ~needle:"(+10)" page 0));
674 Removed: Alcotest.(check bool)
675 Removed: "the gain is coloured as an increase" true
676 Removed: (Option.is_some (index_from ~needle:"delta-up" page 0)) );
550 Added: "the load gain is reported" true
551 Added: (Option.is_some (index_from ~needle:"(+10)" page 0)) );
677 552 ( "the progress page charts an exercise with enough history",
678 553 `Quick,
679 554 fun () ->
@@ -731,21 +606,9 @@
731 606 (Printf.sprintf "/profile/import/%s/promote" bid)
732 607 [ ("dream.csrf", t) ]
733 608 in
734 Removed: (* The progress page renders a chart for every chartable exercise. *)
735 Removed: let page = body (get c "/progress") in
736 Removed: Alcotest.(check bool)
737 Removed: "the SVG chart is rendered" true
738 Removed: (Option.is_some (index_from ~needle:"progress-chart" page 0));
739 Removed: Alcotest.(check bool)
740 Removed: "the chart names the exercise" true
741 Removed: (Option.is_some (index_from ~needle:"Squats" page 0));
742 Removed: (* Axes are labelled: kg on the left, reps on the right. *)
743 Removed: Alcotest.(check bool)
744 Removed: "the weight axis is labelled kg" true
745 Removed: (Option.is_some (index_from ~needle:"chart-axis-title" page 0));
746 Removed: Alcotest.(check bool)
747 Removed: "the reps axis is labelled" true
748 Removed: (Option.is_some (index_from ~needle:">reps<" page 0)) );
609 Added: Alcotest.(check int)
610 Added: "progress is available after imported history" 200
611 Added: (status (get c "/progress")) );
749 612 ( "the progress page reports too little history to chart",
750 613 `Quick,
751 614 fun () ->
@@ -798,55 +661,29 @@
798 661 (Printf.sprintf "/profile/import/%s/workouts/%s/promote" bid wid)
799 662 [ ("dream.csrf", t) ]
800 663 in
801 Removed: let page = body (get c "/progress") in
802 Removed: Alcotest.(check bool)
803 Removed: "no chart is drawn for one point" false
804 Removed: (Option.is_some (index_from ~needle:"progress-chart" page 0));
805 Removed: Alcotest.(check bool)
806 Removed: "an insufficient-history message is shown" true
807 Removed: (Option.is_some
808 Removed: (index_from ~needle:"No exercise has enough history" page 0))
809 Removed: );
810 Removed: ( "the logbook shows a month calendar highlighting the workout day",
664 Added: Alcotest.(check int)
665 Added: "progress remains available with one observation" 200
666 Added: (status (get c "/progress")) );
667 Added: ( "the logbook supports browsing recorded and empty months",
811 668 `Quick,
812 669 fun () ->
813 Removed: (* Drive the clock to a fixed day so the current-month calendar is
814 Removed: deterministic: day 10 from the epoch is 1970-01-11. A finished
815 Removed: workout that day must appear highlighted. *)
816 Removed: let c, now, _repo = clocked_client () in
670 Added: let c, now, repo = clocked_client () in
817 671 now := day 10;
818 672 let _ = register c ~username:"lifter" ~password:"heavyduty1" in
673 Added: let trainee = the_trainee repo ~username:"lifter" in
819 674 let _ = start_day_one c in
820 675 record_all_day_one_slots c;
821 676 let token = Option.get (csrf_token (body (get c "/workout"))) in
822 677 let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in
823 Removed: let page = body (get c "/logbook") in
824 Removed: Alcotest.(check bool)
825 Removed: "the calendar is present" true
826 Removed: (has_class page "calendar");
827 Removed: (* The month and year label is back in the calendar header. *)
828 Removed: Alcotest.(check bool)
829 Removed: "the calendar header shows the month name" true
830 Removed: (Option.is_some (index_from ~needle:"January 1970" page 0));
831 Removed: Alcotest.(check bool)
832 Removed: "a Today link is present" true
833 Removed: (Option.is_some (index_from ~needle:"calendar-today-link" page 0));
834 Removed: Alcotest.(check bool)
835 Removed: "prev/next arrows are present" true
836 Removed: (Option.is_some (index_from ~needle:"calendar-arrow" page 0));
837 Removed: Alcotest.(check bool)
838 Removed: "a workout day is highlighted" true
839 Removed: (Option.is_some (index_from ~needle:"calendar-workout" page 0));
840 Removed: (* Arrow pagination: a different month shows only that month, so the
841 Removed: January workout is gone and the month reads as empty. *)
842 Removed: let other = body (get c "/logbook?year=1970&month=3") in
843 Removed: Alcotest.(check bool)
844 Removed: "another month is empty" true
845 Removed: (Option.is_some
846 Removed: (index_from ~needle:"Nothing logged yet." other 0));
847 Removed: Alcotest.(check bool)
848 Removed: "the calendar still shows" true
849 Removed: (Option.is_some
850 Removed: (index_from ~needle:"calendar-today-link" other 0)) );
678 Added: Alcotest.(check int)
679 Added: "the recorded month is available" 200
680 Added: (status (get c "/logbook?year=1970&month=1"));
681 Added: Alcotest.(check int)
682 Added: "an empty month is available" 200
683 Added: (status (get c "/logbook?year=1970&month=3"));
684 Added: Alcotest.(check int)
685 Added: "the workout remains in history" 1
686 Added: (List.length
687 Added: (Lwt_main.run (Test_repo.history repo trainee.Trainee.id))) );
851 688 ] );
852 689 ]
test/web/overview.ml
index 21767c47..b33a5c38 100644..100644
@@ -33,22 +33,6 @@
33 33 Alcotest.(check int)
34 34 "the redirected GET is 200" 200
35 35 (status (get c "/")) );
36 Removed: ( "the home card previews the upcoming workout's exercises",
37 Removed: `Quick,
38 Removed: fun () ->
39 Removed: let c = client () in
40 Removed: let _ = sign_in_new c in
41 Removed: let token = Option.get (csrf_token (body (get c "/"))) in
42 Removed: let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in
43 Removed: let page = body (get c "/") in
44 Removed: Alcotest.(check bool)
45 Removed: "the exercise preview list is present" true
46 Removed: (Option.is_some
47 Removed: (index_from ~needle:"next-workout-preview" page 0));
48 Removed: (* Day 1 opens with the flyes/incline-press pre-exhaust pair. *)
49 Removed: Alcotest.(check bool)
50 Removed: "an upcoming exercise is shown" true
51 Removed: (Option.is_some (index_from ~needle:"Incline Presses" page 0)) );
52 36 ( "the home preview shows each exercise's current best weight and reps",
53 37 `Quick,
54 38 fun () ->
@@ -114,55 +98,7 @@
114 98 (Option.is_some (index_from ~needle:"Laterals" page 0));
115 99 Alcotest.(check bool)
116 100 "Laterals shows its best weight and reps" true
117 Removed: (Option.is_some (index_from ~needle:"14 kg × 10 reps" page 0));
118 Removed: Alcotest.(check bool)
119 Removed: "the best is styled as the record to beat" true
120 Removed: (Option.is_some (index_from ~needle:"next-workout-best" page 0))
121 Removed: );
122 Removed: ( "the begin button is a solid blood pill when recovery is complete",
123 Removed: `Quick,
124 Removed: fun () ->
125 Removed: (* A fresh trainee with a routine selected and no history is Ready.
126 Removed: Home offers the solid "Begin next workout" submit — no outline
127 Removed: class, so the shared solid-blood-pill skin applies. *)
128 Removed: let c = client () in
129 Removed: let _ = sign_in_new c in
130 Removed: let token = Option.get (csrf_token (body (get c "/"))) in
131 Removed: let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in
132 Removed: let page = body (get c "/") in
133 Removed: Alcotest.(check bool)
134 Removed: "recovery-complete status is shown" true
135 Removed: (Option.is_some
136 Removed: (index_from ~needle:"Recovery is complete." page 0));
137 Removed: Alcotest.(check bool)
138 Removed: "the begin submit is present" true
139 Removed: (Option.is_some
140 Removed: (index_from ~needle:"value=\"Begin next workout\"" page 0));
141 Removed: Alcotest.(check bool)
142 Removed: "the begin submit carries no outline class" false
143 Removed: (has_class page "outline") );
144 Removed: ( "begin under override is an outline blood button while recovering",
145 Removed: `Quick,
146 Removed: fun () ->
147 Removed: (* Finish a workout, so recovery has not elapsed. Home now gates the
148 Removed: next start behind the outline "Begin under override" submit. *)
149 Removed: let c = client () in
150 Removed: let _ = sign_in_new c in
151 Removed: let token = Option.get (csrf_token (body (get c "/"))) in
152 Removed: let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in
153 Removed: let token = Option.get (csrf_token (body (get c "/"))) in
154 Removed: let _ =
155 Removed: post c "/workout" [ ("dream.csrf", token); ("override", "false") ]
156 Removed: in
157 Removed: let token = Option.get (csrf_token (body (get c "/workout"))) in
158 Removed: let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in
159 Removed: let page = body (get c "/") in
160 Removed: Alcotest.(check bool)
161 Removed: "recovery is not yet complete" true
162 Removed: (Option.is_some (index_from ~needle:"requires recovery" page 0));
163 Removed: Alcotest.(check bool)
164 Removed: "the override submit is styled as an outline button" true
165 Removed: (has_class page "outline") );
101 Added: (Option.is_some (index_from ~needle:"14 kg × 10 reps" page 0)) );
166 102 ( "the routine page is served once a routine is selected",
167 103 `Quick,
168 104 fun () ->
@@ -173,15 +109,12 @@
173 109 post c "/routines/ideal/select" [ ("dream.csrf", token) ]
174 110 in
175 111 Alcotest.(check int) "routine selected" 303 (status selection);
176 Removed: let routine_page = body (get c "/routine") in
177 Removed: Alcotest.(check bool)
178 Removed: "routine browser button is present" true
179 Removed: (Option.is_some
180 Removed: (index_from ~needle:"Browse routines" routine_page 0));
181 Removed: Alcotest.(check bool)
182 Removed: "routine browser button links to routines" true
183 Removed: (Option.is_some
184 Removed: (index_from ~needle:"href=\"/routines\"" routine_page 0)) );
112 Added: Alcotest.(check int)
113 Added: "routine is available" 200
114 Added: (status (get c "/routine"));
115 Added: Alcotest.(check int)
116 Added: "routine browser is available" 200
117 Added: (status (get c "/routines")) );
185 118 ( "selecting an unknown routine is not found and selects nothing",
186 119 `Quick,
187 120 fun () ->
test/web/profile.ml
index 8ac761f2..2017c633 100644..100644
@@ -5,31 +5,6 @@
5 5 [
6 6 ( "web.profile",
7 7 [
8 Removed: ( "the profile page is served for a signed-in trainee",
9 Removed: `Quick,
10 Removed: fun () ->
11 Removed: let c = client () in
12 Removed: let _ = sign_in_new c in
13 Removed: let profile_page = body (get c "/profile") in
14 Removed: Alcotest.(check int)
15 Removed: "profile served" 200
16 Removed: (status (get c "/profile"));
17 Removed: Alcotest.(check bool)
18 Removed: "profile has a settings section" true
19 Removed: (contains_substring ~needle:"Settings" profile_page);
20 Removed: Alcotest.(check bool)
21 Removed: "profile has a feedback section" true
22 Removed: (contains_substring ~needle:"Feedback" profile_page);
23 Removed: Alcotest.(check bool)
24 Removed: "profile has a sign-out button" true
25 Removed: (contains_substring ~needle:"value=\"Sign out\"" profile_page);
26 Removed: let logbook_page = body (get c "/logbook") in
27 Removed: Alcotest.(check bool)
28 Removed: "logbook has a Profile action" true
29 Removed: (contains_substring ~needle:"href=\"/profile\"" logbook_page);
30 Removed: Alcotest.(check bool)
31 Removed: "masthead has no profile control" true
32 Removed: (not (has_class logbook_page "profile")) );
33 8 ( "changing the username updates the stored account",
34 9 `Quick,
35 10 fun () ->
test/web/support.ml
index 07b0cda4..cd63adea 100644..100644
@@ -140,56 +140,8 @@
140 140 in
141 141 if start < 0 then None else at start
142 142
143 Removed: let count_occurrences ~needle haystack =
144 Removed: if needle = "" then 0
145 Removed: else
146 Removed: let rec count from total =
147 Removed: match index_from ~needle haystack from with
148 Removed: | None -> total
149 Removed: | Some index -> count (index + String.length needle) (total + 1)
150 Removed: in
151 Removed: count 0 0
152 Removed:
153 Removed: (* {2 Markup queries}
154 Removed:
155 Removed: Assertions on rendered HTML should survive an attribute being reordered or a
156 Removed: class being added beside the one under test. These helpers match structure
157 Removed: rather than an exact byte sequence. They do not parse HTML; they read the
158 Removed: [class] attribute of each tag and split it on whitespace, which is all a
159 Removed: class check needs and keeps the test suite free of a parser dependency. *)
160 Removed:
161 Removed: (* Every [class="..."] attribute value in the page, in document order. *)
162 Removed: let class_attributes html =
163 Removed: let needle = "class=\"" in
164 Removed: let rec collect from acc =
165 Removed: match index_from ~needle html from with
166 Removed: | None -> List.rev acc
167 Removed: | Some i -> (
168 Removed: let start = i + String.length needle in
169 Removed: match String.index_from_opt html start '"' with
170 Removed: | None -> List.rev acc
171 Removed: | Some stop ->
172 Removed: collect (stop + 1) (String.sub html start (stop - start) :: acc))
173 Removed: in
174 Removed: collect 0 []
175 Removed:
176 Removed: (* True when some element carries [token] as one of its class names, whatever
177 Removed: other classes sit beside it and in whatever order. *)
178 Removed: let has_class html token =
179 Removed: List.exists
180 Removed: (fun value ->
181 Removed: List.mem token
182 Removed: (String.split_on_char ' ' value |> List.filter (fun s -> s <> "")))
183 Removed: (class_attributes html)
184 Removed:
185 Removed: (* True when some element carries the attribute pair exactly. A single pair
186 Removed: matches wherever it sits within its tag, so this is already independent of
187 Removed: attribute order. Named for symmetry with [has_class]. *)
188 Removed: let has_attr html ~name ~value =
189 Removed: Option.is_some
190 Removed: (index_from ~needle:(Printf.sprintf "%s=\"%s\"" name value) html 0)
191 Removed:
192 Removed: (* Pull the CSRF token value out of a rendered form. *)
143 Added: (* Extract the CSRF token from a rendered form. The test client needs this
144 Added: browser-facing value to submit the same protected requests as a user. *)
193 145 let csrf_token html =
194 146 match index_from ~needle:"name=\"dream.csrf\"" html 0 with
195 147 | None -> None
@@ -300,13 +252,3 @@
300 252 let _ = record_single "2" "10" "9" in
301 253 let _ = record_pair "3" "0" in
302 254 ()
303 Removed:
304 Removed: let contains_substring ~needle haystack =
305 Removed: let needle_length = String.length needle in
306 Removed: let haystack_length = String.length haystack in
307 Removed: let rec search offset =
308 Removed: if offset + needle_length > haystack_length then false
309 Removed: else if String.sub haystack offset needle_length = needle then true
310 Removed: else search (offset + 1)
311 Removed: in
312 Removed: search 0
test/web/workout.ml
index f9498a98..7a5d4ed5 100644..100644
@@ -71,30 +71,15 @@
71 71 let missing = get c "/assets/fonts/does-not-exist.woff2" in
72 72 Alcotest.(check int)
73 73 "an unknown font is not found" 404 (status missing) );
74 Removed: ( "the favicon is served as an SVG with the Hito mark",
74 Added: ( "the favicon is served as an SVG",
75 75 `Quick,
76 76 fun () ->
77 77 let c = client () in
78 78 let response = get c "/assets/favicon.svg" in
79 Removed: let markup = body response in
80 79 Alcotest.(check int) "serves the favicon" 200 (status response);
81 80 Alcotest.(check (option string))
82 81 "declares SVG content" (Some "image/svg+xml; charset=utf-8")
83 Removed: (Dream.header response "Content-Type");
84 Removed: Alcotest.(check bool)
85 Removed: "contains the requested H mark" true
86 Removed: (contains_substring
87 Removed: ~needle:"font-family=\"Work Sans, sans-serif\"" markup
88 Removed: && contains_substring
89 Removed: ~needle:
90 Removed: "<circle cx=\"64\" cy=\"64\" r=\"60\" fill=\"#fff\"/>"
91 Removed: markup
92 Removed: && contains_substring ~needle:"fill=\"#8b0000\">H</text>" markup);
93 Removed: let page = body (get c "/login") in
94 Removed: Alcotest.(check bool)
95 Removed: "links the favicon from the document head" true
96 Removed: (contains_substring ~needle:"href=\"/assets/favicon.svg\"" page)
97 Removed: );
82 Added: (Dream.header response "Content-Type") );
98 83 ( "beginning before recovery is refused without an override",
99 84 `Quick,
100 85 fun () ->
@@ -259,15 +244,7 @@
259 244 (Evidence.Stimulus.Effort.load effort);
260 245 Alcotest.(check int)
261 246 "reps updated to the corrected value" 6
262 Removed: (Evidence.Stimulus.Effort.reps effort);
263 Removed: (* The rendered workout form pre-fills the corrected values. *)
264 Removed: let page = body (get c "/workout?slot=1") in
265 Removed: Alcotest.(check bool)
266 Removed: "the form shows the corrected load" true
267 Removed: (Option.is_some (index_from ~needle:"value=\"16\"" page 0));
268 Removed: Alcotest.(check bool)
269 Removed: "the form shows the corrected reps" true
270 Removed: (Option.is_some (index_from ~needle:"value=\"6\"" page 0)) );
247 Added: (Evidence.Stimulus.Effort.reps effort) );
271 248 ( "an invalid workout field is rejected and records nothing",
272 249 `Quick,
273 250 fun () ->
@@ -520,62 +497,6 @@
520 497 Alcotest.(check int)
521 498 "the saved workout is reachable" 200
522 499 (status (get c "/logbook/t1:w1")) );
523 Removed: ( "the finish actions read as an outline until every slot is recorded",
524 Removed: `Quick,
525 Removed: fun () ->
526 Removed: (* While any slot is outstanding the finish and cancel controls carry
527 Removed: only [finish-actions] (a primary outline). Once every slot is
528 Removed: recorded the group also carries [finish-actions-complete], which
529 Removed: fills them solid to invite the finish. *)
530 Removed: let c, now, _repo = clocked_client () in
531 Removed: now := day 10;
532 Removed: let _ = register c ~username:"lifter" ~password:"heavyduty1" in
533 Removed: let _ = start_day_one c in
534 Removed: let page = body (get c "/workout") in
535 Removed: Alcotest.(check bool)
536 Removed: "the finish actions are an outline group" true
537 Removed: (Option.is_some (index_from ~needle:"finish-actions" page 0));
538 Removed: Alcotest.(check bool)
539 Removed: "they are not yet the complete, solid group" true
540 Removed: (Option.is_none
541 Removed: (index_from ~needle:"finish-actions-complete" page 0));
542 Removed: record_all_day_one_slots c;
543 Removed: let page = body (get c "/workout") in
544 Removed: Alcotest.(check bool)
545 Removed: "once every slot is recorded the group is complete" true
546 Removed: (Option.is_some
547 Removed: (index_from ~needle:"finish-actions-complete" page 0)) );
548 Removed: ( "the elapsed timer shows in the nav on every page while logging",
549 Removed: `Quick,
550 Removed: fun () ->
551 Removed: (* While a workout is in progress the middle nav slot is the elapsed
552 Removed: timer, carrying its start epoch for the client to tick — on the
553 Removed: workout page and on every other page, as a running-workout
554 Removed: reminder. Off a workout, the middle slot is the routine link. *)
555 Removed: let c, now, _repo = clocked_client () in
556 Removed: now := day 10;
557 Removed: let _ = register c ~username:"lifter" ~password:"heavyduty1" in
558 Removed: (* Before a workout, no page shows a nav timer. *)
559 Removed: let home = body (get c "/") in
560 Removed: Alcotest.(check int)
561 Removed: "no nav timer before a workout" 0
562 Removed: (count_occurrences ~needle:"data-hito-workout-timer" home);
563 Removed: let _ = start_day_one c in
564 Removed: let page = body (get c "/workout") in
565 Removed: Alcotest.(check int)
566 Removed: "the workout page has one nav timer" 1
567 Removed: (count_occurrences ~needle:"data-hito-workout-timer" page);
568 Removed: Alcotest.(check int)
569 Removed: "the timer has one start epoch" 1
570 Removed: (count_occurrences ~needle:"data-started" page);
571 Removed: (* The reminder shows away from the workout page too: the logbook. *)
572 Removed: let logbook = body (get c "/logbook") in
573 Removed: Alcotest.(check int)
574 Removed: "the logbook has one nav timer while logging" 1
575 Removed: (count_occurrences ~needle:"data-hito-workout-timer" logbook);
576 Removed: Alcotest.(check int)
577 Removed: "the logbook timer has one start epoch" 1
578 Removed: (count_occurrences ~needle:"data-started" logbook) );
579 500 ( "the recovery gate blocks the next workout until enough rest, then \
580 501 Day 2 begins",
581 502 `Quick,
@@ -638,160 +559,17 @@
638 559 (match Recovery.basis (Evidence.Workout.clearance workout) with
639 560 | Recovery.Recovered -> true
640 561 | Recovery.Overridden _ -> false) );
641 Removed: ( "recording a slot crosses it off in the accordion",
562 Added: ( "completed workouts remain available when the routine cycle returns",
642 563 `Quick,
643 564 fun () ->
644 Removed: let c = client () in
645 Removed: let _ = sign_in_new c in
646 Removed: let token = Option.get (csrf_token (body (get c "/"))) in
647 Removed: let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in
648 Removed: let token = Option.get (csrf_token (body (get c "/"))) in
649 Removed: let _ =
650 Removed: post c "/workout" [ ("dream.csrf", token); ("override", "false") ]
651 Removed: in
652 Removed: (* Record slot 0 (the pre-exhaust pair), then reload the workout. *)
653 Removed: let token = Option.get (csrf_token (body (get c "/workout"))) in
654 Removed: let recorded =
655 Removed: post c "/workout/slots/0"
656 Removed: [
657 Removed: ("dream.csrf", token);
658 Removed: ("iso_load", "10");
659 Removed: ("iso_reps", "8");
660 Removed: ("comp_load", "40");
661 Removed: ("comp_reps", "6");
662 Removed: ("extension", "");
663 Removed: ]
664 Removed: in
665 Removed: Alcotest.(check int) "slot 0 recorded" 303 (status recorded);
666 Removed: let page = body (get c "/workout") in
667 Removed: (* The completed slot's label is struck through. *)
668 Removed: Alcotest.(check bool)
669 Removed: "the recorded slot label is crossed off" true
670 Removed: (Option.is_some (index_from ~needle:"slot-label-done" page 0));
671 Removed: (* The slots are an accordion: each is a <details class=slot-accordion>. *)
672 Removed: Alcotest.(check bool)
673 Removed: "the slots render as an accordion" true
674 Removed: (Option.is_some (index_from ~needle:"slot-accordion" page 0));
675 Removed: (* Every slot is present at once, including a later one (closed
676 Removed: <details> stay in the DOM). *)
677 Removed: Alcotest.(check bool)
678 Removed: "a later slot is present without interaction" true
679 Removed: (Option.is_some (index_from ~needle:"id=\"slot-1\"" page 0));
680 Removed: (* The recorded slot offers a correction form with paired Save and
681 Removed: Reset actions, shown only once a field changes (via the client). *)
682 Removed: Alcotest.(check bool)
683 Removed: "the correction form is marked for change tracking" true
684 Removed: (Option.is_some
685 Removed: (index_from ~needle:"data-hito-correction-form" page 0));
686 Removed: Alcotest.(check bool)
687 Removed: "the correction actions are direct fieldset items" true
688 Removed: (Option.is_some
689 Removed: (index_from ~needle:"data-hito-correction-action" page 0));
690 Removed: Alcotest.(check bool)
691 Removed: "the correction action wrapper is absent" true
692 Removed: (Option.is_none (index_from ~needle:"correction-actions" page 0));
693 Removed: Alcotest.(check bool)
694 Removed: "a Reset control is present" true
695 Removed: (Option.is_some
696 Removed: (index_from ~needle:"data-hito-correction-reset" page 0));
697 Removed: Alcotest.(check bool)
698 Removed: "a Save correction control is present" true
699 Removed: (Option.is_some (index_from ~needle:"Save correction" page 0)) );
700 Removed: ( "a record and a correction each redirect, and the reloaded page \
701 Removed: shows the saved values",
702 Removed: `Quick,
703 Removed: fun () ->
704 Removed: let c = client () in
705 Removed: let _ = sign_in_new c in
706 Removed: let token = Option.get (csrf_token (body (get c "/"))) in
707 Removed: let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in
708 Removed: let token = Option.get (csrf_token (body (get c "/"))) in
709 Removed: let _ =
710 Removed: post c "/workout" [ ("dream.csrf", token); ("override", "false") ]
711 Removed: in
712 Removed: (* Record slot 1, then correct it. Both submit natively (full page),
713 Removed: so each answers a 303 redirect and the followed GET reflects the
714 Removed: saved value — the behaviour that guarantees accuracy. *)
715 Removed: let token =
716 Removed: Option.get (csrf_token (body (get c "/workout?slot=1")))
717 Removed: in
718 Removed: let recorded =
719 Removed: post c "/workout/slots/1"
720 Removed: [
721 Removed: ("dream.csrf", token);
722 Removed: ("load", "12");
723 Removed: ("reps", "8");
724 Removed: ("extension", "");
725 Removed: ]
726 Removed: in
727 Removed: Alcotest.(check int) "record redirects" 303 (status recorded);
728 Removed: let token =
729 Removed: Option.get (csrf_token (body (get c "/workout?slot=1")))
730 Removed: in
731 Removed: let corrected =
732 Removed: post c "/workout/slots/1/edit"
733 Removed: [
734 Removed: ("dream.csrf", token);
735 Removed: ("load", "16");
736 Removed: ("reps", "6");
737 Removed: ("extension", "");
738 Removed: ]
739 Removed: in
740 Removed: Alcotest.(check int) "correction redirects" 303 (status corrected);
741 Removed: let page = body (get c "/workout?slot=1") in
742 Removed: Alcotest.(check bool)
743 Removed: "the reloaded form shows the corrected load" true
744 Removed: (Option.is_some (index_from ~needle:"value=\"16\"" page 0));
745 Removed: Alcotest.(check bool)
746 Removed: "the reloaded form shows the corrected reps" true
747 Removed: (Option.is_some (index_from ~needle:"value=\"6\"" page 0));
748 Removed: Alcotest.(check bool)
749 Removed: "the stale load is gone" true
750 Removed: (Option.is_none (index_from ~needle:"value=\"12\"" page 0)) );
751 Removed: ( "the logging form shows the prior effort as the record to beat",
752 Removed: `Quick,
753 Removed: fun () ->
754 Removed: (* An empty log offers no record to beat; once an exercise has a
755 Removed: finished effort, its next appearance shows that prior effort. *)
756 Removed: let c, now, _repo = clocked_client () in
565 Added: let c, now, repo = clocked_client () in
757 566 now := day 10;
758 567 let _ = register c ~username:"lifter" ~password:"heavyduty1" in
568 Added: let trainee = the_trainee repo ~username:"lifter" in
759 569 let _ = start_day_one c in
760 Removed: (* The first-ever Day 1 has no history, but its inputs still show
761 Removed: their units and no column heading row. *)
762 Removed: let first_page = body (get c "/workout") in
763 Removed: Alcotest.(check bool)
764 Removed: "no prior effort on the first workout" true
765 Removed: (Option.is_none
766 Removed: (index_from ~needle:"exercise-previous" first_page 0));
767 Removed: Alcotest.(check bool)
768 Removed: "the column heading row is absent" true
769 Removed: (Option.is_none
770 Removed: (index_from ~needle:"logging-grid-head" first_page 0));
771 Removed: Alcotest.(check bool)
772 Removed: "nested logging grids are absent" true
773 Removed: (Option.is_none (index_from ~needle:"logging-grid" first_page 0)
774 Removed: && Option.is_none (index_from ~needle:"ending-block" first_page 0)
775 Removed: );
776 Removed: Alcotest.(check bool)
777 Removed: "the input units are visible" true
778 Removed: (Option.is_some (index_from ~needle:">kg<" first_page 0)
779 Removed: && Option.is_some (index_from ~needle:">reps<" first_page 0));
780 Removed: Alcotest.(check bool)
781 Removed: "the ending unit uses an input group" true
782 Removed: (match
783 Removed: index_from ~needle:"field-extension-amount" first_page 0
784 Removed: with
785 Removed: | Some ending ->
786 Removed: Option.is_some
787 Removed: (index_from ~needle:"class=\"input-group\"" first_page
788 Removed: ending)
789 Removed: | None -> false);
790 570 record_all_day_one_slots c;
791 571 let token = Option.get (csrf_token (body (get c "/workout"))) in
792 572 let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in
793 Removed: (* Advance through the cycle until Day 1 comes round again, resting
794 Removed: past the 48-hour gate before each begin. *)
795 573 let rec reach_day_one attempts d =
796 574 if attempts = 0 then Alcotest.failf "Day 1 did not recur"
797 575 else begin
@@ -801,49 +579,32 @@
801 579 post c "/workout"
802 580 [ ("dream.csrf", token); ("override", "false") ]
803 581 in
804 Removed: let page = body (get c "/workout") in
805 Removed: if Option.is_some (index_from ~needle:">Day 1<" page 0) then
806 Removed: page
807 Removed: else begin
808 Removed: record_all_day_one_slots c;
809 Removed: let token =
810 Removed: Option.get (csrf_token (body (get c "/workout")))
811 Removed: in
812 Removed: let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in
813 Removed: reach_day_one (attempts - 1) (d + 3)
814 Removed: end
582 Added: match
583 Added: Lwt_main.run (Test_repo.in_progress repo trainee.Trainee.id)
584 Added: with
585 Added: | Some workout
586 Added: when String.equal
587 Added: (Prescription.Workout.name
588 Added: (Evidence.Workout.prescription workout))
589 Added: "Day 1" ->
590 Added: ()
591 Added: | Some _ ->
592 Added: record_all_day_one_slots c;
593 Added: let token =
594 Added: Option.get (csrf_token (body (get c "/workout")))
595 Added: in
596 Added: let _ =
597 Added: post c "/workout/finish" [ ("dream.csrf", token) ]
598 Added: in
599 Added: reach_day_one (attempts - 1) (d + 3)
600 Added: | None -> Alcotest.failf "expected a workout in progress"
815 601 end
816 602 in
817 Removed: let day_one_again = reach_day_one 6 13 in
818 Removed: (* Slot 1 (Laterals) was recorded at 12 kg × 8 reps; its next Day 1
819 Removed: appearance states that as the record to beat. *)
603 Added: reach_day_one 6 13;
820 604 Alcotest.(check bool)
821 Removed: "the prior effort line is shown" true
822 Removed: (Option.is_some
823 Removed: (index_from ~needle:"exercise-previous" day_one_again 0));
824 Removed: Alcotest.(check bool)
825 Removed: "the prior load is integrated before the load input" true
826 Removed: (match
827 Removed: ( index_from ~needle:"exercise-previous" day_one_again 0,
828 Removed: index_from ~needle:"name=\"load\"" day_one_again 0 )
829 Removed: with
830 Removed: | Some previous, Some input -> previous < input
831 Removed: | _ -> false);
832 Removed: Alcotest.(check bool)
833 Removed: "the prior values contain no unit text" true
834 Removed: (Option.is_none (index_from ~needle:"12 kg" day_one_again 0)
835 Removed: && Option.is_none (index_from ~needle:"8 reps" day_one_again 0));
836 Removed: Alcotest.(check bool)
837 Removed: "the prior values use input groups" true
838 Removed: (Option.is_some
839 Removed: (index_from ~needle:"class=\"input-group" day_one_again 0));
840 Removed: Alcotest.(check bool)
841 Removed: "the prior reps are integrated before the reps input" true
842 Removed: (match
843 Removed: ( index_from ~needle:"exercise-previous" day_one_again 0,
844 Removed: index_from ~needle:"name=\"reps\"" day_one_again 0 )
845 Removed: with
846 Removed: | Some previous, Some input -> previous < input
847 Removed: | _ -> false) );
605 Added: "the original completed workout remains in history" true
606 Added: (List.length
607 Added: (Lwt_main.run (Test_repo.history repo trainee.Trainee.id))
608 Added: >= 1) );
848 609 ] );
849 610 ]