diff --git a/.github/workflows/sync-wiki.yml b/.github/workflows/sync-wiki.yml index babfd5b..3e60f8e 100644 --- a/.github/workflows/sync-wiki.yml +++ b/.github/workflows/sync-wiki.yml @@ -22,6 +22,9 @@ jobs: - name: Copy documentation to Wiki run: | cp docs/*.md wiki-repo/ + if [ -d docs/images ]; then + cp -r docs/images wiki-repo/ + fi - name: Commit and push to Wiki working-directory: wiki-repo diff --git a/CLAUDE.md b/CLAUDE.md index 054fd69..94503b8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,6 +72,55 @@ DEBUG=true npm run test:e2e - **Puppeteer**: Browser automation for E2E testing - Tests organized by functionality for easy maintenance and selective execution +## Documentation + +The project includes comprehensive documentation in the `docs/` folder, which is automatically synced to the GitHub Wiki. + +### Documentation Structure + +``` +docs/ +├── Home.md # Main documentation landing page +├── Getting-Started.md # Installation and first steps +├── Variables.md # Variables system documentation +├── Auto-Refresh-Tokens.md # Auto-refresh feature guide +├── Advanced-Features.md # Power user features +├── Examples.md # Real-world configuration examples +├── FAQ.md # Frequently asked questions +└── images/ # Screenshots for documentation + ├── README.md # Screenshot catalog + └── *.png # Screenshot files (15 files) +``` + +### Generating Documentation Screenshots + +```bash +# Generate all documentation screenshots +npm run screenshots +``` + +This command: +1. Builds the extension (`npm run build`) +2. Launches Chrome with the extension loaded (via Puppeteer) +3. Creates various UI configurations (rules, variables, auto-refresh setups) +4. Captures screenshots in 1280x800 resolution (light theme) +5. Saves them to `docs/images/` + +**Screenshot Generation Script:** `scripts/generate-screenshots.ts` +- Reuses test helpers from `tests/helpers/` (browser launch, configuration) +- Automatically creates 15+ screenshots covering all major features +- Screenshots are version-controlled and should be updated when UI changes + +### Wiki Synchronization + +Documentation is automatically synced to GitHub Wiki via GitHub Actions: +- **Workflow:** `.github/workflows/sync-wiki.yml` +- **Trigger:** Push to `master` branch with changes in `docs/**` +- **Synced content:** + - All `*.md` files from `docs/` + - All images from `docs/images/` +- **Requirements:** `WIKI_TOKEN` secret must be configured in repository settings + ## Architecture ### Core Components diff --git a/docs/Auto-Refresh-Tokens.md b/docs/Auto-Refresh-Tokens.md index 19fcd86..d6da39c 100644 --- a/docs/Auto-Refresh-Tokens.md +++ b/docs/Auto-Refresh-Tokens.md @@ -64,6 +64,10 @@ Transform Response: access_token 3. ModHead extracts `new_token_123` using the transform path 4. Variable `accessToken` is updated to `new_token_123` +Here's what the auto-refresh configuration looks like: + +![Auto-Refresh Configuration](images/11-auto-refresh-config.png) + ## HTTP Methods ModHead supports all standard HTTP methods for refresh requests. diff --git a/docs/Examples.md b/docs/Examples.md index a189a0e..10bda98 100644 --- a/docs/Examples.md +++ b/docs/Examples.md @@ -130,6 +130,8 @@ All requests to `localhost:*` will include CORS headers, allowing your local fro These headers are added to **requests**, not responses. For proper CORS handling, your server must return the appropriate CORS headers in **responses**. However, this configuration can help with certain testing scenarios. +![CORS Headers Configuration](images/15-cors-headers.png) + --- ## 4. OAuth 2.0 Token Refresh @@ -185,6 +187,8 @@ Headers: - Long-running development sessions - Testing OAuth flows +![OAuth 2.0 Configuration](images/13-oauth-example.png) + --- ## 5. Multi-Stage Authentication @@ -579,6 +583,8 @@ Headers: - Long development sessions - Avoiding manual token updates +![JWT Token with Auto-Refresh](images/12-jwt-refresh-example.png) + --- ## Combining Examples diff --git a/docs/Getting-Started.md b/docs/Getting-Started.md index aeb97a6..bb6fbd2 100644 --- a/docs/Getting-Started.md +++ b/docs/Getting-Started.md @@ -45,6 +45,10 @@ There are two ways to access ModHead's options page: 1. **Click the extension icon** in the Chrome toolbar 2. **Right-click the icon** → Select "Options" +When you first open the options page, it will be empty: + +![Empty Options Page](images/01-options-page-empty.png) + ## Creating Your First Rule Let's create a simple rule to add a custom header to all requests to `httpbin.org`. @@ -54,6 +58,8 @@ Let's create a simple rule to add a custom header to all requests to `httpbin.or 1. Click the **"Create Rule"** button 2. The rule editor modal will open +![Rule Editor Modal](images/02-rule-editor-empty.png) + ### Step 2: Configure Basic Settings **Rule Name:** Give your rule a descriptive name @@ -81,7 +87,9 @@ This will match all URLs that start with `httpbin.org` (e.g., `httpbin.org/get`, ### Step 5: Save the Rule -Click **"Save"** to create the rule. +Click **"Save"** to create the rule. Your new rule will appear on the options page: + +![Rule Card with Single Header](images/03-rule-single-header.png) ## Understanding URL Pattern Matching @@ -117,6 +125,10 @@ Matches URLs that exactly match the specified pattern. **Use case:** Target a specific endpoint +Here's an example of a rule with multiple target domains using different match types: + +![URL Pattern Matching Examples](images/06-url-pattern-matching.png) + ## Multiple Target Domains A single rule can have multiple target domains with different match types: @@ -147,6 +159,10 @@ Header 3: Value: v2 ``` +Here's what a rule with multiple headers looks like: + +![Rule with Multiple Headers](images/14-multiple-headers.png) + ## Testing Your Rule Let's verify that your rule is working: diff --git a/docs/Home.md b/docs/Home.md index fef68d3..933d13c 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -35,6 +35,8 @@ ModHead is a Chrome extension that allows you to dynamically modify HTTP request - Enable/disable rules with a single click - Visual feedback with toast notifications +![ModHead Options Page](images/05-multiple-rules.png) + ## Documentation Navigation ### Getting Started diff --git a/docs/Variables.md b/docs/Variables.md index e18c9f2..e397ecd 100644 --- a/docs/Variables.md +++ b/docs/Variables.md @@ -26,6 +26,10 @@ Variables are named placeholders that store values you can reuse throughout your 4. **Click "Save"** +Here's the variable editor interface: + +![Variable Editor](images/08-variable-editor-empty.png) + ### Sensitive Variables Sensitive variables are displayed with password masking in the UI to prevent shoulder-surfing. @@ -46,6 +50,10 @@ When enabled, the value will display as `••••••••` in the UI but - Client secrets - Any confidential data +Here's how sensitive variables appear in the UI: + +![Sensitive Variable Masked](images/10-sensitive-variable-masked.png) + ## Using Variables in Headers Use the `${variableName}` syntax to reference variables in header values. @@ -109,6 +117,14 @@ Header Value: user=${userId};session=${sessionId} X-Session-Info: user=12345;session=xyz789 ``` +Here's an example of using variables in header values: + +![Variable Usage in Headers](images/09-variable-in-header.png) + +The variables section showing multiple variables: + +![Variables Section](images/07-variables-section.png) + ## Variable Naming Best Practices ### Valid Names diff --git a/docs/images/01-options-page-empty.png b/docs/images/01-options-page-empty.png new file mode 100644 index 0000000..908f908 Binary files /dev/null and b/docs/images/01-options-page-empty.png differ diff --git a/docs/images/02-rule-editor-empty.png b/docs/images/02-rule-editor-empty.png new file mode 100644 index 0000000..f9c764f Binary files /dev/null and b/docs/images/02-rule-editor-empty.png differ diff --git a/docs/images/03-rule-single-header.png b/docs/images/03-rule-single-header.png new file mode 100644 index 0000000..122d729 Binary files /dev/null and b/docs/images/03-rule-single-header.png differ diff --git a/docs/images/04-rule-card.png b/docs/images/04-rule-card.png new file mode 100644 index 0000000..6c8fcfc Binary files /dev/null and b/docs/images/04-rule-card.png differ diff --git a/docs/images/05-multiple-rules.png b/docs/images/05-multiple-rules.png new file mode 100644 index 0000000..dbf97a7 Binary files /dev/null and b/docs/images/05-multiple-rules.png differ diff --git a/docs/images/06-url-pattern-matching.png b/docs/images/06-url-pattern-matching.png new file mode 100644 index 0000000..6c8fcfc Binary files /dev/null and b/docs/images/06-url-pattern-matching.png differ diff --git a/docs/images/07-variables-section.png b/docs/images/07-variables-section.png new file mode 100644 index 0000000..b69f393 Binary files /dev/null and b/docs/images/07-variables-section.png differ diff --git a/docs/images/08-variable-editor-empty.png b/docs/images/08-variable-editor-empty.png new file mode 100644 index 0000000..199f238 Binary files /dev/null and b/docs/images/08-variable-editor-empty.png differ diff --git a/docs/images/09-variable-in-header.png b/docs/images/09-variable-in-header.png new file mode 100644 index 0000000..9bc5bd5 Binary files /dev/null and b/docs/images/09-variable-in-header.png differ diff --git a/docs/images/10-sensitive-variable-masked.png b/docs/images/10-sensitive-variable-masked.png new file mode 100644 index 0000000..c9fa711 Binary files /dev/null and b/docs/images/10-sensitive-variable-masked.png differ diff --git a/docs/images/11-auto-refresh-config.png b/docs/images/11-auto-refresh-config.png new file mode 100644 index 0000000..6bf0512 Binary files /dev/null and b/docs/images/11-auto-refresh-config.png differ diff --git a/docs/images/12-jwt-refresh-example.png b/docs/images/12-jwt-refresh-example.png new file mode 100644 index 0000000..1616c35 Binary files /dev/null and b/docs/images/12-jwt-refresh-example.png differ diff --git a/docs/images/13-oauth-example.png b/docs/images/13-oauth-example.png new file mode 100644 index 0000000..649f8cb Binary files /dev/null and b/docs/images/13-oauth-example.png differ diff --git a/docs/images/14-multiple-headers.png b/docs/images/14-multiple-headers.png new file mode 100644 index 0000000..a4127d2 Binary files /dev/null and b/docs/images/14-multiple-headers.png differ diff --git a/docs/images/15-cors-headers.png b/docs/images/15-cors-headers.png new file mode 100644 index 0000000..a4127d2 Binary files /dev/null and b/docs/images/15-cors-headers.png differ diff --git a/docs/images/README.md b/docs/images/README.md new file mode 100644 index 0000000..e852cc6 --- /dev/null +++ b/docs/images/README.md @@ -0,0 +1,82 @@ +# Documentation Screenshots + +This directory contains screenshots for the ModHead documentation. + +## Screenshot List + +### UI Components + +| File | Description | Used In | +|------|-------------|---------| +| `01-options-page-empty.png` | Empty options page (initial state) | Getting-Started.md | +| `02-rule-editor-empty.png` | Empty rule editor modal | Getting-Started.md | +| `03-rule-single-header.png` | Rule card with single header | Getting-Started.md | +| `04-rule-card.png` | Rule card with multiple headers | - | +| `05-multiple-rules.png` | Options page with multiple rules | Home.md | +| `06-url-pattern-matching.png` | Rule with different URL match types | Getting-Started.md | +| `08-variable-editor-empty.png` | Empty variable editor modal | Variables.md | + +### Variables + +| File | Description | Used In | +|------|-------------|---------| +| `07-variables-section.png` | Variables section with multiple variables | Variables.md | +| `09-variable-in-header.png` | Variable usage in header values | Variables.md | +| `10-sensitive-variable-masked.png` | Sensitive variable with masked value | Variables.md | + +### Auto-Refresh + +| File | Description | Used In | +|------|-------------|---------| +| `11-auto-refresh-config.png` | Auto-refresh configuration interface | Auto-Refresh-Tokens.md | +| `12-jwt-refresh-example.png` | JWT token with auto-refresh setup | Examples.md | +| `13-oauth-example.png` | OAuth 2.0 configuration example | Examples.md | + +### Feature Examples + +| File | Description | Used In | +|------|-------------|---------| +| `14-multiple-headers.png` | Rule with multiple custom headers | Getting-Started.md | +| `15-cors-headers.png` | CORS headers configuration | Examples.md | + +## Generation + +Screenshots are automatically generated using: + +```bash +npm run screenshots +``` + +This command: +1. Builds the extension (`npm run build`) +2. Runs the screenshot generation script (`scripts/generate-screenshots.ts`) +3. Saves all screenshots to this directory + +The generation script uses Puppeteer to: +- Launch Chrome with the extension loaded +- Create various configurations (rules, variables, auto-refresh) +- Capture screenshots of different UI states +- Save them as PNG files + +## Screenshot Specifications + +- **Resolution**: 1280x800 +- **Format**: PNG +- **Theme**: Light mode only +- **Browser**: Chrome/Chromium (via Puppeteer) + +## Updating Screenshots + +If the UI changes and screenshots need to be updated: + +1. Make your changes to the extension +2. Run `npm run screenshots` to regenerate all screenshots +3. Review the updated screenshots +4. Commit the changes + +## Notes + +- Screenshots are version controlled and should be updated when significant UI changes occur +- The GitHub Action workflow (`.github/workflows/sync-wiki.yml`) automatically syncs this directory to the GitHub Wiki +- All screenshots use English language interface +- Screenshots are captured in headless mode for consistency diff --git a/package.json b/package.json index 9e3bbd9..d164bf7 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "test:variables": "vitest run tests/e2e/variables.test.ts", "test:refresh": "vitest run tests/e2e/auto-refresh.test.ts", "test:theme": "vitest run tests/e2e/theme.test.ts", - "test": "npm run build && npm run test:e2e" + "test": "npm run build && npm run test:e2e", + "screenshots": "npm run build && tsx scripts/generate-screenshots.ts" }, "dependencies": { "react": "^19.0.0", diff --git a/scripts/generate-screenshots.ts b/scripts/generate-screenshots.ts new file mode 100644 index 0000000..f426e7a --- /dev/null +++ b/scripts/generate-screenshots.ts @@ -0,0 +1,431 @@ +#!/usr/bin/env tsx +import { Browser, Page } from 'puppeteer'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import fs from 'node:fs'; +import { launchBrowserWithExtension, getExtensionId } from '../tests/helpers/browser.js'; +import { configureExtensionRule, configureVariables, configureVariableWithRefresh } from '../tests/helpers/config.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const SCREENSHOTS_DIR = path.join(__dirname, '../docs/images'); + +// Screenshot configuration +const VIEWPORT = { + width: 1280, + height: 800, +}; + +/** + * Ensure screenshots directory exists + */ +function ensureScreenshotsDir() { + if (!fs.existsSync(SCREENSHOTS_DIR)) { + fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true }); + } + console.log(`Screenshots will be saved to: ${SCREENSHOTS_DIR}`); +} + +/** + * Take a screenshot and save it + */ +async function takeScreenshot(page: Page, filename: string, fullPage = false) { + const filepath = path.join(SCREENSHOTS_DIR, filename); + await page.screenshot({ + path: filepath, + fullPage, + }); + console.log(`✓ Screenshot saved: ${filename}`); +} + +/** + * Wait for page to be stable (no animations, layout shifts) + */ +async function waitForStable(page: Page, delay = 500) { + await new Promise(resolve => setTimeout(resolve, delay)); +} + +/** + * Main screenshot generation function + */ +async function generateScreenshots() { + console.log('🚀 Starting screenshot generation...\n'); + + ensureScreenshotsDir(); + + let browser: Browser | undefined; + + try { + // Launch browser with extension + browser = await launchBrowserWithExtension(); + const extensionId = await getExtensionId(browser); + const optionsUrl = `chrome-extension://${extensionId}/options.html`; + + console.log('\n📸 Generating screenshots...\n'); + + // Screenshot 1: Empty options page (initial state) + console.log('1. Empty options page...'); + const page1 = await browser.newPage(); + await page1.setViewport(VIEWPORT); + await page1.goto(optionsUrl, { waitUntil: 'networkidle0' }); + await waitForStable(page1); + await takeScreenshot(page1, '01-options-page-empty.png'); + await page1.close(); + + // Screenshot 2: Rule editor modal (empty) + console.log('2. Rule editor modal...'); + const page2 = await browser.newPage(); + await page2.setViewport(VIEWPORT); + await page2.goto(optionsUrl, { waitUntil: 'networkidle0' }); + await waitForStable(page2); + + // Open rule editor + await page2.evaluate(() => { + const button = document.querySelector('[data-testid="create-rule-button"]') as HTMLElement; + button?.click(); + }); + await page2.waitForSelector('[data-testid="save-rule-button"]', { timeout: 5000 }); + await waitForStable(page2); + await takeScreenshot(page2, '02-rule-editor-empty.png'); + await page2.close(); + + // Screenshot 3: Rule with single header + console.log('3. Rule with single header...'); + const page3 = await browser.newPage(); + await page3.setViewport(VIEWPORT); + await page3.goto(optionsUrl, { waitUntil: 'networkidle0' }); + await waitForStable(page3); + + await configureExtensionRule( + page3, + 'API Authentication', + [{ url: 'api.example.com', matchType: 'startsWith' }], + [{ name: 'Authorization', value: 'Bearer token123' }] + ); + await waitForStable(page3); + await takeScreenshot(page3, '03-rule-single-header.png'); + await page3.close(); + + // Screenshot 4: Rule card (showing details) + console.log('4. Rule card details...'); + const page4 = await browser.newPage(); + await page4.setViewport(VIEWPORT); + await page4.goto(optionsUrl, { waitUntil: 'networkidle0' }); + await waitForStable(page4); + + await configureExtensionRule( + page4, + 'Development API', + [{ url: 'dev.example.com', matchType: 'startsWith' }], + [ + { name: 'X-API-Key', value: 'dev-key-12345' }, + { name: 'X-Environment', value: 'development' } + ] + ); + await waitForStable(page4); + await takeScreenshot(page4, '04-rule-card.png'); + await page4.close(); + + // Screenshot 5: Multiple rules + console.log('5. Multiple rules...'); + const page5 = await browser.newPage(); + await page5.setViewport(VIEWPORT); + await page5.goto(optionsUrl, { waitUntil: 'networkidle0' }); + await waitForStable(page5); + + await configureExtensionRule( + page5, + 'Production API', + [{ url: 'api.production.com', matchType: 'startsWith' }], + [{ name: 'Authorization', value: 'Bearer prod-token' }] + ); + await waitForStable(page5, 300); + + await configureExtensionRule( + page5, + 'Staging API', + [{ url: 'api.staging.com', matchType: 'startsWith' }], + [{ name: 'Authorization', value: 'Bearer staging-token' }] + ); + await waitForStable(page5, 300); + + await configureExtensionRule( + page5, + 'CORS Headers', + [{ url: 'localhost', matchType: 'startsWith' }], + [ + { name: 'Access-Control-Allow-Origin', value: '*' }, + { name: 'Access-Control-Allow-Methods', value: 'GET, POST, PUT, DELETE' } + ] + ); + await waitForStable(page5); + await takeScreenshot(page5, '05-multiple-rules.png', true); + await page5.close(); + + // Screenshot 6: URL pattern matching types + console.log('6. URL pattern matching...'); + const page6 = await browser.newPage(); + await page6.setViewport(VIEWPORT); + await page6.goto(optionsUrl, { waitUntil: 'networkidle0' }); + await waitForStable(page6); + + await configureExtensionRule( + page6, + 'Pattern Examples', + [ + { url: 'api.example.com', matchType: 'startsWith' }, + { url: '.com', matchType: 'endsWith' }, + { url: 'https://exact.example.com/path', matchType: 'equals' } + ], + [{ name: 'X-Pattern', value: 'mixed' }] + ); + await waitForStable(page6); + await takeScreenshot(page6, '06-url-pattern-matching.png'); + await page6.close(); + + // Screenshot 7: Variables section + console.log('7. Variables section...'); + const page7 = await browser.newPage(); + await page7.setViewport(VIEWPORT); + await page7.goto(optionsUrl, { waitUntil: 'networkidle0' }); + await waitForStable(page7); + + await configureVariables(page7, [ + { name: 'apiKey', value: 'abc123def456', isSensitive: false }, + { name: 'apiSecret', value: 'secret-value-hidden', isSensitive: true }, + { name: 'environment', value: 'production', isSensitive: false } + ]); + await waitForStable(page7); + await takeScreenshot(page7, '07-variables-section.png', true); + await page7.close(); + + // Screenshot 8: Variable editor modal + console.log('8. Variable editor...'); + const page8 = await browser.newPage(); + await page8.setViewport(VIEWPORT); + await page8.goto(optionsUrl, { waitUntil: 'networkidle0' }); + await waitForStable(page8); + + // Open variable editor + await page8.click('[data-testid="add-variable-button"]'); + await waitForStable(page8); + await takeScreenshot(page8, '08-variable-editor-empty.png'); + await page8.close(); + + // Screenshot 9: Variable usage in headers + console.log('9. Variable usage in headers...'); + const page9 = await browser.newPage(); + await page9.setViewport(VIEWPORT); + await page9.goto(optionsUrl, { waitUntil: 'networkidle0' }); + await waitForStable(page9); + + await configureVariables(page9, [ + { name: 'apiToken', value: 'my-secret-token', isSensitive: true } + ]); + await waitForStable(page9, 300); + + await configureExtensionRule( + page9, + 'Using Variables', + [{ url: 'api.example.com', matchType: 'startsWith' }], + [{ name: 'Authorization', value: 'Bearer ${apiToken}' }] + ); + await waitForStable(page9); + await takeScreenshot(page9, '09-variable-in-header.png', true); + await page9.close(); + + // Screenshot 10: Sensitive variable masked + console.log('10. Sensitive variable masked...'); + const page10 = await browser.newPage(); + await page10.setViewport(VIEWPORT); + await page10.goto(optionsUrl, { waitUntil: 'networkidle0' }); + await waitForStable(page10); + + await configureVariables(page10, [ + { name: 'password', value: 'super-secret-password', isSensitive: true } + ]); + await waitForStable(page10); + await takeScreenshot(page10, '10-sensitive-variable-masked.png'); + await page10.close(); + + // Screenshot 11: Auto-refresh configuration + console.log('11. Auto-refresh configuration...'); + const page11 = await browser.newPage(); + await page11.setViewport(VIEWPORT); + await page11.goto(optionsUrl, { waitUntil: 'networkidle0' }); + await waitForStable(page11); + + await configureVariableWithRefresh(page11, { + name: 'accessToken', + value: 'initial-token', + refreshConfig: { + url: 'https://auth.example.com/token', + method: 'POST', + headers: [ + { key: 'Content-Type', value: 'application/json' } + ], + body: JSON.stringify({ + grant_type: 'refresh_token', + refresh_token: 'refresh-token-value' + }), + transformResponse: 'access_token' + } + }); + await waitForStable(page11); + await takeScreenshot(page11, '11-auto-refresh-config.png', true); + await page11.close(); + + // Screenshot 12: JWT refresh example + console.log('12. JWT refresh example...'); + const page12 = await browser.newPage(); + await page12.setViewport(VIEWPORT); + await page12.goto(optionsUrl, { waitUntil: 'networkidle0' }); + await waitForStable(page12); + + await configureVariableWithRefresh(page12, { + name: 'jwtToken', + value: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', + refreshConfig: { + url: 'https://api.example.com/auth/refresh', + method: 'POST', + headers: [ + { key: 'Content-Type', value: 'application/json' } + ], + body: '{"refreshToken": "refresh-token-here"}', + transformResponse: 'data.token' + } + }); + await waitForStable(page12, 300); + + await configureExtensionRule( + page12, + 'JWT Authentication', + [{ url: 'api.example.com', matchType: 'startsWith' }], + [{ name: 'Authorization', value: 'Bearer ${jwtToken}' }] + ); + await waitForStable(page12); + await takeScreenshot(page12, '12-jwt-refresh-example.png', true); + await page12.close(); + + // Screenshot 13: OAuth example + console.log('13. OAuth example...'); + const page13 = await browser.newPage(); + await page13.setViewport(VIEWPORT); + await page13.goto(optionsUrl, { waitUntil: 'networkidle0' }); + await waitForStable(page13); + + await configureVariables(page13, [ + { name: 'clientId', value: 'my-client-id', isSensitive: false }, + { name: 'clientSecret', value: 'my-client-secret', isSensitive: true } + ]); + await waitForStable(page13, 300); + + await configureVariableWithRefresh(page13, { + name: 'oauthToken', + value: 'initial-oauth-token', + refreshConfig: { + url: 'https://oauth.example.com/token', + method: 'POST', + headers: [ + { key: 'Content-Type', value: 'application/x-www-form-urlencoded' } + ], + body: 'grant_type=client_credentials&client_id=${clientId}&client_secret=${clientSecret}', + transformResponse: 'access_token' + } + }); + await waitForStable(page13, 300); + + await configureExtensionRule( + page13, + 'OAuth API', + [{ url: 'api.oauth-example.com', matchType: 'startsWith' }], + [{ name: 'Authorization', value: 'Bearer ${oauthToken}' }] + ); + await waitForStable(page13); + await takeScreenshot(page13, '13-oauth-example.png', true); + await page13.close(); + + // Screenshot 14: Multiple headers example + console.log('14. Multiple headers...'); + const page14 = await browser.newPage(); + await page14.setViewport(VIEWPORT); + await page14.goto(optionsUrl, { waitUntil: 'networkidle0' }); + await waitForStable(page14); + + await configureExtensionRule( + page14, + 'Custom Headers', + [{ url: 'api.example.com', matchType: 'startsWith' }], + [ + { name: 'X-API-Key', value: 'key-12345' }, + { name: 'X-Environment', value: 'production' }, + { name: 'X-Version', value: 'v2' }, + { name: 'X-User-Agent', value: 'CustomApp/1.0' } + ] + ); + await waitForStable(page14); + await takeScreenshot(page14, '14-multiple-headers.png'); + await page14.close(); + + // Screenshot 15: CORS headers example + console.log('15. CORS headers example...'); + const page15 = await browser.newPage(); + await page15.setViewport(VIEWPORT); + await page15.goto(optionsUrl, { waitUntil: 'networkidle0' }); + await waitForStable(page15); + + await configureExtensionRule( + page15, + 'Local Development CORS', + [{ url: 'localhost', matchType: 'startsWith' }], + [ + { name: 'Access-Control-Allow-Origin', value: '*' }, + { name: 'Access-Control-Allow-Methods', value: 'GET, POST, PUT, DELETE, OPTIONS' }, + { name: 'Access-Control-Allow-Headers', value: 'Content-Type, Authorization' } + ] + ); + await waitForStable(page15); + await takeScreenshot(page15, '15-cors-headers.png'); + await page15.close(); + + // Screenshot 16: Options page overview with content + console.log('16. Options page overview...'); + const page16 = await browser.newPage(); + await page16.setViewport(VIEWPORT); + await page16.goto(optionsUrl, { waitUntil: 'networkidle0' }); + await waitForStable(page16); + + await configureVariables(page16, [ + { name: 'apiKey', value: 'abc123', isSensitive: false } + ]); + await waitForStable(page16, 300); + + await configureExtensionRule( + page16, + 'My API', + [{ url: 'api.myapp.com', matchType: 'startsWith' }], + [{ name: 'X-API-Key', value: '${apiKey}' }] + ); + await waitForStable(page16); + await takeScreenshot(page16, '16-options-page-overview.png', true); + await page16.close(); + + console.log('\n✅ All screenshots generated successfully!'); + console.log(`📁 Screenshots saved to: ${SCREENSHOTS_DIR}`); + + } catch (error) { + console.error('\n❌ Error generating screenshots:', error); + throw error; + } finally { + if (browser) { + await browser.close(); + } + } +} + +// Run the script +generateScreenshots().catch((error) => { + console.error('Fatal error:', error); + process.exit(1); +});