Skip to content

Feat/28 direct mode#33

Merged
helebest merged 7 commits into
mainfrom
feat/28-direct-mode
Apr 20, 2026
Merged

Feat/28 direct mode#33
helebest merged 7 commits into
mainfrom
feat/28-direct-mode

Conversation

@helebest

Copy link
Copy Markdown
Owner

No description provided.

hele.hl added 7 commits April 20, 2026 15:22
- New SET_DIRECT_MODE message routes to chrome.proxy.settings.set({mode:'direct'}),
  giving users a true "bypass everything" option that overrides the OS/IE proxy
  that System mode otherwise honors.
- Storage schema v2: introduce top-level mode field
  ('direct' | 'system' | 'profile'). One-way migrateData() infers mode from the
  existing activeProfileId for v1 payloads; stale ids are dropped safely.
- Popup gets a dedicated Direct button next to System, with a matching status
  indicator state (green dot, no pulse).
- Tests: 10 new Vitest cases for migrateData edge cases and 4 new Playwright
  specs exercising the Direct button end-to-end against real Chromium.
- Options dark-mode polish (bundled): restore the missing --border-radius /
  --transition tokens that were silently falling back to 0, add a
  prefers-color-scheme: dark block, align focus-ring and danger-hover colors
  with the iOS palette.
- Version: 1.5.2 -> 1.6.0 in manifest, package*, options About, docs JSON-LD,
  STORE_LISTING, README, CHANGELOG.

Closes #28.
… popup

Activating a profile in the popup did not turn the toolbar icon to the
profile's color until the user interacted with the address bar. The popup
call path is: popup click -> ACTIVATE_PROFILE -> activateProxy() ->
updateIconForActiveTab() -> chrome.tabs.query({currentWindow:true}).
At that moment the popup still has focus and the extension popup is its
own windowType:'popup' with no browsing tabs, so the query returned
empty, tab.url fell through to '', and the icon was set to the inactive
gray. Only after the popup closed (typically when the user clicked into
the address bar) did onUpdated refire and pick up the real active tab.

Fix: resolve the active tab via chrome.windows.getLastFocused with
windowTypes:['normal'], which excludes our popup window, then query
active tab by that window id. Applied in both updateIconForActiveTab()
and the onUpdated listener's sibling lookup.
The earlier "icon stays gray until address-bar interaction" bug wasn't
caught by any automated test. Two gaps:

1. Our e2e fixture loads popup.html into a *regular* browser tab, so
   chrome.tabs.query({currentWindow:true}) always found that tab — the
   broken codepath (currentWindow = windowType:'popup') was never
   exercised. Manual testing with the real extension popup was the only
   way to trigger it.
2. Tests only inspected DOM (status text, class) and storage, never the
   toolbar icon — the most user-visible signal.

Fix both:

- Expose last-applied icon color through GET_STATE. background.js now
  tracks `lastIconColor` in module state and returns it in the GET_STATE
  response, giving tests an observable surface for icon repaint without
  needing a chrome.action.getIcon API (which doesn't exist).
- New e2e spec popup-window-icon.spec.ts creates a real popup-type window
  via chrome.windows.create({type:'popup'}), activates a red-colored
  profile from inside it, and asserts lastIconColor matches the profile
  color. Verified the spec does detect the bug: reverting the
  getActiveBrowserTab() fix makes it fail with Received: null,
  Expected: "#F44336" — the exact symptom the user reported.

Total: 81 unit + 55 e2e, all green.
Real root cause of the "icon stays gray until address-bar interaction"
bug, which survived the earlier windowType:'popup' fix.

updateIconForActiveTab was designed to be per-tab: it required the
active tab's URL to start with http(s):// before painting the profile
color. On chrome://newtab, about:blank, or any extension page, the
check failed and the icon stayed gray even with a profile active. Users
open the extension popup from the new-tab page constantly, so they saw
the gray icon until navigating.

The per-tab indicator is useful when routing rules are enabled — it
tells you whether the current site is actually going through the proxy
or bypassing it. With no routing rules, every http request goes through
the proxy unconditionally, so the nuance only confuses.

Fix: split the branches. Profile without routing rules → paint profile
color unconditionally. Profile with routing rules → keep existing
per-tab check.

Extended e2e/popup-window-icon.spec.ts from 1 to 3 tests, including the
critical non-http-tab case (about:blank browsing tab) that reproduces
the user's scenario. Verified the new test fails without this fix
(Expected: "#F44336", Received: null) and passes with it. Also added a
test that locks in the per-tab behavior for the routing-rules case so
the fix doesn't regress that feature.

81 unit + 57 e2e all green.
…Popup + raw CDP

The existing popup-window-icon.spec.ts simulates the popup via
chrome.windows.create({type:'popup'}) — semantically close but not
identical to the toolbar popup a user actually clicks. The real popup
uses chrome.action.openPopup() and renders as an internal Chromium
widget that Playwright's Page API does not expose (verified: target
exists in CDP but never surfaces as a context.pages() entry).

