View raw

1 open Hito_app 2 open Support 3 4 let suite = 5 [ 6 ( "web.logbook", 7 [ 8 ( "the logbook provides history, progress, and feedback pages", 9 `Quick, 10 fun () -> 11 let c = client () in 12 let _ = sign_in_new c in 13 Alcotest.(check int) 14 "the calendar view is served" 200 15 (status (get c "/logbook")); 16 Alcotest.(check int) 17 "the progress view is served" 200 18 (status (get c "/logbook?tab=progress")); 19 Alcotest.(check int) 20 "the old progress endpoint is gone" 404 21 (status (get c "/progress")); 22 Alcotest.(check int) 23 "the feedback modal stays available" 200 24 (status (get c "/logbook?feedback=start")) ); 25 ( "the calendar marks the next routine workout after recovery", 26 `Quick, 27 fun () -> 28 let c, now, _repo = clocked_client () in 29 now := day 10; 30 let _ = sign_in_new c in 31 let _ = start_day_one c in 32 record_all_day_one_slots c; 33 now := day 10 + 3600; 34 let token = Option.get (csrf_token (body (get c "/workout"))) in 35 let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in 36 let page = body (get c "/logbook?year=1970&month=1&day=13") in 37 Alcotest.(check bool) 38 "the next workout is planned on the selected date" true 39 (Option.is_some 40 (index_from ~needle:"Planned on 13th January 1970" page 0)); 41 Alcotest.(check bool) 42 "the planned workout is Day 2" true 43 (Option.is_some (index_from ~needle:"Day 2" page 0)) ); 44 ( "the Logbook keeps an overdue next workout tentative", 45 `Quick, 46 fun () -> 47 let c, now, _repo = clocked_client () in 48 now := day 10; 49 let _ = sign_in_new c in 50 let _ = start_day_one c in 51 record_all_day_one_slots c; 52 let token = Option.get (csrf_token (body (get c "/workout"))) in 53 let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in 54 (* Day 2 was due after recovery, but was not performed. The Logbook 55 must keep Day 2 visible today. It must not advance to Day 3. *) 56 now := day 20; 57 let page = body (get c "/logbook") in 58 Alcotest.(check bool) 59 "the overdue workout is planned today" true 60 (Option.is_some 61 (index_from ~needle:"Planned on 21st January 1970" page 0)); 62 Alcotest.(check bool) 63 "the overdue workout is still Day 2" true 64 (Option.is_some (index_from ~needle:"Day 2" page 0)); 65 Alcotest.(check bool) 66 "the routine does not advance silently" true 67 (Option.is_none (index_from ~needle:"Day 3" page 0)) ); 68 ( "a faster repeat of the identical workout shows a favourable \ 69 duration delta", 70 `Quick, 71 fun () -> 72 let c, now, repo = clocked_client () in 73 now := day 10; 74 let _ = sign_in_new c in 75 let trainee = the_trainee repo ~username:"lifter" in 76 let _ = start_day_one c in 77 record_all_day_one_slots c; 78 (* The first Day 1 takes a full hour. *) 79 now := day 10 + 3600; 80 let token = Option.get (csrf_token (body (get c "/workout"))) in 81 let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in 82 (* Advance through the cycle until Day 1 comes round again, resting 83 past the 48-hour gate before each begin, exactly as 84 Web.Workout's "the record to beat" test does. Intermediate days 85 are recorded and finished promptly; only the Day 1 repeat's own 86 duration is timed. *) 87 let rec reach_day_one attempts d = 88 if attempts = 0 then Alcotest.failf "Day 1 did not recur" 89 else begin 90 now := day d; 91 let token = Option.get (csrf_token (body (get c "/"))) in 92 let _ = 93 post c "/workout" 94 [ ("dream.csrf", token); ("override", "false") ] 95 in 96 match 97 Lwt_main.run (Test_repo.in_progress repo trainee.Trainee.id) 98 with 99 | Some workout 100 when String.equal 101 (Prescription.Workout.name 102 (Evidence.Workout.prescription workout)) 103 "Day 1" -> 104 () 105 | Some _ -> 106 record_all_day_one_slots c; 107 let token = 108 Option.get (csrf_token (body (get c "/workout"))) 109 in 110 let _ = 111 post c "/workout/finish" [ ("dream.csrf", token) ] 112 in 113 reach_day_one (attempts - 1) (d + 3) 114 | None -> Alcotest.failf "expected a workout in progress" 115 end 116 in 117 reach_day_one 6 13; 118 (* The Day 1 repeat finishes in forty minutes — faster, at the 119 same effort. *) 120 record_all_day_one_slots c; 121 let finished_at = !now + 2400 in 122 now := finished_at; 123 let token = Option.get (csrf_token (body (get c "/workout"))) in 124 let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in 125 let page = body (get c "/logbook?year=1970&month=1&day=20") in 126 Alcotest.(check bool) 127 "the faster workout shows its own duration" true 128 (Option.is_some (index_from ~needle:"40 min" page 0)); 129 Alcotest.(check bool) 130 "the delta states twenty minutes faster" true 131 (Option.is_some (index_from ~needle:"-20.0 min" page 0)) ); 132 ( "the logbook shows exact Heavy Duty progress evidence", 133 `Quick, 134 fun () -> 135 let c, repo = client_with_repo () in 136 let _ = register c ~username:"lifter" ~password:"heavyduty1" in 137 let trainee = the_trainee repo ~username:"lifter" in 138 let day_one = 139 List.hd (Prescription.Routine.workouts Routines.ideal) 140 in 141 let laterals = 142 Option.get 143 (Exercise.find 144 { 145 equipment = Unspecified; 146 movement = Lateral_raise; 147 variation = Standard; 148 }) 149 in 150 let saved ~started ~load ~reps = 151 let workout = 152 Evidence.Workout.start day_one 153 ~clearance:(Option.get (Recovery.clear Recovery.Ready)) 154 ~started_at:(Recovery.timestamp_of_unix_seconds started) 155 in 156 let effort = 157 Result.get_ok 158 (Evidence.Stimulus.Effort.make ~exercise:laterals ~load ~reps 159 ~outcome:Evidence.Stimulus.Positive_failure) 160 in 161 let workout = 162 Evidence.Workout.add_stimulus workout 163 (Evidence.Stimulus.make (Evidence.Stimulus.Single effort)) 164 in 165 Evidence.Workout.finish workout 166 ~ended_at:(Recovery.timestamp_of_unix_seconds (started + 1080)) 167 in 168 ignore 169 (Lwt_main.run 170 (Test_repo.finish_workout repo trainee.Trainee.id 171 (saved ~started:0 ~load:10. ~reps:8))); 172 ignore 173 (Lwt_main.run 174 (Test_repo.finish_workout repo trainee.Trainee.id 175 (saved ~started:172_800 ~load:12. ~reps:9))); 176 (* The later workout progressed, so its history reports the exact 177 observed values and deltas. *) 178 let page = body (get c "/logbook?year=1970&month=1&day=3") in 179 List.iter 180 (fun needle -> 181 Alcotest.(check bool) 182 ("history reports " ^ needle) 183 true 184 (Option.is_some (index_from ~needle page 0))) 185 [ "Laterals"; "12"; "+2"; "9"; "+1" ] ); 186 ( "each saved workout has an edit and confirmation-gated delete action", 187 `Quick, 188 fun () -> 189 let c, repo = client_with_repo () in 190 let _ = register c ~username:"lifter" ~password:"heavyduty1" in 191 let trainee = the_trainee repo ~username:"lifter" in 192 let _ = start_day_one c in 193 record_all_day_one_slots c; 194 let token = Option.get (csrf_token (body (get c "/workout"))) in 195 let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in 196 let confirm = body (get c "/logbook?delete=t1:w1") in 197 let confirm_token = Option.get (csrf_token confirm) in 198 let deleted = 199 post c "/logbook/t1:w1/delete" [ ("dream.csrf", confirm_token) ] 200 in 201 Alcotest.(check int) "delete redirects" 303 (status deleted); 202 Alcotest.(check int) 203 "record removed from history" 0 204 (List.length 205 (Lwt_main.run (Test_repo.history repo trainee.Trainee.id))); 206 Alcotest.(check bool) 207 "record cannot be found" true 208 (Option.is_none 209 (Lwt_main.run 210 (Test_repo.find repo trainee.Trainee.id 211 (Repository.workout_id "t1:w1")))) ); 212 ( "a saved workout's slot can be corrected from the logbook view", 213 `Quick, 214 fun () -> 215 let c, repo = client_with_repo () in 216 let _ = register c ~username:"lifter" ~password:"heavyduty1" in 217 let trainee = the_trainee repo ~username:"lifter" in 218 let token = Option.get (csrf_token (body (get c "/"))) in 219 let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in 220 let token = Option.get (csrf_token (body (get c "/"))) in 221 let _ = 222 post c "/workout" [ ("dream.csrf", token); ("override", "false") ] 223 in 224 let token = 225 Option.get (csrf_token (body (get c "/workout?slot=1"))) 226 in 227 let _ = 228 post c "/workout/slots/1" 229 [ 230 ("dream.csrf", token); 231 ("load", "12"); 232 ("reps", "8"); 233 ("extension", ""); 234 ] 235 in 236 let token = Option.get (csrf_token (body (get c "/workout"))) in 237 let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in 238 (* The one saved workout is t1:w1 for this trainee. Correct its slot 239 through the logbook path, then confirm the store still has one 240 filled slot — a correction, not extra volume. *) 241 let record_page = body (get c "/logbook/t1:w1?slot=1") in 242 let token = Option.get (csrf_token record_page) in 243 let corrected = 244 post c "/logbook/t1:w1/slots/1/edit" 245 [ 246 ("dream.csrf", token); 247 ("load", "20"); 248 ("reps", "7"); 249 ("extension", ""); 250 ] 251 in 252 Alcotest.(check int) "corrected" 303 (status corrected); 253 let record = 254 Lwt_main.run 255 (Test_repo.find repo trainee.Trainee.id 256 (Repository.workout_id "t1:w1")) 257 in 258 Alcotest.(check int) 259 "still one filled slot" 1 260 (match record with 261 | Some r -> Evidence.Workout.filled_slots r.Repository.workout 262 | None -> Alcotest.failf "expected the saved workout t1:w1") ); 263 ( "an outstanding slot of a saved workout can be recorded, not only \ 264 corrected", 265 `Quick, 266 fun () -> 267 (* Finish a workout with a single slot recorded, so the saved record 268 still has outstanding slots. Then record one of those through the 269 plain add path [POST /logbook/:id/slots/:d], which the edit tests 270 never exercise. *) 271 let c, repo = client_with_repo () in 272 let _ = register c ~username:"lifter" ~password:"heavyduty1" in 273 let trainee = the_trainee repo ~username:"lifter" in 274 let token = Option.get (csrf_token (body (get c "/"))) in 275 let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in 276 let token = Option.get (csrf_token (body (get c "/"))) in 277 let _ = 278 post c "/workout" [ ("dream.csrf", token); ("override", "false") ] 279 in 280 (* Record only slot 1, then finish. Slots 0, 2, 3 stay outstanding. *) 281 let token = 282 Option.get (csrf_token (body (get c "/workout?slot=1"))) 283 in 284 let _ = 285 post c "/workout/slots/1" 286 [ 287 ("dream.csrf", token); 288 ("load", "12"); 289 ("reps", "8"); 290 ("extension", ""); 291 ] 292 in 293 let token = Option.get (csrf_token (body (get c "/workout"))) in 294 let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in 295 let filled_of_record () = 296 match 297 Lwt_main.run 298 (Test_repo.find repo trainee.Trainee.id 299 (Repository.workout_id "t1:w1")) 300 with 301 | Some r -> Evidence.Workout.filled_slots r.Repository.workout 302 | None -> Alcotest.failf "expected the saved workout t1:w1" 303 in 304 Alcotest.(check int) 305 "saved workout starts with one slot" 1 (filled_of_record ()); 306 (* Add slot 2 through the non-edit path. *) 307 let token = 308 Option.get (csrf_token (body (get c "/logbook/t1:w1?slot=2"))) 309 in 310 let added = 311 post c "/logbook/t1:w1/slots/2" 312 [ 313 ("dream.csrf", token); 314 ("load", "10"); 315 ("reps", "9"); 316 ("extension", ""); 317 ] 318 in 319 Alcotest.(check int) 320 "recorded a saved-workout slot" 303 (status added); 321 Alcotest.(check int) "now two filled slots" 2 (filled_of_record ()) 322 ); 323 ( "recording an already-filled slot through the add path is refused", 324 `Quick, 325 fun () -> 326 (* The add path lists only outstanding slots. A filled slot is not 327 awaiting a record, so posting to it is a 404, not a silent second 328 entry that would add volume. *) 329 let c, repo = client_with_repo () in 330 let _ = register c ~username:"lifter" ~password:"heavyduty1" in 331 let trainee = the_trainee repo ~username:"lifter" in 332 let token = Option.get (csrf_token (body (get c "/"))) in 333 let _ = post c "/routines/ideal/select" [ ("dream.csrf", token) ] in 334 let token = Option.get (csrf_token (body (get c "/"))) in 335 let _ = 336 post c "/workout" [ ("dream.csrf", token); ("override", "false") ] 337 in 338 let token = 339 Option.get (csrf_token (body (get c "/workout?slot=1"))) 340 in 341 let _ = 342 post c "/workout/slots/1" 343 [ 344 ("dream.csrf", token); 345 ("load", "12"); 346 ("reps", "8"); 347 ("extension", ""); 348 ] 349 in 350 let token = Option.get (csrf_token (body (get c "/workout"))) in 351 let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in 352 (* Slot 1 is filled. Re-posting to its add path is refused. *) 353 let token = 354 Option.get (csrf_token (body (get c "/logbook/t1:w1?slot=2"))) 355 in 356 let refused = 357 post c "/logbook/t1:w1/slots/1" 358 [ 359 ("dream.csrf", token); 360 ("load", "20"); 361 ("reps", "6"); 362 ("extension", ""); 363 ] 364 in 365 Alcotest.(check int) 366 "filled slot is not awaiting" 404 (status refused); 367 (* The refused post added no volume: still one filled slot. *) 368 Alcotest.(check int) 369 "still one filled slot" 1 370 (match 371 Lwt_main.run 372 (Test_repo.find repo trainee.Trainee.id 373 (Repository.workout_id "t1:w1")) 374 with 375 | Some r -> Evidence.Workout.filled_slots r.Repository.workout 376 | None -> Alcotest.failf "expected the saved workout t1:w1") ); 377 ( "submitting one factor stores only that signal", 378 `Quick, 379 fun () -> 380 let c, repo = client_with_repo () in 381 let _ = register c ~username:"lifter" ~password:"heavyduty1" in 382 let trainee = the_trainee repo ~username:"lifter" in 383 let token = Option.get (csrf_token (body (get c "/logbook"))) in 384 let saved = 385 post c "/feedback" 386 [ ("dream.csrf", token); ("motivation", "better") ] 387 in 388 Alcotest.(check int) "submit redirects" 303 (status saved); 389 Alcotest.(check string) 390 "back to the logbook" "/logbook" (location saved); 391 let reports = 392 Lwt_main.run (Test_repo.feedback repo trainee.Trainee.id) 393 in 394 Alcotest.(check int) "one report stored" 1 (List.length reports); 395 Alcotest.(check bool) 396 "only motivation, better than usual" true 397 (Feedback.signals (List.hd reports) 398 = [ Feedback.Motivation Feedback.Better_than_usual ]) ); 399 ( "submitting every factor stores exactly one signal per category", 400 `Quick, 401 fun () -> 402 let c, repo = client_with_repo () in 403 let _ = register c ~username:"lifter" ~password:"heavyduty1" in 404 let trainee = the_trainee repo ~username:"lifter" in 405 let token = Option.get (csrf_token (body (get c "/logbook"))) in 406 let saved = 407 post c "/feedback" 408 [ 409 ("dream.csrf", token); 410 ("motivation", "worse"); 411 ("difficulty", "usual"); 412 ] 413 in 414 Alcotest.(check int) "submit redirects" 303 (status saved); 415 let reports = 416 Lwt_main.run (Test_repo.feedback repo trainee.Trainee.id) 417 in 418 Alcotest.(check int) "one report stored" 1 (List.length reports); 419 let signals = Feedback.signals (List.hd reports) in 420 let count p = List.length (List.filter p signals) in 421 let open Feedback in 422 Alcotest.(check int) 423 "one motivation signal" 1 424 (count (function Motivation _ -> true | _ -> false)); 425 Alcotest.(check int) 426 "one difficulty signal" 1 427 (count (function Difficulty _ -> true | _ -> false)) ); 428 ( "submitting with nothing selected stores nothing", 429 `Quick, 430 fun () -> 431 let c, repo = client_with_repo () in 432 let _ = register c ~username:"lifter" ~password:"heavyduty1" in 433 let trainee = the_trainee repo ~username:"lifter" in 434 let token = Option.get (csrf_token (body (get c "/logbook"))) in 435 let empty = post c "/feedback" [ ("dream.csrf", token) ] in 436 Alcotest.(check int) "empty submit redirects" 303 (status empty); 437 Alcotest.(check string) 438 "back to the logbook" "/logbook" (location empty); 439 Alcotest.(check int) 440 "no report stored" 0 441 (List.length 442 (Lwt_main.run (Test_repo.feedback repo trainee.Trainee.id))) ); 443 ( "a value outside the three choices is ignored", 444 `Quick, 445 fun () -> 446 (* A crafted or stale value for a factor is treated as unselected, 447 so it can neither store a bad level nor reach the domain. *) 448 let c, repo = client_with_repo () in 449 let _ = register c ~username:"lifter" ~password:"heavyduty1" in 450 let trainee = the_trainee repo ~username:"lifter" in 451 let token = Option.get (csrf_token (body (get c "/logbook"))) in 452 let bad = 453 post c "/feedback" 454 [ 455 ("dream.csrf", token); 456 ("motivation", "7"); 457 ("difficulty", "better"); 458 ] 459 in 460 Alcotest.(check int) "submit redirects" 303 (status bad); 461 let reports = 462 Lwt_main.run (Test_repo.feedback repo trainee.Trainee.id) 463 in 464 Alcotest.(check int) "one report stored" 1 (List.length reports); 465 Alcotest.(check bool) 466 "only the valid factor survives" true 467 (Feedback.signals (List.hd reports) 468 = [ Feedback.Difficulty Feedback.Better_than_usual ]) ); 469 ( "a promoted import is available in the logbook history", 470 `Quick, 471 fun () -> 472 (* Upload one failure set, map its exercise, promote it, then read 473 the logbook. The promoted import shows tagged and read-only, and 474 its title interleaves with native records. *) 475 let c, repo = client_with_repo () in 476 let _ = register c ~username:"lifter" ~password:"heavyduty1" in 477 let trainee = the_trainee repo ~username:"lifter" in 478 let header = 479 "title,start_time,end_time,description,exercise_title,superset_id,exercise_notes,set_index,set_type,weight_kg,reps,distance_km,duration_seconds,rpe\r\n" 480 in 481 let source = 482 header 483 ^ "Legs,\"01 Jan 2024, 10:00\",\"01 Jan 2024, 11:00\",,Barbell \ 484 Squat,,,0,failure,100,8,,,\r\n" 485 in 486 let token = Option.get (csrf_token (body (get c "/profile"))) in 487 let _ = 488 post_multipart c "/profile/import" 489 ~file:(Some ("csv", "workouts.csv", source)) 490 ~fields:[ ("dream.csrf", token); ("utc_offset_seconds", "0") ] 491 in 492 let batch = 493 List.hd 494 (Lwt_main.run 495 (Test_repo.import_batches repo trainee.Trainee.id)) 496 in 497 let bid = 498 Workout_import.batch_id_to_string 499 (Workout_import.batch_identity batch) 500 in 501 let wid = 502 Workout_import.workout_id_to_string 503 (Workout_import.workout_identity 504 (List.hd (Workout_import.batch_workouts batch))) 505 in 506 let review = "/profile/import/" ^ bid in 507 let t = Option.get (csrf_token (body (get c review))) in 508 let _ = 509 post c 510 (Printf.sprintf "/profile/import/%s/workouts/%s/map" bid wid) 511 [ 512 ("dream.csrf", t); 513 ("source_name", "Barbell Squat"); 514 ("exercise_id", "squats"); 515 ] 516 in 517 let t = Option.get (csrf_token (body (get c review))) in 518 let _ = 519 post c 520 (Printf.sprintf "/profile/import/%s/workouts/%s/promote" bid wid) 521 [ ("dream.csrf", t) ] 522 in 523 Alcotest.(check int) 524 "the import is promoted" 1 525 (List.length 526 (Lwt_main.run 527 (Test_repo.imported_records repo trainee.Trainee.id))); 528 Alcotest.(check int) 529 "imported history is available" 200 530 (status (get c "/logbook?year=2024&month=1")) ); 531 ( "an imported exercise shows progress since it was last performed", 532 `Quick, 533 fun () -> 534 (* Two imports of the same exercise on different days. The later 535 one's menu reports the load gain since the earlier performance, 536 not "first performance". *) 537 let c, repo = client_with_repo () in 538 let _ = register c ~username:"lifter" ~password:"heavyduty1" in 539 let trainee = the_trainee repo ~username:"lifter" in 540 let header = 541 "title,start_time,end_time,description,exercise_title,superset_id,exercise_notes,set_index,set_type,weight_kg,reps,distance_km,duration_seconds,rpe\r\n" 542 in 543 let source = 544 header 545 ^ "Legs,\"01 Jan 2024, 10:00\",\"01 Jan 2024, 11:00\",,Barbell \ 546 Squat,,,0,failure,100,8,,,\r\n" 547 ^ "Legs,\"08 Jan 2024, 10:00\",\"08 Jan 2024, 11:00\",,Barbell \ 548 Squat,,,0,failure,110,8,,,\r\n" 549 in 550 let token = Option.get (csrf_token (body (get c "/profile"))) in 551 let _ = 552 post_multipart c "/profile/import" 553 ~file:(Some ("csv", "workouts.csv", source)) 554 ~fields:[ ("dream.csrf", token); ("utc_offset_seconds", "0") ] 555 in 556 let batch = 557 List.hd 558 (Lwt_main.run 559 (Test_repo.import_batches repo trainee.Trainee.id)) 560 in 561 let bid = 562 Workout_import.batch_id_to_string 563 (Workout_import.batch_identity batch) 564 in 565 let review = "/profile/import/" ^ bid in 566 (* Map the exercise in both workouts, then promote the whole batch. *) 567 List.iter 568 (fun w -> 569 let wid = 570 Workout_import.workout_id_to_string 571 (Workout_import.workout_identity w) 572 in 573 let t = Option.get (csrf_token (body (get c review))) in 574 ignore 575 (post c 576 (Printf.sprintf "/profile/import/%s/workouts/%s/map" bid 577 wid) 578 [ 579 ("dream.csrf", t); 580 ("source_name", "Barbell Squat"); 581 ("exercise_id", "squats"); 582 ])) 583 (Workout_import.batch_workouts batch); 584 let t = Option.get (csrf_token (body (get c review))) in 585 let _ = 586 post c 587 (Printf.sprintf "/profile/import/%s/promote" bid) 588 [ ("dream.csrf", t) ] 589 in 590 Alcotest.(check int) 591 "both imports promoted" 2 592 (List.length 593 (Lwt_main.run 594 (Test_repo.imported_records repo trainee.Trainee.id))); 595 let page = body (get c "/logbook?year=2024&month=1&day=8") in 596 (* The later import gained 10 kg over the earlier one, shown as a 597 green weight delta without repeating the unit in the value. *) 598 Alcotest.(check bool) 599 "the load gain is reported" true 600 (Option.is_some (index_from ~needle:"+10" page 0)) ); 601 ( "the progress page charts an exercise with enough history", 602 `Quick, 603 fun () -> 604 (* Two promoted imports of the same exercise give two points, enough 605 to draw the dual-axis line chart. *) 606 let c, repo = client_with_repo () in 607 let _ = register c ~username:"lifter" ~password:"heavyduty1" in 608 let trainee = the_trainee repo ~username:"lifter" in 609 let header = 610 "title,start_time,end_time,description,exercise_title,superset_id,exercise_notes,set_index,set_type,weight_kg,reps,distance_km,duration_seconds,rpe\r\n" 611 in 612 let source = 613 header 614 ^ "Legs,\"01 Jan 2024, 10:00\",\"01 Jan 2024, 11:00\",,Barbell \ 615 Squat,,,0,failure,100,8,,,\r\n" 616 ^ "Legs,\"08 Jan 2024, 10:00\",\"08 Jan 2024, 11:00\",,Barbell \ 617 Squat,,,0,failure,110,8,,,\r\n" 618 in 619 let token = Option.get (csrf_token (body (get c "/profile"))) in 620 let _ = 621 post_multipart c "/profile/import" 622 ~file:(Some ("csv", "workouts.csv", source)) 623 ~fields:[ ("dream.csrf", token); ("utc_offset_seconds", "0") ] 624 in 625 let batch = 626 List.hd 627 (Lwt_main.run 628 (Test_repo.import_batches repo trainee.Trainee.id)) 629 in 630 let bid = 631 Workout_import.batch_id_to_string 632 (Workout_import.batch_identity batch) 633 in 634 let review = "/profile/import/" ^ bid in 635 List.iter 636 (fun w -> 637 let wid = 638 Workout_import.workout_id_to_string 639 (Workout_import.workout_identity w) 640 in 641 let t = Option.get (csrf_token (body (get c review))) in 642 ignore 643 (post c 644 (Printf.sprintf "/profile/import/%s/workouts/%s/map" bid 645 wid) 646 [ 647 ("dream.csrf", t); 648 ("source_name", "Barbell Squat"); 649 ("exercise_id", "squats"); 650 ])) 651 (Workout_import.batch_workouts batch); 652 let t = Option.get (csrf_token (body (get c review))) in 653 let _ = 654 post c 655 (Printf.sprintf "/profile/import/%s/promote" bid) 656 [ ("dream.csrf", t) ] 657 in 658 Alcotest.(check int) 659 "progress is available after imported history" 200 660 (status (get c "/logbook?tab=progress")) ); 661 ( "the progress page reports too little history to chart", 662 `Quick, 663 fun () -> 664 (* A single performance cannot draw a line. *) 665 let c, repo = client_with_repo () in 666 let _ = register c ~username:"lifter" ~password:"heavyduty1" in 667 let trainee = the_trainee repo ~username:"lifter" in 668 let header = 669 "title,start_time,end_time,description,exercise_title,superset_id,exercise_notes,set_index,set_type,weight_kg,reps,distance_km,duration_seconds,rpe\r\n" 670 in 671 let source = 672 header 673 ^ "Legs,\"01 Jan 2024, 10:00\",\"01 Jan 2024, 11:00\",,Barbell \ 674 Squat,,,0,failure,100,8,,,\r\n" 675 in 676 let token = Option.get (csrf_token (body (get c "/profile"))) in 677 let _ = 678 post_multipart c "/profile/import" 679 ~file:(Some ("csv", "workouts.csv", source)) 680 ~fields:[ ("dream.csrf", token); ("utc_offset_seconds", "0") ] 681 in 682 let batch = 683 List.hd 684 (Lwt_main.run 685 (Test_repo.import_batches repo trainee.Trainee.id)) 686 in 687 let bid = 688 Workout_import.batch_id_to_string 689 (Workout_import.batch_identity batch) 690 in 691 let wid = 692 Workout_import.workout_id_to_string 693 (Workout_import.workout_identity 694 (List.hd (Workout_import.batch_workouts batch))) 695 in 696 let review = "/profile/import/" ^ bid in 697 let t = Option.get (csrf_token (body (get c review))) in 698 let _ = 699 post c 700 (Printf.sprintf "/profile/import/%s/workouts/%s/map" bid wid) 701 [ 702 ("dream.csrf", t); 703 ("source_name", "Barbell Squat"); 704 ("exercise_id", "squats"); 705 ] 706 in 707 let t = Option.get (csrf_token (body (get c review))) in 708 let _ = 709 post c 710 (Printf.sprintf "/profile/import/%s/workouts/%s/promote" bid wid) 711 [ ("dream.csrf", t) ] 712 in 713 Alcotest.(check int) 714 "progress remains available with one observation" 200 715 (status (get c "/logbook?tab=progress")) ); 716 ( "the logbook supports browsing recorded and empty months", 717 `Quick, 718 fun () -> 719 let c, now, repo = clocked_client () in 720 now := day 10; 721 let _ = register c ~username:"lifter" ~password:"heavyduty1" in 722 let trainee = the_trainee repo ~username:"lifter" in 723 let _ = start_day_one c in 724 record_all_day_one_slots c; 725 let token = Option.get (csrf_token (body (get c "/workout"))) in 726 let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in 727 Alcotest.(check int) 728 "the recorded month is available" 200 729 (status (get c "/logbook?year=1970&month=1")); 730 Alcotest.(check int) 731 "an empty month is available" 200 732 (status (get c "/logbook?year=1970&month=3")); 733 Alcotest.(check int) 734 "the workout remains in history" 1 735 (List.length 736 (Lwt_main.run (Test_repo.history repo trainee.Trainee.id))) ); 737 ] ); 738 ] 739