A music player that connects to your cloud/distributed storage.
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

test: more queue tests + fix browser test runner

+376 -35
+2 -1
_config.ts
··· 294 294 next: RequestHandler, 295 295 ): Promise<Response> { 296 296 const { pathname } = new URL(request.url); 297 - const isFacetHtml = pathname.endsWith(".html"); 297 + const isFacetHtml = pathname.endsWith(".html") && 298 + !pathname.startsWith("/testing/"); 298 299 const response = await next(request); 299 300 300 301 if (!isFacetHtml || !response.headers.get("content-type")?.includes("html")) {
+239
src/components/engine/queue/worker.js
··· 28 28 29 29 /** 30 30 * @type {Actions['add']} 31 + * 32 + * @example Appends tracks to the back of the queue 33 + * ```js 34 + * import { add, $future } from "~/components/engine/queue/worker.js"; 35 + * 36 + * add({ trackIds: ["a", "b"] }); 37 + * 38 + * if ($future.value.length !== 2) throw new Error("expected 2 items"); 39 + * if ($future.value[0].id !== "a") throw new Error("wrong first item"); 40 + * if ($future.value[1].id !== "b") throw new Error("wrong second item"); 41 + * if (!$future.value[0].manualEntry) throw new Error("items should be manualEntry: true"); 42 + * ``` 43 + * 44 + * @example Prepends tracks to the front with inFront: true 45 + * ```js 46 + * import { add, $future } from "~/components/engine/queue/worker.js"; 47 + * 48 + * add({ inFront: false, trackIds: ["c"] }); 49 + * add({ inFront: true, trackIds: ["a", "b"] }); 50 + * 51 + * if ($future.value[0].id !== "a") throw new Error("expected 'a' first"); 52 + * if ($future.value[1].id !== "b") throw new Error("expected 'b' second"); 53 + * if ($future.value[2].id !== "c") throw new Error("expected 'c' last"); 54 + * ``` 31 55 */ 32 56 export function add({ inFront, trackIds }) { 33 57 const items = trackIds.map((id) => { ··· 41 65 42 66 /** 43 67 * @type {Actions['clear']} 68 + * 69 + * @example Keeps manual entries when manualOnly is true 70 + * ```js 71 + * import { clear, $future } from "~/components/engine/queue/worker.js"; 72 + * 73 + * $future.value = [ 74 + * { id: "manual", manualEntry: true }, 75 + * { id: "auto", manualEntry: false }, 76 + * ]; 77 + * clear({ manualOnly: true }); 78 + * 79 + * if ($future.value.length !== 1) throw new Error("expected 1 item remaining"); 80 + * if ($future.value[0].id !== "manual") throw new Error("manual entry should remain"); 81 + * ``` 82 + * 83 + * @example Clears all items when manualOnly is false 84 + * ```js 85 + * import { clear, $future } from "~/components/engine/queue/worker.js"; 86 + * 87 + * $future.value = [ 88 + * { id: "manual", manualEntry: true }, 89 + * { id: "auto", manualEntry: false }, 90 + * ]; 91 + * clear({ manualOnly: false }); 92 + * 93 + * if ($future.value.length !== 0) throw new Error("expected empty queue"); 94 + * ``` 44 95 */ 45 96 export function clear({ manualOnly }) { 46 97 $future.value = manualOnly ··· 71 122 72 123 /** 73 124 * @type {Actions['supply']} 125 + * 126 + * @example Sets the track pool and computes a fingerprint 127 + * ```js 128 + * import { supply, $lake, $supplyFingerprint } from "~/components/engine/queue/worker.js"; 129 + * 130 + * supply({ trackIds: ["a", "b", "c"] }); 131 + * 132 + * if ($lake.value.join(",") !== "a,b,c") throw new Error("lake not set correctly"); 133 + * if (typeof $supplyFingerprint.value !== "string") throw new Error("fingerprint should be a string"); 134 + * ``` 135 + * 136 + * @example Returns undefined fingerprint for an empty supply 137 + * ```js 138 + * import { supply, $supplyFingerprint } from "~/components/engine/queue/worker.js"; 139 + * 140 + * supply({ trackIds: [] }); 141 + * 142 + * if ($supplyFingerprint.value !== undefined) throw new Error("fingerprint should be undefined for empty supply"); 143 + * ``` 144 + * 145 + * @example Same track IDs produce the same fingerprint 146 + * ```js 147 + * import { supply, $supplyFingerprint } from "~/components/engine/queue/worker.js"; 148 + * 149 + * supply({ trackIds: ["x", "y"] }); 150 + * const first = $supplyFingerprint.value; 151 + * 152 + * supply({ trackIds: ["x", "y"] }); 153 + * const second = $supplyFingerprint.value; 154 + * 155 + * if (first !== second) throw new Error("same tracks should produce the same fingerprint"); 156 + * ``` 74 157 */ 75 158 export function supply({ trackIds }) { 76 159 $lake.value = trackIds; ··· 81 164 82 165 /** 83 166 * @type {Actions['unshift']} 167 + * 168 + * @example Moves the last past item back to now, pushing now to the front of future 169 + * ```js 170 + * import { unshift, $future, $now, $past } from "~/components/engine/queue/worker.js"; 171 + * 172 + * $past.value = [{ id: "prev", manualEntry: false }]; 173 + * $now.value = { id: "current", manualEntry: false }; 174 + * $future.value = []; 175 + * 176 + * unshift(); 177 + * 178 + * if ($now.value?.id !== "prev") throw new Error("expected 'prev' as now"); 179 + * if ($past.value.length !== 0) throw new Error("expected empty past"); 180 + * if ($future.value[0]?.id !== "current") throw new Error("expected 'current' back at front of future"); 181 + * ``` 182 + * 183 + * @example Does nothing when past is empty 184 + * ```js 185 + * import { unshift, $now, $past } from "~/components/engine/queue/worker.js"; 186 + * 187 + * $past.value = []; 188 + * $now.value = { id: "current", manualEntry: false }; 189 + * 190 + * unshift(); 191 + * 192 + * if ($now.value?.id !== "current") throw new Error("now should remain unchanged"); 193 + * ``` 84 194 */ 85 195 export function unshift() { 86 196 const p = $past.value; ··· 162 272 * @param {number} fillAmount 163 273 * @param {Item[]} future 164 274 * @returns {Item[]} 275 + * 276 + * @example Fills sequentially from the start of the lake 277 + * ```js 278 + * import { fillSequentially, $lake, $now } from "~/components/engine/queue/worker.js"; 279 + * 280 + * $lake.value = ["a", "b", "c", "d"]; 281 + * $now.value = null; 282 + * 283 + * const result = fillSequentially(3, []); 284 + * 285 + * if (result.length !== 3) throw new Error("expected 3 items"); 286 + * if (result[0].id !== "a") throw new Error("expected to start from 'a'"); 287 + * if (result[1].id !== "b") throw new Error("expected 'b' second"); 288 + * if (result[2].id !== "c") throw new Error("expected 'c' third"); 289 + * if (result[0].manualEntry !== false) throw new Error("auto items should have manualEntry: false"); 290 + * ``` 291 + * 292 + * @example Continues from after the current now item 293 + * ```js 294 + * import { fillSequentially, $lake, $now } from "~/components/engine/queue/worker.js"; 295 + * 296 + * $lake.value = ["a", "b", "c", "d"]; 297 + * $now.value = { id: "b", manualEntry: false }; 298 + * 299 + * const result = fillSequentially(2, []); 300 + * 301 + * if (result[0].id !== "c") throw new Error("expected to start after now ('c')"); 302 + * if (result[1].id !== "d") throw new Error("expected 'd' second"); 303 + * ``` 304 + * 305 + * @example Wraps around to the beginning when reaching the end of the lake 306 + * ```js 307 + * import { fillSequentially, $lake, $now } from "~/components/engine/queue/worker.js"; 308 + * 309 + * $lake.value = ["a", "b", "c"]; 310 + * $now.value = { id: "b", manualEntry: false }; 311 + * 312 + * const result = fillSequentially(3, []); 313 + * 314 + * if (result[0].id !== "c") throw new Error("expected 'c'"); 315 + * if (result[1].id !== "a") throw new Error("expected wrap around to 'a'"); 316 + * if (result[2].id !== "b") throw new Error("expected 'b'"); 317 + * ``` 318 + * 319 + * @example Preserves existing manual entries 320 + * ```js 321 + * import { fillSequentially, $lake, $now } from "~/components/engine/queue/worker.js"; 322 + * 323 + * $lake.value = ["a", "b", "c"]; 324 + * $now.value = null; 325 + * 326 + * const future = [{ id: "manual", manualEntry: true }]; 327 + * const result = fillSequentially(2, future); 328 + * 329 + * if (result[0].id !== "manual") throw new Error("manual entry should be preserved"); 330 + * if (result.length !== 3) throw new Error("expected manual + 2 auto items"); 331 + * ``` 165 332 */ 166 333 export function fillSequentially(fillAmount, future) { 167 334 const onlyManual = future.filter((i) => i.manualEntry); ··· 195 362 * @param {Item[]} future 196 363 * @param {number} autoFutureCount 197 364 * @returns {Item[]} 365 + * 366 + * @example Adds shuffled items to reach the fill amount 367 + * ```js 368 + * import { fillShuffle, $lake, $past } from "~/components/engine/queue/worker.js"; 369 + * 370 + * $lake.value = ["a", "b", "c", "d", "e"]; 371 + * $past.value = []; 372 + * 373 + * const result = fillShuffle(3, [], 0); 374 + * 375 + * if (result.length !== 3) throw new Error("expected 3 items"); 376 + * if (!result.every((i) => i.manualEntry === false)) throw new Error("all items should be auto"); 377 + * ``` 378 + * 379 + * @example Only adds enough to reach the fill amount given existing auto items 380 + * ```js 381 + * import { fillShuffle, $lake, $past } from "~/components/engine/queue/worker.js"; 382 + * 383 + * $lake.value = ["a", "b", "c", "d"]; 384 + * $past.value = []; 385 + * 386 + * const existing = [ 387 + * { id: "x", manualEntry: false }, 388 + * { id: "y", manualEntry: false }, 389 + * ]; 390 + * 391 + * const result = fillShuffle(4, existing, 2); 392 + * 393 + * if (result.length !== 4) throw new Error("expected 4 total items (2 existing + 2 new)"); 394 + * ``` 198 395 */ 199 396 export function fillShuffle(fillAmount, future, autoFutureCount) { 200 397 // Determine pool of available queue items ··· 224 421 225 422 /** 226 423 * @param {Item[]} [future] 424 + * 425 + * @example Moves the first future item to now 426 + * ```ts 427 + * import { _shift, $future, $now } from "~/components/engine/queue/worker.js"; 428 + * import type { Item } from "./types.d.ts" 429 + * 430 + * $now.value = null as null | Item; 431 + * $future.value = [{ id: "a", manualEntry: false }, { id: "b", manualEntry: false }]; 432 + * 433 + * _shift(); 434 + * 435 + * if ($now.value?.id !== "a") throw new Error("expected 'a' as now"); 436 + * if ($future.value.length !== 1) throw new Error("expected 1 item remaining in future"); 437 + * if ($future.value[0].id !== "b") throw new Error("expected 'b' remaining in future"); 438 + * ``` 439 + * 440 + * @example Moves previous now to past 441 + * ```js 442 + * import { _shift, $future, $now, $past } from "~/components/engine/queue/worker.js"; 443 + * 444 + * $past.value = []; 445 + * $now.value = { id: "prev", manualEntry: false }; 446 + * $future.value = [{ id: "next", manualEntry: false }]; 447 + * 448 + * _shift(); 449 + * 450 + * if ($now.value?.id !== "next") throw new Error("expected 'next' as now"); 451 + * if ($past.value.length !== 1) throw new Error("expected 1 past item"); 452 + * if ($past.value[0].id !== "prev") throw new Error("expected 'prev' in past"); 453 + * ``` 454 + * 455 + * @example Does nothing when future is empty 456 + * ```js 457 + * import { _shift, $future, $now } from "~/components/engine/queue/worker.js"; 458 + * 459 + * $future.value = []; 460 + * $now.value = null; 461 + * 462 + * _shift(); 463 + * 464 + * if ($now.value !== null) throw new Error("now should remain null"); 465 + * ``` 227 466 */ 228 467 export function _shift(future) { 229 468 const n = $now.value;
+18 -16
src/testing/index.vto
··· 1 - --- 2 - layout: layouts/diffuse.vto 3 - 4 - base: "../" 5 - 6 - styles: 7 - - styles/base.css 8 - --- 1 + <html> 2 + <head> 3 + <base href="../" /> 4 + </head> 5 + <body> 6 + <script type="importmap"> 7 + { 8 + "imports": { 9 + "~/": "./", 9 10 10 - <script type="importmap"> 11 - { 12 - "imports": { 13 - "~/components/": "./components/", 14 - "@testing/": "./testing/" 15 - } 16 - } 17 - </script> 11 + "@atcute/tid": "./vendor/@atcute/tid/index.js", 12 + "@awesome.me/webawesome/dist/": "./vendor/@awesome.me/webawesome/", 13 + "@awesome.me/webawesome/dist-cdn/": "./vendor/@awesome.me/webawesome/", 14 + "@js-temporal/polyfill": "./vendor/@js-temporal/polyfill/index.js" 15 + } 16 + } 17 + </script> 18 + </body> 19 + </html>
+104 -5
tests/components/engine/queue/test.ts
··· 2 2 import { expect } from "@std/expect"; 3 3 4 4 import { testWeb } from "@tests/common/index.ts"; 5 - import { tracks } from "@testing/sample/tracks.js"; 5 + import { tracks } from "~/testing/sample/tracks.js"; 6 6 7 7 describe("components/engine/queue", () => { 8 8 it("adds tracks", async () => { ··· 12 12 13 13 document.body.append(engine); 14 14 15 - const { tracks } = await import("@testing/sample/tracks.js"); 15 + const { tracks } = await import("~/testing/sample/tracks.js"); 16 16 17 17 await engine.add({ trackIds: tracks.map((t) => t.id) }); 18 18 return engine.future(); ··· 31 31 32 32 document.body.append(engine); 33 33 34 - const { tracks } = await import("@testing/sample/tracks.js"); 34 + const { tracks } = await import("~/testing/sample/tracks.js"); 35 35 36 36 await engine.supply({ trackIds: tracks.map((t) => t.id) }); 37 37 await engine.fill({ amount: 1, shuffled: false }); ··· 51 51 52 52 document.body.append(engine); 53 53 54 - const { tracks } = await import("@testing/sample/tracks.js"); 54 + const { tracks } = await import("~/testing/sample/tracks.js"); 55 55 56 56 await engine.add({ trackIds: tracks.map((t) => t.id) }); 57 57 await engine.shift(); ··· 64 64 expect(item?.id).toBe(tracks[0].id); 65 65 }); 66 66 67 + it("adds tracks to the front with inFront: true", async () => { 68 + const items = await testWeb(async () => { 69 + const QueueEngine = await import("~/components/engine/queue/element.js"); 70 + const engine = new QueueEngine.CLASS(); 71 + 72 + document.body.append(engine); 73 + 74 + const { tracks } = await import("~/testing/sample/tracks.js"); 75 + 76 + await engine.add({ trackIds: [tracks[1].id] }); 77 + await engine.add({ inFront: true, trackIds: [tracks[0].id] }); 78 + return engine.future(); 79 + }); 80 + 81 + expect(items[0].id).toBe(tracks[0].id); 82 + expect(items[1].id).toBe(tracks[1].id); 83 + }); 84 + 85 + it("clears only auto-filled items when manualOnly is true", async () => { 86 + const items = await testWeb(async () => { 87 + const QueueEngine = await import("~/components/engine/queue/element.js"); 88 + const engine = new QueueEngine.CLASS(); 89 + 90 + document.body.append(engine); 91 + 92 + const { tracks } = await import("~/testing/sample/tracks.js"); 93 + 94 + await engine.supply({ trackIds: tracks.map((t) => t.id) }); 95 + await engine.add({ trackIds: [tracks[0].id] }); 96 + await engine.fill({ amount: 2, shuffled: false }); 97 + await engine.clear({ manualOnly: true }); 98 + return engine.future(); 99 + }); 100 + 101 + expect(items.length).toBe(1); 102 + expect(items[0].manualEntry).toBe(true); 103 + }); 104 + 105 + it("clears all items when manualOnly is false", async () => { 106 + const count = await testWeb(async () => { 107 + const QueueEngine = await import("~/components/engine/queue/element.js"); 108 + const engine = new QueueEngine.CLASS(); 109 + 110 + document.body.append(engine); 111 + 112 + const { tracks } = await import("~/testing/sample/tracks.js"); 113 + 114 + await engine.add({ trackIds: tracks.map((t) => t.id) }); 115 + await engine.clear({ manualOnly: false }); 116 + return (await engine.future()).length; 117 + }); 118 + 119 + expect(count).toBe(0); 120 + }); 121 + 122 + it("supply fingerprint is consistent for the same track IDs", async () => { 123 + const [fp1, fp2, fp3] = await testWeb(async () => { 124 + const QueueEngine = await import("~/components/engine/queue/element.js"); 125 + const engine = new QueueEngine.CLASS(); 126 + 127 + document.body.append(engine); 128 + 129 + const { tracks } = await import("~/testing/sample/tracks.js"); 130 + const trackIds = tracks.map((t) => t.id); 131 + 132 + await engine.supply({ trackIds }); 133 + const fp1 = await engine.supplyFingerprint(); 134 + 135 + await engine.supply({ trackIds }); 136 + const fp2 = await engine.supplyFingerprint(); 137 + 138 + await engine.supply({ trackIds: [trackIds[0]] }); 139 + const fp3 = await engine.supplyFingerprint(); 140 + 141 + return [fp1, fp2, fp3]; 142 + }); 143 + 144 + expect(fp1).toBe(fp2); 145 + expect(fp1).not.toBe(fp3); 146 + }); 147 + 148 + it("fill with augment adds on top of existing auto items", async () => { 149 + const count = await testWeb(async () => { 150 + const QueueEngine = await import("~/components/engine/queue/element.js"); 151 + const engine = new QueueEngine.CLASS(); 152 + 153 + document.body.append(engine); 154 + 155 + const { tracks } = await import("~/testing/sample/tracks.js"); 156 + 157 + await engine.supply({ trackIds: tracks.map((t) => t.id) }); 158 + await engine.fill({ amount: 2, shuffled: true }); 159 + await engine.fill({ augment: true, amount: 1, shuffled: true }); 160 + return (await engine.future()).length; 161 + }); 162 + 163 + expect(count).toBe(3); 164 + }); 165 + 67 166 it("[shared worker] has the correct past", async () => { 68 167 const item = await testWeb(async () => { 69 168 const QueueEngine = await import("~/components/engine/queue/element.js"); ··· 72 171 73 172 document.body.append(engine); 74 173 75 - const { tracks } = await import("@testing/sample/tracks.js"); 174 + const { tracks } = await import("~/testing/sample/tracks.js"); 76 175 77 176 await engine.add({ trackIds: tracks.map((t) => t.id) }); 78 177 await engine.shift();
+9 -9
tests/components/orchestrator/favourites/test.ts
··· 2 2 import { expect } from "@std/expect"; 3 3 4 4 import { testWeb } from "@tests/common/index.ts"; 5 - import { tracks } from "@testing/sample/tracks.js"; 5 + import { tracks } from "~/testing/sample/tracks.js"; 6 6 7 7 describe("components/orchestrator/favourites", () => { 8 8 it("includes tracks", async () => { ··· 13 13 const Favourites = await import( 14 14 "~/components/orchestrator/favourites/element.js" 15 15 ); 16 - const { tracks } = await import("@testing/sample/tracks.js"); 16 + const { tracks } = await import("~/testing/sample/tracks.js"); 17 17 18 18 const output = new Output.CLASS(); 19 19 output.id = "test-output"; ··· 44 44 const Favourites = await import( 45 45 "~/components/orchestrator/favourites/element.js" 46 46 ); 47 - const { tracks } = await import("@testing/sample/tracks.js"); 47 + const { tracks } = await import("~/testing/sample/tracks.js"); 48 48 49 49 const output = new Output.CLASS(); 50 50 output.id = "test-output"; ··· 77 77 const Favourites = await import( 78 78 "~/components/orchestrator/favourites/element.js" 79 79 ); 80 - const { tracks } = await import("@testing/sample/tracks.js"); 80 + const { tracks } = await import("~/testing/sample/tracks.js"); 81 81 82 82 const output = new Output.CLASS(); 83 83 output.id = "test-output"; ··· 107 107 const Favourites = await import( 108 108 "~/components/orchestrator/favourites/element.js" 109 109 ); 110 - const { tracks } = await import("@testing/sample/tracks.js"); 110 + const { tracks } = await import("~/testing/sample/tracks.js"); 111 111 112 112 const output = new Output.CLASS(); 113 113 output.id = "test-output"; ··· 144 144 const Favourites = await import( 145 145 "~/components/orchestrator/favourites/element.js" 146 146 ); 147 - const { tracks } = await import("@testing/sample/tracks.js"); 147 + const { tracks } = await import("~/testing/sample/tracks.js"); 148 148 149 149 const output = new Output.CLASS(); 150 150 output.id = "test-output"; ··· 176 176 const Favourites = await import( 177 177 "~/components/orchestrator/favourites/element.js" 178 178 ); 179 - const { tracks } = await import("@testing/sample/tracks.js"); 179 + const { tracks } = await import("~/testing/sample/tracks.js"); 180 180 181 181 const output = new Output.CLASS(); 182 182 output.id = "test-output"; ··· 213 213 const Favourites = await import( 214 214 "~/components/orchestrator/favourites/element.js" 215 215 ); 216 - const { tracks } = await import("@testing/sample/tracks.js"); 216 + const { tracks } = await import("~/testing/sample/tracks.js"); 217 217 218 218 const output = new Output.CLASS(); 219 219 output.id = "test-output"; ··· 252 252 const Favourites = await import( 253 253 "~/components/orchestrator/favourites/element.js" 254 254 ); 255 - const { tracks } = await import("@testing/sample/tracks.js"); 255 + const { tracks } = await import("~/testing/sample/tracks.js"); 256 256 257 257 const output = new Output.CLASS(); 258 258 output.id = "test-output";
+4 -4
tests/components/processor/search/test.ts
··· 2 2 import { expect } from "@std/expect"; 3 3 4 4 import { testWeb } from "@tests/common/index.ts"; 5 - import { trackA, trackB } from "@testing/sample/tracks.js"; 5 + import { trackA, trackB } from "~/testing/sample/tracks.js"; 6 6 7 7 describe("components/processor/search", () => { 8 8 it("finds tracks by album", async () => { ··· 15 15 document.body.append(processor); 16 16 17 17 // Add sample tracks to the supply first 18 - const { tracks } = await import("@testing/sample/tracks.js"); 18 + const { tracks } = await import("~/testing/sample/tracks.js"); 19 19 await processor.supply({ tracks }); 20 20 21 21 // Search for a specific term ··· 35 35 document.body.append(processor); 36 36 37 37 // Add sample tracks to the supply first 38 - const { tracks } = await import("@testing/sample/tracks.js"); 38 + const { tracks } = await import("~/testing/sample/tracks.js"); 39 39 await processor.supply({ tracks }); 40 40 41 41 // Search for a specific term ··· 55 55 document.body.append(processor); 56 56 57 57 // Add sample tracks to the supply first 58 - const { tracks } = await import("@testing/sample/tracks.js"); 58 + const { tracks } = await import("~/testing/sample/tracks.js"); 59 59 await processor.supply({ tracks }); 60 60 61 61 // Search for a specific term