Work around Playwright's limitation by:
  1. Opening a browser-level CDPSession (browser.newBrowserCDPSession()).
  2. Triggering chrome.action.openPopup() from the service worker.
  3. Polling Target.getTargets for the popup.html page target.
  4. Target.attachToTarget with flatten:false, then piping Runtime.evaluate
     through Target.sendMessageToTarget — Playwright doesn't expose a
     public CDPSession for non-Page targets, so messages are routed
     manually with our own id/sessionId dispatcher.

Test leaves the active browsing tab on about:blank so it exercises the
exact scenario the user hit from the new-tab page. Verified the spec
does detect the bug: reverting the no-routing-rules branch makes it
fail with Expected: "#F44336", Received: null.

58 e2e all green (was 57).
Extract the CDP bridge into e2e/real-popup.ts (openRealPopup,
seedProfiles, makeProfile) so popup-click specs stay readable, then add
five more click-interaction cases against the REAL toolbar popup:

- Click Direct -> mode='direct', icon cleared
- Click System after a profile is active -> mode='system', icon cleared
- Switch between three color-coded profiles -> icon tracks each click
- profile -> Direct -> profile round-trip leaves state consistent
- Popup DOM (status text, .selected / .active classes) mirrors mode

All driven via chrome.action.openPopup() + Target.sendMessageToTarget,
same plumbing as the original spec. The earlier monolithic test is now
one of six members of the `Real browser-action popup — click
interactions` describe block.

81 unit + 63 e2e all green.
…d to system

Codex review flagged that migrateData trusted already-v2 payloads with
mode='profile' without re-verifying that activeProfileId still points
to a real profile. Reachable window: options.js deleteProfile writes
activeProfileId=undefined then asks background to DEACTIVATE_PROFILE.
If the SW restarts between those two writes, initializeProxyState
reads {mode:'profile', activeProfileId:undefined}, and would end up
with currentMode='profile' + activeProfile=null — UI state flags out
of sync until the next user action.

Fix: unify v1 and v2 paths through a single activeIdValid check.
Already-v2 data is still respected for 'direct' / 'system' modes, but
'profile' is demoted to 'system' whenever activeProfileId doesn't
resolve. v1 inference unchanged.

Added two vitest cases covering the demote path (stale id; undefined
id). 83 unit + 63 e2e all green.

Copy link
Copy Markdown
Owner Author

Code Review — PR #33: Feat/28 Direct Mode

Summary

