Instant-replay game clipping for Linux. Forked from https://github.com/eklonofficial/Vice
0

Configure Feed

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

Update playlist tests for editable and deletable auto playlists

Andrew Marin (Jul 20, 2026, 5:54 PM -0500) 83f046bf 9365e132

+57 -13
+38 -7
tests/test_playlists.py
··· 63 63 self.assertRegex(bad["color1"], r"^#[0-9a-fA-F]{6}$") 64 64 self.assertRegex(bad["color2"], r"^#[0-9a-fA-F]{6}$") 65 65 66 - store.update_custom(p["id"], {"name": "Epic fails"}) 66 + store.update_playlist(p["id"], {"name": "Epic fails"}) 67 67 self.assertEqual(store.get(p["id"])["name"], "Epic fails") 68 68 with self.assertRaises(KeyError): 69 - store.update_custom("missing", {"name": "X"}) 69 + store.update_playlist("missing", {"name": "X"}) 70 70 71 71 store.delete(p["id"]) 72 72 self.assertIsNone(store.get(p["id"])) 73 73 74 - def test_auto_playlists_cannot_be_edited_or_deleted(self) -> None: 74 + def test_auto_playlists_can_be_edited_and_stay_auto(self) -> None: 75 + store = PlaylistStore(self.path) 76 + store.record_auto("Minecraft", "clip") 77 + 78 + edited = store.update_playlist("auto:minecraft", {"name": "MC", "emoji": "⛏️", "color1": "#123456"}) 79 + self.assertEqual(edited["name"], "MC") 80 + self.assertEqual(edited["emoji"], "⛏️") 81 + self.assertEqual(edited["kind"], "auto") # still auto: new clips keep filing in 82 + self.assertTrue(edited["edited"]) 83 + 84 + # A new clip of the same game still lands in the edited playlist and 85 + # does not overwrite the user's name. 86 + store.record_auto("Minecraft", "clip2") 87 + self.assertEqual(store.get("auto:minecraft")["name"], "MC") 88 + self.assertIn("clip2", store.get("auto:minecraft")["clip_slugs"]) 89 + 90 + def test_deleting_an_auto_playlist_sticks_across_restart(self) -> None: 91 + store = PlaylistStore(self.path) 92 + store.record_auto("Minecraft", "Vice_Clip_1_Minecraft") 93 + store.delete("auto:minecraft") 94 + self.assertIsNone(store.get("auto:minecraft")) 95 + 96 + # Backfill from the still-tagged clip must not resurrect it. 97 + reloaded = PlaylistStore(self.path) 98 + reloaded.backfill({"Vice_Clip_1_Minecraft"}, {}) 99 + self.assertIsNone(reloaded.get("auto:minecraft")) 100 + 101 + # But clipping the game again revives it. 102 + reloaded.record_auto("Minecraft", "Vice_Clip_2_Minecraft") 103 + self.assertIsNotNone(reloaded.get("auto:minecraft")) 104 + 105 + def test_edited_auto_playlist_is_not_pruned_when_empty(self) -> None: 75 106 store = PlaylistStore(self.path) 76 107 store.record_auto("Minecraft", "clip") 77 - with self.assertRaises(ValueError): 78 - store.update_custom("auto:minecraft", {"name": "X"}) 79 - with self.assertRaises(ValueError): 80 - store.delete("auto:minecraft") 108 + store.update_playlist("auto:minecraft", {"emoji": "⛏️"}) 109 + 110 + store.remove_clip("auto:minecraft", "clip") 111 + self.assertIsNotNone(store.get("auto:minecraft")) 81 112 82 113 def test_rename_sweeps_every_playlist(self) -> None: 83 114 store = PlaylistStore(self.path)
+10 -4
tests/test_share_server.py
··· 410 410 self.assertEqual(resp.status, 200) 411 411 self.assertNotIn(pid, [p["id"] for p in await self._playlists()]) 412 412 413 - async def test_auto_playlists_reject_edit_and_delete(self) -> None: 413 + async def test_auto_playlists_can_be_edited_and_deleted(self) -> None: 414 414 async with self.client.patch(f"{self.base}/api/playlists/auto:minecraft", 415 - json={"name": "Nope"}) as resp: 416 - self.assertEqual(resp.status, 400) 415 + json={"name": "MC", "emoji": "⛏️"}) as resp: 416 + self.assertEqual(resp.status, 200) 417 + edited = (await resp.json())["playlist"] 418 + self.assertEqual(edited["name"], "MC") 419 + self.assertEqual(edited["kind"], "auto") 420 + 417 421 async with self.client.delete(f"{self.base}/api/playlists/auto:minecraft") as resp: 418 - self.assertEqual(resp.status, 400) 422 + self.assertEqual(resp.status, 200) 423 + self.assertNotIn("auto:minecraft", [p["id"] for p in await self._playlists()]) 424 + 419 425 async with self.client.patch(f"{self.base}/api/playlists/missing", 420 426 json={"name": "X"}) as resp: 421 427 self.assertEqual(resp.status, 404)
+9 -2
tests/test_ui_static.py
··· 183 183 self.assertIn("onClipDragStart", clips_js) 184 184 for fn in ("onClipDragStart", "onClipDragEnd", "onPlaylistDragOver", "onPlaylistDrop"): 185 185 self.assertIn(f"function {fn}", playlists_js) 186 - # Auto playlists sort themselves by game, so only custom ones take drops. 187 - self.assertIn("pl.kind === 'custom' ?", playlists_js) 186 + # Every sidebar playlist row is a drop target, including auto ones. 187 + self.assertNotIn("pl.kind === 'custom' ?", playlists_js) 188 188 sidebar_css = (REPO_ROOT / "vice" / "ui" / "styles" / "sidebar.css").read_text() 189 189 self.assertIn(".side-pl-row.drop-over", sidebar_css) 190 + 191 + def test_auto_playlists_are_editable_and_deletable_in_ui(self) -> None: 192 + playlists_js = (REPO_ROOT / "vice" / "ui" / "scripts" / "playlists.js").read_text() 193 + # The header Edit/Delete controls and the edit/delete handlers must not 194 + # gate on kind === 'custom' anymore. 195 + self.assertNotIn("pl.kind !== 'custom'", playlists_js) 196 + self.assertNotIn("p.kind === 'custom'", playlists_js) 190 197 191 198 def test_clip_filename_template_is_wired(self) -> None: 192 199 self.assertIn('id="s-clip-name"', self.index)