diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 222b528..dcf7204 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,8 +5,13 @@ on: branches: - main - master + - develop pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: test: runs-on: ubuntu-latest @@ -29,17 +34,36 @@ jobs: - name: Check code formatting run: npx prettier --check . + - name: Lint TypeScript code + run: npm run lint + - name: Lint commit messages run: | if [ "${{ github.event_name }}" = "pull_request" ]; then npx commitlint --from origin/${{ github.base_ref }} --to HEAD - else + elif git cat-file -e "${{ github.event.before }}" 2>/dev/null; then npx commitlint --from ${{ github.event.before }} --to ${{ github.sha }} + else + # Force-push or new branch: the "before" SHA no longer exists; lint just the head commit. + npx commitlint --last fi - name: Run tests with coverage run: npm run ci:test + - name: Cache Playwright Chromium + uses: actions/cache@v4 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }} + + - name: Install Playwright Chromium dependencies + run: npx playwright install --with-deps chromium + + - name: Run Playwright smoke test + run: xvfb-run --auto-servernum npm run smoke + - name: Upload coverage artifact if: always() uses: actions/upload-artifact@v6 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8153236 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,78 @@ +name: Automated Release + +on: + push: + branches: + - main + +jobs: + release: + name: Build & Create GitHub Release + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Read version from package.json + id: get_version + run: | + VERSION=$(node -p "require('./package.json').version") + echo "VERSION=$VERSION" >> $GITHUB_ENV + echo "TAG_NAME=v$VERSION" >> $GITHUB_ENV + + - name: Check if tag already exists + id: check_tag + run: | + if git rev-parse "${{ env.TAG_NAME }}" >/dev/null 2>&1; then + echo "EXISTS=true" >> $GITHUB_OUTPUT + else + echo "EXISTS=false" >> $GITHUB_OUTPUT + fi + + - name: Build store release package + if: steps.check_tag.outputs.EXISTS != 'true' + run: npm run package:store + + - name: Create GitHub Release + if: steps.check_tag.outputs.EXISTS != 'true' + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ env.TAG_NAME }} + name: Network Overrides DevTools ${{ env.TAG_NAME }} + body: | + ## Network Overrides API (DevTools) ${{ env.TAG_NAME }} + + ### 🚀 Automatic Build & Store Package + - **Zip Package**: `network-overrides-api-devtools-${{ env.TAG_NAME }}-store.zip` + - **Chrome Web Store**: [Link](https://chromewebstore.google.com/detail/network-overrides-api-dev/holdjgmcnpelgclhopiejilhhkfcmpba) + + *Refer to `README.md` and `USE.md` for installation and usage instructions.* + files: release/*.zip + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload & Publish to Chrome Web Store + if: steps.check_tag.outputs.EXISTS != 'true' && secrets.CWS_CLIENT_ID != '' + uses: chrome-stats/chrome-extension-upload-action@v2 + with: + extension-id: 'holdjgmcnpelgclhopiejilhhkfcmpba' + file-path: release/network-overrides-api-devtools-${{ env.TAG_NAME }}-store.zip + client-id: ${{ secrets.CWS_CLIENT_ID }} + client-secret: ${{ secrets.CWS_CLIENT_SECRET }} + refresh-token: ${{ secrets.CWS_REFRESH_TOKEN }} diff --git a/.gitignore b/.gitignore index c9fa1af..3e28ac6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ release/ Thumbs.db *.env .env* +.worktrees/ +test-output.txt +test-results.txt diff --git a/.husky/commit-msg b/.husky/commit-msg index 26d79a2..4226661 100644 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,2 +1,15 @@ #!/usr/bin/env sh -npx commitlint --edit "$1" +if ! npx commitlint --edit "$1" > /dev/null 2>&1; then + echo "" + echo "=======================================================" + echo "❌ ERROR: Invalid commit message format!" + echo "Please use the standard format (Conventional Commits):" + echo "👉 [optional scope]: " + echo "" + echo "Valid examples:" + echo " feat: add new feature" + echo " fix: resolve bug" + echo " chore: update config" + echo "=======================================================" + exit 1 +fi diff --git a/.husky/pre-push b/.husky/pre-push index a36cc54..51f9bbe 100644 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,7 +1,12 @@ #!/usr/bin/env sh +set -e + # Lint all commits that are about to be pushed -upstream=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null) +upstream=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null || true) if [ -z "$upstream" ]; then upstream="origin/main" fi npx commitlint --from "$upstream" --to HEAD --verbose + +# Only allow the push when the build and the full test suite pass +npm test diff --git a/README.md b/README.md index a0efb35..fc14aa9 100644 --- a/README.md +++ b/README.md @@ -1,192 +1,240 @@ # Network Overrides DevTools -A Chrome/Edge DevTools extension that intercepts network responses and replaces their content during debugging. It uses `chrome.debugger` and the Chrome DevTools Protocol to pause requests at the `Response` stage, then returns a mocked body based on the rules you configure. +A Chrome/Edge DevTools extension that intercepts network responses and replaces their content during debugging. It uses `chrome.debugger` and the Chrome DevTools Protocol (Fetch domain) to pause requests, then returns a mocked body or redirects to a different URL based on configured rules. + +🛒 **Chrome Web Store**: [Network Overrides API (DevTools)](https://chromewebstore.google.com/detail/network-overrides-api-dev/holdjgmcnpelgclhopiejilhhkfcmpba) + +## Screenshots + +| Live API capture | Override editor | +| --------------------------------------------------------------- | ------------------------------------------------------------------- | +| ![Captured APIs](store-assets/screenshots/01-captured-apis.png) | ![Override editor](store-assets/screenshots/02-override-editor.png) | + +| Reusable override rules | Save and apply feedback | +| ----------------------------------------------------------------- | -------------------------------------------------------------------------- | +| ![Override rules](store-assets/screenshots/03-override-rules.png) | ![Save and retry feedback](store-assets/screenshots/04-save-and-retry.png) | ## Features -- Enable or disable overrides per active tab. -- Create override rules using: - - a URL substring - - `*` or `all` to match everything - - a regex in `/pattern/flags` format -- View recently captured API requests. -- Click an API directly from the list to create or edit an override. -- Auto-fill the editor with the current response body when available. -- Manage overrides from both the popup and the DevTools panel. -- Persist state with `chrome.storage.local`. +- **Enable/disable** overrides per active tab via a toggle switch. +- **Per-rule enable/disable toggle**: disable an individual rule without deleting it; it stays visible (dimmed) and is skipped by the background worker until re-enabled. +- **HTTP method matching**: scope a rule to `GET`/`POST`/`PUT`/`PATCH`/`DELETE`, or leave it at `Any` to match every method (default, pre-filled from the captured request when available). +- **Import/export rules as JSON**: back up or share the current domain's rules as a downloadable file, and load them back in with a merge-or-replace choice. +- **Rule Profiles & Presets**: save and load named rule presets per domain to switch quickly between different testing scenarios. +- **Duplicate Rules**: 1-click clone any override rule directly in the rules list. +- **Request Headers & Response Headers Overriding**: inject or modify request headers (e.g. `Authorization: Bearer token`) during the request stage or extra response headers during the response stage. +- **Response Image & Visual Preview**: instant image preview (Base64 PNG/JPG, SVG) directly inside the editor modal. +- **Dynamic Captured Resource Filters**: toggle body capture for XHR, Fetch, Document, Script, or Stylesheet resources. +- **Three pattern matching modes** for override rules: + - URL substring match (e.g. `/api/users`) + - Wildcard `*` glob (e.g. `https://old.com/api/*/users` → `*` captures matching segments) + - Regex `/pattern/flags` (e.g. `/api\/v1\/users\/\d+/i`) + - `*` or `all` matches every request. +- **Three override types:** + - **Override body**: Replace the response body with custom text or raw base64 content. + - **Redirect URL**: Redirect the request to a different URL (supports `*` wildcard substitution from captured groups). + - **Fail request**: Kill the request at the network layer with a chosen error reason — the page's `fetch`/XHR rejects as if the network failed. +- **Status, headers, delay, and fail mocking**: a body rule can force the response status (100–599), add or overwrite response headers, and delay the response up to 120 s; a fail rule kills the request at the network layer (`Failed`, `TimedOut`, `ConnectionRefused`, `NameNotResolved`, `InternetDisconnected`). +- **View captured APIs**, grouped by resource type (XHR, Fetch, JS, CSS, Img, Doc, WS, etc.), with real-time updates from the background service worker. +- **Search APIs** by URL substring. +- **One-click override creation**: Click any API in the list to open the modal and create/edit an override rule. +- **Auto-fill response body**: When creating a new override, the current response body is automatically fetched from the background worker and pre-filled into the editor. +- **JSON formatting**: Auto-detect and format JSON bodies with a single button. +- **Copy cURL**: Copy any API request as a cURL command. +- **Manual rule editor** (DevTools panel only): Quickly add a rule without opening the modal. +- **Persistent storage**: All rules, profiles, and settings survive browser restarts via `chrome.storage.local`. + +## Architecture -## Main Structure +``` +src/ +├── background.ts # Service-worker bootstrap +├── background/ # Debugger lifecycle, interception, encoding, capture, message routing +├── ui.ts # Shared UI state and controller orchestration +├── ui/ # Reusable modal, rules, headers, dialogs, notifications, profiles, import/export +├── shared.ts # Shared OverrideRule, ApiEntry, and header types +├── tab-state.ts # Per-tab state, session persistence, and worker rehydration +├── panel.ts # DevTools panel initialization and HAR streaming +├── popup.ts # Action popup initialization +└── utils.ts # Pattern matching, wildcard, and origin helpers + +styles.css # CSS entrypoint +styles/ # Base, feature, modal/rules, primitive, and guide styles +scripts/ # Smoke, Store screenshot, and packaging automation +store-assets/screenshots/ # Chrome Web Store-ready screenshots +dist/ # Compiled JavaScript (generated by TypeScript) +panel.html # DevTools panel shell +popup.html # Action popup shell +manifest.json # Manifest V3 configuration +``` -- `src/background.ts`: manages the debugger, intercepts requests/responses, applies overrides, and stores recent APIs and response bodies. -- `src/ui.ts`: shared UI logic for the popup and panel. -- `src/popup.ts`: initializes the popup UI. -- `src/panel.ts`: initializes the DevTools panel UI. -- `src/devtools.ts`: registers the `Overrides` tab in DevTools. -- `scripts/`: utility scripts, including store packaging. -- `tests/`: unit and interaction tests, including shared test harness utilities. -- `manifest.json`: Manifest V3 extension configuration. -- `dist/`: compiled JavaScript output generated from TypeScript. -- `old-backup/`: legacy JavaScript files kept only for reference and no longer used as the main entry points. +### Key flows -## Setup and Build +1. **Initialization**: `devtools.ts` creates a DevTools panel → `panel.ts` fires up UI + listens to `chrome.devtools.network` events (HAR + `onRequestFinished`). Popup uses `popup.ts` instead, without HAR or manual editor. -Requirements: +2. **Debugger attachment**: When "Enable Overrides" is checked, `background.ts` calls `chrome.debugger.attach` on the active tab, then enables `Network` and `Fetch` domains (both Request and Response stages). Detachment happens on disable, tab close, or debugger disconnect. -- Node.js -- Chrome or Microsoft Edge +3. **Request interception** (`Fetch.requestPaused`): + - **Request stage**: Checks override rules in precedence order. A rule with `failReason` kills the request via `Fetch.failRequest` (after `delayMs`, if set). Otherwise a rule with `redirectUrl` redirects via `Fetch.continueRequest` with a modified URL — wildcards (`*`) in the redirect URL are substituted with captured groups from the pattern match. + - **Response stage**: Checks override rules for a body replacement. If found, `Fetch.fulfillRequest` sends the custom body (base64-encoded) with the original status and headers — unless the rule overrides them via `statusCode`/`responseHeaders` (same-name headers overwritten case-insensitively) — plus the `x-network-overrides: true` and `x-network-overrides-pattern` markers, delayed by `delayMs` if set. If no rule matches, XHR/Fetch response bodies are stored for later auto-fill via `Fetch.getResponseBody`. -Steps: +4. **Recent API tracking**: `Network.requestWillBeSent` captures request metadata into an in-memory Map (per tabId), mirrored to `chrome.storage.session` under key `tabState_{tabId}` (debounced). Capped at 500 URLs and 100 bodies. On worker startup, this state is rehydrated from `chrome.storage.session` and the debugger is re-attached to tabs that were enabled; any legacy `recentApis_{tabId}` / `recentApiBodies_{tabId}` keys left over from older versions in `chrome.storage.local` are removed automatically. -1. Install dependencies: +5. **UI state**: `enabled`, per-domain rules (`overrides_{origin}`), and `apiSearchTerm` are persisted in `chrome.storage.local` and survive across DevTools sessions and browser restarts. -```bash -npm install -``` +## Storage -2. Build the TypeScript sources: +Rule and UI state is stored in `chrome.storage.local` (permanent); per-tab runtime state is stored in `chrome.storage.session` (cleared when the browser exits): -```bash -npm run build -``` +| Key | Type | Persistence | +| -------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| `enabled` | `boolean` | Permanent — survives browser restart | +| `overrides_{origin}` | `OverrideRule[]` (rules for one domain, e.g. `overrides_https://a.test`) | Permanent — survives browser restart | +| `apiSearchTerm` | `string` | Permanent — survives browser restart | +| `tabState_{tabId}` | per-tab snapshot (`enabled`, `origin`, `overrides`, captured APIs/bodies) | `chrome.storage.session` — cleared when the browser exits; rehydrated and re-attached on worker startup | + +**Important**: Override rules are never lost. Recent API data is keyed by `tabId`, lives only for the current browser session, and is only visible when the same tab is active. Legacy keys from older extension versions are migrated automatically: a flat `overrides` list is moved to the current domain's `overrides_{origin}` key on UI load, and `recentApis_{tabId}` / `recentApiBodies_{tabId}` keys are removed from `chrome.storage.local` on worker startup. + +## Pattern Reference -3. Load the extension: +Override rules are evaluated in order; the first matching rule for a URL is used. -- Open `chrome://extensions` or `edge://extensions` -- Enable `Developer mode` -- Click `Load unpacked` -- Select the project root folder containing `manifest.json` +| Pattern | Matches | +| ----------------------------- | ------------------------------------------------------------ | +| `/api/users` | Any URL containing `/api/users` | +| `*` or `all` | Every request | +| `https://site.com/api/*/list` | URLs matching the glob; `*` captures zero or more characters | +| `/\/api\/v\d+\/users/` | Regex match (literal `/` delimiters, no flags) | +| `/\/api\/user\/(\d+)/gi` | Regex with flags `g` and `i` | -Note: +In redirect URLs, `*` substitutes captured wildcards in order. For example: -- `manifest.json` points to files inside `dist/`, so you need to build before loading the extension and after any TypeScript changes. +- Pattern: `https://old.com/api/*/item/*` +- Redirect: `https://new.com/api/*/product/*` +- Request URL: `https://old.com/api/v2/item/5` +- Redirected to: `https://new.com/api/v2/product/5` + +### Rule fields + +Optional fields on an override rule: + +| Field | Type | Description | +| ----------------- | -------------------------- | ----------------------------------------------------------------------------------------------------- | +| `statusCode` | optional integer 100–599 | Forces the mocked response status (body rules only). | +| `responseHeaders` | optional `{name, value}[]` | Added to the mocked response; same-name headers are overwritten case-insensitively (body rules only). | +| `delayMs` | optional number 0–120000 | Delays the response/failure by N ms (body and fail rules). | +| `failReason` | optional enum | Makes the rule fail the request at the network layer instead of answering. | ## Usage ### 1. Open the extension UI -You can use either of these entry points: +Two entry points: -- Click the extension icon to open the popup. -- Open DevTools and switch to the `Overrides` tab. +- **Popup**: Click the extension icon in the toolbar. Shows "Captured APIs", "Overridden", and "Rules" tabs. +- **DevTools panel**: Open DevTools (F12) → "Overrides" tab. Same UI plus a manual rule editor row at the top of the "Rules" tab. ### 2. Enable overrides -Check `Enable Overrides` to attach the debugger to the current tab and start intercepting responses. +Toggle **Enable Overrides** on. The extension attaches the debugger to the current tab. -### 3. Choose an API to override +### 3. Capture APIs -- In the `Recent APIs` section, the extension shows recently captured requests. -- Click an API to open the create/edit override dialog. -- If `Auto-fill from payload` is enabled, the current response body is prefilled when captured data is available. +Browse your application as normal. Requests appear in the **Captured APIs** tab, grouped by resource type (XHR, Fetch, JS, CSS, etc.). Use the search bar to filter by URL. -### 4. Define the pattern +### 4. Create an override -Examples: +Click any API in the list to open the override modal. You can also add a rule manually (DevTools panel only) by filling in the pattern, body, and clicking "Add override". -- `/api/users` -- `all` -- `*` -- `/api\\/v1\\/users\\/\\d+/i` +In the modal, choose: -Rules are evaluated in the same order they appear in the `Overrides` list, and the first matching rule is used for a URL. +- **Override body**: Enter custom response body text. Use `Text` mode for raw text or `Raw base64` for pre-encoded content. The **Format JSON** action appears when the body contains valid JSON. +- **Redirect to URL**: Enter the target URL. Use `*` to substitute wildcards captured from the pattern match. +- **Fail request**: Pick a fail reason (`Failed`, `TimedOut`, `ConnectionRefused`, `NameNotResolved`, `InternetDisconnected`) — the request fails at the network layer instead of receiving a response. -### 5. Choose the mode +The advanced fields can override **Request Headers** and **Response Headers** in either Key–Value or Raw mode, force a **Status** (100–599), and set a **Delay** in milliseconds. Body and fail rules can use delay; combine `TimedOut` with a long delay to simulate a real timeout. -- `Text`: the input content is encoded and returned as the response body. -- `Raw base64`: use this when you already have the body in base64 format. +### 5. Manage rules -### 6. Save the override +Switch to the **Rules** tab to edit, copy, delete, or enable/disable saved rules. A disabled rule stays visible but dimmed and is skipped by the background worker. Use **Export**/**Import** to share the current domain's rules; import provides explicit **Append rules** and **Replace rules** choices when rules already exist. The **Overridden** tab shows captured APIs currently matched by a rule. -After saving: +### 6. Copy cURL -- The rule is stored in `chrome.storage.local` -- The background script receives the updated configuration -- Future matching requests will receive the overridden body +Each API entry has a "cURL" button that copies the request as a cURL command (method, headers, and post data included). -## How It Works +## Setup & Build -- The extension attaches to a tab with `chrome.debugger.attach`. -- `Network.requestWillBeSent` is used to store the recent request list. -- `Fetch.requestPaused` at the `Response` stage is used to: - - retrieve the original response body - - or replace the response body with `Fetch.fulfillRequest` -- The extension also adds these headers: - - `x-network-overrides: true` - - `x-network-overrides-pattern: ` +Requirements: Node.js 22+, Chrome or Edge. -## Notes +```bash +npm install +npm run build +``` -- The extension requires `debugger`, `storage`, and `host_permissions: `. -- Overrides only apply to the tab currently attached to the debugger. -- Recent data is intentionally capped to avoid memory growth: - - up to 500 recent API URLs - - up to 100 recent response bodies +Load the extension: + +1. Open `chrome://extensions` or `edge://extensions` +2. Enable **Developer mode** +3. Click **Load unpacked** +4. Select the project root (the folder containing `manifest.json`) + +The `manifest.json` points to files in `dist/`, so re-run `npm run build` after any TypeScript changes. ## Scripts ```bash -npm run build -npm test -npm run coverage -npm run ci:test -npm run package:store +npm run build # Compile TypeScript → dist/ +npm run lint # ESLint over src/ +npm test # Build + run test suite +npm run coverage # Build + run tests with coverage report +npm run ci:test # CI pipeline (same as coverage) +npm run smoke # Real-browser smoke test (loads the unpacked extension into Chromium) +npm run store:screenshots # Recreate the four 1280x800 Chrome Web Store screenshots +npm run package:store # Create a ZIP for Chrome Web Store / Edge Add-ons ``` +## Development Standards + +- **Formatting**: Prettier via lint-staged (pre-commit hook). +- **Commit messages**: Conventional Commits enforced by commitlint + Husky hooks. +- **Commit types**: `add`, `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`, `ci`, `revert`, `build`. + ## Testing -- `npm test`: builds the project and runs the full test suite. -- `npm run coverage`: builds the project, runs the tests, and generates coverage reports in `coverage/`. -- `npm run ci:test`: the same pipeline used by GitHub Actions. +Tests live in `tests/` and cover: -Current test coverage includes: +- Pattern matching logic (`helpers.test.mjs`) +- UI behavior with DOM mocks (`ui-behavior.test.mjs`) +- Background debugger event handling (`background-flow.test.mjs`) +- Per-tab state store: session mirror, caps, dispose, rehydrate (`tab-state.test.mjs`) +- Entrypoint bootstrapping (`entrypoints.test.mjs`) -- helper logic -- popup and panel UI behavior with DOM-based mocks -- `chrome.runtime.sendMessage` flows -- background debugger event handling -- entrypoint bootstrapping for popup, panel, and DevTools +`npm run smoke` (`scripts/smoke.mjs`) additionally drives the real unpacked extension in Chromium via Playwright — attach status, interception, status override, network-layer fail, worker-restart recovery, cross-origin navigation, and attach failures. Run it before releases; it needs a display (headed browser) and downloads Chromium on first use. -## CI +`npm run store:screenshots` (`scripts/capture-store-screenshots.mjs`) loads the real extension with deterministic demo API data and recreates the four `1280x800` PNG files in `store-assets/screenshots/`. It also needs a headed Chromium session. -GitHub Actions is configured in `.github/workflows/ci.yml` to: +## CI -- install dependencies with `npm ci` -- run the test and coverage pipeline -- upload the generated `coverage/` report as a workflow artifact +GitHub Actions (`.github/workflows/ci.yml`) installs dependencies, checks formatting with Prettier, lints commit messages with commitlint, runs tests with coverage, and uploads the coverage report as an artifact. ## Store Packaging -To create an uploadable ZIP for the Chrome Web Store or Edge Add-ons store, run: - ```bash npm run package:store ``` -This command will: +Produces a ZIP in `release/` containing only the runtime files (`manifest.json`, `*.html`, `styles.css`, `dist/`, `icons/`) plus `privacy_policy.md`. `dist/` is cleaned and rebuilt first so stale compiled files never ship. -- build the TypeScript output -- collect only the files needed for the extension package -- generate a ZIP file inside `release/` -- exclude source map files (`.map`) from the store package -- try to keep one ZIP per extension version while preserving ZIPs from older versions -- clean old staging folders when they are not locked by another process +## Permissions -Packaging notes: +- `debugger` — required to intercept and modify network requests via Chrome DevTools Protocol. +- `storage` — required to persist override rules, settings, and recent API data. +- `host_permissions: ` — required to attach the debugger to any tab. -- If the current version ZIP is not locked, it is replaced in place. -- If Windows or another tool is locking the existing ZIP, the script falls back to a timestamped ZIP in `release/` so packaging still succeeds. -- If old staging folders or ZIP files are locked by the OS, they may remain until those handles are released. +## Limitations -The ZIP includes the runtime assets only, such as: - -- `manifest.json` -- `devtools.html` -- `panel.html` -- `popup.html` -- `styles.css` -- `dist/` -- `icons/` - -## Legacy Version - -The `old-backup/` folder contains the older JavaScript version from before the TypeScript migration. It can still be useful for reference, but the active implementation now lives in `src/` and is compiled into `dist/`. +- Overrides only apply to the tab currently attached to the debugger. +- Recent API data is capped at 500 URLs and 100 response bodies per tab. +- Recent API bodies are only stored for XHR and Fetch resource types by default. Configure `CAPTURED_BODY_TYPES` in `src/background.ts` to add more types. +- Recent API lists are keyed by `tabId` and reset when the tab is closed and reopened. +- The extension is designed for developer debugging only, not for end-user production use. diff --git a/TODO.md b/TODO.md deleted file mode 100644 index b7b4d96..0000000 --- a/TODO.md +++ /dev/null @@ -1,29 +0,0 @@ -# Existing Tasks (JS era complete, TS migration done) - -## Old Plan Steps (Completed) - -1-9 ✅ TypeScript migration, tests, build verified. - -## New Task: Add Git Husky for code formatting (Prettier) and commitlint before push/commit - -### Steps: - -- ✅ Step 1: Update package.json (deps ✅, scripts optional - use npx). -- ✅ Step 2: Create .prettierrc.json -- ✅ Step 3: Create lint-staged.config.mjs -- ✅ Step 4: Create commitlint.config.mjs -- ✅ Step 5: Update .husky/pre-commit to `npx lint-staged` -- ✅ Step 6: Create .husky/prepare-commit-msg `npx commitlint --edit $1` -- ✅ Step 7: `npm install` -- ✅ Step 8: `npx husky init` -- ✅ Step 9: `npx prettier --write .` formatted codebase. -- ✅ Step 10: Add lint to CI. -- ✅ Step 11: Task complete, test hooks with `git commit`. -- ✅ Step 12: Fix remaining Prettier issues in TODO.md itself. -- ✅ Step 13: Verify pre-commit (lint-staged Prettier all files), commitlint (types: feat, fix, docs, chore...). - -**Hooks Ready ✅** - -## Fix lint-staged Pre-commit Error ✅ - -- Prettier `"No parser"` error fixed — lint-staged now passes `${files.join(' ')}` correctly. diff --git a/USE-EN.md b/USE-EN.md new file mode 100644 index 0000000..92bdc0d --- /dev/null +++ b/USE-EN.md @@ -0,0 +1,489 @@ +# Network Overrides DevTools — User Guide + +## Table of Contents + +1. [Introduction](#1-introduction) +2. [Installation](#2-installation) +3. [Interface Overview](#3-interface-overview) +4. [Basic Usage](#4-basic-usage) +5. [URL Patterns](#5-url-patterns) +6. [Override Body](#6-override-body) +7. [Redirect URL](#7-redirect-url) +8. [Managing Rules](#8-managing-rules) +9. [Additional Features](#9-additional-features) +10. [Important Notes](#10-important-notes) +11. [FAQ](#11-faq) + +--- + +## 1. Introduction + +**Network Overrides DevTools** is a Chrome/Edge extension for developers that **intercepts and overrides** API response data directly in the browser, without modifying backend code. + +🛒 **Chrome Web Store**: [Network Overrides API (DevTools)](https://chromewebstore.google.com/detail/network-overrides-api-dev/holdjgmcnpelgclhopiejilhhkfcmpba) + +**Common use cases:** + +- Mock API responses to test the frontend when the backend isn't ready. +- Inject or override Request Headers (e.g. `Authorization: Bearer token`). +- Debug by altering responses to simulate different states (errors, empty data, edge cases, etc.). +- Redirect requests from an old API to a new one without changing frontend code. +- 1-click Duplicate Rules and manage Rule Profiles / Presets per domain. +- Real-time Image and SVG visual preview inside the modal editor. +- Quickly inspect captured API requests and copy them as cURL commands. + +--- + +## 2. Installation + +### Requirements + +- Chrome or Microsoft Edge (latest version). +- Install from [Chrome Web Store](https://chromewebstore.google.com/detail/network-overrides-api-dev/holdjgmcnpelgclhopiejilhhkfcmpba) or load as unpacked from source. + +### Steps + +1. Open `chrome://extensions` (or `edge://extensions`). +2. Enable **Developer mode** (top right). +3. Click **Load unpacked**. +4. Select the project root directory (the folder containing `manifest.json`). +5. The "Network Overrides API (DevTools)" extension will appear. + +### Verify + +- The extension icon appears on the toolbar. +- Open DevTools (F12) → **Overrides** tab. + +--- + +## 3. Interface Overview + +The extension has **2 entry points**: + +### 3.1. Popup + +Click the extension icon on the toolbar. Contains: + +- **Header**: "Network Overrides API" +- **Refresh button** (↻): Reload the captured API list. +- **Enable Overrides toggle**: Turn override functionality on/off. +- **3 tabs**: + - **Captured APIs (N)**: List of captured requests, grouped by resource type (XHR, Fetch, JS, CSS, etc.). + - **Overridden (N)**: APIs currently matched by an active rule. + - **Rules (N)**: List of all saved override rules. +- **Search bar**: Filter APIs by URL substring. + +### 3.2. DevTools Panel + +Open DevTools (F12) → **Overrides** tab. Same as popup, plus: + +- **Manual editor**: A quick-add form (pattern + body) above the Rules tab. +- **HAR auto-load**: Automatically loads request history from `chrome.devtools.network.getHAR()` on panel open. + +### 3.3. Override Modal + +Click an API to open the modal with: + +- **Pattern**: URL pattern for matching. +- **HTTP Method**: Dropdown (`Any`, `GET`, `POST`, `PUT`, `PATCH`, `DELETE`) to scope the rule to a specific request method. Defaults to `Any`, which matches every method (same as before this field existed). Pre-filled from the captured request's method when opening the modal from a captured API. +- **Override body / Redirect to URL**: Choose the override type. +- **Response body**: Custom response content (for body override). +- **Redirect URL**: Target URL (for redirect). +- **Format JSON**: Pretty-print JSON body. +- **Body type badge**: Auto-detects `text` or `json`. +- **Save Override**: Save the rule. + +--- + +## 4. Basic Usage + +### Step 1: Open the extension + +- **Option A**: Click the extension icon on the toolbar → popup opens. +- **Option B**: Open DevTools (F12) → **Overrides** tab. + +### Step 2: Enable Overrides + +Flip the **Enable Overrides** toggle ON. + +> The extension will attach the debugger to the current tab and start monitoring network requests. + +### Step 3: Capture APIs + +Browse your application normally. API requests will appear automatically in the **Captured APIs** tab. + +### Step 4: Create an Override + +**Method 1 (Click API):** + +1. Go to **Captured APIs** or **Overridden** tab. +2. Click an API you want to override. +3. The modal opens with the pattern pre-filled, and the **Method** dropdown pre-selected to the captured request's method if it's one of `GET`/`POST`/`PUT`/`PATCH`/`DELETE` (otherwise it defaults to `Any`). +4. If the API has a stored response body, it will be auto-filled into the **Response body** field. +5. Edit the content → **Save Override**. + +**Method 2 (Manual — DevTools panel only):** + +1. Go to the **Rules** tab. +2. In the form above, enter a **Pattern** and **Response body**. +3. Click **Add override**. + +### Step 5: Verify + +- Overridden APIs show a blue border in the list (`active` class). +- The **Overridden** tab shows the count and list of matched APIs. +- The real response is replaced with your custom content. + +### Step 6: Disable + +- Toggle the switch OFF to disable all overrides. +- Or go to **Rules** tab → click ✕ to delete a specific rule. + +--- + +## 5. URL Patterns + +### 5.1. Substring (default) + +Any URL **containing** the pattern string is a match. + +``` +Pattern: /api/users +Matches: https://example.com/api/users + https://example.com/api/users/123 + https://example.com/v2/api/users/list +No match: https://example.com/api/admin +``` + +### 5.2. Wildcard `*` + +Use `*` to match any URL segment. Each `*` also **captures** the matched value for use in Redirect URLs. + +``` +Pattern: /api/*/users/* +Matches: /api/v1/users/123 → captures: ["v1", "123"] + /api/v2/users/abc → captures: ["v2", "abc"] +``` + +Multiple `*` wildcards are supported, each corresponding to one capturing group. + +### 5.3. Regex `/pattern/flags` + +Patterns starting and ending with `/` are treated as regex. Optional flags follow the closing `/`. + +``` +Pattern: /\/api\/v\d+\/users/i +Matches: /api/v1/users (case-insensitive) + /API/V2/Users +No match: /api/admin/users +``` + +``` +Pattern: /\/api\/user\/(\d+)/ +Matches: /api/user/42 (captures: ["42"]) +``` + +### 5.4. Match everything + +``` +Pattern: * +Pattern: all +``` + +Matches **every request**. + +### 5.5. Rule order + +Rules are evaluated in list order. **The first matching rule wins**. Drag-and-drop reordering is not supported — to change priority, delete and recreate rules in the desired order. + +### 5.6. HTTP method + +Besides the URL pattern, a rule can also be scoped to a specific HTTP method via the **Method** field in the override modal (see [3.3](#33-override-modal) and [4](#4-basic-usage)). A rule with a specific method (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`) only applies to requests using that method; `Any` (the default) matches every method, regardless of pattern type. + +--- + +## 6. Override Body + +### 6.1. Text mode + +Content is base64-encoded and returned as the response body. + +```json +// Example: Mock JSON response +{ + "status": "ok", + "data": [ + { "id": 1, "name": "Alice" }, + { "id": 2, "name": "Bob" } + ] +} +``` + +### 6.2. Raw base64 mode + +Use when you already have base64-encoded content (e.g., binary data, images, pre-encoded files). + +### 6.3. Body type badge + +- **`text`**: Content is not valid JSON (blue badge). +- **`json`**: Content is valid JSON (green badge). Auto-detected as you type. + +### 6.4. Format JSON + +Click **Format JSON** to pretty-print the response body. If the content is not valid JSON, the button has no effect. + +### 6.5. Auto-fill + +When creating a new override from an API: + +- If the API has a stored response body (in `recentApiBodies`), it is pre-filled automatically. +- Otherwise, the extension sends a `getApiData` message to the background worker to retrieve the stored body. +- This feature only works when **Auto-fill on open** is enabled (default: on). + +--- + +## 7. Redirect URL + +### 7.1. How it works + +When you select **Redirect to URL**, instead of overriding the response body, the extension redirects the request at the **request stage** (before the actual request is sent). + +### 7.2. Wildcard substitution + +Use `*` in the Redirect URL to substitute captured values from the pattern match. + +**Examples:** + +| Pattern | Request URL | Captures | Redirect URL | Result | +| -------------------------- | ----------------------------- | --------------- | ----------------------------- | -------------------------------- | +| `/api/*` | `https://site.com/api/user` | `["user"]` | `https://site.com/api/v2/*` | `https://site.com/api/v2/user` | +| `/api/old/*/data` | `/api/old/v1/data` | `["v1"]` | `/api/new/*/data` | `/api/new/v1/data` | +| `https://old.com/*/item/*` | `https://old.com/shop/item/5` | `["shop", "5"]` | `https://new.com/*/product/*` | `https://new.com/shop/product/5` | + +If any `*` remains unsubstituted in the Redirect URL, the extension logs an error and lets the request proceed normally (no redirect). + +**Common mistake:** Pattern has 1 `*` but Redirect URL has 2 `*`: + +| Pattern | Request URL | Captures | Redirect URL | Result | +| ------------------------ | --------------------------- | ---------- | ---------------------------- | ------------------------------------ | +| `https://site.com/api/*` | `https://site.com/api/user` | `["user"]` | `https://site.com/api/v2/**` | ❌ Error: second `*` not substituted | + +### 7.3. When to use + +- Redirect from an old API to a new API without changing frontend code. +- Point requests from production to a staging/local server for debugging. +- Suppress certain requests by redirecting to an empty endpoint. + +--- + +## 8. Managing Rules + +### 8.1. View rules + +Go to the **Rules** tab. Each rule displays: + +- **Enabled checkbox**: at the start of the row. Unchecked means the rule is disabled (see [8.5](#85-enabledisable-a-rule)). +- **Pattern**: Bold blue text. +- **Method badge** (if set to something other than `Any`): a small badge (e.g. `POST`) next to the body preview. +- **Redirect URL** (if set): Arrow → followed by the URL. +- **Body preview**: First 80 characters + mode label (`text`/`file`). + +### 8.2. Edit a rule + +Click **✎** next to a rule → modal opens with current values → edit → **Save Override**. + +### 8.3. Delete a rule + +Click **✕** → rule is removed immediately. + +### 8.4. Persistence + +- Rules are stored in `chrome.storage.local` → **they never disappear** on page refresh, DevTools close, or browser restart. +- No need to worry about losing your configuration. + +### 8.5. Enable/disable a rule + +Each rule has a checkbox at the start of its row. Unchecking it **disables** the rule (`enabled: false`) without deleting it: + +- The rule stays visible in the **Rules** list, dimmed. +- Any API it targets stays in the **Overridden** tab (it still "would apply" by pattern and method) but is also shown dimmed, since a disabled rule is no longer actively applied. +- The background service worker skips disabled rules when deciding which override to apply to a request. + +Checking the box re-enables the rule. New rules, and rules that existed before this feature was added, default to **enabled**. + +### 8.6. Export rules + +Click **Export** in the **Rules** tab to download the rules for the **currently active domain** as a JSON file (named after the domain). The file has this shape: + +```json +{ + "version": 1, + "domain": "https://example.com", + "exportedAt": "2026-07-07T00:00:00.000Z", + "overrides": [ + /* OverrideRule[] */ + ] +} +``` + +Export only includes rules for the domain currently open in the panel, not all domains. + +### 8.7. Import rules + +Click **Import** in the **Rules** tab and pick a previously exported (or hand-crafted) JSON file: + +- If the file's `domain` field is present and doesn't match the domain currently active in the panel, a confirm dialog warns you and asks whether to import into the current domain anyway. Canceling aborts the import with no changes. +- If the current domain **already has rules**, a confirm dialog asks how to combine them: **OK** merges — the imported rules are appended to the end of the existing list; **Cancel** replaces — all existing rules for the current domain are overwritten by the imported ones. +- If the current domain **has no rules yet**, the import is applied directly with no prompt. +- Invalid files (not valid JSON, missing the `overrides` array, or a rule missing required fields) are rejected with an alert, and nothing is changed. +- Only known rule fields (`pattern`, `mode`, `body`, `redirectUrl`, `method`, `enabled`) are kept from each imported rule; any other properties in the file are dropped. + +Like Export, Import always operates on the domain currently active in the panel — never all domains at once. + +--- + +## 9. Additional Features + +### 9.1. Copy cURL + +Each API entry has a **cURL** button. Click to copy the request as a cURL command: + +```bash +curl 'https://api.example.com/data' \ + -X 'POST' \ + -H 'Content-Type: application/json' \ + -H 'Authorization: Bearer xxx' \ + --data-raw '{"key":"value"}' +``` + +### 9.2. Search APIs + +Search box in the **Captured APIs** and **Overridden** tabs. Searches by URL substring (case-insensitive). Matching text is highlighted with a yellow background. + +### 9.3. Refresh API list + +Click the **Refresh** button (↻) in the top right. The extension retries up to 5 times (each 250ms apart) to load APIs from the background worker. + +### 9.4. Type grouping + +APIs are grouped by resource type: + +| Type | Label | +| ----------- | ----------- | +| XHR | XHR | +| Fetch | Fetch | +| JS | JS | +| CSS | CSS | +| Image | Img | +| Media | Media | +| Font | Font | +| Document | Doc | +| WebSocket | WS | +| Manifest | Manifest | +| EventSource | EventSource | +| TextTrack | TextTrack | +| Other | Other | + +Click a group header (e.g. "XHR ▼") to collapse/expand. Collapse state is persisted in storage. + +### 9.5. Marker headers + +When a request is overridden, the extension adds these response headers: + +- `x-network-overrides: true` +- `x-network-overrides-pattern: ` + +This lets you easily identify overridden requests in the DevTools Network tab. + +--- + +## 10. Important Notes + +### 10.1. Scope + +- Overrides only apply to the **currently attached tab**. +- Each time you toggle ON, the extension attaches to the **active tab**. +- Switching to another tab disables overrides for the new tab until you toggle again. + +### 10.2. Capacity limits + +- Maximum **500 URLs** in the recent APIs list. +- Maximum **100 response bodies** stored. +- When exceeded, the oldest entries are evicted (FIFO). + +### 10.3. Body storage + +- Only **XHR** and **Fetch** resource types have their response bodies stored (for auto-fill). +- Other types (JS, CSS, Image, etc.) are not stored. + +### 10.4. Tab ID dependency + +Recent APIs and response bodies are keyed by `recentApis_{tabId}` and `recentApiBodies_{tabId}`. When you close a tab and reopen it, the new `tabId` differs → old data is not shown. + +### 10.5. Not for production use + +This extension is designed for **developer debugging only**. Do not use it in end-user production environments. + +### 10.6. Required permissions + +The extension requires: + +- `debugger` — to intercept network requests. +- `storage` — to persist rules and data. +- `` — to attach the debugger to any tab. + +--- + +## 11. FAQ + +### Q: Override not working? + +**Checklist:** + +1. Is **Enable Overrides** turned ON? (Toggle should be blue.) +2. Does the pattern match the URL? Try `*` to match everything. +3. Is the current tab the one being debugged? (Try refreshing the extension.) +4. Open DevTools → extension's Console to check for errors. + +### Q: Turned off overrides but requests are still being modified? + +Try refreshing the page. If the issue persists, disable and re-enable the extension. + +### Q: How do I delete all rules at once? + +Go to the **Rules** tab and click ✕ on each rule. There is no "Clear all" button. + +### Q: Old API data from yesterday is still showing? + +Override rules are permanent and survive restarts. Recent APIs, however, are linked to `tabId`. If you see old data, you might be on the same tab you used before. Closing and reopening the tab assigns a new `tabId`, so old data won't appear. + +### Q: Extension doesn't work in incognito mode? + +Go to `chrome://extensions` → click **Details** on the extension → enable **Allow in incognito**. + +### Q: How can I tell if a request was overridden? + +Check the DevTools Network tab: + +- Response header `x-network-overrides: true` is added. +- In the extension, the API entry is highlighted in blue and appears in the **Overridden** tab. + +### Q: APIs are not showing up in Captured APIs? + +Click the **Refresh** button (↻). The extension retries 5 times over 1.25 seconds. If still empty: + +1. Verify the toggle is ON. +2. Check the Network tab to confirm requests are being sent. +3. Try the DevTools panel instead of the popup (panel uses `chrome.devtools.network`, which may capture more). + +### Q: Does this support localStorage or sync storage? + +No. The extension uses `chrome.storage.local` (10MB limit). Sync storage is not used since rules may contain large response bodies. + +### Q: Can I override WebSocket connections? + +No. The extension only intercepts HTTP requests (XHR, Fetch) via Chrome's Fetch domain. WebSocket is not supported. + +### Q: Can I import rules exported from a different domain? + +Yes. Import always writes into the domain that's currently active in the panel, regardless of which domain the file's `domain` field says it was exported from — but if that field doesn't match the current domain, a confirm dialog warns you first, so the mismatch isn't silent. diff --git a/USE.md b/USE.md new file mode 100644 index 0000000..b64f480 --- /dev/null +++ b/USE.md @@ -0,0 +1,489 @@ +# Hướng dẫn sử dụng Network Overrides DevTools + +## Mục lục + +1. [Giới thiệu](#1-giới-thiệu) +2. [Cài đặt](#2-cài-đặt) +3. [Giao diện](#3-giao-diện) +4. [Cách sử dụng cơ bản](#4-cách-sử-dụng-cơ-bản) +5. [Pattern (Mẫu URL)](#5-pattern-mẫu-url) +6. [Override body](#6-override-body) +7. [Redirect URL](#7-redirect-url) +8. [Quản lý rules](#8-quản-lý-rules) +9. [Tính năng bổ sung](#9-tính-năng-bổ-sung) +10. [Lưu ý quan trọng](#10-lưu-ý-quan-trọng) +11. [FAQ](#11-faq) + +--- + +## 1. Giới thiệu + +**Network Overrides DevTools** là extension cho Chrome/Edge dành cho developer, cho phép **chặn và ghi đè** response của các API request ngay trong trình duyệt mà không cần sửa code backend. + +🛒 **Chrome Web Store**: [Network Overrides API (DevTools)](https://chromewebstore.google.com/detail/network-overrides-api-dev/holdjgmcnpelgclhopiejilhhkfcmpba) + +**Công dụng chính:** + +- Mock API response để test frontend khi backend chưa sẵn sàng. +- Can thiệp / Ghi đè Request Headers (ví dụ: `Authorization: Bearer token`). +- Debug bằng cách thay đổi response để kiểm tra các trạng thái khác nhau (lỗi, timeout, dữ liệu rỗng,...). +- Redirect request từ API cũ sang API mới mà không cần sửa code. +- Nhân bản quy tắc (Duplicate Rule) và quản lý Rule Profiles / Presets linh hoạt. +- Xem trước trực tiếp phản hồi hình ảnh (Base64 PNG/JPG, SVG). +- Xem nhanh các API request đã gửi và copy cURL để dùng lại. + +--- + +## 2. Cài đặt + +### Yêu cầu + +- Trình duyệt Chrome hoặc Microsoft Edge (bản mới nhất). +- Tải từ [Chrome Web Store](https://chromewebstore.google.com/detail/network-overrides-api-dev/holdjgmcnpelgclhopiejilhhkfcmpba) hoặc cài extension dạng "unpacked" (tải từ source). + +### Các bước cài đặt + +1. Mở `chrome://extensions` (hoặc `edge://extensions`). +2. Bật **Developer mode** (góc trên bên phải). +3. Click **Load unpacked**. +4. Chọn thư mục chứa `manifest.json` (thư mục gốc của project). +5. Extension "Network Overrides API (DevTools)" sẽ xuất hiện. + +### Kiểm tra + +- Icon extension xuất hiện trên thanh toolbar. +- Mở DevTools (F12) → tab **Overrides**. + +--- + +## 3. Giao diện + +Extension có **2 cửa sổ**: + +### 3.1. Popup + +Click icon extension trên thanh toolbar. Gồm: + +- **Tiêu đề**: "Network Overrides API" +- **Nút Refresh** (↻): Tải lại danh sách API đã capture. +- **Toggle Enable Overrides**: Bật/tắt tính năng ghi đè. +- **3 tabs**: + - **Captured APIs (N)**: Danh sách API đã capture được, nhóm theo loại (XHR, Fetch, JS, CSS...). + - **Overridden (N)**: API đang bị ghi đè bởi rule. + - **Rules (N)**: Danh sách các override rules. +- **Thanh tìm kiếm**: Lọc API theo URL. + +### 3.2. DevTools Panel + +Mở DevTools (F12) → tab **Overrides**. Giống popup nhưng có thêm: + +- **Manual editor**: Form nhập pattern + body nhanh phía trên tab Rules. +- **Auto-load HAR**: Tự động load lịch sử request từ `chrome.devtools.network.getHAR()` khi mở panel. + +### 3.3. Modal tạo/sửa Override + +Khi click vào một API, modal hiện ra với: + +- **Pattern**: URL pattern để matching. +- **HTTP Method**: Dropdown chọn method (`Any`, `GET`, `POST`, `PUT`, `PATCH`, `DELETE`) để giới hạn rule chỉ áp dụng cho một method cụ thể. Mặc định là `Any`, khớp với mọi method (giống như hành vi trước khi có field này). Khi mở modal từ một API đã capture, method sẽ được điền sẵn theo method của request đó. +- **Override body / Redirect to URL**: Chọn loại override. +- **Response body**: Nội dung response ghi đè (nếu chọn Override body). +- **Redirect URL**: URL chuyển hướng (nếu chọn Redirect to URL). +- **Format JSON**: Làm đẹp JSON body. +- **Body type badge**: Nhãn tự động phát hiện `text` hoặc `json`. +- **Save Override**: Lưu rule. + +--- + +## 4. Cách sử dụng cơ bản + +### Bước 1: Mở extension + +- **Cách 1**: Click icon extension trên toolbar → mở popup. +- **Cách 2**: Mở DevTools (F12) → tab **Overrides**. + +### Bước 2: Bật Overrides + +Gạt toggle **Enable Overrides** sang ON. + +> Khi bật, extension sẽ attach `debugger` vào tab hiện tại và bắt đầu theo dõi request. + +### Bước 3: Capture API + +Duyệt web / dùng ứng dụng như bình thường. Các API request sẽ tự động xuất hiện trong tab **Captured APIs**. + +### Bước 4: Tạo Override + +**Cách 1 (Click API):** + +1. Vào tab **Captured APIs** hoặc **Overridden**. +2. Click vào API muốn ghi đè. +3. Modal hiện ra với pattern được điền sẵn, và dropdown **Method** được chọn sẵn theo method của request đã capture nếu đó là `GET`/`POST`/`PUT`/`PATCH`/`DELETE` (nếu không, mặc định là `Any`). +4. Nếu API có response body, nó sẽ được tự động điền vào ô **Response body**. +5. Chỉnh sửa nội dung → **Save Override**. + +**Cách 2 (Manual - DevTools panel):** + +1. Vào tab **Rules**. +2. Ở form phía trên, nhập **Pattern** và **Response body**. +3. Click **Add override**. + +### Bước 5: Kiểm tra + +- API có override sẽ có viền xanh trong danh sách (class `active`). +- Tab **Overridden** hiển thị số lượng và danh sách API đang bị ghi đè. +- Response thật sẽ được thay thế bằng nội dung bạn đã nhập. + +### Bước 6: Tắt Override + +- Gạt toggle OFF để tắt hoàn toàn. +- Hoặc vào tab **Rules** → click ✕ để xóa rule. + +--- + +## 5. Pattern (Mẫu URL) + +### 5.1. Substring (mặc định) + +Pattern là một chuỗi bất kỳ. URL nào **chứa** chuỗi đó sẽ khớp. + +``` +Pattern: /api/users +Khớp với: https://example.com/api/users + https://example.com/api/users/123 + https://example.com/v2/api/users/list +Không: https://example.com/api/admin +``` + +### 5.2. Ký tự đại diện `*` + +Dùng `*` để match bất kỳ segment nào. `*` cũng **captures** giá trị để dùng trong Redirect URL. + +``` +Pattern: /api/*/users/* +Khớp với: /api/v1/users/123 → captures: ["v1", "123"] + /api/v2/users/abc → captures: ["v2", "abc"] +``` + +Có thể capture nhiều `*`, mỗi `*` tương ứng với một capturing group. + +### 5.3. Regex `/pattern/flags` + +Pattern bắt đầu và kết thúc bằng `/`, có thể kèm flags. + +``` +Pattern: /\/api\/v\d+\/users/i +Khớp với: /api/v1/users (case-insensitive) + /API/V2/Users +Không: /api/admin/users +``` + +``` +Pattern: /\/api\/user\/(\d+)/ +Khớp với: /api/user/42 (captures: ["42"]) +``` + +### 5.4. Match tất cả + +``` +Pattern: * +Pattern: all +``` + +Khớp với **mọi request**. + +### 5.5. Thứ tự ưu tiên + +Rules được duyệt theo thứ tự trong danh sách. **Rule đầu tiên** khớp sẽ được áp dụng. Kéo thả không hỗ trợ — nếu cần ưu tiên, xóa và tạo lại rule theo thứ tự mong muốn. + +### 5.6. HTTP method + +Bên cạnh URL pattern, một rule còn có thể được giới hạn theo HTTP method cụ thể thông qua field **Method** trong modal (xem mục 3.3 và 4 ở trên). Rule có method cụ thể (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`) chỉ áp dụng cho request dùng đúng method đó; `Any` (mặc định) khớp với mọi method, không phân biệt loại pattern. + +--- + +## 6. Override Body + +### 6.1. Chế độ Text + +Nội dung nhập vào sẽ được encode thành base64 và trả về như response body. + +```json +// Ví dụ: Mock JSON response +{ + "status": "ok", + "data": [ + { "id": 1, "name": "Alice" }, + { "id": 2, "name": "Bob" } + ] +} +``` + +### 6.2. Chế độ Raw base64 + +Dùng khi bạn đã có sẵn nội dung dạng base64 (ví dụ: binary, image, file đã encode trước). + +### 6.3. Body type badge + +- **`text`**: Nội dung không phải JSON (màu xanh dương). +- **`json`**: Nội dung là JSON hợp lệ (màu xanh lá), tự động phát hiện khi gõ. + +### 6.4. Format JSON + +Click **Format JSON** để làm đẹp nội dung JSON (pretty-print). Nếu không phải JSON, nút không có tác dụng. + +### 6.5. Auto-fill + +Khi tạo override mới từ một API: + +- Nếu API có response body trong bộ nhớ (`recentApiBodies`), nó sẽ được tự động điền vào. +- Nếu không, extension sẽ gửi message `getApiData` đến background worker để lấy body đã lưu. +- Tính năng này chỉ hoạt động nếu toggle **Auto-fill on open** được bật (mặc định: có). + +--- + +## 7. Redirect URL + +### 7.1. Cách hoạt động + +Khi chọn **Redirect to URL**, thay vì ghi đè body, extension sẽ chuyển hướng request đến URL khác ngay ở **request stage** (trước khi request thật được gửi đi). + +### 7.2. Wildcard substitution + +Dùng `*` trong Redirect URL để thay thế bằng giá trị đã capture từ pattern. + +**Ví dụ:** + +| Pattern | Request URL | Captures | Redirect URL | Kết quả | +| -------------------------- | ----------------------------- | --------------- | ----------------------------- | -------------------------------- | +| `/api/*` | `https://site.com/api/user` | `["user"]` | `https://site.com/api/v2/*` | `https://site.com/api/v2/user` | +| `/api/old/*/data` | `/api/old/v1/data` | `["v1"]` | `/api/new/*/data` | `/api/new/v1/data` | +| `https://old.com/*/item/*` | `https://old.com/shop/item/5` | `["shop", "5"]` | `https://new.com/*/product/*` | `https://new.com/shop/product/5` | + +Nếu còn `*` chưa được thay thế trong Redirect URL, extension sẽ log lỗi và request sẽ proceed bình thường (không redirect). + +**Ví dụ lỗi thường gặp:** Pattern có 1 `*` nhưng Redirect URL có 2 `*`: + +| Pattern | Request URL | Captures | Redirect URL | Kết quả | +| ------------------------ | --------------------------- | ---------- | ---------------------------- | --------------------------------------- | +| `https://site.com/api/*` | `https://site.com/api/user` | `["user"]` | `https://site.com/api/v2/**` | ❌ Lỗi: `*` thứ hai không được thay thế | + +### 7.3. Khi nào dùng + +- Chuyển từ API cũ sang API mới mà không cần sửa frontend. +- Chuyển request từ production sang staging/local để debug. +- Bỏ qua một số request bằng cách redirect đến một endpoint rỗng. + +--- + +## 8. Quản lý rules + +### 8.1. Xem rules + +Vào tab **Rules**. Mỗi rule hiển thị: + +- **Checkbox enable**: ở đầu dòng. Bỏ tick nghĩa là rule đang bị tắt (xem mục 8.5). +- **Pattern**: In đậm màu xanh. +- **Method badge** (nếu khác `Any`): một badge nhỏ (ví dụ `POST`) cạnh phần preview body. +- **Redirect URL** (nếu có): Mũi tên → kèm URL. +- **Body preview**: 80 ký tự đầu của body, kèm mode (`text`/`file`). + +### 8.2. Sửa rule + +Click **✎** bên cạnh rule → modal hiện ra với thông tin hiện tại → chỉnh sửa → **Save Override**. + +### 8.3. Xóa rule + +Click **✕** → rule bị xóa ngay lập tức. + +### 8.4. Tính bền vững + +- Rules được lưu trong `chrome.storage.local` → **không mất** khi refresh trang, đóng/mở DevTools, restart trình duyệt. +- Không cần lo lắng về việc mất dữ liệu. + +### 8.5. Bật/tắt một rule + +Mỗi rule có một checkbox ở đầu dòng. Bỏ tick sẽ **tắt** rule đó (`enabled: false`) mà không xóa rule: + +- Rule vẫn hiển thị trong danh sách **Rules**, nhưng bị làm mờ (dimmed). +- Các API mà rule này nhắm tới vẫn nằm trong tab **Overridden** (vì vẫn khớp pattern và method), nhưng cũng được hiển thị mờ đi, vì rule đã tắt thì không còn thực sự được áp dụng. +- Background service worker sẽ bỏ qua các rule đang tắt khi quyết định override request nào. + +Tick lại checkbox để bật rule trở lại. Rule mới tạo, cũng như các rule đã tồn tại trước khi có tính năng này, mặc định là **đang bật (enabled)**. + +### 8.6. Export rules + +Click **Export** trong tab **Rules** để tải xuống các rule của **domain đang active** dưới dạng file JSON (đặt tên theo domain). File có cấu trúc: + +```json +{ + "version": 1, + "domain": "https://example.com", + "exportedAt": "2026-07-07T00:00:00.000Z", + "overrides": [ + /* OverrideRule[] */ + ] +} +``` + +Export chỉ bao gồm rules của domain đang mở trong panel, không phải tất cả các domain. + +### 8.7. Import rules + +Click **Import** trong tab **Rules** và chọn một file JSON đã export trước đó (hoặc tự tạo tay): + +- Nếu field `domain` trong file có giá trị và khác với domain đang active trong panel, một hộp thoại confirm sẽ cảnh báo và hỏi có muốn import vào domain hiện tại không. Nhấn Cancel sẽ hủy import, không có gì thay đổi. +- Nếu domain hiện tại **đã có rules**, một hộp thoại confirm sẽ hỏi cách kết hợp: **OK** = merge — các rule import được nối thêm vào cuối danh sách hiện có; **Cancel** = replace — toàn bộ rule hiện có của domain này bị thay thế bằng các rule trong file import. +- Nếu domain hiện tại **chưa có rule nào**, import sẽ được áp dụng ngay, không hỏi. +- File không hợp lệ (không phải JSON hợp lệ, thiếu mảng `overrides`, hoặc có rule thiếu field bắt buộc) sẽ bị từ chối kèm cảnh báo (alert), và không có gì thay đổi. +- Chỉ các field hợp lệ của rule (`pattern`, `mode`, `body`, `redirectUrl`, `method`, `enabled`) được giữ lại từ mỗi rule import; các property khác trong file sẽ bị bỏ qua. + +Giống Export, Import luôn thao tác trên domain đang active trong panel — không bao giờ áp dụng cho tất cả domain cùng lúc. + +--- + +## 9. Tính năng bổ sung + +### 9.1. Copy cURL + +Mỗi API entry có nút **cURL**. Click để copy request dưới dạng lệnh cURL: + +```bash +curl 'https://api.example.com/data' \ + -X 'POST' \ + -H 'Content-Type: application/json' \ + -H 'Authorization: Bearer xxx' \ + --data-raw '{"key":"value"}' +``` + +### 9.2. Tìm kiếm API + +Ô tìm kiếm ở tab **Captured APIs** và **Overridden**. Tìm kiếm theo URL substring, không phân biệt hoa thường. Kết quả tìm kiếm được highlight (nền vàng). + +### 9.3. Refresh API list + +Click nút **Refresh** (↻) ở góc trên bên phải. Extension sẽ thử 5 lần (mỗi lần cách 250ms) để load APIs từ background worker. + +### 9.4. Phân loại API theo type + +API được nhóm theo resource type: + +| Type | Hiển thị | Màu/Icon | +| ----------- | ----------- | -------- | +| XHR | XHR | -- | +| Fetch | Fetch | -- | +| JS | JS | -- | +| CSS | CSS | -- | +| Image | Img | -- | +| Media | Media | -- | +| Font | Font | -- | +| Document | Doc | -- | +| WebSocket | WS | -- | +| Manifest | Manifest | -- | +| EventSource | EventSource | -- | +| TextTrack | TextTrack | -- | +| Khác | Other | -- | + +Click vào tiêu đề nhóm (ví dụ "XHR ▼") để thu gọn/mở rộng. Trạng thái thu gọn được lưu trong storage. + +### 9.5. Headers đánh dấu + +Khi một request bị ghi đè, extension thêm các header sau vào response: + +- `x-network-overrides: true` +- `x-network-overrides-pattern: ` + +Điều này giúp bạn dễ dàng nhận biết request nào đã bị can thiệp ngay trong Network tab của DevTools. + +--- + +## 10. Lưu ý quan trọng + +### 10.1. Phạm vi ảnh hưởng + +- Override chỉ áp dụng cho **tab hiện tại** (tab đang được attach debugger). +- Mỗi lần bật toggle, extension sẽ attach debugger vào tab **đang active**. +- Chuyển tab khác → override không còn tác dụng cho đến khi bật lại. + +### 10.2. Dung lượng + +- Tối đa **500 URL** trong danh sách recent APIs. +- Tối đa **100 response bodies** được lưu. +- Khi vượt quá, dữ liệu cũ nhất sẽ bị xóa (FIFO). + +### 10.3. Response body storage + +- Chỉ **XHR** và **Fetch** mới được lưu response body (dùng cho auto-fill). +- Các loại khác (JS, CSS, Image...) không được lưu body. + +### 10.4. Tab ID + +Recent APIs và response bodies được lưu với key `recentApis_{tabId}` và `recentApiBodies_{tabId}`. Khi bạn đóng tab và mở lại, `tabId` mới sẽ khác → dữ liệu cũ không hiển thị. + +### 10.5. Không dùng cho production + +Extension này chỉ dành cho **developer debugging**. Không sử dụng trong môi trường production cho người dùng cuối. + +### 10.6. Permissions + +Extension yêu cầu các quyền: + +- `debugger` — để can thiệp request. +- `storage` — để lưu rules và dữ liệu. +- `` — để attach debugger vào mọi tab. + +--- + +## 11. FAQ + +### Q: Override không hoạt động? + +**Kiểm tra:** + +1. Đã bật **Enable Overrides** chưa? (Toggle phải xanh). +2. Pattern có khớp với URL không? Thử pattern `*` để match all. +3. Tab hiện tại có phải tab đang được debug không? (Thử refresh extension). +4. Mở DevTools → Console của extension để xem lỗi (nếu có). + +### Q: Tắt override rồi mà request vẫn bị ảnh hưởng? + +Có thể cần refresh lại trang. Nếu vẫn tiếp diễn, tắt hẳn extension và bật lại. + +### Q: Làm sao xóa tất cả rules? + +Vào tab **Rules**, click ✕ trên từng rule. Extension không có nút "Clear all" — cần xóa thủ công từng cái. + +### Q: Dữ liệu API cũ từ hôm qua vẫn còn? + +Override rules thì có (lưu vĩnh viễn). Recent APIs thì không — khi tab cũ đóng, tab mới có `tabId` khác. Nếu bạn thấy API cũ, có thể do bạn đang mở lại đúng tab cũ. + +### Q: Extension không hoạt động với tab ẩn danh (incognito)? + +Vào `chrome://extensions` → click **Details** của extension → bật **Allow in incognito**. + +### Q: Làm sao biết request đã bị override? + +Kiểm tra Network tab trong DevTools: + +- Header `x-network-overrides: true` được thêm vào response. +- Trong extension, API entry có highlight xanh (class `active`) và xuất hiện trong tab **Overridden**. + +### Q: Không thấy API hiện ra trong Captured APIs? + +Click nút **Refresh** (↻). Extension thử 5 lần trong 1.25 giây. Nếu vẫn không thấy: + +1. Kiểm tra toggle đã bật chưa. +2. Kiểm tra tab có thực sự gửi request không (nhìn Network tab). +3. Mở DevTools panel thay vì popup (panel dùng `chrome.devtools.network` nên có thể bắt được nhiều hơn). + +### Q: Có hỗ trợ localStorage hoặc sync storage không? + +Không. Extension dùng `chrome.storage.local` (dung lượng 10MB). Không dùng sync storage vì rules có thể chứa dữ liệu lớn (response body). + +### Q: Có thể override WebSocket không? + +Không. Extension chỉ hoạt động với HTTP request (XHR, Fetch) thông qua Fetch domain của CDP. WebSocket không nằm trong phạm vi này. + +### Q: Có thể import file rules được export từ domain khác không? + +Có. Import luôn ghi vào domain đang active trong panel, bất kể field `domain` trong file được export từ domain nào — nhưng nếu field đó khác domain hiện tại, một hộp thoại confirm sẽ cảnh báo trước, để sự khác biệt này không bị bỏ qua âm thầm. diff --git a/commitlint.config.mjs b/commitlint.config.mjs index 36cc406..5a0763b 100644 --- a/commitlint.config.mjs +++ b/commitlint.config.mjs @@ -1,7 +1,7 @@ export default { extends: ['@commitlint/config-conventional'], rules: { - 'header-max-length': [2, 'always', 72], + 'header-max-length': [2, 'always', 100], 'type-enum': [ 2, 'always', diff --git a/devtools.html b/devtools.html index ef9afad..d1009a3 100644 --- a/devtools.html +++ b/devtools.html @@ -6,6 +6,7 @@ + diff --git a/docs/superpowers/plans/2026-07-07-rule-toggle-method-import-export.md b/docs/superpowers/plans/2026-07-07-rule-toggle-method-import-export.md new file mode 100644 index 0000000..71530b2 --- /dev/null +++ b/docs/superpowers/plans/2026-07-07-rule-toggle-method-import-export.md @@ -0,0 +1,1175 @@ +# Per-rule toggle, method matching, and import/export Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add per-rule enable/disable, HTTP-method-scoped matching, and JSON import/export to the existing override-rule system in this Chrome extension. + +**Architecture:** Extend the existing `OverrideRule` schema with two optional fields (`enabled`, `method`), add one shared matching helper in `utils.ts` consumed by both `background.ts` (service worker, via the existing `importScripts` wiring) and `ui.ts` (devtools panel / popup, via the existing `NetworkOverridesUtils` namespace delegation), then layer UI for toggling, method selection, and file-based import/export on top. + +**Tech Stack:** TypeScript (compiled via `tsc` to `dist/`), vanilla DOM (no framework), `node:test` + `jsdom` + `node:vm` for tests (see `tests/test-harness.mjs`). + +## Global Constraints + +- `OverrideRule.enabled` is optional; `undefined` or `true` means enabled, only `false` means disabled. No migration step for existing stored rules. +- `OverrideRule.method` is optional; `undefined` or `'ANY'` means it matches every HTTP method. Only method matching is in scope — no status-code matching (explicitly out of scope per spec). +- Import/Export operates on the **currently active domain only** — never all domains at once. +- Import merge-vs-replace choice uses the native `confirm()` dialog: `OK` (`true`) = merge/append, `Cancel` (`false`) = replace. There is no third "abort" choice once a valid file is selected. +- Export file JSON shape: `{ "version": 1, "domain": string, "exportedAt": ISO-8601 string, "overrides": OverrideRule[] }`. +- Every task must leave `npm run build`, `npm run lint`, and `npm run test` passing before moving to the next task. +- Spec reference: `docs/superpowers/specs/2026-07-07-rule-toggle-method-import-export-design.md`. + +--- + +### Task 1: Data model + shared `matchesMethod` helper + +**Files:** + +- Modify: `src/shared.ts` +- Modify: `src/utils.ts` +- Test: `tests/helpers.test.mjs` + +**Interfaces:** + +- Produces: `NetworkOverridesShared.OverrideRule.enabled?: boolean`, `NetworkOverridesShared.OverrideRule.method?: string`; `NetworkOverridesUtils.matchesMethod(ruleMethod: string | undefined, requestMethod: string | undefined): boolean`. + +- [ ] **Step 1: Write the failing test** + +Add to `tests/helpers.test.mjs` (append at the end of the file, after the last existing `test(...)` block): + +```js +test('matchesMethod matches ANY/undefined and is case-insensitive', () => { + const { NetworkOverridesUtils } = createUiContext(); + + assert.equal(NetworkOverridesUtils.matchesMethod(undefined, 'GET'), true); + assert.equal(NetworkOverridesUtils.matchesMethod('ANY', 'POST'), true); + assert.equal(NetworkOverridesUtils.matchesMethod('get', 'GET'), true); + assert.equal(NetworkOverridesUtils.matchesMethod('GET', 'get'), true); + assert.equal(NetworkOverridesUtils.matchesMethod('POST', 'GET'), false); + assert.equal(NetworkOverridesUtils.matchesMethod('POST', undefined), false); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `npm run build && node --test tests/helpers.test.mjs` +Expected: FAIL — `TypeError: NetworkOverridesUtils.matchesMethod is not a function` + +- [ ] **Step 3: Implement the schema change and helper** + +In `src/shared.ts`, change the `OverrideRule` interface (currently lines 5-10) to: + +```ts +interface OverrideRule { + pattern: string; + body: string; + mode: OverrideMode; + redirectUrl?: string; + enabled?: boolean; // undefined/true = enabled, false = disabled + method?: string; // undefined/'ANY' = any method, or 'GET'|'POST'|'PUT'|'PATCH'|'DELETE' +} +``` + +In `src/utils.ts`, add this function inside the `namespace NetworkOverridesUtils { ... }` block, after `export function getOrigin` (currently the last function, ending at line 61): + +```ts +export function matchesMethod( + ruleMethod: string | undefined, + requestMethod: string | undefined +): boolean { + if (!ruleMethod || ruleMethod.toUpperCase() === 'ANY') return true; + if (!requestMethod) return false; + return ruleMethod.toUpperCase() === requestMethod.toUpperCase(); +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `npm run build && node --test tests/helpers.test.mjs` +Expected: PASS (all tests in the file, including the new one) + +- [ ] **Step 5: Run the full suite and lint** + +Run: `npm run lint && npm run test` +Expected: `npm run lint` exits 0 (warnings about namespace names being "unused" are pre-existing and expected — see `eslint.config.mjs` comment); all tests in `npm run test` PASS. + +- [ ] **Step 6: Commit** + +```bash +git add src/shared.ts src/utils.ts tests/helpers.test.mjs +git commit -m "feat: add enabled/method fields to OverrideRule and matchesMethod helper" +``` + +--- + +### Task 2: Background enforcement — `findOverride` respects `enabled` and `method` + +**Files:** + +- Modify: `src/background.ts:522-533` (`findOverride`), `src/background.ts:571` and `src/background.ts:615` (call sites) +- Test: `tests/background-flow.test.mjs` + +**Interfaces:** + +- Consumes: `NetworkOverridesUtils.matchesMethod` (Task 1). +- Produces: `findOverride(url: string, method: string | undefined, overrides: OverrideRule[]): { override: OverrideRule; captures: string[] } | null` (signature change — one new required parameter in the middle). + +- [ ] **Step 1: Write the failing test** + +Add to `tests/background-flow.test.mjs` (append at the end of the file): + +```js +test('Background skips disabled rules and method-mismatched rules, falling through to the next match', async () => { + const harness = createBackgroundHarness(); + + harness.callMessage({ + type: 'update', + tabId: 7, + tabUrl: `${TEST_DOMAIN}/`, + enabled: true, + overrides: [ + { pattern: '/users$/', body: '{"disabled":true}', mode: 'text', enabled: false }, + { pattern: '/users$/', body: '{"wrongMethod":true}', mode: 'text', method: 'POST' }, + { pattern: '/users$/', body: '{"matched":true}', mode: 'text', method: 'GET' }, + ], + }); + await Promise.resolve(); + + harness.emitDebuggerEvent('Fetch.requestPaused', { + requestId: 'req-method', + request: { url: TEST_API_URL, method: 'GET' }, + responseStatusCode: 200, + resourceType: 'Fetch', + }); + + const fulfill = harness.commandLog.find( + ({ method, params }) => method === 'Fetch.fulfillRequest' && params.requestId === 'req-method' + ); + assert.ok(fulfill); + assert.equal(Buffer.from(fulfill.params.body, 'base64').toString('utf8'), '{"matched":true}'); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `npm run build && node --test tests/background-flow.test.mjs` +Expected: FAIL — the first (disabled) rule currently wins the match, so `fulfill.params.body` decodes to `{"disabled":true}`, not `{"matched":true}`. + +- [ ] **Step 3: Implement** + +In `src/background.ts`, replace the `findOverride` function (currently lines 522-533): + +```ts +function findOverride( + url: string, + method: string | undefined, + overrides: OverrideRule[] +): { override: OverrideRule; captures: string[] } | null { + for (const test of overrides) { + if (test.enabled === false) continue; + if (!NetworkOverridesUtils.matchesMethod(test.method, method)) continue; + const captures = matchPattern(test.pattern, url); + if (captures !== null) { + return { override: test, captures }; + } + } + return null; +} +``` + +Then update both call sites. At line 571 (inside `handleRequestPaused`, request stage): + +```ts +const match = findOverride(url, params.request?.method, info.overrides); +``` + +(replacing `const match = findOverride(url, info.overrides);`) + +At line 615 (inside `handleRequestPaused`, response stage): + +```ts +const match = findOverride(url, params.request?.method, info.overrides); +``` + +(replacing `const match = findOverride(url, info.overrides);`) + +- [ ] **Step 4: Run test to verify it passes** + +Run: `npm run build && node --test tests/background-flow.test.mjs` +Expected: PASS (all tests in the file) + +- [ ] **Step 5: Run the full suite and lint** + +Run: `npm run lint && npm run test` +Expected: both pass with no regressions. + +- [ ] **Step 6: Commit** + +```bash +git add src/background.ts tests/background-flow.test.mjs +git commit -m "feat: findOverride skips disabled rules and method-mismatched rules" +``` + +--- + +### Task 3: UI — `wouldApply` split for tab classification + dim API items matched by a disabled rule + +**Files:** + +- Modify: `src/ui.ts` (add `wouldApply`; update `renderApis`, `updateTabLabels`, `renderApiSection`) +- Modify: `styles.css` (add `.api-item--rule-disabled`) +- Test: `tests/ui-behavior.test.mjs` + +**Interfaces:** + +- Consumes: `NetworkOverridesUtils.matchesMethod` (Task 1), existing `patternMatches` (unchanged). +- Produces: namespace-level `wouldApply(override: OverrideRule, api: ApiEntry): boolean` in `ui.ts`, usable by later tasks. + +- [ ] **Step 1: Write the failing test** + +Add to `tests/ui-behavior.test.mjs` (append at the end of the file): + +```js +test('A disabled rule still counts an API as Overridden but dims it; a method-mismatched rule does not count it at all', async () => { + const harness = createUiHarness({ + storageState: { + enabled: true, + overrides: [ + { pattern: 'users', body: '{}', mode: 'text', enabled: false }, + { pattern: 'orders', body: '{}', mode: 'text', method: 'POST' }, + ], + }, + apis: [ + { url: `${TEST_DOMAIN}/api/users`, type: 'fetch', method: 'GET' }, + { url: `${TEST_DOMAIN}/api/orders`, type: 'fetch', method: 'GET' }, + ], + }); + + await flushUi(harness.window); + + harness.document + .querySelector('[data-tab="overridden"]') + .dispatchEvent(new harness.window.MouseEvent('click', { bubbles: true })); + await flushUi(harness.window); + + const overriddenItems = harness.document.querySelectorAll('.api-item'); + assert.equal(overriddenItems.length, 1); + assert.equal(overriddenItems[0].classList.contains('api-item--rule-disabled'), true); + + harness.document + .querySelector('[data-tab="other"]') + .dispatchEvent(new harness.window.MouseEvent('click', { bubbles: true })); + await flushUi(harness.window); + + assert.equal(harness.document.querySelectorAll('.api-item').length, 1); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `npm run build && node --test tests/ui-behavior.test.mjs` +Expected: FAIL — today, `patternMatches` alone decides "Overridden", so the method-mismatched `orders` rule still counts `orders` as Overridden (both APIs end up in the Overridden tab, or the dimmed class doesn't exist at all). + +- [ ] **Step 3: Implement** + +In `src/ui.ts`, add a new namespace-level function right after the `patternMatches` export (currently lines 51-53): + +```ts +function wouldApply(override: OverrideRule, api: ApiEntry): boolean { + return ( + NetworkOverridesUtils.matchesMethod(override.method, api.method) && + patternMatches(override.pattern, api.url) + ); +} +``` + +In `renderApis()` (currently lines 352-381), replace the `overriddenApis`/`otherApis` computation: + +```ts +const overriddenApis = visibleApis.filter(api => + state.overrides.some(override => wouldApply(override, api)) +); +const otherApis = visibleApis.filter( + api => !state.overrides.some(override => wouldApply(override, api)) +); +``` + +(replacing the two `state.overrides.some(override => patternMatches(override.pattern, api.url))` lines) + +In `updateTabLabels()` (currently lines 402-414), replace the `overriddenCount` computation: + +```ts +const overriddenCount = state.apis.filter(api => + state.overrides.some(override => wouldApply(override, api)) +).length; +``` + +In `renderApiSection()` (currently lines 416-483), replace this block: + +```ts +const isOverridden = state.overrides.some(override => patternMatches(override.pattern, api.url)); +const isSelected = state.selectedApi ? patternMatches(state.selectedApi, api.url) : false; +if (isOverridden) li.classList.add('active'); +if (isSelected) li.classList.add('selected'); +``` + +with: + +```ts +const matchedOverride = state.overrides.find(override => wouldApply(override, api)); +const isOverridden = !!matchedOverride; +const isSelected = state.selectedApi ? patternMatches(state.selectedApi, api.url) : false; +if (isOverridden) li.classList.add('active'); +if (matchedOverride?.enabled === false) li.classList.add('api-item--rule-disabled'); +if (isSelected) li.classList.add('selected'); +``` + +In `styles.css`, add after the `.api-status` rule block (search for `.api-status {` — currently starts at line 616): + +```css +.api-item--rule-disabled { + opacity: 0.55; +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `npm run build && node --test tests/ui-behavior.test.mjs` +Expected: PASS (all tests in the file) + +- [ ] **Step 5: Run the full suite and lint** + +Run: `npm run lint && npm run test` +Expected: both pass with no regressions. + +- [ ] **Step 6: Commit** + +```bash +git add src/ui.ts styles.css tests/ui-behavior.test.mjs +git commit -m "feat: split rule matching from enabled state so disabled rules stay visible but dimmed" +``` + +--- + +### Task 4: UI — per-rule enable/disable checkbox in the Rules list + +**Files:** + +- Modify: `src/ui.ts` (`renderList`; add a `change` listener on `elements.listEl`) +- Modify: `styles.css` (`.override-item--disabled`, `.override-enabled-toggle`, `.override-method-badge`) +- Test: `tests/ui-behavior.test.mjs` + +**Interfaces:** + +- Consumes: none new. +- Produces: none new (internal UI wiring only). + +- [ ] **Step 1: Write the failing test** + +Add to `tests/ui-behavior.test.mjs` (append at the end of the file): + +```js +test('Toggling a rule checkbox persists its enabled state, dims the row, and notifies background', async () => { + const harness = createUiHarness({ + storageState: { + enabled: true, + overrides: [{ pattern: 'users', body: '{"ok":true}', mode: 'text' }], + }, + apis: [{ url: `${TEST_DOMAIN}/api/users`, type: 'fetch' }], + }); + + await flushUi(harness.window); + + harness.document + .querySelector('[data-tab="overrides"]') + .dispatchEvent(new harness.window.MouseEvent('click', { bubbles: true })); + await flushUi(harness.window); + + const checkbox = harness.document.querySelector('.override-enabled-toggle'); + assert.equal(checkbox.checked, true); + + checkbox.checked = false; + checkbox.dispatchEvent(new harness.window.Event('change', { bubbles: true })); + await flushUi(harness.window); + + assert.equal(harness.localState.overrides[0].enabled, false); + assert.equal( + harness.document.querySelector('.override-item').classList.contains('override-item--disabled'), + true + ); + + const lastUpdate = harness.sentMessages.filter(message => message.type === 'update').at(-1); + assert.equal(lastUpdate.overrides[0].enabled, false); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `npm run build && node --test tests/ui-behavior.test.mjs` +Expected: FAIL — `harness.document.querySelector('.override-enabled-toggle')` is `null` today (element doesn't exist), causing a `TypeError` on `checkbox.checked`. + +- [ ] **Step 3: Implement** + +In `src/ui.ts`, replace `renderList()` (currently lines 221-242): + +```ts +function renderList(): void { + elements.listEl.innerHTML = ''; + state.overrides.forEach((override, index) => { + const li = document.createElement('li'); + li.className = 'override-item'; + if (override.enabled === false) { + li.classList.add('override-item--disabled'); + } + const methodBadge = + override.method && override.method !== 'ANY' + ? `${escapeHtml(override.method)} ` + : ''; + li.innerHTML = ` +
+ +
+ ${escapeHtml(override.pattern)} + ${override.redirectUrl ? `
→ ${escapeHtml(override.redirectUrl)}
` : ''} +
${methodBadge}${escapeHtml(override.mode)}${override.body ? ` · ${escapeHtml(override.body.substring(0, 80))}${override.body.length > 80 ? '…' : ''}` : ''}
+
+
+ + +
+
+ `; + elements.listEl.appendChild(li); + }); + updateTabLabels(); +} +``` + +Then add a new `change` listener on `elements.listEl`. Insert it directly after the existing `elements.listEl.addEventListener('click', ...)` block (currently ends at line 649, right before `elements.closeModal.addEventListener('click', closeOverrideModal);` at line 651): + +```ts +elements.listEl.addEventListener('change', async event => { + const target = event.target as HTMLElement; + if (!target.classList.contains('override-enabled-toggle')) { + return; + } + const index = Number(target.dataset.index); + if (Number.isNaN(index) || !state.overrides[index]) { + return; + } + state.overrides[index].enabled = (target as HTMLInputElement).checked; + await chrome.storage.local.set({ [domainKey('overrides')]: state.overrides }); + renderList(); + renderApis(); + await notifyBackground(); +}); +``` + +In `styles.css`, add after the `.override-item-actions button:hover` rule (currently ends at line 233): + +```css +.override-item--disabled { + opacity: 0.55; +} +.override-enabled-toggle { + flex-shrink: 0; + margin-top: 2px; + cursor: pointer; +} +.override-method-badge { + display: inline-block; + padding: 0 4px; + border-radius: 3px; + background: #eef4fa; + color: #35506b; + font-weight: 600; + font-size: 0.78em; + margin-right: 4px; +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `npm run build && node --test tests/ui-behavior.test.mjs` +Expected: PASS (all tests in the file) + +- [ ] **Step 5: Run the full suite and lint** + +Run: `npm run lint && npm run test` +Expected: both pass with no regressions. + +- [ ] **Step 6: Commit** + +```bash +git add src/ui.ts styles.css tests/ui-behavior.test.mjs +git commit -m "feat: add per-rule enable/disable checkbox to the Rules list" +``` + +--- + +### Task 5: UI — HTTP method field on the add/edit modal + +**Files:** + +- Modify: `src/ui.ts` (`Elements` interface, `getElements`, `openOverrideModalForIndex`, `openOverrideModal`, `addApiBtn` handler, `saveOverrideBtn` handler) +- Modify: `panel.html`, `popup.html` (add `` right after the `Pattern` field (search for `Pattern` in each file): + +```html + + +``` + +(replacing just the existing `` block with itself plus the new method label — i.e., insert the new `