diff --git a/.github/workflows/index.yml b/.github/workflows/index.yml index b862e14..1919704 100644 --- a/.github/workflows/index.yml +++ b/.github/workflows/index.yml @@ -16,5 +16,8 @@ jobs: - name: Verify index run: python3 scripts/build_index.py --check + - name: Test classmap contracts and promotion + run: python3 -m unittest discover -s scripts + - name: Validate exposure patches run: python3 scripts/validate_expose.py diff --git a/1030000/META.json b/1030000/META.json index 98ee0ea..a4782d0 100644 --- a/1030000/META.json +++ b/1030000/META.json @@ -15,7 +15,7 @@ "unresolved_missing": 0, "stale": 0, "cdp_hit_rate": 0.6389, - "overlay_entries": 280 + "overlay_entries": 284 }, "required_paths": { "main.playbar.buttons.button.wrapper": "verified_cdp", @@ -51,5 +51,50 @@ "deep_navigation_succeeded": 7, "css_overlay_conflicts": 0, "css_overlay_ambiguous_excluded": 82 + }, + "regression_checks": { + "topbar_right_button": { + "date": "2026-09-15", + "spotify_version": "1.3.0.277", + "path": "main.topbar.right.button_t.wrapper", + "class": "nR3ekhH3c1IU7dX9smcR", + "staged_class": "main-topBar-buddyFeed", + "before_hitbox_px": [ + 16, + 16 + ], + "after_hitbox_px": [ + 32, + 32 + ], + "native_hitbox_px": [ + 32, + 32 + ], + "gap_px": 8, + "verification": "Measured Bookmark, Full App Display, and Popup Lyrics after Rust apply and a client restart. Their hitboxes and gaps match the native notification and activity buttons. The unchanged 1.2.97 classmap resolves to the same semantic class, whose archived CSS also specifies 32px width and height." + }, + "root_containers": { + "date": "2026-09-15", + "spotify_version": "1.3.0.277", + "mappings": { + "bQetA_KiP9n0DQVcGa7m": "Root__globalNav", + "PIP22o58Crv8RXY4wB2o": "Root__nav-bar", + "itzKVxWhS4n59fp_KIVc": "Root__now-playing-bar" + }, + "verification": "Stock CSS and live child controls confirm the global-nav, left-sidebar, and now-playing-bar roles. After applying with Rust CLI 3.0.0-beta.17 on macOS Spotify 1.3.0.277, all three hooks exist and Text's Spicetify scheme paints them rgb(46, 40, 55). Navigation borders and spacing return; no Text CSS change was needed. Unthemed container rectangles, padding, borders, and backgrounds match the pre-apply measurements. Flow's player occupies its own 200px right column without overlapping the main view. Profile navigation, library sorting, the compact-library toggle and restoration, a Settings select, Bookmark, and the queue were exercised through pointer events. Module and native toolbar buttons retain 32px hitboxes and 8px gaps.", + "previous_version_check": { + "spotify_version": "1.2.94", + "method": "archived stock CSS inspection", + "verification": "The embedded CSS-map already maps qPaMr9Jzt0_Doy3C, vEHGULrufZMHBNrp, and YdGOYWQYr6kqh7KU to the same three hooks. Their stock declarations preserve the same grid areas, sizing, and player gutters as 1.3.0; black library and player container backgrounds are already present in 1.2.94. Older version maps remain unchanged. This is archived CSS evidence, not a live older-client check." + } + }, + "settings_toggle_input": { + "date": "2026-09-15", + "spotify_version": "1.3.0.277", + "class": "utdiMuyxdKvPowN1CIBs", + "staged_class": "x-toggle-input", + "verification": "The native checkbox uses opacity:0, pointer-events:none, and position:absolute in five stock stylesheets. The missing semantic hook left stdlib Settings rows showing both a native checkbox and their styled indicator. Reapply restores the shared input styling; live Spicetify Settings inputs have opacity 0 and absolute positioning while their toggle indicators remain visible. Regression coverage fails without the mapping." + } } } diff --git a/1030000/classmap-19f856aefd5.json b/1030000/classmap-19f856aefd5.json index 8eae55b..f6d2cea 100644 --- a/1030000/classmap-19f856aefd5.json +++ b/1030000/classmap-19f856aefd5.json @@ -18,7 +18,7 @@ "wrapper": "e-10860-legacy-button e-10860-button-tertiary--condensed-all" }, "button_t": { - "wrapper": "e-10860-legacy-button e-10860-button-tertiary--condensed-all" + "wrapper": "nR3ekhH3c1IU7dX9smcR" }, "upgrade_button": { "wrapper": "e-10860-legacy-button e-10860-legacy-button-primary" diff --git a/1030000/css-map.json b/1030000/css-map.json index 85c161a..2bca146 100644 --- a/1030000/css-map.json +++ b/1030000/css-map.json @@ -74,6 +74,7 @@ "OweqfPQPNscHYqA1Ke4y": "profile-userEditDetails-labelText", "P91StDUW8iiKx4vRlEpx": "artist-artistDiscography-headerContainer", "PGUsx7TTOQt2hFluVtHK": "main-trackList-rowSectionEnd", + "PIP22o58Crv8RXY4wB2o": "Root__nav-bar", "PSn5qg6vyMedF0JBEN5g": "artist-artistDiscography-headerImage", "PeQnLdb9Gd0blACadFa7": "main-dropDown-isSafari", "Pucs9mmakJk6QyGoxAdu": "x-settings-zoomButton", @@ -140,6 +141,7 @@ "at0g9yMjiRK9mm2WoIqv": "x-settings-zoomButtonIconWrapper", "axzh7Jhch7v2hTblCyPy": "main-nowPlayingView-headerCloseButton", "azTGVyS_7WuUbhEDoCgH": "main-globalNav-historyButtonsContainer", + "bQetA_KiP9n0DQVcGa7m": "Root__globalNav", "bQxJ0kehcml02vaIAnXT": "x-proxySettings-ProxySettings", "bRRJlkldFGQjaL5FAFze": "main-globalNav-searchInputTextWrapper", "bbnZAUOa3NQHoL3gTSPA": "main-yourLibraryX-library", @@ -190,6 +192,7 @@ "iV28E0oeNJEXWZlJhIB3": "main-embedWidgetGenerator-dimensionLabel", "ikEUGwFAayomtVGFTrSA": "main-embedWidgetGenerator-contentIframe", "inPROcpr9gEShIW_XxcW": "view-homeShortcutsGrid-name", + "itzKVxWhS4n59fp_KIVc": "Root__now-playing-bar", "j2bAoqgf5XIMUds91Cqx": "volume-bar__icon-button", "jS5CsFdh2O_GQ7Ai6YfO": "main-embedWidgetGenerator-tooltipInitiator", "jhHgzVPKuWbfL02G8MAz": "main-useDropTarget-base", @@ -250,6 +253,7 @@ "uR1BMutIFRmR6BBWSAWo": "main-nowPlayingView-contextItemInfo", "uguMZhYNPFPT0K39gHNI": "main-trackList-rowImagePlayButton", "upDNlpL8xEkxmKWWw9IF": "lyrics-lyricsContent-provider", + "utdiMuyxdKvPowN1CIBs": "x-toggle-input", "v6gpDRQbTaN60R0FTiAA": "main-playlistEditDetailsModal-characterCounter", "vBiCRVHx1pQjZnYVFTSs": "main-embedWidgetGenerator-widthField", "vvokofZHMkIo13xngnyn": "playlist-playlist-sectionTitleWrapper", diff --git a/index.json b/index.json index 04279c4..7ab7093 100644 --- a/index.json +++ b/index.json @@ -133,15 +133,15 @@ "1030000": { "classmap": { "file": "classmap-19f856aefd5.json", - "sha256": "51644e39368847eefdb9befd6684f13b4534cbc67adf7cbd0a3c3e63ba14d8ff" + "sha256": "17d9575c38a5a7cce9d6bb28665aff0c6e88b4ce4d94955bad297e19c97b33dc" }, "cssMapOverlay": { "file": "css-map.json", - "sha256": "a32733c0e135b556ac6eab8633d48a85c02d5f5d9ba595601116a148b698745e" + "sha256": "29351b698ec06c6731a6069628ed007218e20acd507b98d5a89248ba3770370b" }, "meta": { "file": "META.json", - "sha256": "a0f481a553daf219f4b3c1b9f34d5dfdc4152b01733729315ae146e031d2616c" + "sha256": "c1dfef890570cb5a02524558c9c6b8fd89f103bc59f08ce8d2079df0ceebfc6e" }, "spotifyVersion": "1.3.0.277", "status": "verified" diff --git a/scripts/test_classmap_contracts.py b/scripts/test_classmap_contracts.py new file mode 100644 index 0000000..e8d2732 --- /dev/null +++ b/scripts/test_classmap_contracts.py @@ -0,0 +1,47 @@ +"""Check classmap roles after the CSS-map rewrite used during staging.""" + +import json +import unittest + +from build_index import ROOT, classmap_file + + +class TopbarButtonContractTests(unittest.TestCase): + def test_spotify_130_topbar_buttons_keep_the_native_hitbox_after_staging(self): + key = ROOT / "1030000" + mapping = json.loads(classmap_file(key).read_text()) + overlay = json.loads((key / "css-map.json").read_text()) + wrapper = mapping["main"]["topbar"]["right"]["button_t"]["wrapper"] + staged = [overlay.get(token, token) for token in wrapper.split()] + self.assertIn( + "main-topBar-buddyFeed", staged, + "The toolbar wrapper must retain the native button hitbox, " + "not just a generic Encore class that exists in the DOM.", + ) + self.assertFalse( + any("--condensed-" in token for token in staged), + "Mapped classes must not strip the icon-only button padding.", + ) + + +class RootContainerContractTests(unittest.TestCase): + def test_settings_toggle_hides_the_native_input_behind_its_indicator(self): + overlay = json.loads((ROOT / "1030000" / "css-map.json").read_text()) + self.assertEqual(overlay.get("utdiMuyxdKvPowN1CIBs"), "x-toggle-input") + + def test_spotify_130_restores_the_three_layout_container_hooks(self): + overlay = json.loads((ROOT / "1030000" / "css-map.json").read_text()) + # Verified against stock grid-area declarations and live child controls. + expected = { + "bQetA_KiP9n0DQVcGa7m": "Root__globalNav", + "PIP22o58Crv8RXY4wB2o": "Root__nav-bar", + "itzKVxWhS4n59fp_KIVc": "Root__now-playing-bar", + } + for source, target in expected.items(): + with self.subTest(container=target): + self.assertEqual(overlay.get(source), target) + self.assertEqual(list(overlay.values()).count(target), 1) + + +if __name__ == "__main__": + unittest.main()