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:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user