Public: anyone can read this; the crew can change it.

The milestones page's HTML is pinned on a 20-milestone, 1,000-issue seed #3464

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/3428-the-milestones-page-s-html-is-pinned-on → main 3 commits fork

  • Merged. scout/3428-the-milestones-page-s-html-is-pinned-on was merged into main as 4d41d3f.

Review

Sign in to review this change.
3 files changed +1158 −0
test/fixtures/milestones-golden.html added +995
@@ -0,0 +1,995 @@
1 <div class="page-head mt">
2 <h1>Milestones</h1>
3 <p class="muted">
4 How far each thing asked for is. Open one to see who is building it
5 and what it waits for.
6 </p>
7 </div>
8 <div id="ms-limbs">
9 <div
10 class="card"
11
12
13
14 >
15
16 <ul class="list setting-list"><li>
17 <div class="row"
18
19 >
20 <span class="row-main">
21 <span
22 class="row-title"
23
24 ><a href="/alice/forge/milestones/1"
25 >Milestone 0000</a
26 ></span
27 >
28 <span class="row-meta"><progress
29 max="49"
30 value="45"
31 aria-label="Milestone 0000: 45 of 49 done"
32 ></progress>
33 4 open · 15 merged in 7 days · moved <time datetime="T" title="T"
34 >AGO</time
35 ></span>
36 </span>
37 <span class="row-trail"><span class="pill"
38 >45 of 49 done</span
39 ><span class="bar"
40 ><a
41 href="/alice/forge/milestones/1/limb?action=lower&amp;under=top"
42
43 class="btn"
44 data-variant="ghost"
45 data-size="sm"
46
47
48
49
50
51 >Lower</a
52 ></span
53 ></span></div>
54 </li><li>
55 <div class="row"
56
57 >
58 <span class="row-main">
59 <span
60 class="row-title"
61
62 ><a href="/alice/forge/milestones/66"
63 >Milestone 0001</a
64 ></span
65 >
66 <span class="row-meta"><progress
67 max="49"
68 value="45"
69 aria-label="Milestone 0001: 45 of 49 done"
70 ></progress>
71 4 open · 15 merged in 7 days · moved <time datetime="T" title="T"
72 >AGO</time
73 ></span>
74 </span>
75 <span class="row-trail"><span class="pill"
76 >45 of 49 done</span
77 ><span class="bar"
78 ><a
79 href="/alice/forge/milestones/66/limb?action=raise&amp;under=top"
80
81 class="btn"
82 data-variant="ghost"
83 data-size="sm"
84
85
86
87
88
89 >Raise</a
90 ><a
91 href="/alice/forge/milestones/66/limb?action=lower&amp;under=top"
92
93 class="btn"
94 data-variant="ghost"
95 data-size="sm"
96
97
98
99
100
101 >Lower</a
102 ></span
103 ></span></div>
104 </li><li>
105 <div class="row"
106
107 >
108 <span class="row-main">
109 <span
110 class="row-title"
111
112 ><a href="/alice/forge/milestones/131"
113 >Milestone 0002</a
114 ></span
115 >
116 <span class="row-meta"><progress
117 max="49"
118 value="45"
119 aria-label="Milestone 0002: 45 of 49 done"
120 ></progress>
121 4 open · 15 merged in 7 days · moved <time datetime="T" title="T"
122 >AGO</time
123 ></span>
124 </span>
125 <span class="row-trail"><span class="pill"
126 >45 of 49 done</span
127 ><span class="bar"
128 ><a
129 href="/alice/forge/milestones/131/limb?action=raise&amp;under=top"
130
131 class="btn"
132 data-variant="ghost"
133 data-size="sm"
134
135
136
137
138
139 >Raise</a
140 ><a
141 href="/alice/forge/milestones/131/limb?action=lower&amp;under=top"
142
143 class="btn"
144 data-variant="ghost"
145 data-size="sm"
146
147
148
149
150
151 >Lower</a
152 ></span
153 ></span></div>
154 </li><li>
155 <div class="row"
156
157 >
158 <span class="row-main">
159 <span
160 class="row-title"
161
162 ><a href="/alice/forge/milestones/196"
163 >Milestone 0003</a
164 ></span
165 >
166 <span class="row-meta"><progress
167 max="49"
168 value="45"
169 aria-label="Milestone 0003: 45 of 49 done"
170 ></progress>
171 4 open · 15 merged in 7 days · moved <time datetime="T" title="T"
172 >AGO</time
173 ></span>
174 </span>
175 <span class="row-trail"><span class="pill"
176 >45 of 49 done</span
177 ><span class="bar"
178 ><a
179 href="/alice/forge/milestones/196/limb?action=raise&amp;under=top"
180
181 class="btn"
182 data-variant="ghost"
183 data-size="sm"
184
185
186
187
188
189 >Raise</a
190 ><a
191 href="/alice/forge/milestones/196/limb?action=lower&amp;under=top"
192
193 class="btn"
194 data-variant="ghost"
195 data-size="sm"
196
197
198
199
200
201 >Lower</a
202 ></span
203 ></span></div>
204 </li><li>
205 <div class="row"
206
207 >
208 <span class="row-main">
209 <span
210 class="row-title"
211
212 ><a href="/alice/forge/milestones/261"
213 >Milestone 0004</a
214 ></span
215 >
216 <span class="row-meta"><progress
217 max="49"
218 value="45"
219 aria-label="Milestone 0004: 45 of 49 done"
220 ></progress>
221 4 open · 15 merged in 7 days · moved <time datetime="T" title="T"
222 >AGO</time
223 ></span>
224 </span>
225 <span class="row-trail"><span class="pill"
226 >45 of 49 done</span
227 ><span class="bar"
228 ><a
229 href="/alice/forge/milestones/261/limb?action=raise&amp;under=top"
230
231 class="btn"
232 data-variant="ghost"
233 data-size="sm"
234
235
236
237
238
239 >Raise</a
240 ><a
241 href="/alice/forge/milestones/261/limb?action=lower&amp;under=top"
242
243 class="btn"
244 data-variant="ghost"
245 data-size="sm"
246
247
248
249
250
251 >Lower</a
252 ></span
253 ></span></div>
254 </li><li>
255 <div class="row"
256
257 >
258 <span class="row-main">
259 <span
260 class="row-title"
261
262 ><a href="/alice/forge/milestones/326"
263 >Milestone 0005</a
264 ></span
265 >
266 <span class="row-meta"><progress
267 max="49"
268 value="45"
269 aria-label="Milestone 0005: 45 of 49 done"
270 ></progress>
271 4 open · 15 merged in 7 days · moved <time datetime="T" title="T"
272 >AGO</time
273 ></span>
274 </span>
275 <span class="row-trail"><span class="pill"
276 >45 of 49 done</span
277 ><span class="bar"
278 ><a
279 href="/alice/forge/milestones/326/limb?action=raise&amp;under=top"
280
281 class="btn"
282 data-variant="ghost"
283 data-size="sm"
284
285
286
287
288
289 >Raise</a
290 ><a
291 href="/alice/forge/milestones/326/limb?action=lower&amp;under=top"
292
293 class="btn"
294 data-variant="ghost"
295 data-size="sm"
296
297
298
299
300
301 >Lower</a
302 ></span
303 ></span></div>
304 </li><li>
305 <div class="row"
306
307 >
308 <span class="row-main">
309 <span
310 class="row-title"
311
312 ><a href="/alice/forge/milestones/391"
313 >Milestone 0006</a
314 ></span
315 >
316 <span class="row-meta"><progress
317 max="49"
318 value="45"
319 aria-label="Milestone 0006: 45 of 49 done"
320 ></progress>
321 4 open · 15 merged in 7 days · moved <time datetime="T" title="T"
322 >AGO</time
323 ></span>
324 </span>
325 <span class="row-trail"><span class="pill"
326 >45 of 49 done</span
327 ><span class="bar"
328 ><a
329 href="/alice/forge/milestones/391/limb?action=raise&amp;under=top"
330
331 class="btn"
332 data-variant="ghost"
333 data-size="sm"
334
335
336
337
338
339 >Raise</a
340 ><a
341 href="/alice/forge/milestones/391/limb?action=lower&amp;under=top"
342
343 class="btn"
344 data-variant="ghost"
345 data-size="sm"
346
347
348
349
350
351 >Lower</a
352 ></span
353 ></span></div>
354 </li><li>
355 <div class="row"
356
357 >
358 <span class="row-main">
359 <span
360 class="row-title"
361
362 ><a href="/alice/forge/milestones/456"
363 >Milestone 0007</a
364 ></span
365 >
366 <span class="row-meta"><progress
367 max="49"
368 value="45"
369 aria-label="Milestone 0007: 45 of 49 done"
370 ></progress>
371 4 open · 15 merged in 7 days · moved <time datetime="T" title="T"
372 >AGO</time
373 ></span>
374 </span>
375 <span class="row-trail"><span class="pill"
376 >45 of 49 done</span
377 ><span class="bar"
378 ><a
379 href="/alice/forge/milestones/456/limb?action=raise&amp;under=top"
380
381 class="btn"
382 data-variant="ghost"
383 data-size="sm"
384
385
386
387
388
389 >Raise</a
390 ><a
391 href="/alice/forge/milestones/456/limb?action=lower&amp;under=top"
392
393 class="btn"
394 data-variant="ghost"
395 data-size="sm"
396
397
398
399
400
401 >Lower</a
402 ></span
403 ></span></div>
404 </li><li>
405 <div class="row"
406
407 >
408 <span class="row-main">
409 <span
410 class="row-title"
411
412 ><a href="/alice/forge/milestones/521"
413 >Milestone 0008</a
414 ></span
415 >
416 <span class="row-meta"><progress
417 max="49"
418 value="45"
419 aria-label="Milestone 0008: 45 of 49 done"
420 ></progress>
421 4 open · 15 merged in 7 days · moved <time datetime="T" title="T"
422 >AGO</time
423 ></span>
424 </span>
425 <span class="row-trail"><span class="pill"
426 >45 of 49 done</span
427 ><span class="bar"
428 ><a
429 href="/alice/forge/milestones/521/limb?action=raise&amp;under=top"
430
431 class="btn"
432 data-variant="ghost"
433 data-size="sm"
434
435
436
437
438
439 >Raise</a
440 ><a
441 href="/alice/forge/milestones/521/limb?action=lower&amp;under=top"
442
443 class="btn"
444 data-variant="ghost"
445 data-size="sm"
446
447
448
449
450
451 >Lower</a
452 ></span
453 ></span></div>
454 </li><li>
455 <div class="row"
456
457 >
458 <span class="row-main">
459 <span
460 class="row-title"
461
462 ><a href="/alice/forge/milestones/586"
463 >Milestone 0009</a
464 ></span
465 >
466 <span class="row-meta"><progress
467 max="49"
468 value="45"
469 aria-label="Milestone 0009: 45 of 49 done"
470 ></progress>
471 4 open · 15 merged in 7 days · moved <time datetime="T" title="T"
472 >AGO</time
473 ></span>
474 </span>
475 <span class="row-trail"><span class="pill"
476 >45 of 49 done</span
477 ><span class="bar"
478 ><a
479 href="/alice/forge/milestones/586/limb?action=raise&amp;under=top"
480
481 class="btn"
482 data-variant="ghost"
483 data-size="sm"
484
485
486
487
488
489 >Raise</a
490 ><a
491 href="/alice/forge/milestones/586/limb?action=lower&amp;under=top"
492
493 class="btn"
494 data-variant="ghost"
495 data-size="sm"
496
497
498
499
500
501 >Lower</a
502 ></span
503 ></span></div>
504 </li><li>
505 <div class="row"
506
507 >
508 <span class="row-main">
509 <span
510 class="row-title"
511
512 ><a href="/alice/forge/milestones/651"
513 >Milestone 0010</a
514 ></span
515 >
516 <span class="row-meta"><progress
517 max="49"
518 value="45"
519 aria-label="Milestone 0010: 45 of 49 done"
520 ></progress>
521 4 open · 15 merged in 7 days · moved <time datetime="T" title="T"
522 >AGO</time
523 ></span>
524 </span>
525 <span class="row-trail"><span class="pill"
526 >45 of 49 done</span
527 ><span class="bar"
528 ><a
529 href="/alice/forge/milestones/651/limb?action=raise&amp;under=top"
530
531 class="btn"
532 data-variant="ghost"
533 data-size="sm"
534
535
536
537
538
539 >Raise</a
540 ><a
541 href="/alice/forge/milestones/651/limb?action=lower&amp;under=top"
542
543 class="btn"
544 data-variant="ghost"
545 data-size="sm"
546
547
548
549
550
551 >Lower</a
552 ></span
553 ></span></div>
554 </li><li>
555 <div class="row"
556
557 >
558 <span class="row-main">
559 <span
560 class="row-title"
561
562 ><a href="/alice/forge/milestones/716"
563 >Milestone 0011</a
564 ></span
565 >
566 <span class="row-meta"><progress
567 max="49"
568 value="45"
569 aria-label="Milestone 0011: 45 of 49 done"
570 ></progress>
571 4 open · 15 merged in 7 days · moved <time datetime="T" title="T"
572 >AGO</time
573 ></span>
574 </span>
575 <span class="row-trail"><span class="pill"
576 >45 of 49 done</span
577 ><span class="bar"
578 ><a
579 href="/alice/forge/milestones/716/limb?action=raise&amp;under=top"
580
581 class="btn"
582 data-variant="ghost"
583 data-size="sm"
584
585
586
587
588
589 >Raise</a
590 ><a
591 href="/alice/forge/milestones/716/limb?action=lower&amp;under=top"
592
593 class="btn"
594 data-variant="ghost"
595 data-size="sm"
596
597
598
599
600
601 >Lower</a
602 ></span
603 ></span></div>
604 </li><li>
605 <div class="row"
606
607 >
608 <span class="row-main">
609 <span
610 class="row-title"
611
612 ><a href="/alice/forge/milestones/781"
613 >Milestone 0012</a
614 ></span
615 >
616 <span class="row-meta"><progress
617 max="49"
618 value="45"
619 aria-label="Milestone 0012: 45 of 49 done"
620 ></progress>
621 4 open · 15 merged in 7 days · moved <time datetime="T" title="T"
622 >AGO</time
623 ></span>
624 </span>
625 <span class="row-trail"><span class="pill"
626 >45 of 49 done</span
627 ><span class="bar"
628 ><a
629 href="/alice/forge/milestones/781/limb?action=raise&amp;under=top"
630
631 class="btn"
632 data-variant="ghost"
633 data-size="sm"
634
635
636
637
638
639 >Raise</a
640 ><a
641 href="/alice/forge/milestones/781/limb?action=lower&amp;under=top"
642
643 class="btn"
644 data-variant="ghost"
645 data-size="sm"
646
647
648
649
650
651 >Lower</a
652 ></span
653 ></span></div>
654 </li><li>
655 <div class="row"
656
657 >
658 <span class="row-main">
659 <span
660 class="row-title"
661
662 ><a href="/alice/forge/milestones/846"
663 >Milestone 0013</a
664 ></span
665 >
666 <span class="row-meta"><progress
667 max="49"
668 value="45"
669 aria-label="Milestone 0013: 45 of 49 done"
670 ></progress>
671 4 open · 15 merged in 7 days · moved <time datetime="T" title="T"
672 >AGO</time
673 ></span>
674 </span>
675 <span class="row-trail"><span class="pill"
676 >45 of 49 done</span
677 ><span class="bar"
678 ><a
679 href="/alice/forge/milestones/846/limb?action=raise&amp;under=top"
680
681 class="btn"
682 data-variant="ghost"
683 data-size="sm"
684
685
686
687
688
689 >Raise</a
690 ><a
691 href="/alice/forge/milestones/846/limb?action=lower&amp;under=top"
692
693 class="btn"
694 data-variant="ghost"
695 data-size="sm"
696
697
698
699
700
701 >Lower</a
702 ></span
703 ></span></div>
704 </li><li>
705 <div class="row"
706
707 >
708 <span class="row-main">
709 <span
710 class="row-title"
711
712 ><a href="/alice/forge/milestones/911"
713 >Milestone 0014</a
714 ></span
715 >
716 <span class="row-meta"><progress
717 max="49"
718 value="45"
719 aria-label="Milestone 0014: 45 of 49 done"
720 ></progress>
721 4 open · 15 merged in 7 days · moved <time datetime="T" title="T"
722 >AGO</time
723 ></span>
724 </span>
725 <span class="row-trail"><span class="pill"
726 >45 of 49 done</span
727 ><span class="bar"
728 ><a
729 href="/alice/forge/milestones/911/limb?action=raise&amp;under=top"
730
731 class="btn"
732 data-variant="ghost"
733 data-size="sm"
734
735
736
737
738
739 >Raise</a
740 ><a
741 href="/alice/forge/milestones/911/limb?action=lower&amp;under=top"
742
743 class="btn"
744 data-variant="ghost"
745 data-size="sm"
746
747
748
749
750
751 >Lower</a
752 ></span
753 ></span></div>
754 </li><li>
755 <div class="row"
756
757 >
758 <span class="row-main">
759 <span
760 class="row-title"
761
762 ><a href="/alice/forge/milestones/976"
763 >Milestone 0015</a
764 ></span
765 >
766 <span class="row-meta"><progress
767 max="49"
768 value="45"
769 aria-label="Milestone 0015: 45 of 49 done"
770 ></progress>
771 4 open · 15 merged in 7 days · moved <time datetime="T" title="T"
772 >AGO</time
773 ></span>
774 </span>
775 <span class="row-trail"><span class="pill"
776 >45 of 49 done</span
777 ><span class="bar"
778 ><a
779 href="/alice/forge/milestones/976/limb?action=raise&amp;under=top"
780
781 class="btn"
782 data-variant="ghost"
783 data-size="sm"
784
785
786
787
788
789 >Raise</a
790 ><a
791 href="/alice/forge/milestones/976/limb?action=lower&amp;under=top"
792
793 class="btn"
794 data-variant="ghost"
795 data-size="sm"
796
797
798
799
800
801 >Lower</a
802 ></span
803 ></span></div>
804 </li><li>
805 <div class="row"
806
807 >
808 <span class="row-main">
809 <span
810 class="row-title"
811
812 ><a href="/alice/forge/milestones/1041"
813 >Milestone 0016</a
814 ></span
815 >
816 <span class="row-meta"><progress
817 max="49"
818 value="45"
819 aria-label="Milestone 0016: 45 of 49 done"
820 ></progress>
821 4 open · 15 merged in 7 days · moved <time datetime="T" title="T"
822 >AGO</time
823 ></span>
824 </span>
825 <span class="row-trail"><span class="pill"
826 >45 of 49 done</span
827 ><span class="bar"
828 ><a
829 href="/alice/forge/milestones/1041/limb?action=raise&amp;under=top"
830
831 class="btn"
832 data-variant="ghost"
833 data-size="sm"
834
835
836
837
838
839 >Raise</a
840 ><a
841 href="/alice/forge/milestones/1041/limb?action=lower&amp;under=top"
842
843 class="btn"
844 data-variant="ghost"
845 data-size="sm"
846
847
848
849
850
851 >Lower</a
852 ></span
853 ></span></div>
854 </li><li>
855 <div class="row"
856
857 >
858 <span class="row-main">
859 <span
860 class="row-title"
861
862 ><a href="/alice/forge/milestones/1106"
863 >Milestone 0017</a
864 ></span
865 >
866 <span class="row-meta"><progress
867 max="49"
868 value="45"
869 aria-label="Milestone 0017: 45 of 49 done"
870 ></progress>
871 4 open · 15 merged in 7 days · moved <time datetime="T" title="T"
872 >AGO</time
873 ></span>
874 </span>
875 <span class="row-trail"><span class="pill"
876 >45 of 49 done</span
877 ><span class="bar"
878 ><a
879 href="/alice/forge/milestones/1106/limb?action=raise&amp;under=top"
880
881 class="btn"
882 data-variant="ghost"
883 data-size="sm"
884
885
886
887
888
889 >Raise</a
890 ><a
891 href="/alice/forge/milestones/1106/limb?action=lower&amp;under=top"
892
893 class="btn"
894 data-variant="ghost"
895 data-size="sm"
896
897
898
899
900
901 >Lower</a
902 ></span
903 ></span></div>
904 </li><li>
905 <div class="row"
906
907 >
908 <span class="row-main">
909 <span
910 class="row-title"
911
912 ><a href="/alice/forge/milestones/1171"
913 >Milestone 0018</a
914 ></span
915 >
916 <span class="row-meta"><progress
917 max="49"
918 value="45"
919 aria-label="Milestone 0018: 45 of 49 done"
920 ></progress>
921 4 open · 15 merged in 7 days · moved <time datetime="T" title="T"
922 >AGO</time
923 ></span>
924 </span>
925 <span class="row-trail"><span class="pill"
926 >45 of 49 done</span
927 ><span class="bar"
928 ><a
929 href="/alice/forge/milestones/1171/limb?action=raise&amp;under=top"
930
931 class="btn"
932 data-variant="ghost"
933 data-size="sm"
934
935
936
937
938
939 >Raise</a
940 ><a
941 href="/alice/forge/milestones/1171/limb?action=lower&amp;under=top"
942
943 class="btn"
944 data-variant="ghost"
945 data-size="sm"
946
947
948
949
950
951 >Lower</a
952 ></span
953 ></span></div>
954 </li><li>
955 <div class="row"
956
957 >
958 <span class="row-main">
959 <span
960 class="row-title"
961
962 ><a href="/alice/forge/milestones/1236"
963 >Milestone 0019</a
964 ></span
965 >
966 <span class="row-meta"><progress
967 max="49"
968 value="45"
969 aria-label="Milestone 0019: 45 of 49 done"
970 ></progress>
971 4 open · 15 merged in 7 days · moved <time datetime="T" title="T"
972 >AGO</time
973 ></span>
974 </span>
975 <span class="row-trail"><span class="pill"
976 >45 of 49 done</span
977 ><span class="bar"
978 ><a
979 href="/alice/forge/milestones/1236/limb?action=raise&amp;under=top"
980
981 class="btn"
982 data-variant="ghost"
983 data-size="sm"
984
985
986
987
988
989 >Raise</a
990 ></span
991 ></span></div>
992 </li></ul>
993
994 </div>
995 </div>↵
test/fixtures/milestones-seed.ts added +68
@@ -0,0 +1,68 @@
1 /**
2 * One repository for the milestones page to be pinned on: 20 milestones, each
3 * an open umbrella waiting on 49 children, over 1,000 issues, 900 of them
4 * closed, and a week of merged pulls. Ids and numbers are fixed; times are
5 * hours before `now`, and the page reads the real clock, so a caller seeds
6 * with the real `now` and normalises the times it renders.
7 */
8 import type { Database } from "bun:sqlite";
9
10 export const MILESTONES = 20;
11 export const CHILDREN = 49;
12 export const CLOSED_PER_MILESTONE = 45;
13 /** Every third closed child landed by a merged pull this week. */
14 export const MERGE_EVERY = 3;
15
16 const pad = (n: number): string => String(n).padStart(4, "0");
17
18 export function seedMilestones(
19 db: Database,
20 repoId: string,
21 authorId: string,
22 now: Date,
23 ): void {
24 const ago = (hours: number): string =>
25 new Date(now.getTime() - hours * 3_600_000).toISOString();
26 const issue = db.prepare(
27 `INSERT INTO issue (id, repo_id, number, kind, title, body, author_id, state, created_at, updated_at, closed_at)
28 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?9, ?10)`,
29 );
30 const edge = db.prepare(
31 `INSERT INTO issue_dependency (issue_id, blocked_by_id, created_at) VALUES (?1, ?2, ?3)`,
32 );
33 const pull = db.prepare(
34 `INSERT INTO pull (issue_id, base_branch, head_branch, base_oid, head_oid, merged_at)
35 VALUES (?1, 'main', ?2, ?3, ?3, ?4)`,
36 );
37 const oid = "0".repeat(40);
38 let number = 0;
39 let merges = 0;
40 const tx = db.transaction(() => {
41 const children: { id: string; number: number }[] = [];
42 for (let m = 0; m < MILESTONES; m++) {
43 const umbrella = { id: `ms-${pad(m)}`, number: ++number };
44 issue.run(umbrella.id, repoId, umbrella.number, "issue", `Milestone ${pad(m)}`, "", authorId, "open", ago(500), null);
45 for (let c = 0; c < CHILDREN; c++) {
46 const child = { id: `ch-${pad(m)}-${pad(c)}`, number: ++number };
47 const closed = c < CLOSED_PER_MILESTONE;
48 issue.run(
49 child.id, repoId, child.number, "issue", `Step ${pad(m)}.${pad(c)}`, "", authorId,
50 closed ? "closed" : "open", ago(400), closed ? ago(24 + ((m * 7 + c) % 100)) : null,
51 );
52 edge.run(umbrella.id, child.id, ago(400));
53 children.push(child);
54 if (closed && c % MERGE_EVERY === 0) {
55 // Within the 7-day window, spread over the week.
56 const at = ago(1 + ((m * 13 + c * 5) % 160));
57 const p = { id: `pl-${pad(merges)}`, number: 0 };
58 merges++;
59 p.number = ++number;
60 issue.run(p.id, repoId, p.number, "pull", `Land step ${pad(m)}.${pad(c)}`, `Closes #${child.number}`, authorId, "closed", at, at);
61 pull.run(p.id, `feature/step-${pad(m)}-${pad(c)}`, oid, at);
62 }
63 }
64 }
65 db.run(`UPDATE repo SET issue_seq = ?1 WHERE id = ?2`, [number, repoId]);
66 });
67 tx();
68 }
test/milestones-golden.test.ts added +95
@@ -0,0 +1,95 @@
1 /**
2 * The milestones page's HTML, pinned on a 20-milestone, 1,000-issue seed
3 * (#3428), so a speed change that alters the output fails here and not on a
4 * clock. No wall-clock assertion. The page reads the real clock and the seed
5 * is offsets from it, so rendered times are normalised away: the 7-day merge
6 * window is pinned by its count, not at its edges. Regenerate with `UPDATE_GOLDEN=1 bun test
7 * test/milestones-golden.test.ts` and read the diff.
8 */
9 import { afterAll, beforeAll, expect, test } from "bun:test";
10 import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
11 import { tmpdir } from "node:os";
12 import { join } from "node:path";
13 import { createRepo } from "../src/actions/repos.ts";
14 import { createApp, type App } from "../src/app.ts";
15 import { createUser } from "../src/core/users.ts";
16 import { serve, type Servers } from "../src/http/server.ts";
17 import { seedMilestones } from "./fixtures/milestones-seed.ts";
18 import { quietLogs } from "./helpers/globals.ts";
19 import { proveAll } from "./helpers/verified.ts";
20
21 quietLogs();
22
23 const GOLDEN = join(import.meta.dir, "fixtures", "milestones-golden.html");
24
25 let root = "";
26 let app: App;
27 let servers: Servers;
28
29 beforeAll(async () => {
30 root = mkdtempSync(join(tmpdir(), "forge-milestones-golden-"));
31 app = createApp({ dataDir: root, port: 0, forgeRepo: "alice/forge" });
32 const u = await createUser(app.db, {
33 origin: "seed",
34 trusted: true,
35 username: "alice",
36 email: "[email protected]",
37 password: "a-good-passphrase",
38 adminIfFirst: true,
39 });
40 if (!u.ok) throw new Error(u.error);
41 proveAll(app);
42 const made = await createRepo(app, { id: u.value.id, username: "alice" } as never, { name: "forge" });
43 if (!made.ok) throw new Error(made.error);
44 seedMilestones(app.db, made.value.id, u.value.id, new Date());
45 servers = await serve(app);
46 });
47 afterAll(() => {
48 servers.stop();
49 app.close();
50 rmSync(root, { recursive: true, force: true });
51 });
52
53 /**
54 * The page with what changes between runs replaced (the nonce, the port, every
55 * rendered time), cut to the milestones' own output: from the page head to the
56 * end of `#ms-limbs`. The shell around it (menus, assets, footer) is not this
57 * page's to pin.
58 */
59 function normalise(htmlText: string): string {
60 const start = htmlText.indexOf('<div class="page-head');
61 const end = htmlText.search(/\n\s*<div\s+id="live"/);
62 if (start < 0 || end < start) throw new Error("the milestones content was not found in the page");
63 return htmlText
64 .slice(start, end)
65 .replaceAll(servers.url, "http://ORIGIN")
66 .replace(/nonce="[^"]*"/g, 'nonce="NONCE"')
67 .replace(/(<time datetime=")[^"]*(" title=")[^"]*("\s*>)[^<]*(<\/time\s*>)/g, "$1T$2T$3AGO$4")
68 .replace(/\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?Z/g, "TIME");
69 }
70
71 /** Compare to the pinned file; a missing file is a failure unless UPDATE_GOLDEN=1. */
72 function pinned(file: string, got: string): void {
73 if (process.env.UPDATE_GOLDEN === "1") writeFileSync(file, got);
74 if (!existsSync(file)) throw new Error(`${file} is missing; run with UPDATE_GOLDEN=1 to write it`);
75 expect(got).toBe(readFileSync(file, "utf8"));
76 }
77
78 test("GET /:owner/:name/milestones renders the pinned HTML", async () => {
79 const signin = await fetch(`${servers.url}/signin`, {
80 method: "POST",
81 headers: { origin: servers.url },
82 body: new URLSearchParams({ username: "alice", password: "a-good-passphrase" }),
83 redirect: "manual",
84 });
85 const cookie = (signin.headers.get("set-cookie") ?? "").split(";")[0]!;
86 const res = await fetch(`${servers.url}/alice/forge/milestones`, { headers: { cookie } });
87 expect(res.status).toBe(200);
88 pinned(GOLDEN, normalise(await res.text()));
89 });
90
91 test("a guest is told nothing of a private repository's milestones", async () => {
92 const res = await fetch(`${servers.url}/alice/forge/milestones`);
93 expect(res.status).toBe(404);
94 expect(await res.text()).not.toContain("Milestone 0000");
95 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs