Compare commits
10
Commits
1d5358d215
...
ef51fdcfff
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef51fdcfff | ||
|
|
3859c836e5 | ||
|
|
6b201e7e63 | ||
|
|
5d20a21499 | ||
|
|
a4bca75946 | ||
|
|
a9e746d520 | ||
|
|
790d2f0e1d | ||
|
|
7afe5c1fe8 | ||
|
|
7c06204ac2 | ||
|
|
36bdc0610e |
@@ -31,3 +31,59 @@
|
||||
{"timestamp":"2026-05-07T10:56:15.344Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":7,"inputTokens":1120,"outputTokens":1046,"totalTokens":2166,"prefillTokensPerSec":50.27,"generationTokensPerSec":15.14,"combinedTokensPerSec":23.71,"totalDurationMs":91350,"timeToFirstTokenMs":22281,"rawTimestamps":{"ttftMs":22281,"allTtftMs":[22281],"generationDurationMs":69069,"turns":[{"turnId":"turn-0","durationMs":6803},{"turnId":"turn-1","durationMs":9002},{"turnId":"turn-2","durationMs":9423},{"turnId":"turn-3","durationMs":8839},{"turnId":"turn-4","durationMs":21020},{"turnId":"turn-5","durationMs":7403},{"turnId":"turn-6","durationMs":28860,"ttftMs":22281}]}}
|
||||
{"timestamp":"2026-05-07T10:57:51.083Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":11,"inputTokens":847,"outputTokens":1039,"totalTokens":1886,"prefillTokensPerSec":458.83,"generationTokensPerSec":13.33,"combinedTokensPerSec":23.63,"totalDurationMs":79819,"timeToFirstTokenMs":1846,"rawTimestamps":{"ttftMs":1846,"allTtftMs":[1846],"generationDurationMs":77973,"turns":[{"turnId":"turn-0","durationMs":4602},{"turnId":"turn-1","durationMs":4772},{"turnId":"turn-2","durationMs":9277},{"turnId":"turn-3","durationMs":7327},{"turnId":"turn-4","durationMs":6075},{"turnId":"turn-5","durationMs":11215},{"turnId":"turn-6","durationMs":5261},{"turnId":"turn-7","durationMs":7154},{"turnId":"turn-8","durationMs":8922},{"turnId":"turn-9","durationMs":5088},{"turnId":"turn-10","durationMs":10126,"ttftMs":1846}]}}
|
||||
{"timestamp":"2026-05-07T11:44:31.762Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":1,"inputTokens":29,"outputTokens":1453,"totalTokens":1482,"prefillTokensPerSec":0.63,"generationTokensPerSec":25.21,"combinedTokensPerSec":14.34,"totalDurationMs":103354,"timeToFirstTokenMs":45712,"rawTimestamps":{"ttftMs":45712,"allTtftMs":[45712],"generationDurationMs":57642,"turns":[{"turnId":"turn-0","durationMs":103354,"ttftMs":45712}]}}
|
||||
{"timestamp":"2026-05-07T11:49:17.424Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":20,"inputTokens":2908,"outputTokens":2259,"totalTokens":5167,"prefillTokensPerSec":576.64,"generationTokensPerSec":11.38,"combinedTokensPerSec":25.39,"totalDurationMs":203478,"timeToFirstTokenMs":5043,"rawTimestamps":{"ttftMs":5043,"allTtftMs":[5043],"generationDurationMs":198435,"turns":[{"turnId":"turn-0","durationMs":7406},{"turnId":"turn-1","durationMs":8731},{"turnId":"turn-2","durationMs":18904},{"turnId":"turn-3","durationMs":10628},{"turnId":"turn-4","durationMs":7590},{"turnId":"turn-5","durationMs":8434},{"turnId":"turn-6","durationMs":6380},{"turnId":"turn-7","durationMs":11269},{"turnId":"turn-8","durationMs":31805},{"turnId":"turn-9","durationMs":11914},{"turnId":"turn-10","durationMs":9418},{"turnId":"turn-11","durationMs":8427},{"turnId":"turn-12","durationMs":5672},{"turnId":"turn-13","durationMs":8463},{"turnId":"turn-14","durationMs":6268},{"turnId":"turn-15","durationMs":7554},{"turnId":"turn-16","durationMs":6114},{"turnId":"turn-17","durationMs":7812},{"turnId":"turn-18","durationMs":6089},{"turnId":"turn-19","durationMs":14600,"ttftMs":5043}]}}
|
||||
{"timestamp":"2026-05-07T12:23:00.698Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":52,"inputTokens":26465,"outputTokens":20704,"totalTokens":47169,"prefillTokensPerSec":9398.08,"generationTokensPerSec":11.03,"combinedTokensPerSec":25.1,"totalDurationMs":1879573,"timeToFirstTokenMs":2816,"rawTimestamps":{"ttftMs":2816,"allTtftMs":[2816,10170,65028,235220,117161,353893,91713,2451,2498,2927,1877,61437,41723,17718,6349,4740,1],"generationDurationMs":1876757,"turns":[{"turnId":"turn-0","durationMs":30454},{"turnId":"turn-1","durationMs":5727},{"turnId":"turn-2","durationMs":10687,"ttftMs":2816},{"turnId":"turn-3","durationMs":17901},{"turnId":"turn-4","durationMs":14927,"ttftMs":10170},{"turnId":"turn-5","durationMs":4974},{"turnId":"turn-6","durationMs":70205,"ttftMs":65028},{"turnId":"turn-7","durationMs":10421},{"turnId":"turn-8","durationMs":250712,"ttftMs":235220},{"turnId":"turn-9","durationMs":121868,"ttftMs":117161},{"turnId":"turn-10","durationMs":26319},{"turnId":"turn-11","durationMs":10498},{"turnId":"turn-12","durationMs":8040},{"turnId":"turn-13","durationMs":357629,"ttftMs":353893},{"turnId":"turn-14","durationMs":119802,"ttftMs":91713},{"turnId":"turn-15","durationMs":17411,"ttftMs":2451},{"turnId":"turn-16","durationMs":20892,"ttftMs":2498},{"turnId":"turn-17","durationMs":96951,"ttftMs":2927},{"turnId":"turn-18","durationMs":36436,"ttftMs":1877},{"turnId":"turn-19","durationMs":13108},{"turnId":"turn-20","durationMs":11605},{"turnId":"turn-21","durationMs":9548},{"turnId":"turn-22","durationMs":7530},{"turnId":"turn-23","durationMs":17564},{"turnId":"turn-24","durationMs":1129},{"turnId":"turn-25","durationMs":7810},{"turnId":"turn-26","durationMs":7152},{"turnId":"turn-27","durationMs":66842,"ttftMs":61437},{"turnId":"turn-28","durationMs":127759,"ttftMs":41723},{"turnId":"turn-29","durationMs":7755},{"turnId":"turn-30","durationMs":16499},{"turnId":"turn-31","durationMs":14999},{"turnId":"turn-32","durationMs":12641},{"turnId":"turn-33","durationMs":28267},{"turnId":"turn-34","durationMs":14505},{"turnId":"turn-35","durationMs":6545},{"turnId":"turn-36","durationMs":45348},{"turnId":"turn-37","durationMs":11660},{"turnId":"turn-38","durationMs":15989},{"turnId":"turn-39","durationMs":41281},{"turnId":"turn-40","durationMs":12805},{"turnId":"turn-41","durationMs":32023,"ttftMs":17718},{"turnId":"turn-42","durationMs":6866},{"turnId":"turn-43","durationMs":24151},{"turnId":"turn-44","durationMs":36240},{"turnId":"turn-45","durationMs":11290},{"turnId":"turn-46","durationMs":9438},{"turnId":"turn-47","durationMs":17588,"ttftMs":6349},{"turnId":"turn-48","durationMs":11779,"ttftMs":4740},{"turnId":"turn-49","durationMs":1},{"turnId":"turn-50","durationMs":0},{"turnId":"turn-51","durationMs":2,"ttftMs":1}]}}
|
||||
{"timestamp":"2026-05-07T12:24:17.200Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":3,"inputTokens":237,"outputTokens":285,"totalTokens":522,"prefillTokensPerSec":129.08,"generationTokensPerSec":10.96,"combinedTokensPerSec":18.76,"totalDurationMs":27832,"timeToFirstTokenMs":1836,"rawTimestamps":{"ttftMs":1836,"allTtftMs":[1836],"generationDurationMs":25996,"turns":[{"turnId":"turn-0","durationMs":7168},{"turnId":"turn-1","durationMs":16917},{"turnId":"turn-2","durationMs":3747,"ttftMs":1836}]}}
|
||||
{"timestamp":"2026-05-07T21:26:10.637Z","provider":"mtplx","model":"mtplx-qwen36-27b-optimized-speed","turnCount":1,"inputTokens":4235,"outputTokens":74,"totalTokens":4309,"prefillTokensPerSec":197.46,"generationTokensPerSec":29.67,"combinedTokensPerSec":179.98,"totalDurationMs":23941,"timeToFirstTokenMs":21447,"rawTimestamps":{"ttftMs":21447,"allTtftMs":[21447],"generationDurationMs":2494,"turns":[{"turnId":"turn-0","durationMs":23941,"ttftMs":21447}]}}
|
||||
{"timestamp":"2026-05-09T19:38:05.353Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":1,"inputTokens":0,"outputTokens":0,"totalTokens":0,"prefillTokensPerSec":0,"generationTokensPerSec":0,"combinedTokensPerSec":0,"totalDurationMs":3107,"rawTimestamps":{"allTtftMs":[],"turns":[{"turnId":"turn-0","durationMs":3107}]}}
|
||||
{"timestamp":"2026-05-09T19:48:18.966Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":4,"inputTokens":1670,"outputTokens":761,"totalTokens":2431,"prefillTokensPerSec":2275.2,"generationTokensPerSec":50.15,"combinedTokensPerSec":152.83,"totalDurationMs":15907,"timeToFirstTokenMs":734,"rawTimestamps":{"ttftMs":734,"allTtftMs":[734],"generationDurationMs":15173,"turns":[{"turnId":"turn-0","durationMs":3512},{"turnId":"turn-1","durationMs":3412},{"turnId":"turn-2","durationMs":3367},{"turnId":"turn-3","durationMs":5616,"ttftMs":734}]}}
|
||||
{"timestamp":"2026-05-13T20:46:56.858Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":13,"inputTokens":7828,"outputTokens":2680,"totalTokens":10508,"prefillTokensPerSec":1055.7,"generationTokensPerSec":47.56,"combinedTokensPerSec":164.81,"totalDurationMs":63760,"timeToFirstTokenMs":7415,"rawTimestamps":{"ttftMs":7415,"allTtftMs":[7415,1600,2500,1244,955,1149,2365,1985],"generationDurationMs":56345,"turns":[{"turnId":"turn-0","durationMs":11164,"ttftMs":7415},{"turnId":"turn-1","durationMs":6978,"ttftMs":1600},{"turnId":"turn-2","durationMs":2824},{"turnId":"turn-3","durationMs":3851,"ttftMs":2500},{"turnId":"turn-4","durationMs":7489},{"turnId":"turn-5","durationMs":1959},{"turnId":"turn-6","durationMs":4377,"ttftMs":1244},{"turnId":"turn-7","durationMs":1598},{"turnId":"turn-8","durationMs":3951,"ttftMs":955},{"turnId":"turn-9","durationMs":1577},{"turnId":"turn-10","durationMs":7759,"ttftMs":1149},{"turnId":"turn-11","durationMs":3915,"ttftMs":2365},{"turnId":"turn-12","durationMs":6318,"ttftMs":1985}]}}
|
||||
{"timestamp":"2026-05-13T20:47:50.561Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":3,"inputTokens":647,"outputTokens":831,"totalTokens":1478,"prefillTokensPerSec":172.21,"generationTokensPerSec":64.9,"combinedTokensPerSec":89.24,"totalDurationMs":16562,"timeToFirstTokenMs":3757,"rawTimestamps":{"ttftMs":3757,"allTtftMs":[3757],"generationDurationMs":12805,"turns":[{"turnId":"turn-0","durationMs":9204},{"turnId":"turn-1","durationMs":2557},{"turnId":"turn-2","durationMs":4801,"ttftMs":3757}]}}
|
||||
{"timestamp":"2026-05-13T20:48:25.529Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":4,"inputTokens":680,"outputTokens":332,"totalTokens":1012,"prefillTokensPerSec":888.89,"generationTokensPerSec":43.33,"combinedTokensPerSec":120.09,"totalDurationMs":8427,"timeToFirstTokenMs":765,"rawTimestamps":{"ttftMs":765,"allTtftMs":[765],"generationDurationMs":7662,"turns":[{"turnId":"turn-0","durationMs":2549},{"turnId":"turn-1","durationMs":1491},{"turnId":"turn-2","durationMs":2700},{"turnId":"turn-3","durationMs":1687,"ttftMs":765}]}}
|
||||
{"timestamp":"2026-05-13T20:48:35.789Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":3,"inputTokens":241,"outputTokens":251,"totalTokens":492,"prefillTokensPerSec":632.55,"generationTokensPerSec":48.33,"combinedTokensPerSec":88.27,"totalDurationMs":5574,"timeToFirstTokenMs":381,"rawTimestamps":{"ttftMs":381,"allTtftMs":[381],"generationDurationMs":5193,"turns":[{"turnId":"turn-0","durationMs":2389},{"turnId":"turn-1","durationMs":2251},{"turnId":"turn-2","durationMs":934,"ttftMs":381}]}}
|
||||
{"timestamp":"2026-05-14T13:11:31.541Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":3,"inputTokens":5399,"outputTokens":887,"totalTokens":6286,"prefillTokensPerSec":4499.17,"generationTokensPerSec":40.34,"combinedTokensPerSec":271.07,"totalDurationMs":23190,"timeToFirstTokenMs":1200,"rawTimestamps":{"ttftMs":1200,"allTtftMs":[1200,1279],"generationDurationMs":21990,"turns":[{"turnId":"turn-0","durationMs":6605},{"turnId":"turn-1","durationMs":7004,"ttftMs":1200},{"turnId":"turn-2","durationMs":9581,"ttftMs":1279}]}}
|
||||
{"timestamp":"2026-05-14T14:34:22.286Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":32,"inputTokens":28376,"outputTokens":5699,"totalTokens":34075,"prefillTokensPerSec":5253.84,"generationTokensPerSec":38.75,"combinedTokensPerSec":223.5,"totalDurationMs":152463,"timeToFirstTokenMs":5401,"rawTimestamps":{"ttftMs":5401,"allTtftMs":[5401,3585,370,1212,1288,1704,1129,3603,1962,0,1,0,0],"generationDurationMs":147062,"turns":[{"turnId":"turn-0","durationMs":8626,"ttftMs":5401},{"turnId":"turn-1","durationMs":6065},{"turnId":"turn-2","durationMs":3000},{"turnId":"turn-3","durationMs":19385,"ttftMs":3585},{"turnId":"turn-4","durationMs":1200},{"turnId":"turn-5","durationMs":5050,"ttftMs":370},{"turnId":"turn-6","durationMs":2453,"ttftMs":1212},{"turnId":"turn-7","durationMs":7630,"ttftMs":1288},{"turnId":"turn-8","durationMs":2868},{"turnId":"turn-9","durationMs":6154,"ttftMs":1704},{"turnId":"turn-10","durationMs":8455},{"turnId":"turn-11","durationMs":3484,"ttftMs":1129},{"turnId":"turn-12","durationMs":3632},{"turnId":"turn-13","durationMs":3091},{"turnId":"turn-14","durationMs":28599},{"turnId":"turn-15","durationMs":4515},{"turnId":"turn-16","durationMs":7130},{"turnId":"turn-17","durationMs":4364},{"turnId":"turn-18","durationMs":3361},{"turnId":"turn-19","durationMs":9264,"ttftMs":3603},{"turnId":"turn-20","durationMs":5422},{"turnId":"turn-21","durationMs":3263},{"turnId":"turn-22","durationMs":3531,"ttftMs":1962},{"turnId":"turn-23","durationMs":1909},{"turnId":"turn-24","durationMs":2},{"turnId":"turn-25","durationMs":0},{"turnId":"turn-26","durationMs":1},{"turnId":"turn-27","durationMs":1,"ttftMs":0},{"turnId":"turn-28","durationMs":0},{"turnId":"turn-29","durationMs":7,"ttftMs":1},{"turnId":"turn-30","durationMs":1,"ttftMs":0},{"turnId":"turn-31","durationMs":0,"ttftMs":0}]}}
|
||||
{"timestamp":"2026-05-14T14:35:37.325Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":3,"inputTokens":116,"outputTokens":284,"totalTokens":400,"prefillTokensPerSec":32.95,"generationTokensPerSec":70.95,"combinedTokensPerSec":53.16,"totalDurationMs":7524,"timeToFirstTokenMs":3521,"rawTimestamps":{"ttftMs":3521,"allTtftMs":[3521],"generationDurationMs":4003,"turns":[{"turnId":"turn-0","durationMs":1970},{"turnId":"turn-1","durationMs":5036,"ttftMs":3521},{"turnId":"turn-2","durationMs":518}]}}
|
||||
{"timestamp":"2026-05-15T10:49:34.264Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":34,"inputTokens":11209,"outputTokens":4755,"totalTokens":15964,"prefillTokensPerSec":1523.17,"generationTokensPerSec":62.18,"combinedTokensPerSec":190.42,"totalDurationMs":83835,"timeToFirstTokenMs":7359,"rawTimestamps":{"ttftMs":7359,"allTtftMs":[7359,478,623,648,2511,1286,2844,2715,2402,801,0,0,1,0,0],"generationDurationMs":76476,"turns":[{"turnId":"turn-0","durationMs":9931,"ttftMs":7359},{"turnId":"turn-1","durationMs":1589},{"turnId":"turn-2","durationMs":3336,"ttftMs":478},{"turnId":"turn-3","durationMs":2417,"ttftMs":623},{"turnId":"turn-4","durationMs":1862,"ttftMs":648},{"turnId":"turn-5","durationMs":1945},{"turnId":"turn-6","durationMs":1150},{"turnId":"turn-7","durationMs":4422,"ttftMs":2511},{"turnId":"turn-8","durationMs":1893},{"turnId":"turn-9","durationMs":1987},{"turnId":"turn-10","durationMs":3645},{"turnId":"turn-11","durationMs":2576},{"turnId":"turn-12","durationMs":3972,"ttftMs":1286},{"turnId":"turn-13","durationMs":2337},{"turnId":"turn-14","durationMs":3141},{"turnId":"turn-15","durationMs":3070},{"turnId":"turn-16","durationMs":2305},{"turnId":"turn-17","durationMs":7110,"ttftMs":2844},{"turnId":"turn-18","durationMs":5955},{"turnId":"turn-19","durationMs":3529},{"turnId":"turn-20","durationMs":5359,"ttftMs":2715},{"turnId":"turn-21","durationMs":4366,"ttftMs":2402},{"turnId":"turn-22","durationMs":3139,"ttftMs":801},{"turnId":"turn-23","durationMs":2786},{"turnId":"turn-24","durationMs":2},{"turnId":"turn-25","durationMs":1},{"turnId":"turn-26","durationMs":0,"ttftMs":0},{"turnId":"turn-27","durationMs":1},{"turnId":"turn-28","durationMs":0,"ttftMs":0},{"turnId":"turn-29","durationMs":3},{"turnId":"turn-30","durationMs":3},{"turnId":"turn-31","durationMs":1,"ttftMs":1},{"turnId":"turn-32","durationMs":1,"ttftMs":0},{"turnId":"turn-33","durationMs":1,"ttftMs":0}]}}
|
||||
{"timestamp":"2026-05-15T10:51:47.217Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":6,"inputTokens":332,"outputTokens":803,"totalTokens":1135,"prefillTokensPerSec":225.24,"generationTokensPerSec":52.52,"combinedTokensPerSec":67.7,"totalDurationMs":16764,"timeToFirstTokenMs":1474,"rawTimestamps":{"ttftMs":1474,"allTtftMs":[1474],"generationDurationMs":15290,"turns":[{"turnId":"turn-0","durationMs":2480},{"turnId":"turn-1","durationMs":1555},{"turnId":"turn-2","durationMs":2223},{"turnId":"turn-3","durationMs":5185},{"turnId":"turn-4","durationMs":2690},{"turnId":"turn-5","durationMs":2631,"ttftMs":1474}]}}
|
||||
{"timestamp":"2026-05-15T10:52:22.219Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":2,"inputTokens":212,"outputTokens":217,"totalTokens":429,"prefillTokensPerSec":201.14,"generationTokensPerSec":53.82,"combinedTokensPerSec":84.35,"totalDurationMs":5086,"timeToFirstTokenMs":1054,"rawTimestamps":{"ttftMs":1054,"allTtftMs":[1054],"generationDurationMs":4032,"turns":[{"turnId":"turn-0","durationMs":2312},{"turnId":"turn-1","durationMs":2774,"ttftMs":1054}]}}
|
||||
{"timestamp":"2026-05-15T10:53:14.017Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":6,"inputTokens":2752,"outputTokens":611,"totalTokens":3363,"prefillTokensPerSec":2564.77,"generationTokensPerSec":33.42,"combinedTokensPerSec":173.75,"totalDurationMs":19355,"timeToFirstTokenMs":1073,"rawTimestamps":{"ttftMs":1073,"allTtftMs":[1073,2916,890],"generationDurationMs":18282,"turns":[{"turnId":"turn-0","durationMs":2568},{"turnId":"turn-1","durationMs":2126},{"turnId":"turn-2","durationMs":4607,"ttftMs":1073},{"turnId":"turn-3","durationMs":5843,"ttftMs":2916},{"turnId":"turn-4","durationMs":1926},{"turnId":"turn-5","durationMs":2285,"ttftMs":890}]}}
|
||||
{"timestamp":"2026-05-15T13:58:01.754Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":8,"inputTokens":9084,"outputTokens":1515,"totalTokens":10599,"prefillTokensPerSec":964.84,"generationTokensPerSec":51.2,"combinedTokensPerSec":271.76,"totalDurationMs":39002,"timeToFirstTokenMs":9415,"rawTimestamps":{"ttftMs":9415,"allTtftMs":[9415,4643,1692],"generationDurationMs":29587,"turns":[{"turnId":"turn-0","durationMs":12718,"ttftMs":9415},{"turnId":"turn-1","durationMs":3001},{"turnId":"turn-2","durationMs":2771},{"turnId":"turn-3","durationMs":4305},{"turnId":"turn-4","durationMs":1971},{"turnId":"turn-5","durationMs":8398,"ttftMs":4643},{"turnId":"turn-6","durationMs":1356},{"turnId":"turn-7","durationMs":4482,"ttftMs":1692}]}}
|
||||
{"timestamp":"2026-05-15T13:59:22.964Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":3,"inputTokens":490,"outputTokens":469,"totalTokens":959,"prefillTokensPerSec":135.36,"generationTokensPerSec":79.93,"combinedTokensPerSec":101.08,"totalDurationMs":9488,"timeToFirstTokenMs":3620,"rawTimestamps":{"ttftMs":3620,"allTtftMs":[3620,821],"generationDurationMs":5868,"turns":[{"turnId":"turn-0","durationMs":6452,"ttftMs":3620},{"turnId":"turn-1","durationMs":1650},{"turnId":"turn-2","durationMs":1386,"ttftMs":821}]}}
|
||||
{"timestamp":"2026-05-18T16:04:53.686Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":0,"outputTokens":0,"totalTokens":0,"prefillTokensPerSec":0,"generationTokensPerSec":0,"combinedTokensPerSec":0,"totalDurationMs":1493,"rawTimestamps":{"allTtftMs":[],"turns":[{"turnId":"turn-0","durationMs":1493}]}}
|
||||
{"timestamp":"2026-05-18T16:04:59.425Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":0,"outputTokens":0,"totalTokens":0,"prefillTokensPerSec":0,"generationTokensPerSec":0,"combinedTokensPerSec":0,"totalDurationMs":1282,"rawTimestamps":{"allTtftMs":[],"turns":[{"turnId":"turn-0","durationMs":1282}]}}
|
||||
{"timestamp":"2026-05-18T16:05:07.244Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":0,"outputTokens":0,"totalTokens":0,"prefillTokensPerSec":0,"generationTokensPerSec":0,"combinedTokensPerSec":0,"totalDurationMs":1373,"rawTimestamps":{"allTtftMs":[],"turns":[{"turnId":"turn-0","durationMs":1373}]}}
|
||||
{"timestamp":"2026-05-18T16:05:18.879Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":0,"outputTokens":0,"totalTokens":0,"prefillTokensPerSec":0,"generationTokensPerSec":0,"combinedTokensPerSec":0,"totalDurationMs":1222,"rawTimestamps":{"allTtftMs":[],"turns":[{"turnId":"turn-0","durationMs":1222}]}}
|
||||
{"timestamp":"2026-05-18T16:05:39.663Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":2,"inputTokens":6049,"outputTokens":520,"totalTokens":6569,"prefillTokensPerSec":2581.73,"generationTokensPerSec":41.42,"combinedTokensPerSec":440.93,"totalDurationMs":14898,"timeToFirstTokenMs":2343,"rawTimestamps":{"ttftMs":2343,"allTtftMs":[2343],"generationDurationMs":12555,"turns":[{"turnId":"turn-0","durationMs":5221},{"turnId":"turn-1","durationMs":9677,"ttftMs":2343}]}}
|
||||
{"timestamp":"2026-05-18T16:06:49.310Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":24,"outputTokens":951,"totalTokens":975,"prefillTokensPerSec":2.88,"generationTokensPerSec":109.29,"combinedTokensPerSec":57.23,"totalDurationMs":17037,"timeToFirstTokenMs":8335,"rawTimestamps":{"ttftMs":8335,"allTtftMs":[8335],"generationDurationMs":8702,"turns":[{"turnId":"turn-0","durationMs":17037,"ttftMs":8335}]}}
|
||||
{"timestamp":"2026-05-18T16:14:33.629Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":10,"inputTokens":549,"outputTokens":1110,"totalTokens":1659,"prefillTokensPerSec":683.69,"generationTokensPerSec":48.38,"combinedTokensPerSec":69.87,"totalDurationMs":23745,"timeToFirstTokenMs":803,"rawTimestamps":{"ttftMs":803,"allTtftMs":[803],"generationDurationMs":22942,"turns":[{"turnId":"turn-0","durationMs":2273},{"turnId":"turn-1","durationMs":2599},{"turnId":"turn-2","durationMs":1569},{"turnId":"turn-3","durationMs":2705},{"turnId":"turn-4","durationMs":1677},{"turnId":"turn-5","durationMs":3140},{"turnId":"turn-6","durationMs":1769},{"turnId":"turn-7","durationMs":3296},{"turnId":"turn-8","durationMs":1226},{"turnId":"turn-9","durationMs":3491,"ttftMs":803}]}}
|
||||
{"timestamp":"2026-05-18T16:25:00.013Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":36,"inputTokens":8805,"outputTokens":7546,"totalTokens":16351,"prefillTokensPerSec":13587.96,"generationTokensPerSec":51.5,"combinedTokensPerSec":111.1,"totalDurationMs":147178,"timeToFirstTokenMs":648,"rawTimestamps":{"ttftMs":648,"allTtftMs":[648,1264,2424,1730,2366,1,6141,2028,1209,3327],"generationDurationMs":146530,"turns":[{"turnId":"turn-0","durationMs":3100},{"turnId":"turn-1","durationMs":1611},{"turnId":"turn-2","durationMs":2836},{"turnId":"turn-3","durationMs":1701},{"turnId":"turn-4","durationMs":7045},{"turnId":"turn-5","durationMs":5537},{"turnId":"turn-6","durationMs":9904,"ttftMs":648},{"turnId":"turn-7","durationMs":2297},{"turnId":"turn-8","durationMs":3572},{"turnId":"turn-9","durationMs":6941,"ttftMs":1264},{"turnId":"turn-10","durationMs":4770},{"turnId":"turn-11","durationMs":5741,"ttftMs":2424},{"turnId":"turn-12","durationMs":2288},{"turnId":"turn-13","durationMs":2018},{"turnId":"turn-14","durationMs":2203},{"turnId":"turn-15","durationMs":1905},{"turnId":"turn-16","durationMs":2054},{"turnId":"turn-17","durationMs":1797},{"turnId":"turn-18","durationMs":1620},{"turnId":"turn-19","durationMs":2448},{"turnId":"turn-20","durationMs":2930},{"turnId":"turn-21","durationMs":5691,"ttftMs":1730},{"turnId":"turn-22","durationMs":5988},{"turnId":"turn-23","durationMs":7813,"ttftMs":2366},{"turnId":"turn-24","durationMs":1},{"turnId":"turn-25","durationMs":1099,"ttftMs":1},{"turnId":"turn-26","durationMs":1652},{"turnId":"turn-27","durationMs":3354},{"turnId":"turn-28","durationMs":14059,"ttftMs":6141},{"turnId":"turn-29","durationMs":1673},{"turnId":"turn-30","durationMs":3813,"ttftMs":2028},{"turnId":"turn-31","durationMs":1979},{"turnId":"turn-32","durationMs":2149},{"turnId":"turn-33","durationMs":7972,"ttftMs":1209},{"turnId":"turn-34","durationMs":4344},{"turnId":"turn-35","durationMs":11273,"ttftMs":3327}]}}
|
||||
{"timestamp":"2026-05-18T16:26:14.186Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":7,"inputTokens":2011,"outputTokens":1232,"totalTokens":3243,"prefillTokensPerSec":1028.64,"generationTokensPerSec":26.77,"combinedTokensPerSec":67.61,"totalDurationMs":47969,"timeToFirstTokenMs":1955,"rawTimestamps":{"ttftMs":1955,"allTtftMs":[1955,2520,1853,694],"generationDurationMs":46014,"turns":[{"turnId":"turn-0","durationMs":5149},{"turnId":"turn-1","durationMs":10442,"ttftMs":1955},{"turnId":"turn-2","durationMs":6081},{"turnId":"turn-3","durationMs":10493,"ttftMs":2520},{"turnId":"turn-4","durationMs":5028,"ttftMs":1853},{"turnId":"turn-5","durationMs":6746},{"turnId":"turn-6","durationMs":4030,"ttftMs":694}]}}
|
||||
{"timestamp":"2026-05-18T16:28:35.851Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":2,"inputTokens":87,"outputTokens":301,"totalTokens":388,"prefillTokensPerSec":129.08,"generationTokensPerSec":42.92,"combinedTokensPerSec":50.47,"totalDurationMs":7687,"timeToFirstTokenMs":674,"rawTimestamps":{"ttftMs":674,"allTtftMs":[674],"generationDurationMs":7013,"turns":[{"turnId":"turn-0","durationMs":5103},{"turnId":"turn-1","durationMs":2584,"ttftMs":674}]}}
|
||||
{"timestamp":"2026-05-18T16:29:33.286Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":40,"outputTokens":531,"totalTokens":571,"prefillTokensPerSec":17.44,"generationTokensPerSec":57.31,"combinedTokensPerSec":49.4,"totalDurationMs":11558,"timeToFirstTokenMs":2293,"rawTimestamps":{"ttftMs":2293,"allTtftMs":[2293],"generationDurationMs":9265,"turns":[{"turnId":"turn-0","durationMs":11558,"ttftMs":2293}]}}
|
||||
{"timestamp":"2026-05-18T16:31:22.157Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":58,"outputTokens":618,"totalTokens":676,"prefillTokensPerSec":7.43,"generationTokensPerSec":134.11,"combinedTokensPerSec":54.44,"totalDurationMs":12417,"timeToFirstTokenMs":7809,"rawTimestamps":{"ttftMs":7809,"allTtftMs":[7809],"generationDurationMs":4608,"turns":[{"turnId":"turn-0","durationMs":12417,"ttftMs":7809}]}}
|
||||
{"timestamp":"2026-05-18T16:32:43.573Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":2,"inputTokens":408,"outputTokens":1498,"totalTokens":1906,"prefillTokensPerSec":75.25,"generationTokensPerSec":58.97,"combinedTokensPerSec":61.84,"totalDurationMs":30823,"timeToFirstTokenMs":5422,"rawTimestamps":{"ttftMs":5422,"allTtftMs":[5422,14765],"generationDurationMs":25401,"turns":[{"turnId":"turn-0","durationMs":7357,"ttftMs":5422},{"turnId":"turn-1","durationMs":23466,"ttftMs":14765}]}}
|
||||
{"timestamp":"2026-05-18T16:50:30.294Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":44,"outputTokens":2533,"totalTokens":2577,"prefillTokensPerSec":0.93,"generationTokensPerSec":485.99,"combinedTokensPerSec":48.86,"totalDurationMs":52746,"timeToFirstTokenMs":47534,"rawTimestamps":{"ttftMs":47534,"allTtftMs":[47534],"generationDurationMs":5212,"turns":[{"turnId":"turn-0","durationMs":52746,"ttftMs":47534}]}}
|
||||
{"timestamp":"2026-05-18T16:57:06.934Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":17,"inputTokens":9721,"outputTokens":6329,"totalTokens":16050,"prefillTokensPerSec":513.66,"generationTokensPerSec":44.57,"combinedTokensPerSec":99.73,"totalDurationMs":160936,"timeToFirstTokenMs":18925,"rawTimestamps":{"ttftMs":18925,"allTtftMs":[18925,16190,4022,7901,1873,10528,1481],"generationDurationMs":142011,"turns":[{"turnId":"turn-0","durationMs":21666,"ttftMs":18925},{"turnId":"turn-1","durationMs":25748,"ttftMs":16190},{"turnId":"turn-2","durationMs":18958,"ttftMs":4022},{"turnId":"turn-3","durationMs":2499},{"turnId":"turn-4","durationMs":14503,"ttftMs":7901},{"turnId":"turn-5","durationMs":7021},{"turnId":"turn-6","durationMs":8323,"ttftMs":1873},{"turnId":"turn-7","durationMs":8202},{"turnId":"turn-8","durationMs":8887},{"turnId":"turn-9","durationMs":2925},{"turnId":"turn-10","durationMs":2921},{"turnId":"turn-11","durationMs":3410},{"turnId":"turn-12","durationMs":2903},{"turnId":"turn-13","durationMs":5754},{"turnId":"turn-14","durationMs":14057,"ttftMs":10528},{"turnId":"turn-15","durationMs":7970},{"turnId":"turn-16","durationMs":5189,"ttftMs":1481}]}}
|
||||
{"timestamp":"2026-05-18T17:00:32.909Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":25,"outputTokens":715,"totalTokens":740,"prefillTokensPerSec":2.76,"generationTokensPerSec":94.53,"combinedTokensPerSec":44.48,"totalDurationMs":16637,"timeToFirstTokenMs":9073,"rawTimestamps":{"ttftMs":9073,"allTtftMs":[9073],"generationDurationMs":7564,"turns":[{"turnId":"turn-0","durationMs":16637,"ttftMs":9073}]}}
|
||||
{"timestamp":"2026-05-18T17:03:32.883Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":4,"inputTokens":728,"outputTokens":1603,"totalTokens":2331,"prefillTokensPerSec":477.69,"generationTokensPerSec":49.97,"combinedTokensPerSec":69.36,"totalDurationMs":33605,"timeToFirstTokenMs":1524,"rawTimestamps":{"ttftMs":1524,"allTtftMs":[1524],"generationDurationMs":32081.000000000004,"turns":[{"turnId":"turn-0","durationMs":3655},{"turnId":"turn-1","durationMs":17547},{"turnId":"turn-2","durationMs":8416},{"turnId":"turn-3","durationMs":3987,"ttftMs":1524}]}}
|
||||
{"timestamp":"2026-05-18T17:04:51.598Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":2,"inputTokens":91,"outputTokens":302,"totalTokens":393,"prefillTokensPerSec":120.69,"generationTokensPerSec":42.08,"combinedTokensPerSec":49.56,"totalDurationMs":7930,"timeToFirstTokenMs":754,"rawTimestamps":{"ttftMs":754,"allTtftMs":[754],"generationDurationMs":7176,"turns":[{"turnId":"turn-0","durationMs":5537},{"turnId":"turn-1","durationMs":2393,"ttftMs":754}]}}
|
||||
{"timestamp":"2026-05-18T17:06:35.090Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":5,"inputTokens":595,"outputTokens":991,"totalTokens":1586,"prefillTokensPerSec":107.67,"generationTokensPerSec":50.99,"combinedTokensPerSec":63.53,"totalDurationMs":24963,"timeToFirstTokenMs":5526,"rawTimestamps":{"ttftMs":5526,"allTtftMs":[5526],"generationDurationMs":19437,"turns":[{"turnId":"turn-0","durationMs":2342},{"turnId":"turn-1","durationMs":8629},{"turnId":"turn-2","durationMs":3748},{"turnId":"turn-3","durationMs":1905},{"turnId":"turn-4","durationMs":8339,"ttftMs":5526}]}}
|
||||
{"timestamp":"2026-05-18T17:09:58.845Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":9,"inputTokens":2111,"outputTokens":3124,"totalTokens":5235,"prefillTokensPerSec":316.73,"generationTokensPerSec":38.79,"combinedTokensPerSec":60.03,"totalDurationMs":87211,"timeToFirstTokenMs":6665,"rawTimestamps":{"ttftMs":6665,"allTtftMs":[6665,8492,4911,5143,2588,740],"generationDurationMs":80546,"turns":[{"turnId":"turn-0","durationMs":9004,"ttftMs":6665},{"turnId":"turn-1","durationMs":21348,"ttftMs":8492},{"turnId":"turn-2","durationMs":8350},{"turnId":"turn-3","durationMs":10895,"ttftMs":4911},{"turnId":"turn-4","durationMs":8784},{"turnId":"turn-5","durationMs":11053,"ttftMs":5143},{"turnId":"turn-6","durationMs":8565},{"turnId":"turn-7","durationMs":7853,"ttftMs":2588},{"turnId":"turn-8","durationMs":1359,"ttftMs":740}]}}
|
||||
{"timestamp":"2026-05-18T17:13:46.526Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":3,"inputTokens":5057,"outputTokens":641,"totalTokens":5698,"prefillTokensPerSec":2095.73,"generationTokensPerSec":32.47,"combinedTokensPerSec":257.16,"totalDurationMs":22157,"timeToFirstTokenMs":2413,"rawTimestamps":{"ttftMs":2413,"allTtftMs":[2413,4376],"generationDurationMs":19744,"turns":[{"turnId":"turn-0","durationMs":7565},{"turnId":"turn-1","durationMs":4846,"ttftMs":2413},{"turnId":"turn-2","durationMs":9746,"ttftMs":4376}]}}
|
||||
{"timestamp":"2026-05-18T17:15:51.225Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":10,"inputTokens":4304,"outputTokens":1913,"totalTokens":6217,"prefillTokensPerSec":435.67,"generationTokensPerSec":57.84,"combinedTokensPerSec":144.73,"totalDurationMs":42955,"timeToFirstTokenMs":9879,"rawTimestamps":{"ttftMs":9879,"allTtftMs":[9879,3442,2613,3964,856],"generationDurationMs":33076,"turns":[{"turnId":"turn-0","durationMs":3346},{"turnId":"turn-1","durationMs":11767,"ttftMs":9879},{"turnId":"turn-2","durationMs":5287,"ttftMs":3442},{"turnId":"turn-3","durationMs":3421},{"turnId":"turn-4","durationMs":4258,"ttftMs":2613},{"turnId":"turn-5","durationMs":2424},{"turnId":"turn-6","durationMs":6178,"ttftMs":3964},{"turnId":"turn-7","durationMs":1727},{"turnId":"turn-8","durationMs":2407},{"turnId":"turn-9","durationMs":2140,"ttftMs":856}]}}
|
||||
{"timestamp":"2026-05-18T17:26:11.786Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":34,"inputTokens":43395,"outputTokens":12858,"totalTokens":56253,"prefillTokensPerSec":1200.32,"generationTokensPerSec":25.66,"combinedTokensPerSec":104.72,"totalDurationMs":537177,"timeToFirstTokenMs":36153,"rawTimestamps":{"ttftMs":36153,"allTtftMs":[36153,2689,1366,4236,6406,5356,790,1651,13306,66825,29393,119886,40627,5585,1105,2249],"generationDurationMs":501024,"turns":[{"turnId":"turn-0","durationMs":37128,"ttftMs":36153},{"turnId":"turn-1","durationMs":16635,"ttftMs":2689},{"turnId":"turn-2","durationMs":4975,"ttftMs":1366},{"turnId":"turn-3","durationMs":3817},{"turnId":"turn-4","durationMs":9210,"ttftMs":4236},{"turnId":"turn-5","durationMs":4367},{"turnId":"turn-6","durationMs":6681},{"turnId":"turn-7","durationMs":9766,"ttftMs":6406},{"turnId":"turn-8","durationMs":7955},{"turnId":"turn-9","durationMs":8770,"ttftMs":5356},{"turnId":"turn-10","durationMs":1848},{"turnId":"turn-11","durationMs":2371},{"turnId":"turn-12","durationMs":2166},{"turnId":"turn-13","durationMs":3484},{"turnId":"turn-14","durationMs":2771},{"turnId":"turn-15","durationMs":1892},{"turnId":"turn-16","durationMs":2189},{"turnId":"turn-17","durationMs":2412},{"turnId":"turn-18","durationMs":3483,"ttftMs":790},{"turnId":"turn-19","durationMs":3791,"ttftMs":1651},{"turnId":"turn-20","durationMs":5764},{"turnId":"turn-21","durationMs":19747,"ttftMs":13306},{"turnId":"turn-22","durationMs":69825,"ttftMs":66825},{"turnId":"turn-23","durationMs":37360,"ttftMs":29393},{"turnId":"turn-24","durationMs":2},{"turnId":"turn-25","durationMs":3310},{"turnId":"turn-26","durationMs":133998,"ttftMs":119886},{"turnId":"turn-27","durationMs":21354},{"turnId":"turn-28","durationMs":34613},{"turnId":"turn-29","durationMs":47182,"ttftMs":40627},{"turnId":"turn-30","durationMs":6697},{"turnId":"turn-31","durationMs":7977,"ttftMs":5585},{"turnId":"turn-32","durationMs":9916,"ttftMs":1105},{"turnId":"turn-33","durationMs":3721,"ttftMs":2249}]}}
|
||||
{"timestamp":"2026-05-18T17:28:46.844Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":8,"inputTokens":2243,"outputTokens":1970,"totalTokens":4213,"prefillTokensPerSec":132.06,"generationTokensPerSec":42.23,"combinedTokensPerSec":66.2,"totalDurationMs":63639,"timeToFirstTokenMs":16985,"rawTimestamps":{"ttftMs":16985,"allTtftMs":[16985,1337],"generationDurationMs":46654,"turns":[{"turnId":"turn-0","durationMs":12673},{"turnId":"turn-1","durationMs":20344,"ttftMs":16985},{"turnId":"turn-2","durationMs":4137},{"turnId":"turn-3","durationMs":9417},{"turnId":"turn-4","durationMs":5762},{"turnId":"turn-5","durationMs":2149},{"turnId":"turn-6","durationMs":7088},{"turnId":"turn-7","durationMs":2069,"ttftMs":1337}]}}
|
||||
{"timestamp":"2026-05-18T17:29:26.902Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":2,"inputTokens":416,"outputTokens":265,"totalTokens":681,"prefillTokensPerSec":109.85,"generationTokensPerSec":55.78,"combinedTokensPerSec":79.76,"totalDurationMs":8538,"timeToFirstTokenMs":3787,"rawTimestamps":{"ttftMs":3787,"allTtftMs":[3787],"generationDurationMs":4751,"turns":[{"turnId":"turn-0","durationMs":3102},{"turnId":"turn-1","durationMs":5436,"ttftMs":3787}]}}
|
||||
{"timestamp":"2026-05-18T19:02:24.350Z","provider":"openrouter","model":"deepseek/deepseek-v4-flash","turnCount":2,"inputTokens":68503,"outputTokens":4616,"totalTokens":73119,"prefillTokensPerSec":8757.73,"generationTokensPerSec":152.37,"combinedTokensPerSec":1918.33,"totalDurationMs":38116,"timeToFirstTokenMs":7822,"rawTimestamps":{"ttftMs":7822,"allTtftMs":[7822],"generationDurationMs":30294,"turns":[{"turnId":"turn-0","durationMs":8502,"ttftMs":7822},{"turnId":"turn-1","durationMs":29614}]}}
|
||||
{"timestamp":"2026-05-18T19:11:08.129Z","provider":"openrouter","model":"deepseek/deepseek-v4-flash","turnCount":36,"inputTokens":692307,"outputTokens":15656,"totalTokens":707963,"prefillTokensPerSec":11579.75,"generationTokensPerSec":76.25,"combinedTokensPerSec":2670.5,"totalDurationMs":265105,"timeToFirstTokenMs":59786,"rawTimestamps":{"ttftMs":59786,"allTtftMs":[59786,3861,4337,12069,4515,3773,3224,11472,4712,3959,2260,3896,60110,4270,5140,4180,0,1,0,0,1,0,0,0],"generationDurationMs":205319,"turns":[{"turnId":"turn-0","durationMs":5495},{"turnId":"turn-1","durationMs":59996,"ttftMs":59786},{"turnId":"turn-2","durationMs":14978,"ttftMs":3861},{"turnId":"turn-3","durationMs":5894},{"turnId":"turn-4","durationMs":4717,"ttftMs":4337},{"turnId":"turn-5","durationMs":12969,"ttftMs":12069},{"turnId":"turn-6","durationMs":4223},{"turnId":"turn-7","durationMs":7194,"ttftMs":4515},{"turnId":"turn-8","durationMs":6171},{"turnId":"turn-9","durationMs":4150},{"turnId":"turn-10","durationMs":6994,"ttftMs":3773},{"turnId":"turn-11","durationMs":4149,"ttftMs":3224},{"turnId":"turn-12","durationMs":12091,"ttftMs":11472},{"turnId":"turn-13","durationMs":4703},{"turnId":"turn-14","durationMs":5047,"ttftMs":4712},{"turnId":"turn-15","durationMs":3886},{"turnId":"turn-16","durationMs":5228,"ttftMs":3959},{"turnId":"turn-17","durationMs":3955,"ttftMs":2260},{"turnId":"turn-18","durationMs":9010,"ttftMs":3896},{"turnId":"turn-19","durationMs":61482,"ttftMs":60110},{"turnId":"turn-20","durationMs":4774,"ttftMs":4270},{"turnId":"turn-21","durationMs":5580,"ttftMs":5140},{"turnId":"turn-22","durationMs":3583},{"turnId":"turn-23","durationMs":8814,"ttftMs":4180},{"turnId":"turn-24","durationMs":4,"ttftMs":0},{"turnId":"turn-25","durationMs":5},{"turnId":"turn-26","durationMs":5,"ttftMs":1},{"turnId":"turn-27","durationMs":1,"ttftMs":0},{"turnId":"turn-28","durationMs":0,"ttftMs":0},{"turnId":"turn-29","durationMs":2,"ttftMs":1},{"turnId":"turn-30","durationMs":2},{"turnId":"turn-31","durationMs":1},{"turnId":"turn-32","durationMs":1},{"turnId":"turn-33","durationMs":1,"ttftMs":0},{"turnId":"turn-34","durationMs":0,"ttftMs":0},{"turnId":"turn-35","durationMs":0,"ttftMs":0}]}}
|
||||
{"timestamp":"2026-05-18T19:31:54.781Z","provider":"openrouter","model":"deepseek/deepseek-v4-flash","turnCount":9,"inputTokens":89552,"outputTokens":3597,"totalTokens":93149,"prefillTokensPerSec":21495.92,"generationTokensPerSec":67.54,"combinedTokensPerSec":1622.18,"totalDurationMs":57422,"timeToFirstTokenMs":4166,"rawTimestamps":{"ttftMs":4166,"allTtftMs":[4166,3529,3655,6510],"generationDurationMs":53256,"turns":[{"turnId":"turn-0","durationMs":6647,"ttftMs":4166},{"turnId":"turn-1","durationMs":5466},{"turnId":"turn-2","durationMs":3693},{"turnId":"turn-3","durationMs":4031},{"turnId":"turn-4","durationMs":5936,"ttftMs":3529},{"turnId":"turn-5","durationMs":9830},{"turnId":"turn-6","durationMs":5425},{"turnId":"turn-7","durationMs":4325,"ttftMs":3655},{"turnId":"turn-8","durationMs":12069,"ttftMs":6510}]}}
|
||||
{"timestamp":"2026-05-18T19:37:04.141Z","provider":"openrouter","model":"deepseek/deepseek-v4-flash","turnCount":6,"inputTokens":113452,"outputTokens":3752,"totalTokens":117204,"prefillTokensPerSec":26015.13,"generationTokensPerSec":69.59,"combinedTokensPerSec":2011.22,"totalDurationMs":58275,"timeToFirstTokenMs":4361,"rawTimestamps":{"ttftMs":4361,"allTtftMs":[4361,17827,5563,7617,3362,5079],"generationDurationMs":53914,"turns":[{"turnId":"turn-0","durationMs":5018,"ttftMs":4361},{"turnId":"turn-1","durationMs":18740,"ttftMs":17827},{"turnId":"turn-2","durationMs":6211,"ttftMs":5563},{"turnId":"turn-3","durationMs":15178,"ttftMs":7617},{"turnId":"turn-4","durationMs":4283,"ttftMs":3362},{"turnId":"turn-5","durationMs":8845,"ttftMs":5079}]}}
|
||||
{"timestamp":"2026-05-18T19:38:21.928Z","provider":"openrouter","model":"deepseek/deepseek-v4-flash","turnCount":4,"inputTokens":8546,"outputTokens":1786,"totalTokens":10332,"prefillTokensPerSec":984.45,"generationTokensPerSec":72.01,"combinedTokensPerSec":308.57,"totalDurationMs":33484,"timeToFirstTokenMs":8681,"rawTimestamps":{"ttftMs":8681,"allTtftMs":[8681,5603],"generationDurationMs":24803,"turns":[{"turnId":"turn-0","durationMs":8109},{"turnId":"turn-1","durationMs":5687},{"turnId":"turn-2","durationMs":9773,"ttftMs":8681},{"turnId":"turn-3","durationMs":9915,"ttftMs":5603}]}}
|
||||
{"timestamp":"2026-05-18T19:46:17.168Z","provider":"openrouter","model":"deepseek/deepseek-v4-flash","turnCount":5,"inputTokens":68753,"outputTokens":2154,"totalTokens":70907,"prefillTokensPerSec":9984.46,"generationTokensPerSec":65.6,"combinedTokensPerSec":1785.04,"totalDurationMs":39723,"timeToFirstTokenMs":6886,"rawTimestamps":{"ttftMs":6886,"allTtftMs":[6886,4833],"generationDurationMs":32837,"turns":[{"turnId":"turn-0","durationMs":8479},{"turnId":"turn-1","durationMs":4208},{"turnId":"turn-2","durationMs":16657,"ttftMs":6886},{"turnId":"turn-3","durationMs":4345},{"turnId":"turn-4","durationMs":6034,"ttftMs":4833}]}}
|
||||
{"timestamp":"2026-05-18T19:56:09.763Z","provider":"openrouter","model":"deepseek/deepseek-v4-flash","turnCount":28,"inputTokens":1595354,"outputTokens":25752,"totalTokens":1621106,"prefillTokensPerSec":525305.89,"generationTokensPerSec":69.18,"combinedTokensPerSec":4319.55,"totalDurationMs":375295,"timeToFirstTokenMs":3037,"rawTimestamps":{"ttftMs":3037,"allTtftMs":[3037,13673,28033,9202,4807,4528,3654,5502,3886,4456,3250,13080,12854,3427,4212,19933,24411,9703,16122,1],"generationDurationMs":372258,"turns":[{"turnId":"turn-0","durationMs":3834},{"turnId":"turn-1","durationMs":2495},{"turnId":"turn-2","durationMs":4310,"ttftMs":3037},{"turnId":"turn-3","durationMs":15225,"ttftMs":13673},{"turnId":"turn-4","durationMs":29096,"ttftMs":28033},{"turnId":"turn-5","durationMs":9805,"ttftMs":9202},{"turnId":"turn-6","durationMs":13204,"ttftMs":4807},{"turnId":"turn-7","durationMs":5250,"ttftMs":4528},{"turnId":"turn-8","durationMs":4470,"ttftMs":3654},{"turnId":"turn-9","durationMs":5782,"ttftMs":5502},{"turnId":"turn-10","durationMs":7596,"ttftMs":3886},{"turnId":"turn-11","durationMs":7902,"ttftMs":4456},{"turnId":"turn-12","durationMs":4203,"ttftMs":3250},{"turnId":"turn-13","durationMs":5008},{"turnId":"turn-14","durationMs":3844},{"turnId":"turn-15","durationMs":14795,"ttftMs":13080},{"turnId":"turn-16","durationMs":40603,"ttftMs":12854},{"turnId":"turn-17","durationMs":5471,"ttftMs":3427},{"turnId":"turn-18","durationMs":11695,"ttftMs":4212},{"turnId":"turn-19","durationMs":26970,"ttftMs":19933},{"turnId":"turn-20","durationMs":25652,"ttftMs":24411},{"turnId":"turn-21","durationMs":11640},{"turnId":"turn-22","durationMs":10598,"ttftMs":9703},{"turnId":"turn-23","durationMs":33673,"ttftMs":16122},{"turnId":"turn-24","durationMs":3,"ttftMs":1},{"turnId":"turn-25","durationMs":5},{"turnId":"turn-26","durationMs":51430},{"turnId":"turn-27","durationMs":20736}]}}
|
||||
{"timestamp":"2026-05-18T19:58:52.915Z","provider":"openrouter","model":"deepseek/deepseek-v4-flash","turnCount":6,"inputTokens":10435,"outputTokens":2461,"totalTokens":12896,"prefillTokensPerSec":693.95,"generationTokensPerSec":19.21,"combinedTokensPerSec":90.07,"totalDurationMs":143171,"timeToFirstTokenMs":15037,"rawTimestamps":{"ttftMs":15037,"allTtftMs":[15037,16187,26633],"generationDurationMs":128133.99999999999,"turns":[{"turnId":"turn-0","durationMs":18505},{"turnId":"turn-1","durationMs":23335,"ttftMs":15037},{"turnId":"turn-2","durationMs":25587,"ttftMs":16187},{"turnId":"turn-3","durationMs":31798,"ttftMs":26633},{"turnId":"turn-4","durationMs":32046},{"turnId":"turn-5","durationMs":11900}]}}
|
||||
{"timestamp":"2026-05-18T20:08:44.940Z","provider":"openrouter","model":"deepseek/deepseek-v4-flash","turnCount":21,"inputTokens":3192238,"outputTokens":10000,"totalTokens":3202238,"prefillTokensPerSec":58744.56,"generationTokensPerSec":18.76,"combinedTokensPerSec":5450.45,"totalDurationMs":587518,"timeToFirstTokenMs":54341,"rawTimestamps":{"ttftMs":54341,"allTtftMs":[54341,33453,49862,14811,16941,35825,8885],"generationDurationMs":533177,"turns":[{"turnId":"turn-0","durationMs":62649},{"turnId":"turn-1","durationMs":58961,"ttftMs":54341},{"turnId":"turn-2","durationMs":57096},{"turnId":"turn-3","durationMs":34112,"ttftMs":33453},{"turnId":"turn-4","durationMs":51245,"ttftMs":49862},{"turnId":"turn-5","durationMs":22643},{"turnId":"turn-6","durationMs":24897,"ttftMs":14811},{"turnId":"turn-7","durationMs":18192,"ttftMs":16941},{"turnId":"turn-8","durationMs":37301,"ttftMs":35825},{"turnId":"turn-9","durationMs":16410},{"turnId":"turn-10","durationMs":16345},{"turnId":"turn-11","durationMs":15448},{"turnId":"turn-12","durationMs":44179},{"turnId":"turn-13","durationMs":20599},{"turnId":"turn-14","durationMs":15530},{"turnId":"turn-15","durationMs":14248},{"turnId":"turn-16","durationMs":15685},{"turnId":"turn-17","durationMs":15853},{"turnId":"turn-18","durationMs":16149},{"turnId":"turn-19","durationMs":18121,"ttftMs":8885},{"turnId":"turn-20","durationMs":11855}]}}
|
||||
{"timestamp":"2026-05-18T20:09:48.805Z","provider":"openrouter","model":"deepseek/deepseek-v4-flash","turnCount":2,"inputTokens":715,"outputTokens":197,"totalTokens":912,"prefillTokensPerSec":16.15,"generationTokensPerSec":13.02,"combinedTokensPerSec":15.35,"totalDurationMs":59418,"timeToFirstTokenMs":44282,"rawTimestamps":{"ttftMs":44282,"allTtftMs":[44282],"generationDurationMs":15136,"turns":[{"turnId":"turn-0","durationMs":53061,"ttftMs":44282},{"turnId":"turn-1","durationMs":6357}]}}
|
||||
{"timestamp":"2026-05-18T20:13:13.949Z","provider":"openrouter","model":"deepseek/deepseek-v4-flash","turnCount":9,"inputTokens":1405406,"outputTokens":1702,"totalTokens":1407108,"prefillTokensPerSec":86603.77,"generationTokensPerSec":10.6,"combinedTokensPerSec":7958.31,"totalDurationMs":176810,"timeToFirstTokenMs":16228,"rawTimestamps":{"ttftMs":16228,"allTtftMs":[16228,7810],"generationDurationMs":160582,"turns":[{"turnId":"turn-0","durationMs":17373,"ttftMs":16228},{"turnId":"turn-1","durationMs":36359},{"turnId":"turn-2","durationMs":14630},{"turnId":"turn-3","durationMs":36693},{"turnId":"turn-4","durationMs":14968},{"turnId":"turn-5","durationMs":16571},{"turnId":"turn-6","durationMs":14690},{"turnId":"turn-7","durationMs":14596},{"turnId":"turn-8","durationMs":10930,"ttftMs":7810}]}}
|
||||
{"timestamp":"2026-05-18T20:33:37.419Z","provider":"openrouter","model":"deepseek/deepseek-v4-flash","turnCount":15,"inputTokens":44601,"outputTokens":2028,"totalTokens":46629,"prefillTokensPerSec":11909.48,"generationTokensPerSec":29.39,"combinedTokensPerSec":640.91,"totalDurationMs":72754,"timeToFirstTokenMs":3745,"rawTimestamps":{"ttftMs":3745,"allTtftMs":[3745],"generationDurationMs":69009,"turns":[{"turnId":"turn-0","durationMs":4398},{"turnId":"turn-1","durationMs":3076},{"turnId":"turn-2","durationMs":3511},{"turnId":"turn-3","durationMs":2850},{"turnId":"turn-4","durationMs":3184},{"turnId":"turn-5","durationMs":4136},{"turnId":"turn-6","durationMs":3897},{"turnId":"turn-7","durationMs":2935},{"turnId":"turn-8","durationMs":5794},{"turnId":"turn-9","durationMs":4349},{"turnId":"turn-10","durationMs":4636},{"turnId":"turn-11","durationMs":2644},{"turnId":"turn-12","durationMs":12015},{"turnId":"turn-13","durationMs":9405},{"turnId":"turn-14","durationMs":5924,"ttftMs":3745}]}}
|
||||
|
||||
Executable
+132
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# init.sh — Set up the demo skill's headless Chrome container
|
||||
#
|
||||
# Prerequisites:
|
||||
# - go (for installing showboat & rodney)
|
||||
# - docker
|
||||
#
|
||||
# This script:
|
||||
# 1. Installs showboat and rodney if missing
|
||||
# 2. Downloads Chromium for the host (macOS, via rodney)
|
||||
# 3. Downloads Chromium for the Docker image (Linux_x64, from GCS)
|
||||
# 4. Copies Linux binary into chrome/ (the Docker build context)
|
||||
# 5. Builds the demo-chrome image
|
||||
|
||||
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CHROME_DIR="$SKILL_DIR/chrome"
|
||||
IMAGE_NAME="${1:-demo-chrome}"
|
||||
|
||||
echo "==> Checking prerequisites..."
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 1. Install CLI tools
|
||||
# ------------------------------------------------------------------
|
||||
GO_BIN_DIR="$(go env GOBIN 2>/dev/null || echo "$HOME/go/bin")"
|
||||
export PATH="$GO_BIN_DIR:$PATH"
|
||||
|
||||
if ! command -v showboat &>/dev/null; then
|
||||
echo "==> Installing showboat..."
|
||||
go install github.com/simonw/showboat@latest
|
||||
else
|
||||
echo " showboat: ok"
|
||||
fi
|
||||
|
||||
if ! command -v rodney &>/dev/null; then
|
||||
echo "==> Installing rodney..."
|
||||
go install github.com/simonw/rodney@latest
|
||||
else
|
||||
echo " rodney: ok"
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 2. Download macOS Chromium for local rodney
|
||||
# ------------------------------------------------------------------
|
||||
echo "==> Downloading macOS Chromium via rodney..."
|
||||
rodney start 2>/dev/null || true
|
||||
rodney stop 2>/dev/null || true
|
||||
|
||||
ROD_CACHE="$HOME/.cache/rod/browser"
|
||||
if [ -d "$ROD_CACHE" ]; then
|
||||
CHROMIUM_DIR=$(ls -d "$ROD_CACHE"/chromium-* 2>/dev/null | head -1)
|
||||
else
|
||||
echo "ERROR: rod cache not found at $ROD_CACHE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$CHROMIUM_DIR" ]; then
|
||||
echo "ERROR: No chromium-* directory found in $ROD_CACHE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " macOS Chromium at: $CHROMIUM_DIR"
|
||||
|
||||
# Extract revision number from directory name (e.g. chromium-1321438)
|
||||
REVISION=$(basename "$CHROMIUM_DIR" | sed 's/chromium-//')
|
||||
echo " Revision: $REVISION"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 3. Download Linux Chromium for the Docker image
|
||||
# ------------------------------------------------------------------
|
||||
LINUX_ZIP="/tmp/chrome-linux-$REVISION.zip"
|
||||
LINUX_EXTRACT="/tmp/chrome-linux-extract"
|
||||
|
||||
echo "==> Downloading Linux_x64 Chromium (revision $REVISION)..."
|
||||
|
||||
if [ ! -f "$LINUX_ZIP" ]; then
|
||||
curl -fSL \
|
||||
"https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/$REVISION/chrome-linux.zip" \
|
||||
-o "$LINUX_ZIP"
|
||||
echo " Downloaded: $LINUX_ZIP"
|
||||
else
|
||||
echo " Already cached: $LINUX_ZIP"
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 4. Extract and copy into build context
|
||||
# ------------------------------------------------------------------
|
||||
echo "==> Extracting Linux Chromium into $CHROME_DIR ..."
|
||||
rm -rf "$LINUX_EXTRACT" "$CHROME_DIR"
|
||||
mkdir -p "$LINUX_EXTRACT"
|
||||
unzip -q "$LINUX_ZIP" -d "$LINUX_EXTRACT"
|
||||
|
||||
# chrome-linux.zip contains a single chrome-linux/ directory
|
||||
mv "$LINUX_EXTRACT/chrome-linux" "$CHROME_DIR"
|
||||
|
||||
# IMPORTANT: set suid on the sandbox binary so Chrome can sandbox child processes
|
||||
if [ -f "$CHROME_DIR/chrome_sandbox" ]; then
|
||||
chmod 4755 "$CHROME_DIR/chrome_sandbox"
|
||||
fi
|
||||
|
||||
echo "==> Files in $CHROME_DIR:"
|
||||
ls -lh "$CHROME_DIR/" | head -15
|
||||
|
||||
# Clean up extraction temp
|
||||
rm -rf "$LINUX_EXTRACT"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 5. Build Docker image
|
||||
# ------------------------------------------------------------------
|
||||
echo "==> Building Docker image '$IMAGE_NAME' ..."
|
||||
docker build --tag "$IMAGE_NAME" "$SKILL_DIR"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 6. Verify
|
||||
# ------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "==> Success!"
|
||||
echo " Image: $IMAGE_NAME"
|
||||
echo ""
|
||||
echo " Run the container:"
|
||||
echo " docker run -d --name demo-chrome \\"
|
||||
echo " --cap-add=SYS_ADMIN --cap-drop=ALL \\"
|
||||
echo " --security-opt=no-new-privileges:false \\"
|
||||
echo " -p 9222:9222 \\"
|
||||
echo " $IMAGE_NAME"
|
||||
echo ""
|
||||
echo " Connect rodney to it:"
|
||||
echo " rodney connect localhost:9222"
|
||||
echo ""
|
||||
echo " Stop the container:"
|
||||
echo " docker stop demo-chrome && docker rm demo-chrome"
|
||||
@@ -25,3 +25,28 @@ test:
|
||||
# Format code
|
||||
format:
|
||||
@make -C app format
|
||||
|
||||
# Run Playwright browser-based tests (requires running server)
|
||||
# Set PLAYWRIGHT_BASE_URL to point at the running Phoenix app
|
||||
playwright:
|
||||
@make -C app playwright
|
||||
|
||||
# Run Playwright in UI mode (headed browser)
|
||||
playwright-ui:
|
||||
@make -C app playwright-ui
|
||||
|
||||
# Run Playwright against Docker Chrome container (must be running)
|
||||
playwright-docker:
|
||||
@make -C app playwright-docker
|
||||
|
||||
# Full e2e workflow: start Chrome container, run tests, stop container
|
||||
playwright-full:
|
||||
@make -C app playwright-full
|
||||
|
||||
# Start Docker Chrome container
|
||||
playwright-docker-start:
|
||||
@make -C app playwright-docker-start
|
||||
|
||||
# Stop Docker Chrome container
|
||||
playwright-docker-stop:
|
||||
@make -C app playwright-docker-stop
|
||||
@@ -28,6 +28,70 @@ test: deps compile
|
||||
format:
|
||||
$(MISE_EXEC) mix format
|
||||
|
||||
# =============================================================================
|
||||
# Playwright e2e browser tests
|
||||
# =============================================================================
|
||||
#
|
||||
# Requires the Phoenix dev server running on the default port (8056).
|
||||
# Set PLAYWRIGHT_BASE_URL to point at a different server/port.
|
||||
#
|
||||
# Available browsers (local):
|
||||
# PLAYWRIGHT_BROWSER=firefox (default — works on macOS 15)
|
||||
# PLAYWRIGHT_BROWSER=chromium (crashes on macOS 15, use Docker Chrome)
|
||||
# PLAYWRIGHT_BROWSER=webkit
|
||||
#
|
||||
# Docker Chrome (alternative):
|
||||
# 1. docker compose -f docker-compose.chrome.yml up -d
|
||||
# 2. PLAYWRIGHT_BROWSERS_PATH=assets/node_modules/playwright-core/.local-browsers \
|
||||
# npx playwright test --config=playwright.docker.config.ts
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
# Run Playwright e2e tests (default: local Firefox)
|
||||
# Usage: make playwright
|
||||
# PLAYWRIGHT_BROWSER=chromium make playwright
|
||||
playwright:
|
||||
cd assets && PLAYWRIGHT_BROWSERS_PATH=node_modules/playwright-core/.local-browsers npx playwright test --config=../playwright.config.ts
|
||||
|
||||
# Run a specific Playwright test file
|
||||
# Usage: make playwright-test test/e2e/smoke.spec.ts
|
||||
playwright-test:
|
||||
cd assets && PLAYWRIGHT_BROWSERS_PATH=node_modules/playwright-core/.local-browsers npx playwright test --config=../playwright.config.ts $(filter-out $@,$(MAKECMDGOALS))
|
||||
|
||||
# Run Playwright tests with UI mode (headed browser)
|
||||
playwright-ui:
|
||||
cd assets && PLAYWRIGHT_BROWSERS_PATH=node_modules/playwright-core/.local-browsers npx playwright test --config=../playwright.config.ts --ui
|
||||
|
||||
# Show last Playwright HTML report
|
||||
playwright-report:
|
||||
cd assets && npx playwright show-report
|
||||
|
||||
# Start Docker Chrome container for remote Playwright
|
||||
playwright-docker-start:
|
||||
docker compose -f docker-compose.chrome.yml up -d
|
||||
|
||||
# Stop Docker Chrome container
|
||||
playwright-docker-stop:
|
||||
docker compose -f docker-compose.chrome.yml down
|
||||
|
||||
# Run Playwright against Docker Chrome (container must be running)
|
||||
playwright-docker:
|
||||
cd assets && PLAYWRIGHT_BROWSERS_PATH=node_modules/playwright-core/.local-browsers npx playwright test --config=../playwright.docker.config.ts
|
||||
|
||||
# Run full end-to-end workflow: start Docker Chrome, run tests, stop container
|
||||
playwright-full: playwright-docker-start
|
||||
@echo "Waiting for Chrome to be ready..."
|
||||
@sleep 3
|
||||
cd assets && PLAYWRIGHT_BROWSERS_PATH=node_modules/playwright-core/.local-browsers npx playwright test --config=../playwright.docker.config.ts; \
|
||||
EXIT_CODE=$$?; \
|
||||
cd ..; \
|
||||
docker compose -f docker-compose.chrome.yml down; \
|
||||
exit $$EXIT_CODE
|
||||
|
||||
# =============================================================================
|
||||
# Static analysis
|
||||
# =============================================================================
|
||||
|
||||
# Run Credo static analysis
|
||||
credo:
|
||||
$(MISE_EXEC) mix credo --strict
|
||||
+70
-71
@@ -286,124 +286,123 @@ body { font-family: 'Source Sans 3', sans-serif; }
|
||||
}
|
||||
|
||||
/* highlight.js — Atom One Dark (dark theme) */
|
||||
.source-viewer code.hljs {
|
||||
.source-viewer .hljs {
|
||||
color: #abb2bf;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
.source-viewer code.hljs .hljs-comment,
|
||||
.source-viewer code.hljs .hljs-quote {
|
||||
.source-viewer .hljs .hljs-comment,
|
||||
.source-viewer .hljs .hljs-quote {
|
||||
color: #5c6370;
|
||||
font-style: italic;
|
||||
}
|
||||
.source-viewer code.hljs .hljs-doctag,
|
||||
.source-viewer code.hljs .hljs-keyword,
|
||||
.source-viewer code.hljs .hljs-formula {
|
||||
.source-viewer .hljs .hljs-doctag,
|
||||
.source-viewer .hljs .hljs-keyword,
|
||||
.source-viewer .hljs .hljs-formula {
|
||||
color: #c678dd;
|
||||
}
|
||||
.source-viewer code.hljs .hljs-section,
|
||||
.source-viewer code.hljs .hljs-name,
|
||||
.source-viewer code.hljs .hljs-selector-tag,
|
||||
.source-viewer code.hljs .hljs-deletion,
|
||||
.source-viewer code.hljs .hljs-subst {
|
||||
.source-viewer .hljs .hljs-section,
|
||||
.source-viewer .hljs .hljs-name,
|
||||
.source-viewer .hljs .hljs-selector-tag,
|
||||
.source-viewer .hljs .hljs-deletion,
|
||||
.source-viewer .hljs .hljs-subst {
|
||||
color: #e06c75;
|
||||
}
|
||||
.source-viewer code.hljs .hljs-literal {
|
||||
.source-viewer .hljs .hljs-literal {
|
||||
color: #56b6c2;
|
||||
}
|
||||
.source-viewer code.hljs .hljs-string,
|
||||
.source-viewer code.hljs .hljs-regexp,
|
||||
.source-viewer code.hljs .hljs-addition,
|
||||
.source-viewer code.hljs .hljs-attribute,
|
||||
.source-viewer code.hljs .hljs-meta .hljs-string {
|
||||
.source-viewer .hljs .hljs-string,
|
||||
.source-viewer .hljs .hljs-regexp,
|
||||
.source-viewer .hljs .hljs-addition,
|
||||
.source-viewer .hljs .hljs-attribute,
|
||||
.source-viewer .hljs .hljs-meta .hljs-string {
|
||||
color: #98c379;
|
||||
}
|
||||
.source-viewer code.hljs .hljs-attr,
|
||||
.source-viewer code.hljs .hljs-variable,
|
||||
.source-viewer code.hljs .hljs-template-variable,
|
||||
.source-viewer code.hljs .hljs-type,
|
||||
.source-viewer code.hljs .hljs-selector-class,
|
||||
.source-viewer code.hljs .hljs-selector-attr,
|
||||
.source-viewer code.hljs .hljs-selector-pseudo,
|
||||
.source-viewer code.hljs .hljs-number {
|
||||
.source-viewer .hljs .hljs-attr,
|
||||
.source-viewer .hljs .hljs-variable,
|
||||
.source-viewer .hljs .hljs-template-variable,
|
||||
.source-viewer .hljs .hljs-type,
|
||||
.source-viewer .hljs .hljs-selector-class,
|
||||
.source-viewer .hljs .hljs-selector-attr,
|
||||
.source-viewer .hljs .hljs-selector-pseudo,
|
||||
.source-viewer .hljs .hljs-number {
|
||||
color: #d19a66;
|
||||
}
|
||||
.source-viewer code.hljs .hljs-symbol,
|
||||
.source-viewer code.hljs .hljs-link,
|
||||
.source-viewer code.hljs .hljs-meta,
|
||||
.source-viewer code.hljs .hljs-selector-id,
|
||||
.source-viewer code.hljs .hljs-title {
|
||||
.source-viewer .hljs .hljs-symbol,
|
||||
.source-viewer .hljs .hljs-link,
|
||||
.source-viewer .hljs .hljs-meta,
|
||||
.source-viewer .hljs .hljs-selector-id,
|
||||
.source-viewer .hljs .hljs-title {
|
||||
color: #61aeee;
|
||||
}
|
||||
.source-viewer code.hljs .hljs-built_in,
|
||||
.source-viewer code.hljs .hljs-title.class_,
|
||||
.source-viewer code.hljs .hljs-class .hljs-title {
|
||||
.source-viewer .hljs .hljs-built_in,
|
||||
.source-viewer .hljs .hljs-title.class_,
|
||||
.source-viewer .hljs .hljs-class .hljs-title {
|
||||
color: #e6c07b;
|
||||
}
|
||||
.source-viewer code.hljs .hljs-emphasis {
|
||||
.source-viewer .hljs .hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
.source-viewer code.hljs .hljs-strong {
|
||||
.source-viewer .hljs .hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* highlight.js — Atom One Light (light theme) */
|
||||
[data-theme="light"] .source-viewer code.hljs {
|
||||
[data-theme="light"] .source-viewer .hljs {
|
||||
color: #383a42;
|
||||
background: transparent;
|
||||
}
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-comment,
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-quote {
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-comment,
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-quote {
|
||||
color: #a0a1a7;
|
||||
font-style: italic;
|
||||
}
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-doctag,
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-keyword,
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-formula {
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-doctag,
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-keyword,
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-formula {
|
||||
color: #a626a4;
|
||||
}
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-section,
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-name,
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-selector-tag,
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-deletion,
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-subst {
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-section,
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-name,
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-selector-tag,
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-deletion,
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-subst {
|
||||
color: #e45649;
|
||||
}
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-literal {
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-literal {
|
||||
color: #0184bc;
|
||||
}
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-string,
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-regexp,
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-addition,
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-attribute,
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-meta .hljs-string {
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-string,
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-regexp,
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-addition,
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-attribute,
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-meta .hljs-string {
|
||||
color: #50a14f;
|
||||
}
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-attr,
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-variable,
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-template-variable,
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-type,
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-selector-class,
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-selector-attr,
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-selector-pseudo,
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-number {
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-attr,
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-variable,
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-template-variable,
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-type,
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-selector-class,
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-selector-attr,
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-selector-pseudo,
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-number {
|
||||
color: #986801;
|
||||
}
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-symbol,
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-link,
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-meta,
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-selector-id,
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-title {
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-symbol,
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-link,
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-meta,
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-selector-id,
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-title {
|
||||
color: #4078f2;
|
||||
}
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-built_in,
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-title.class_,
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-class .hljs-title {
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-built_in,
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-title.class_,
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-class .hljs-title {
|
||||
color: #c18401;
|
||||
}
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-emphasis {
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
[data-theme="light"] .source-viewer code.hljs .hljs-strong {
|
||||
[data-theme="light"] .source-viewer .hljs .hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Attached to the `#source-viewer` div via `phx-hook="SourceViewer"`.
|
||||
* Reads `data-highlighted-line` to find the target line element and
|
||||
* scrolls it into view with a smooth animation.
|
||||
* Also applies highlight.js syntax highlighting to code blocks.
|
||||
* Applies highlight.js syntax highlighting to individual line content elements.
|
||||
*/
|
||||
import hljs from "highlight.js/lib/core"
|
||||
|
||||
@@ -20,10 +20,33 @@ export const SourceViewer = {
|
||||
},
|
||||
|
||||
applySyntaxHighlighting() {
|
||||
const codeEl = this.el.querySelector("code")
|
||||
if (codeEl && !codeEl.querySelector(".hljs")) {
|
||||
hljs.highlightElement(codeEl)
|
||||
const container = this.el.querySelector(".sv-lines")
|
||||
if (!container || container.querySelector(".hljs-on")) return;
|
||||
|
||||
// Collect the raw text from all line content spans
|
||||
const lineContentEls = container.querySelectorAll(".sv-line-content")
|
||||
if (lineContentEls.length === 0) return;
|
||||
|
||||
const rawText = Array.from(lineContentEls).map(el => el.textContent).join("\n")
|
||||
|
||||
// Create a temp element for highlight.js to process
|
||||
const temp = document.createElement("code")
|
||||
temp.textContent = rawText
|
||||
hljs.highlightElement(temp)
|
||||
|
||||
// Split the highlighted HTML back into lines
|
||||
// highlight.js innerHTML uses actual newlines between lines of code
|
||||
const highlightedLines = temp.innerHTML.split("\n")
|
||||
|
||||
// Apply each highlighted line to the corresponding .sv-line-content
|
||||
lineContentEls.forEach((el, i) => {
|
||||
if (highlightedLines[i]) {
|
||||
el.innerHTML = highlightedLines[i]
|
||||
}
|
||||
})
|
||||
|
||||
// Mark as highlighted so we don't re-apply on subsequent updates
|
||||
container.classList.add("hljs", "hljs-on")
|
||||
},
|
||||
|
||||
scrollToHighlighted() {
|
||||
|
||||
@@ -4,12 +4,17 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"test:e2e": "playwright test --config=../playwright.config.ts",
|
||||
"test:e2e:ui": "playwright test --config=../playwright.config.ts --ui"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.60.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"highlight.js": "^11.11.1"
|
||||
}
|
||||
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Full e2e workflow: start Docker Chrome, run Playwright tests, stop container.
|
||||
#
|
||||
# Usage: ./bin/e2e-full.sh [extra playwright args...]
|
||||
#
|
||||
# Requires:
|
||||
# - Docker (colima or Docker Desktop running)
|
||||
# - Phoenix dev server running (default http://localhost:8056)
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
echo "=== Starting Chrome container ==="
|
||||
docker compose -f docker-compose.chrome.yml up -d
|
||||
|
||||
echo "=== Waiting for Chrome to be ready... ==="
|
||||
for i in $(seq 1 30); do
|
||||
if curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/ 2>/dev/null | grep -q 200; then
|
||||
echo "Chrome is ready!"
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq 30 ]; then
|
||||
echo "Timed out waiting for Chrome."
|
||||
docker compose -f docker-compose.chrome.yml logs --tail=20
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "=== Running Playwright e2e tests against Docker Chrome ==="
|
||||
cd assets && PLAYWRIGHT_BROWSERS_PATH=node_modules/playwright-core/.local-browsers \
|
||||
npx playwright test --config=../playwright.docker.config.ts "$@"
|
||||
|
||||
EXIT_CODE=$?
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
echo "=== Stopping Chrome container ==="
|
||||
docker compose -f docker-compose.chrome.yml down
|
||||
|
||||
exit $EXIT_CODE
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Helper script to start the Playwright Chrome container via Docker
|
||||
# Usage: ./bin/e2e-start.sh
|
||||
#
|
||||
# Starts a browserless/chrome container that Playwright connects to remotely.
|
||||
# The container runs Chrome with DevTools Protocol on port 9222.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
echo "Starting Chrome container for Playwright e2e tests..."
|
||||
echo "Using docker-compose.chrome.yml"
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
# Check if colima is needed (no Docker Desktop)
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
echo "Docker daemon not accessible. Have you started colima or Docker Desktop?"
|
||||
echo " colima start"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker compose -f docker-compose.chrome.yml up -d
|
||||
|
||||
echo ""
|
||||
echo "Chrome container started! Check status:"
|
||||
echo " docker compose -f docker-compose.chrome.yml ps"
|
||||
echo ""
|
||||
echo "Run tests:"
|
||||
echo " make playwright"
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Helper script to stop the Playwright Chrome container
|
||||
# Usage: ./bin/e2e-stop.sh
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
echo "Stopping Chrome container..."
|
||||
|
||||
docker compose -f docker-compose.chrome.yml down
|
||||
|
||||
echo "Chrome container stopped."
|
||||
@@ -0,0 +1,27 @@
|
||||
# Docker Compose for Playwright Chrome container
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f docker-compose.chrome.yml up -d # start Chrome
|
||||
# docker compose -f docker-compose.chrome.yml down # stop Chrome
|
||||
# make playwright # run tests against it
|
||||
#
|
||||
# The container exposes Chrome DevTools Protocol on port 9222.
|
||||
# Playwright connects via the websocket endpoint.
|
||||
|
||||
services:
|
||||
chrome:
|
||||
# browserless/chrome is self-contained with Chrome + CDP exposed
|
||||
# multi-arch (supports arm64 for Apple Silicon Macs)
|
||||
image: browserless/chrome:latest
|
||||
container_name: firehose-playwright-chrome
|
||||
ports:
|
||||
- "3000:3000" # browserless dashboard (not required but handy)
|
||||
environment:
|
||||
- CONNECTION_TIMEOUT=600000
|
||||
- MAX_CONCURRENT_SESSIONS=10
|
||||
- PREBOOT_CHROME=true
|
||||
- DEMO_MODE=false
|
||||
- ENABLE_CORS=true
|
||||
restart: unless-stopped
|
||||
# Run as non-root to avoid permission issues
|
||||
user: "1001:1001"
|
||||
@@ -7,8 +7,8 @@ defmodule FirehoseWeb.MicroprintsLive do
|
||||
@source_dirs ["app", "blogex"]
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
files = scan_source_files()
|
||||
def mount(params, _session, socket) do
|
||||
files = scan_source_files_with_dir()
|
||||
|
||||
microprints =
|
||||
files
|
||||
@@ -18,9 +18,7 @@ defmodule FirehoseWeb.MicroprintsLive do
|
||||
socket
|
||||
|> assign(:page_title, "Microprints")
|
||||
|> assign(:microprints, microprints)
|
||||
|> assign(:expanded_path, nil)
|
||||
|> assign(:highlighted_path, nil)
|
||||
|> assign(:highlighted_line, nil)}
|
||||
|> restore_state_from_params(params)}
|
||||
rescue
|
||||
e ->
|
||||
{:ok,
|
||||
@@ -28,11 +26,17 @@ defmodule FirehoseWeb.MicroprintsLive do
|
||||
|> assign(:page_title, "Microprints")
|
||||
|> assign(:microprints, [])
|
||||
|> assign(:expanded_path, nil)
|
||||
|> assign(:source_lines, nil)
|
||||
|> assign(:highlighted_path, nil)
|
||||
|> assign(:highlighted_line, nil)
|
||||
|> put_flash(:error, "Error loading microprints: #{inspect(e)}")}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(params, _uri, socket) do
|
||||
{:noreply, restore_state_from_params(socket, params)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
@@ -46,7 +50,7 @@ defmodule FirehoseWeb.MicroprintsLive do
|
||||
<.microprint_legend />
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mt-6">
|
||||
<%= for %{path: path, microprint: microprint, source: source} = item <- @microprints do %>
|
||||
<%= for %{path: path, microprint: microprint, source_dir: _source_dir} = item <- @microprints do %>
|
||||
{error = item[:error]}
|
||||
<div class="card bg-base-100 shadow-sm border border-zinc-200">
|
||||
<div class="card-body p-4">
|
||||
@@ -76,12 +80,12 @@ defmodule FirehoseWeb.MicroprintsLive do
|
||||
highlighted_line={@highlighted_line}
|
||||
/>
|
||||
|
||||
<%= if @expanded_path == path and source do %>
|
||||
<%= if @expanded_path == path and @source_lines do %>
|
||||
<.source_viewer
|
||||
content={source}
|
||||
source_lines={@source_lines}
|
||||
highlighted_line={@highlighted_line}
|
||||
language="elixir"
|
||||
id={"source-viewer-" <> path}
|
||||
file_path={path}
|
||||
/>
|
||||
<% end %>
|
||||
<% else %>
|
||||
@@ -105,10 +109,21 @@ defmodule FirehoseWeb.MicroprintsLive do
|
||||
_ -> String.to_integer(line)
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
# Collapse any currently expanded file when highlighting a different file
|
||||
expanded =
|
||||
case socket.assigns.expanded_path do
|
||||
^path -> socket.assigns.expanded_path
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:highlighted_path, path)
|
||||
|> assign(:highlighted_line, highlighted)}
|
||||
|> assign(:highlighted_line, highlighted)
|
||||
|> assign(:expanded_path, expanded)
|
||||
|> assign(:source_lines, nil)
|
||||
|
||||
{:noreply, push_patch(socket, to: build_params(socket))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
@@ -119,11 +134,78 @@ defmodule FirehoseWeb.MicroprintsLive do
|
||||
_ -> path
|
||||
end
|
||||
|
||||
{:noreply, assign(socket, :expanded_path, expanded)}
|
||||
source_lines =
|
||||
case expanded do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
^path ->
|
||||
item = Enum.find(socket.assigns.microprints, &(&1.path == path))
|
||||
|
||||
if item && item.source_dir do
|
||||
abs_path = resolve_source_path(item.source_dir, path)
|
||||
read_source(abs_path)
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
socket = socket |> assign(:expanded_path, expanded) |> assign(:source_lines, source_lines)
|
||||
|
||||
{:noreply, push_patch(socket, to: build_params(socket))}
|
||||
end
|
||||
|
||||
# Private helpers
|
||||
|
||||
defp restore_state_from_params(socket, params) do
|
||||
expanded_path = params["expanded"]
|
||||
highlighted_path = params["highlighted"]
|
||||
highlighted_line = if params["line"], do: String.to_integer(params["line"]), else: nil
|
||||
|
||||
source_lines =
|
||||
if expanded_path do
|
||||
item = Enum.find(socket.assigns.microprints, &(&1.path == expanded_path))
|
||||
|
||||
if item && item.source_dir do
|
||||
abs_path = resolve_source_path(item.source_dir, expanded_path)
|
||||
read_source(abs_path)
|
||||
end
|
||||
end
|
||||
|
||||
socket
|
||||
|> assign(:expanded_path, expanded_path)
|
||||
|> assign(:source_lines, source_lines)
|
||||
|> assign(:highlighted_path, highlighted_path)
|
||||
|> assign(:highlighted_line, highlighted_line)
|
||||
end
|
||||
|
||||
defp build_params(socket) do
|
||||
params = %{}
|
||||
|
||||
params =
|
||||
if socket.assigns.expanded_path do
|
||||
Map.put(params, "expanded", socket.assigns.expanded_path)
|
||||
else
|
||||
params
|
||||
end
|
||||
|
||||
params =
|
||||
if socket.assigns.highlighted_path do
|
||||
Map.put(params, "highlighted", socket.assigns.highlighted_path)
|
||||
else
|
||||
params
|
||||
end
|
||||
|
||||
params =
|
||||
if socket.assigns.highlighted_line do
|
||||
params |> Map.put("line", socket.assigns.highlighted_line)
|
||||
else
|
||||
params
|
||||
end
|
||||
|
||||
"/microprints" <> if params != %{}, do: "?" <> URI.encode_query(params), else: ""
|
||||
end
|
||||
|
||||
defp sort_by_mtime(files) do
|
||||
app_root = Mix.Project.project_file() |> Path.dirname()
|
||||
|
||||
@@ -136,16 +218,39 @@ defmodule FirehoseWeb.MicroprintsLive do
|
||||
|> Enum.map(fn {file, _mtime} -> file end)
|
||||
end
|
||||
|
||||
defp sort_by_mtime_with_dir(tuples) do
|
||||
app_root = Mix.Project.project_file() |> Path.dirname()
|
||||
|
||||
tuples
|
||||
|> Enum.map(fn {path, dir} ->
|
||||
abs_path = Path.join(app_root, path)
|
||||
{{path, dir}, File.stat!(abs_path).mtime}
|
||||
end)
|
||||
|> Enum.sort_by(fn {{_path, _dir}, mtime} -> mtime end, :desc)
|
||||
|> Enum.map(fn {{path, dir}, _mtime} -> {path, dir} end)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def scan_source_files do
|
||||
@source_dirs
|
||||
|> Enum.flat_map(&collect_elixir_files/1)
|
||||
|> Enum.uniq()
|
||||
|> Enum.uniq_by(&elem(&1, 0))
|
||||
|> Enum.map(&elem(&1, 0))
|
||||
|> Enum.sort()
|
||||
|> sort_by_mtime()
|
||||
|> Enum.take(2)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def scan_source_files_with_dir do
|
||||
@source_dirs
|
||||
|> Enum.flat_map(&collect_elixir_files/1)
|
||||
|> Enum.uniq_by(&elem(&1, 0))
|
||||
|> Enum.sort_by(&elem(&1, 0))
|
||||
|> sort_by_mtime_with_dir()
|
||||
|> Enum.take(2)
|
||||
end
|
||||
|
||||
defp resolve_absolute_paths(files) do
|
||||
app_root = Mix.Project.project_file() |> Path.dirname()
|
||||
|
||||
@@ -174,26 +279,26 @@ defmodule FirehoseWeb.MicroprintsLive do
|
||||
|> Path.wildcard()
|
||||
|> Enum.filter(&File.regular?(&1))
|
||||
|> Enum.reject(&excluded_path?/1)
|
||||
|> Enum.map(&format_path(&1, monorepo_root, dir))
|
||||
|> Enum.map(&format_path_and_dir(&1, monorepo_root, dir))
|
||||
|
||||
false ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp format_path(path, monorepo_root, dir) do
|
||||
defp format_path_and_dir(path, monorepo_root, dir) do
|
||||
relative = Path.relative_to(path, monorepo_root)
|
||||
|
||||
formatted =
|
||||
case dir do
|
||||
"app" ->
|
||||
String.replace_prefix(relative, "app/", "")
|
||||
|
||||
_ ->
|
||||
"../" <> relative
|
||||
end
|
||||
"app" -> String.replace_prefix(relative, "app/", "")
|
||||
_ -> "../" <> relative
|
||||
end
|
||||
|
||||
defp process_file(rel_path) do
|
||||
{formatted, dir}
|
||||
end
|
||||
|
||||
defp process_file({rel_path, source_dir}) do
|
||||
abs_path = resolve_absolute_paths([rel_path]) |> List.first()
|
||||
|
||||
case MicroprintCache.get_microprint(abs_path) do
|
||||
@@ -204,13 +309,10 @@ defmodule FirehoseWeb.MicroprintsLive do
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.map(fn {line, num} -> Map.put(line, :line_number, num) end)
|
||||
|
||||
# Read source code for expand/contract
|
||||
source = read_source(abs_path)
|
||||
|
||||
%{
|
||||
path: rel_path,
|
||||
microprint: Map.put(microprint, :lines, lines_with_numbers),
|
||||
source: source
|
||||
source_dir: source_dir,
|
||||
microprint: Map.put(microprint, :lines, lines_with_numbers)
|
||||
}
|
||||
|
||||
{:error, reason} ->
|
||||
@@ -218,20 +320,55 @@ defmodule FirehoseWeb.MicroprintsLive do
|
||||
end
|
||||
end
|
||||
|
||||
defp resolve_source_path(source_dir, rel_path) do
|
||||
app_root = Mix.Project.project_file() |> Path.dirname()
|
||||
monorepo_root = app_root |> Path.dirname()
|
||||
base = Path.join(monorepo_root, source_dir)
|
||||
Path.join(base, rel_path)
|
||||
end
|
||||
|
||||
defp read_source(abs_path) do
|
||||
case File.read(abs_path) do
|
||||
{:ok, content} ->
|
||||
content
|
||||
|> String.replace("<", "<")
|
||||
|> String.replace(">", ">")
|
||||
|
||||
{:error, _} ->
|
||||
nil
|
||||
{:ok, content} -> String.split(content, "\n")
|
||||
{:error, _} -> nil
|
||||
end
|
||||
end
|
||||
|
||||
# Delegate to MicroprintComponent
|
||||
defdelegate microprint(assigns), to: MicroprintComponent
|
||||
defdelegate microprint_legend(assigns), to: MicroprintComponent
|
||||
defdelegate source_viewer(assigns), to: MicroprintComponent
|
||||
|
||||
# Custom source_viewer with unique DOM IDs per file to prevent LiveView
|
||||
# DOM patching bugs when switching expanded files.
|
||||
def source_viewer(assigns) do
|
||||
assigns =
|
||||
assign(
|
||||
assigns,
|
||||
:viewer_id,
|
||||
"source-viewer-" <> Integer.to_string(:erlang.phash2(assigns.file_path, 1_000_000))
|
||||
)
|
||||
|
||||
~H"""
|
||||
<div
|
||||
id={@viewer_id}
|
||||
class="source-viewer mt-2 max-h-96 overflow-auto rounded font-mono text-xs"
|
||||
phx-hook="SourceViewer"
|
||||
data-highlighted-line={@highlighted_line}
|
||||
data-language={@language}
|
||||
style="background: var(--sv-bg); color: var(--sv-text);"
|
||||
>
|
||||
<div class="sv-lines p-2">
|
||||
<%= for {line, num} <- Enum.with_index(@source_lines || [], 1) do %>
|
||||
<div
|
||||
id={"line-#{num}"}
|
||||
class={"sv-line" <> if @highlighted_line == num, do: " sv-line-highlighted", else: ""}
|
||||
>
|
||||
<span class="sv-line-number">{num}</span>
|
||||
<span class="sv-line-content">{line}</span>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
// Default Playwright e2e configuration.
|
||||
//
|
||||
// Uses Firefox by default (Playwright Chromium crashes on macOS 15
|
||||
// due to Crashpad permission restrictions).
|
||||
//
|
||||
// ## Usage
|
||||
//
|
||||
// make playwright # run with default browser (Firefox)
|
||||
// PLAYWRIGHT_BROWSER=chromium make playwright
|
||||
// PLAYWRIGHT_BROWSER=webkit make playwright
|
||||
//
|
||||
// # Run a single test file:
|
||||
// PLAYWRIGHT_BROWSERS_PATH=assets/node_modules/playwright-core/.local-browsers \
|
||||
// npx playwright test --config=playwright.config.ts test/e2e/smoke.spec.ts
|
||||
//
|
||||
// ## Docker Chrome (alternative)
|
||||
//
|
||||
// Use playwright.docker.config.ts instead of this config:
|
||||
//
|
||||
// docker compose -f docker-compose.chrome.yml up -d
|
||||
// npx playwright test --config=playwright.docker.config.ts
|
||||
|
||||
const browserEnv = process.env.PLAYWRIGHT_BROWSER || "firefox";
|
||||
|
||||
const deviceFor = (name: string) => {
|
||||
switch (name) {
|
||||
case "chromium": return devices["Desktop Chrome"];
|
||||
case "firefox": return devices["Desktop Firefox"];
|
||||
case "webkit": return devices["Desktop Safari"];
|
||||
default:
|
||||
console.warn(`Unknown browser "${name}", falling back to Firefox`);
|
||||
return devices["Desktop Firefox"];
|
||||
}
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./test/e2e",
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: "list",
|
||||
|
||||
use: {
|
||||
baseURL: process.env.PLAYWRIGHT_BASE_URL || "http://localhost:8056",
|
||||
trace: "on-first-retry",
|
||||
headless: process.env.PLAYWRIGHT_HEADLESS !== "false",
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: browserEnv,
|
||||
use: { ...deviceFor(browserEnv) },
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
// Docker Chrome Playwright configuration.
|
||||
// Connects to a remote browserless/chrome container via CDP.
|
||||
//
|
||||
// Usage:
|
||||
// 1. Start the Chrome container:
|
||||
// docker compose -f docker-compose.chrome.yml up -d
|
||||
//
|
||||
// 2. Run tests:
|
||||
// npx playwright test --config=playwright.docker.config.ts
|
||||
|
||||
const CDP_URL = process.env.PLAYWRIGHT_CDP_URL || "http://localhost:3000";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./test/e2e",
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: "list",
|
||||
|
||||
use: {
|
||||
baseURL: process.env.PLAYWRIGHT_BASE_URL || "http://localhost:8056",
|
||||
trace: "on-first-retry",
|
||||
headless: process.env.PLAYWRIGHT_HEADLESS !== "false",
|
||||
},
|
||||
|
||||
// Connect to remote Chrome via CDP instead of launching locally.
|
||||
// browserless/chrome exposes CDP at port 3000.
|
||||
projects: [
|
||||
{
|
||||
name: "docker-chrome",
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
launchOptions: {
|
||||
// Not used for remote connection, but placeholder for clarity
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -3,7 +3,7 @@
|
||||
author: "Willem van den Ende",
|
||||
tags: ~w(AI Reflection),
|
||||
description: "In which the writer reflects on believing AI is conscious",
|
||||
published: false
|
||||
published: true
|
||||
}
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Auth helpers for Playwright e2e tests.
|
||||
*
|
||||
* Provides utilities for logging in as a test user.
|
||||
* The app uses email-only registration (magic-link flow for initial confirmation)
|
||||
* and email+password for subsequent logins.
|
||||
*/
|
||||
|
||||
import { Page } from "@playwright/test";
|
||||
|
||||
export const TEST_USER = {
|
||||
email: process.env.PLAYWRIGHT_EMAIL || "test@firehose.test",
|
||||
password: process.env.PLAYWRIGHT_PASSWORD || "testpassword123!",
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigate to the login page, fill in credentials, and submit.
|
||||
* Returns the page after successful login (redirected to home).
|
||||
*/
|
||||
export async function loginAsTestUser(page: Page) {
|
||||
await page.goto("/users/log-in");
|
||||
// Use the password-based login form
|
||||
await page.getByLabel("Email").fill(TEST_USER.email);
|
||||
await page.getByLabel("Password").fill(TEST_USER.password);
|
||||
// Click "Log in and stay logged in"
|
||||
await page.getByRole("button", { name: /log in and stay logged in/i }).click();
|
||||
await page.waitForURL("/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the registration page, fill in email, and submit.
|
||||
* Note: In dev mode, registration is invite-only by default. Set
|
||||
* `PLAYWRIGHT_ALLOW_REGISTRATION=true` to run tests against a server
|
||||
* with open registration enabled.
|
||||
*/
|
||||
export async function registerTestUser(page: Page) {
|
||||
await page.goto("/users/register");
|
||||
await page.getByLabel("Email").fill(TEST_USER.email);
|
||||
await page.getByRole("button", { name: /create an account/i }).click();
|
||||
await page.waitForURL("/users/log-in");
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { test, expect } from "./fixtures";
|
||||
import { loginAsTestUser } from "./auth-helpers";
|
||||
|
||||
test.describe("Authenticated pages", () => {
|
||||
test("redirects unauthenticated users to login page", async ({ page }) => {
|
||||
const response = await page.goto("/editor/dashboard");
|
||||
// Should redirect to login
|
||||
expect(response?.url()).toContain("/users/log-in");
|
||||
});
|
||||
|
||||
test("can log in and access the editor dashboard", async ({ page }) => {
|
||||
await loginAsTestUser(page);
|
||||
|
||||
// After login, we should be on the home page
|
||||
await expect(page).toHaveURL("/");
|
||||
await expect(page.locator("body")).not.toBeEmpty();
|
||||
});
|
||||
|
||||
test("can log out", async ({ page }) => {
|
||||
await loginAsTestUser(page);
|
||||
|
||||
// Now log out
|
||||
await page.goto("/users/log-out");
|
||||
|
||||
// Should redirect to home
|
||||
await expect(page).toHaveURL("/");
|
||||
|
||||
// Try accessing an authenticated page
|
||||
await page.goto("/editor/dashboard");
|
||||
// Should redirect to login
|
||||
await expect(page).toHaveURL(/\/users\/log-in/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Custom Playwright test fixtures.
|
||||
*
|
||||
* Provides a `test` function that connects to a remote Chrome via CDP
|
||||
* when PLAYWRIGHT_CDP_URL is set (Docker Chrome mode).
|
||||
* Falls back to the default local browser otherwise.
|
||||
*/
|
||||
|
||||
import { test as base, chromium } from "@playwright/test";
|
||||
|
||||
type Fixtures = {
|
||||
// Browser is provided by Playwright Test by default.
|
||||
// We only override it when connecting to remote Docker Chrome.
|
||||
};
|
||||
|
||||
const cdpUrl = process.env.PLAYWRIGHT_CDP_URL;
|
||||
|
||||
export const test = cdpUrl
|
||||
? base.extend<Fixtures>({
|
||||
browser: async ({}, use) => {
|
||||
const browser = await chromium.connectOverCDP(cdpUrl);
|
||||
await use(browser);
|
||||
await browser.close();
|
||||
},
|
||||
})
|
||||
: base;
|
||||
|
||||
export { expect } from "@playwright/test";
|
||||
@@ -0,0 +1,29 @@
|
||||
import { test, expect } from "./fixtures";
|
||||
|
||||
test.describe("Connectivity smoke test", () => {
|
||||
test("visits the homepage and verifies it loads", async ({ page }) => {
|
||||
const response = await page.goto("/");
|
||||
expect(response?.ok()).toBeTruthy();
|
||||
|
||||
// Verify the page has content
|
||||
await expect(page).toHaveTitle(/Firehose/);
|
||||
});
|
||||
|
||||
test("visits the contact page", async ({ page }) => {
|
||||
const response = await page.goto("/contact");
|
||||
expect(response?.ok()).toBeTruthy();
|
||||
await expect(page.locator("body")).not.toBeEmpty();
|
||||
});
|
||||
|
||||
test("visits the blog engineering index", async ({ page }) => {
|
||||
const response = await page.goto("/blog/engineering");
|
||||
expect(response?.ok()).toBeTruthy();
|
||||
await expect(page.locator("body")).not.toBeEmpty();
|
||||
});
|
||||
|
||||
test("visits the blog releases index", async ({ page }) => {
|
||||
const response = await page.goto("/blog/releases");
|
||||
expect(response?.ok()).toBeTruthy();
|
||||
await expect(page.locator("body")).not.toBeEmpty();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,9 @@
|
||||
alias FirehoseWeb.MicroprintsLive
|
||||
|
||||
defmodule FirehoseWeb.MicroprintsLiveTest do
|
||||
use ExUnit.Case, async: true
|
||||
use FirehoseWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
describe "scan_source_files/0" do
|
||||
test "returns exactly 2 files" do
|
||||
@@ -64,4 +68,122 @@ defmodule FirehoseWeb.MicroprintsLiveTest do
|
||||
assert is_list(blogex_files)
|
||||
end
|
||||
end
|
||||
|
||||
describe "expanded_path and highlighted_path coupling" do
|
||||
test "expanding file A and highlighting a line in file B should collapse file A", %{
|
||||
conn: conn
|
||||
} do
|
||||
files = MicroprintsLive.scan_source_files()
|
||||
assert length(files) >= 2, "Need at least 2 files to test coupling"
|
||||
|
||||
[file_a, file_b | _] = files
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/microprints")
|
||||
|
||||
# Expand file A
|
||||
view
|
||||
|> element("button[phx-value-path=\"#{file_a}\"]", "Expand")
|
||||
|> render_click()
|
||||
|
||||
# Verify file A shows "Collapse" button (it's expanded)
|
||||
html = render(view)
|
||||
assert html =~ ~s(phx-value-path="#{file_a}")
|
||||
assert html =~ "Collapse"
|
||||
|
||||
# Highlight a line in file B (rect elements inside SVG)
|
||||
view
|
||||
|> element(~s(svg rect[phx-value-line="1"][phx-value-path="#{file_b}"]))
|
||||
|> render_click()
|
||||
|
||||
# file A should be collapsed when highlighting a different file
|
||||
html = render(view)
|
||||
|
||||
refute html =~ "Collapse",
|
||||
"file A should be collapsed after highlighting a different file, but the Collapse button is still visible (expanded_path is uncoupled from highlighted_path)"
|
||||
end
|
||||
|
||||
test "switching expand from file A to file B shows each file's own source content", %{
|
||||
conn: conn
|
||||
} do
|
||||
files = MicroprintsLive.scan_source_files()
|
||||
assert length(files) >= 2, "Need at least 2 files to test source switching"
|
||||
|
||||
[file_a, file_b | _] = files
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/microprints")
|
||||
|
||||
# Expand file A
|
||||
view
|
||||
|> element("button[phx-value-path=\"#{file_a}\"]", "Expand")
|
||||
|> render_click()
|
||||
|
||||
# Capture file A's source content from the rendered HTML
|
||||
html_a = render(view)
|
||||
# Each .ex file starts with "defmodule", grab the first defmodule line as unique content
|
||||
assert html_a =~ "defmodule",
|
||||
"File A source should be visible when expanded"
|
||||
|
||||
# Expand file B (should auto-collapse A)
|
||||
view
|
||||
|> element("button[phx-value-path=\"#{file_b}\"]", "Expand")
|
||||
|> render_click()
|
||||
|
||||
# Capture file B's source content
|
||||
html_b = render(view)
|
||||
|
||||
# The two source viewers must show different content
|
||||
refute html_a == html_b,
|
||||
"Source content should differ when switching between files"
|
||||
|
||||
# File A's button should now say "Expand" (collapsed)
|
||||
refute html_b =~ ~r(phx-value-path="#{Regex.escape(file_a)}"[^>]*>\s*Collapse\s*</),
|
||||
"file A should be collapsed after switching expand to file B"
|
||||
|
||||
# File B's button should say "Collapse" (expanded)
|
||||
assert html_b =~ ~r(phx-value-path="#{Regex.escape(file_b)}"[^>]*>\s*Collapse\s*</),
|
||||
"file B should show Collapse button when expanded"
|
||||
end
|
||||
end
|
||||
|
||||
describe "URL state persistence" do
|
||||
test "expanded path is restored from query param on mount", %{conn: conn} do
|
||||
files = MicroprintsLive.scan_source_files()
|
||||
file_a = List.first(files)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/microprints?expanded=#{file_a}")
|
||||
|
||||
html = render(view)
|
||||
|
||||
assert html =~ "Collapse",
|
||||
"Expanded file should show Collapse button when restored from URL param"
|
||||
|
||||
assert html =~ ~s(phx-value-path="#{file_a}")
|
||||
end
|
||||
|
||||
test "unknown expanded path in URL is ignored gracefully", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/microprints?expanded=nonexistent.ex")
|
||||
|
||||
html = render(view)
|
||||
|
||||
refute html =~ "Collapse",
|
||||
"Nonexistent expanded path should not show Collapse"
|
||||
end
|
||||
|
||||
test "expand updates URL with expanded param", %{conn: conn} do
|
||||
files = MicroprintsLive.scan_source_files()
|
||||
file_a = List.first(files)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/microprints")
|
||||
|
||||
view
|
||||
|> element("button[phx-value-path=\"#{file_a}\"]", "Expand")
|
||||
|> render_click()
|
||||
|
||||
# URL should contain expanded param after clicking Expand
|
||||
html = render(view)
|
||||
|
||||
assert html =~ "Collapse",
|
||||
"File should remain expanded after push_patch updates URL"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
defmodule FirehoseWeb.SourceViewerTest do
|
||||
use FirehoseWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias FirehoseWeb.MicroprintsLive
|
||||
|
||||
defp expand_file(view, path) do
|
||||
view
|
||||
|> element("button[phx-value-path=\"#{path}\"]", "Expand")
|
||||
|> render_click()
|
||||
end
|
||||
|
||||
defp click_microprint_rect(view, path, line) do
|
||||
render_click(view, "highlight_line", %{"line" => Integer.to_string(line), "path" => path})
|
||||
end
|
||||
|
||||
describe "line elements" do
|
||||
test "render with unique ids when file is expanded", %{conn: conn} do
|
||||
files = MicroprintsLive.scan_source_files()
|
||||
file = List.first(files)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/microprints")
|
||||
expand_file(view, file)
|
||||
|
||||
assert has_element?(view, "#line-1"),
|
||||
"Line 1 should exist in the source viewer"
|
||||
|
||||
assert has_element?(view, "#line-2"),
|
||||
"Line 2 should exist in the source viewer"
|
||||
|
||||
assert has_element?(view, "#line-3"),
|
||||
"Line 3 should exist in the source viewer"
|
||||
end
|
||||
end
|
||||
|
||||
describe "highlight class" do
|
||||
test "is added to line on microprint click", %{conn: conn} do
|
||||
files = MicroprintsLive.scan_source_files()
|
||||
file = List.first(files)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/microprints")
|
||||
expand_file(view, file)
|
||||
|
||||
click_microprint_rect(view, file, 1)
|
||||
|
||||
assert has_element?(view, "#line-1.sv-line-highlighted"),
|
||||
"Line 1 should have the highlighted class after click"
|
||||
end
|
||||
|
||||
test "is removed on re-clicking the same line", %{conn: conn} do
|
||||
files = MicroprintsLive.scan_source_files()
|
||||
file = List.first(files)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/microprints")
|
||||
expand_file(view, file)
|
||||
|
||||
click_microprint_rect(view, file, 1)
|
||||
|
||||
assert has_element?(view, "#line-1.sv-line-highlighted"),
|
||||
"Line 1 should be highlighted after first click"
|
||||
|
||||
click_microprint_rect(view, file, 1)
|
||||
|
||||
refute has_element?(view, "#line-1.sv-line-highlighted"),
|
||||
"Line 1 should no longer be highlighted after second click"
|
||||
end
|
||||
|
||||
test "persists when expand is restored from URL params", %{conn: conn} do
|
||||
files = MicroprintsLive.scan_source_files()
|
||||
file = List.first(files)
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, ~p"/microprints?expanded=#{file}&line=1&highlighted=#{file}")
|
||||
|
||||
assert has_element?(view, "#line-1.sv-line-highlighted"),
|
||||
"Line 1 should be highlighted when restored from URL params"
|
||||
end
|
||||
end
|
||||
|
||||
describe "different file highlighting" do
|
||||
test "collapses file A and removes its source viewer when highlighting file B", %{conn: conn} do
|
||||
files = MicroprintsLive.scan_source_files()
|
||||
assert length(files) >= 2, "Need at least 2 files to test different-file highlighting"
|
||||
|
||||
[file_a, file_b | _] = files
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/microprints")
|
||||
|
||||
expand_file(view, file_a)
|
||||
click_microprint_rect(view, file_a, 1)
|
||||
|
||||
assert has_element?(view, "#line-1.sv-line-highlighted"),
|
||||
"Line 1 in file A should be highlighted before switching"
|
||||
|
||||
click_microprint_rect(view, file_b, 2)
|
||||
|
||||
refute has_element?(view, "#line-1"),
|
||||
"File A source viewer should be gone after highlighting a different file"
|
||||
end
|
||||
end
|
||||
end
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user