Harden spec after adversarial review

- Use div.thing.link selector to exclude comment nodes
- Fix shared module loading (manifest content_scripts array)
- Add FOUC mitigation via CSS-only manifest injection
- Switch to storage.local to avoid sync quota limits
- Add MutationObserver for RES dark mode toggle detection
- Use fallback chain for MutationObserver target container
- Clarify comment count text handling (zero = "0 comments")
- Simplify teardown to page reload on master toggle
- Expand test checklist to cover new behaviors

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-26 14:34:15 -04:00
parent bbe3aa0b50
commit fd00fc1c87

View File

@@ -29,7 +29,7 @@ firefox-reddit-tweaks/
│ ├── tweaks/ │ ├── tweaks/
│ │ └── badges.js # Subreddit + comment badge tweak │ │ └── badges.js # Subreddit + comment badge tweak
│ └── styles/ │ └── styles/
│ └── badges.css # Badge styling │ └── badges.css # Badge styling (also injected via manifest as CSS content script)
├── popup/ ├── popup/
│ ├── popup.html # Settings UI │ ├── popup.html # Settings UI
│ ├── popup.js # Settings logic │ ├── popup.js # Settings logic
@@ -38,35 +38,37 @@ firefox-reddit-tweaks/
│ ├── icon-48.png │ ├── icon-48.png
│ └── icon-96.png │ └── icon-96.png
└── lib/ └── lib/
└── settings.js # Shared settings read/write (browser.storage.sync) └── settings.js # Shared settings read/write (browser.storage.local)
``` ```
### Manifest ### Manifest
- Manifest V2 (Firefox fully supports V2). - Manifest V2 (Firefox fully supports V2).
- `content_scripts` matched to `*://old.reddit.com/*` and `*://www.reddit.com/*`. On `www.reddit.com`, the script naturally handles the old-vs-new ambiguity because it targets `div.thing` elements, which only exist when old Reddit is rendered. - `content_scripts` matched to `*://old.reddit.com/*` and `*://www.reddit.com/*`. On `www.reddit.com`, the script naturally handles the old-vs-new ambiguity because it targets `div.thing.link` elements, which only exist when old Reddit is rendered.
- `content_scripts.css` includes `badges.css` for immediate injection at CSS parse time (prevents FOUC by hiding original elements before JS runs).
- `content_scripts.js` lists all JS files in order: `lib/settings.js`, `content/tweaks/badges.js`, `content/main.js`. Content scripts cannot use ES module `import`/`export`, so all files share the same global scope via the manifest's script array.
- Permissions: `storage` (for user settings), `activeTab`. - Permissions: `storage` (for user settings), `activeTab`.
- `browser_action` for the popup. - `browser_action` for the popup.
### Modularity ### Modularity
Each tweak is an individual file under `content/tweaks/`, responsible for one self-contained modification. `main.js` loads settings and calls each enabled tweak. Adding a new tweak means: drop a file in `tweaks/`, wire it in `main.js`. Each tweak is an individual file under `content/tweaks/`, responsible for one self-contained modification. `main.js` loads settings and calls each enabled tweak. Adding a new tweak means: drop a file in `tweaks/`, add it to the manifest's `content_scripts.js` array, and wire it in `main.js`.
## Badge Tweak — DOM Manipulation ## Badge Tweak — DOM Manipulation
### Source Elements ### Source Elements
Old Reddit renders each post as a `div.thing`. Relevant children: Old Reddit renders each post as a `div.thing.link` (comments use `div.thing.comment` — these are excluded). Relevant children of a post:
- **Subreddit name:** `a.subreddit` inside `p.tagline` - **Subreddit name:** `a.subreddit` inside `p.tagline`
- **Comment count:** `a.comments` inside `ul.flat-list.buttons` - **Comment count:** `a.comments` inside `ul.flat-list.buttons`
### Process ### Process
1. Query all `div.thing` elements on the page. 1. Query all `div.thing.link` elements on the page (the `.link` class distinguishes posts from comments, avoiding wasted iteration over comment nodes).
2. For each post, extract the subreddit link's `href`/text and the comments link's `href`/text (parsing the number from the text). 2. For each post, extract the subreddit link's `href`/text and the comments link's `href`/text. The comments badge uses the original text verbatim, except when the text is `"comment"` (meaning zero comments), in which case the badge displays `"0 comments"`.
3. Create two badge elements (styled `<a>` tags) and insert them into a new container `div.reddit-tweaks-badges`, placed as the first child of the post's `div.entry` (above the title). 3. Create two badge elements (styled `<a>` tags) and insert them into a new container `div.reddit-tweaks-badges`, placed as the first child of the post's `div.entry` (above the title).
4. Hide the original subreddit element in the tagline and the comments link in the flat-list via CSS (`display: none`). 4. The original subreddit element in the tagline and the comments link in the flat-list are hidden via CSS rules in `badges.css` (injected at CSS parse time by the manifest, so they're hidden before JS runs).
5. Mark each processed post with `data-rt-processed` to prevent double-processing. 5. Mark each processed post with `data-rt-processed` to prevent double-processing.
### Badge HTML Structure ### Badge HTML Structure
@@ -82,7 +84,7 @@ All injected elements use an `rt-` prefix to avoid class name collisions with Re
### Dynamic Content ### Dynamic Content
A `MutationObserver` on `div#siteTable` (the post container) watches for new `div.thing` children, covering RES infinite scroll and Reddit's own pagination. New posts are processed on insertion. A `MutationObserver` watches for new `div.thing.link` children being added to the page, covering RES infinite scroll and Reddit's own pagination. The observer target uses a fallback chain for robustness across page types: `#siteTable` (standard listings) → `.content[role="main"]` (fallback) → `document.body` (last resort). New posts are processed on insertion.
## Badge Styling ## Badge Styling
@@ -113,7 +115,7 @@ A `MutationObserver` on `div#siteTable` (the post container) watches for new `di
### Dark Mode Detection ### Dark Mode Detection
The content script checks for RES night mode via the `.res-nightmode` class on `<body>` and applies the dark palette via a CSS class toggle (`rt-dark`). Users can also force a mode in settings (Auto / Light / Dark). The content script checks for RES night mode via the `.res-nightmode` class on `<body>` and applies the dark palette via a CSS class toggle (`rt-dark`). A `MutationObserver` on `<body>` watches for `class` attribute changes to detect RES dark mode being toggled mid-session, updating badge colors reactively. Users can also force a mode in settings (Auto / Light / Dark), which bypasses detection entirely.
### CSS Custom Properties ### CSS Custom Properties
@@ -125,7 +127,7 @@ For per-subreddit overrides, inline styles are set on individual badges when a m
### Storage ### Storage
All settings stored in `browser.storage.sync` for cross-device sync. All settings stored in `browser.storage.local` (no quota concerns, avoids `storage.sync` item size limits for per-subreddit overrides).
### Configurable Properties ### Configurable Properties
@@ -149,7 +151,7 @@ All settings stored in `browser.storage.sync` for cross-device sync.
### Live Updates ### Live Updates
The content script listens for `browser.storage.onChanged` to apply settings changes without requiring a page reload. The content script listens for `browser.storage.onChanged` to apply color/dark-mode settings changes without requiring a page reload. For the master toggle (`enabled`), the page reloads to cleanly apply or remove all tweaks — this avoids complex teardown logic for a rare user action.
## Settings UI (Popup) ## Settings UI (Popup)
@@ -170,8 +172,9 @@ Settings save immediately on change. Clean, minimal styling following Firefox ex
- **Already processed:** `data-rt-processed` attribute prevents double-processing. - **Already processed:** `data-rt-processed` attribute prevents double-processing.
- **RES infinite scroll:** MutationObserver catches dynamically added posts. - **RES infinite scroll:** MutationObserver catches dynamically added posts.
- **Subreddit pages:** Subreddit badge is skipped when the subreddit name isn't present in the post tagline (it's implied by the page context). - **Subreddit pages:** Subreddit badge is skipped when the subreddit name isn't present in the post tagline (it's implied by the page context).
- **Comment pages:** The main post at the top is a `div.thing` and gets badges. Comment entries lack subreddit/comment-count structure and are naturally skipped. - **Comment pages:** The main post at the top is a `div.thing.link` and gets badges. Comment entries are `div.thing.comment` and are excluded by the selector.
- **Extension disabled:** When `enabled` is `false`, the content script does nothing and any previously injected badges/styles are removed. - **Zero comments:** When the original comments link text is `"comment"` (no number), the badge displays `"0 comments"` with the same link destination.
- **Extension disabled:** When `enabled` is toggled to `false`, the page reloads. The content script checks `enabled` on startup and exits early if `false`, leaving the page in its default state.
## Testing Strategy ## Testing Strategy
@@ -183,9 +186,14 @@ Manual testing via `about:debugging` (temporary add-on). No automated test frame
2. Badges appear on subreddit listing pages (subreddit badge omitted when redundant) 2. Badges appear on subreddit listing pages (subreddit badge omitted when redundant)
3. Badges appear on user profile pages 3. Badges appear on user profile pages
4. Badges are clickable links to correct destinations 4. Badges are clickable links to correct destinations
5. Original subreddit/comment elements are hidden 5. Original subreddit/comment elements are hidden immediately (no FOUC)
6. RES infinite scroll loads get badged 6. RES infinite scroll loads get badged
7. Dark mode detection works with RES night mode 7. Dark mode detection works with RES night mode
8. Settings changes apply live without page reload 8. Toggling RES dark mode mid-session updates badge colors
9. Per-subreddit color overrides work 9. Color settings changes apply live without page reload
10. Reset to defaults works 10. Master toggle change triggers page reload and cleanly disables/enables
11. Per-subreddit color overrides work
12. Reset to defaults works
13. Posts with zero comments display "0 comments" badge
14. Comment nodes (`div.thing.comment`) are not processed
15. Badges work on search results and multi-reddit pages