This PR introduces Direct Connection mode (closes #28), fixes the toolbar icon repaint bug, adds options page dark mode, and bumps the version to 1.6.0. 23 files changed across core logic, UI, tests, and documentation.


1. PR Description vs Implementation Consistency ✅

All items described in the PR are fully implemented:

Claimed Feature Implementation Verdict
Direct Connection mode setDirectMode() in background.js calls chrome.proxy.settings.set({ value: { mode: 'direct' } }), new #directConnection button in popup.html, handleDirectMode() in popup.js ✅ Matches
Storage schema v2 migration lib/storage-migration.js with migrateData() — handles v1→v2 inference, stale ID cleanup, unknown mode coercion ✅ Matches
Toolbar icon repaint fix getActiveBrowserTab() uses chrome.windows.getLastFocused({windowTypes:['normal']}), routing-rules-aware icon logic ✅ Matches
Options page dark mode + token fix @media (prefers-color-scheme: dark) block, --border-radius/--transition legacy aliases, --glass-bg tokens ✅ Matches

2. Multi-Dimensional Analysis

Correctness ✅

  • migrateData() robustly handles all edge cases: undefined, empty object, v1 with valid/stale IDs, v2 passthrough, v2 with stale state, and unknown mode strings.
  • setDirectMode() properly clears proxy settings before applying direct mode (with a 100ms settling delay for Chrome's async proxy API).
  • State transitions (currentMode, activeProfile) are kept consistent between background.js and popup.js across all code paths — including error/fallback branches.
  • The writeData() helper stamps version: SCHEMA_VERSION on every write, preventing accidental schema regression.

Standards & Conventions ✅

  • Clean ES module imports (import { migrateData } from './lib/storage-migration.js').
  • Consistent Chrome Extension Manifest V3 patterns throughout.
  • Proper aria-hidden="true" added to decorative SVGs — nice accessibility improvement.
  • Replaced the window.systemProxyActive global with module-scoped currentMode — cleaner encapsulation.
  • Removed verbose console.log debug statements from popup.js while keeping meaningful operational logs.

Security ✅

  • No user input flows unsanitized into any API.
  • chrome.proxy.settings.set({ value: { mode: 'direct' } }) is a safe, well-documented Chrome API call.
  • migrateData() validates types defensively (typeof raw === 'object', Array.isArray()) before accessing properties.
  • No injection vectors introduced in popup/options UI changes.
  • CI Security Scan passed.

Effectiveness ✅

  • getActiveBrowserTab() using getLastFocused({windowTypes:['normal']}) is the correct fix — the extension popup's own window (type: 'popup') was causing chrome.tabs.query({currentWindow:true}) to return empty.
  • The routing-rules check (hasRoutingRules) before per-tab icon logic is an efficient optimization that also fixes the user-facing bug: proxy-on feedback is now immediate for simple profiles.
  • The RealPopup CDP bridge in e2e/real-popup.ts is a creative and thorough approach to testing the actual browser-action popup — something Playwright doesn't natively support.

3. Test Coverage ✅

Excellent coverage across all new functionality:

Test File Type Tests Coverage
tests/mode-migration.test.js Unit (Vitest) 11 All migration edge cases (v1→v2, stale IDs, unknown modes, passthrough)
e2e/direct-mode.spec.ts E2E (Playwright) 4 Direct button visibility, mode transitions, storage state
e2e/popup-window-icon.spec.ts E2E (Playwright) 3 Icon repaint on http tab, non-http tab, routing-rules exclusion
e2e/real-browser-action-popup.spec.ts E2E (Playwright) 6 Real popup: profile activation, Direct/System switching, multi-profile, round-trips, status text
e2e/options.spec.ts E2E (Playwright) 1 updated Version string updated to 1.6.0

4. Regression Verification ✅

  • Local unit tests: All 83 tests across 5 files pass.
  • CI pipeline: All checks green:
    • ✅ Run Tests (Node 20 & 22)
    • ✅ Lint Code
    • ✅ Security Scan
    • ✅ Build Extension
    • ✅ Performance Tests
    • ✅ Browser Compatibility (Chrome & Edge)
    • ✅ E2E & Visual Regression
    • ⏭ Release Dry Run (skipped — expected for non-release branch)

Verdict: Approved

Well-structured PR with thorough test coverage, clean code, and no security concerns. The implementation faithfully delivers on all described features. Merging now.


Generated by Claude Code

@helebest helebest merged commit 76e5de0 into main Apr 20, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant