[OCaml] High Intensity Training Online
test Assert behavioral page capabilities
Changed files
test/web/auth.ml
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
]