|
| 1 | +# Console-aware Popup Copy Implementation Plan |
| 2 | + |
| 3 | +> **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. |
| 4 | +
|
| 5 | +**Goal:** When the popup's active tab is the extension's own Console page, replace the "This tab isn't supported by the extension." lede with "You're on the Rossum Console." — keeping the rest of the panel (Rossum-tab switcher / static fallback) exactly as-is. |
| 6 | + |
| 7 | +**Architecture:** Add an `isConsoleTab(url)` helper to `src/popup/utils.js`; compute it in `App.jsx` when there's no detected site and pass it to `UnsupportedSite`, which swaps only the lede line. `UnsupportedSite` is exported so it can be unit-tested. |
| 8 | + |
| 9 | +**Tech Stack:** Preact, Vitest (jsdom). |
| 10 | + |
| 11 | +## Global Constraints |
| 12 | + |
| 13 | +- Lede for the Console, verbatim: **`You're on the Rossum Console.`** For any other unsupported tab, the lede stays **`This tab isn't supported by the extension.`** |
| 14 | +- Copy-only: no new actions, buttons, or Console-specific UI. The tab switcher, the "It works on … Open one of these sites to get started." fallback, and the "Also works on NetSuite and Coupa." footnote are unchanged. |
| 15 | +- No manifest change, no new permission (`activeTab` already exposes the active tab's URL on click), no new chrome.storage keys. |
| 16 | +- Prop is named `isConsole` (not `console`) to avoid shadowing the global `console`. |
| 17 | +- No git commits in this run (owner rule) — stay on `master`, leave changes uncommitted; each task's final step is a test gate. Per-task diffs are working-tree diffs scoped to the task's files. |
| 18 | +- Tests: `// @vitest-environment jsdom`, `h(Component, props)` + preact `render`, `vi.mock`. |
| 19 | + |
| 20 | +--- |
| 21 | + |
| 22 | +### Task 1: `isConsoleTab` helper |
| 23 | + |
| 24 | +**Files:** |
| 25 | +- Modify: `src/popup/utils.js` (add exported `isConsoleTab`) |
| 26 | +- Test: `tests/popup-utils.test.js` (add an `isConsoleTab` describe block + import) |
| 27 | + |
| 28 | +**Interfaces:** |
| 29 | +- Produces: `isConsoleTab(url: string) => boolean` — true iff `url` starts with `chrome.runtime.getURL('console/console.html')`. |
| 30 | + |
| 31 | +- [ ] **Step 1: Write the failing test** |
| 32 | + |
| 33 | +In `tests/popup-utils.test.js`, add `isConsoleTab` to the existing import line: |
| 34 | + |
| 35 | +```javascript |
| 36 | +import { runInTab, openConsoleTab, detectSite, findRossumTabs, activateTab, isConsoleTab } from '../src/popup/utils.js'; |
| 37 | +``` |
| 38 | + |
| 39 | +Then add this describe block at the end of the file (before the final closing lines). The file's `beforeEach` already sets `chrome.runtime.getURL = (path) => \`chrome-extension://abc/${path}\``: |
| 40 | + |
| 41 | +```javascript |
| 42 | +describe('isConsoleTab', () => { |
| 43 | + it('matches this extension\'s Console page, with or without a query string', () => { |
| 44 | + expect(isConsoleTab('chrome-extension://abc/console/console.html')).toBe(true); |
| 45 | + expect(isConsoleTab('chrome-extension://abc/console/console.html?authId=x')).toBe(true); |
| 46 | + }); |
| 47 | + |
| 48 | + it('rejects other extensions, other own-pages, sites, and empty input', () => { |
| 49 | + expect(isConsoleTab('chrome-extension://zzz/console/console.html')).toBe(false); |
| 50 | + expect(isConsoleTab('chrome-extension://abc/popup/popup.html')).toBe(false); |
| 51 | + expect(isConsoleTab('https://elis.rossum.ai/queues')).toBe(false); |
| 52 | + expect(isConsoleTab('')).toBe(false); |
| 53 | + expect(isConsoleTab(undefined)).toBe(false); |
| 54 | + }); |
| 55 | +}); |
| 56 | +``` |
| 57 | + |
| 58 | +- [ ] **Step 2: Run the test to verify it fails** |
| 59 | + |
| 60 | +Run: `npx vitest run tests/popup-utils.test.js` |
| 61 | +Expected: FAIL — `isConsoleTab` is not exported (import resolves to `undefined`, so calling it throws / assertions fail). |
| 62 | + |
| 63 | +- [ ] **Step 3: Add the helper** |
| 64 | + |
| 65 | +In `src/popup/utils.js`, add immediately after the `detectSite` function (after its closing `}` near line 14): |
| 66 | + |
| 67 | +```javascript |
| 68 | +// True when the URL is this extension's own Console page. chrome.runtime.getURL |
| 69 | +// embeds our extension id, so this matches only our Console — never another |
| 70 | +// extension's pages or a real site. The Console URL carries a ?authId=... query, |
| 71 | +// which startsWith tolerates. |
| 72 | +export function isConsoleTab(url) { |
| 73 | + return !!url && url.startsWith(chrome.runtime.getURL('console/console.html')); |
| 74 | +} |
| 75 | +``` |
| 76 | + |
| 77 | +- [ ] **Step 4: Run the test to verify it passes** |
| 78 | + |
| 79 | +Run: `npx vitest run tests/popup-utils.test.js` |
| 80 | +Expected: PASS (existing `detectSite`/`findRossumTabs`/etc. blocks plus the new `isConsoleTab` block). |
| 81 | + |
| 82 | +- [ ] **Step 5: Verification gate (no commit)** |
| 83 | + |
| 84 | +Run: `npx vitest run tests/popup-utils.test.js` |
| 85 | +Expected: all green. Leave changes uncommitted on `master`. |
| 86 | + |
| 87 | +--- |
| 88 | + |
| 89 | +### Task 2: Wire the Console lede into `App.jsx` |
| 90 | + |
| 91 | +**Files:** |
| 92 | +- Modify: `src/popup/components/App.jsx` (import `isConsoleTab`; compute `isConsole`; pass to `UnsupportedSite`; export `UnsupportedSite`; swap both ledes) |
| 93 | +- Test: `tests/popup-unsupported-site.test.js` (new) |
| 94 | + |
| 95 | +**Interfaces:** |
| 96 | +- Consumes: `isConsoleTab` from `../utils.js` (Task 1). |
| 97 | +- Produces: `export function UnsupportedSite({ tabs, isConsole })` from `src/popup/components/App.jsx`. |
| 98 | + |
| 99 | +- [ ] **Step 1: Write the failing test** |
| 100 | + |
| 101 | +Create `tests/popup-unsupported-site.test.js`. (App.jsx's import graph has no top-level `chrome.*`, so importing it in jsdom is safe; `UnsupportedSite` renders without calling `chrome` — `activateTab` only fires on click.) |
| 102 | + |
| 103 | +```javascript |
| 104 | +// @vitest-environment jsdom |
| 105 | +import { describe, it, expect } from 'vitest'; |
| 106 | +import { h, render } from 'preact'; |
| 107 | +import { UnsupportedSite } from '../src/popup/components/App.jsx'; |
| 108 | + |
| 109 | +function mount(props) { |
| 110 | + const root = document.createElement('div'); |
| 111 | + render(h(UnsupportedSite, props), root); |
| 112 | + return root; |
| 113 | +} |
| 114 | + |
| 115 | +const TABS = [{ id: 1, url: 'https://elis.rossum.ai/queues', title: 'Rossum', favIconUrl: '' }]; |
| 116 | + |
| 117 | +describe('UnsupportedSite', () => { |
| 118 | + it('shows the Console lede and keeps the tab switcher when on the Console with open Rossum tabs', () => { |
| 119 | + const root = mount({ tabs: TABS, isConsole: true }); |
| 120 | + expect(root.textContent).toContain("You're on the Rossum Console."); |
| 121 | + expect(root.textContent).not.toContain("isn't supported"); |
| 122 | + expect(root.querySelector('.rossum-tab-list')).toBeTruthy(); |
| 123 | + expect(root.textContent).toContain('Switch to one of your open Rossum tabs'); |
| 124 | + }); |
| 125 | + |
| 126 | + it('shows the Console lede with the static fallback when on the Console with no Rossum tabs', () => { |
| 127 | + const root = mount({ tabs: [], isConsole: true }); |
| 128 | + expect(root.textContent).toContain("You're on the Rossum Console."); |
| 129 | + expect(root.textContent).toContain('It works on'); |
| 130 | + expect(root.querySelector('.rossum-tab-list')).toBeNull(); |
| 131 | + }); |
| 132 | + |
| 133 | + it('keeps the unsupported lede for a non-Console unsupported tab', () => { |
| 134 | + const root = mount({ tabs: [], isConsole: false }); |
| 135 | + expect(root.textContent).toContain("This tab isn't supported by the extension."); |
| 136 | + expect(root.textContent).not.toContain('Rossum Console'); |
| 137 | + }); |
| 138 | +}); |
| 139 | +``` |
| 140 | + |
| 141 | +- [ ] **Step 2: Run the test to verify it fails** |
| 142 | + |
| 143 | +Run: `npx vitest run tests/popup-unsupported-site.test.js` |
| 144 | +Expected: FAIL — `UnsupportedSite` is not exported from `App.jsx` yet (import is `undefined`), and the `isConsole` lede branch does not exist. |
| 145 | + |
| 146 | +- [ ] **Step 3: Export `UnsupportedSite` and make its lede conditional** |
| 147 | + |
| 148 | +In `src/popup/components/App.jsx`: |
| 149 | + |
| 150 | +(a) Change the function declaration (line 57) from: |
| 151 | + |
| 152 | +```jsx |
| 153 | +function UnsupportedSite({ tabs }) { |
| 154 | +``` |
| 155 | +
|
| 156 | +to: |
| 157 | +
|
| 158 | +```jsx |
| 159 | +export function UnsupportedSite({ tabs, isConsole }) { |
| 160 | +``` |
| 161 | +
|
| 162 | +(b) In the `hasTabs` branch, replace the lede (line 66): |
| 163 | +
|
| 164 | +```jsx |
| 165 | + <p class="unsupported-lede">This tab isn't supported by the extension.</p> |
| 166 | +``` |
| 167 | +
|
| 168 | +with: |
| 169 | +
|
| 170 | +```jsx |
| 171 | + <p class="unsupported-lede">{isConsole ? "You're on the Rossum Console." : "This tab isn't supported by the extension."}</p> |
| 172 | +``` |
| 173 | +
|
| 174 | +(c) In the no-tabs branch, replace the lede (line 92) with the identical conditional: |
| 175 | +
|
| 176 | +```jsx |
| 177 | + <p class="unsupported-lede">{isConsole ? "You're on the Rossum Console." : "This tab isn't supported by the extension."}</p> |
| 178 | +``` |
| 179 | +
|
| 180 | +- [ ] **Step 4: Wire `isConsole` in the `App` component** |
| 181 | +
|
| 182 | +In `src/popup/components/App.jsx`: |
| 183 | +
|
| 184 | +(a) Add `isConsoleTab` to the utils import (line 5): |
| 185 | +
|
| 186 | +```jsx |
| 187 | +import { openConsoleTab, runInTab, detectSite, findRossumTabs, activateTab, isConsoleTab } from '../utils.js'; |
| 188 | +``` |
| 189 | +
|
| 190 | +(b) Immediately after `const site = detectSite(tab?.url || '');` (line 105), add: |
| 191 | +
|
| 192 | +```jsx |
| 193 | + const isConsole = !site && isConsoleTab(tab?.url || ''); |
| 194 | +``` |
| 195 | +
|
| 196 | +(c) Update the `UnsupportedSite` usage (line 232) from: |
| 197 | +
|
| 198 | +```jsx |
| 199 | + <UnsupportedSite tabs={rossumTabs} /> |
| 200 | +``` |
| 201 | +
|
| 202 | +to: |
| 203 | +
|
| 204 | +```jsx |
| 205 | + <UnsupportedSite tabs={rossumTabs} isConsole={isConsole} /> |
| 206 | +``` |
| 207 | +
|
| 208 | +- [ ] **Step 5: Run the new test to verify it passes** |
| 209 | +
|
| 210 | +Run: `npx vitest run tests/popup-unsupported-site.test.js` |
| 211 | +Expected: PASS (3 tests). |
| 212 | +
|
| 213 | +- [ ] **Step 6: Run the full suite and rebuild** |
| 214 | +
|
| 215 | +Run: `npm test` |
| 216 | +Expected: all tests green (new file + the `isConsoleTab` block from Task 1 + no regressions). |
| 217 | +
|
| 218 | +Run: `npm run build` |
| 219 | +Expected: clean build into `dist/` (popup.js emits with no errors). |
| 220 | +
|
| 221 | +- [ ] **Step 7: Manual verification + handoff (no commit)** |
| 222 | +
|
| 223 | +Reload the unpacked extension, open the Console tab (`console/console.html`), and click the extension action: the popup lede reads "You're on the Rossum Console." (with the Rossum-tab switcher if any Rossum tabs are open, else the "It works on …" fallback). On a plain website the lede still reads "This tab isn't supported by the extension.". On a Rossum/NetSuite/Coupa tab the normal popup is unchanged. Leave uncommitted on `master`. |
| 224 | +
|
| 225 | +--- |
| 226 | +
|
| 227 | +## Self-Review |
| 228 | +
|
| 229 | +**Spec coverage:** |
| 230 | +- `isConsoleTab` helper (spec §Design) → Task 1. ✓ |
| 231 | +- Wire in `App.jsx` with `isConsole` prop (spec §Design) → Task 2 Step 4. ✓ |
| 232 | +- `UnsupportedSite` lede swap in both branches, exact wording (spec §Design) → Task 2 Step 3. ✓ |
| 233 | +- Export `UnsupportedSite` for testing (spec §Testing) → Task 2 Step 3(a). ✓ |
| 234 | +- `isConsoleTab` unit tests (spec §Testing) → Task 1 Step 1. ✓ |
| 235 | +- `UnsupportedSite` render tests, both branches + non-Console (spec §Testing) → Task 2 Step 1. ✓ |
| 236 | +- Backward compat (no manifest/permission/storage) → Global Constraints; no such changes in any task. ✓ |
| 237 | +
|
| 238 | +**Placeholder scan:** No TBD/TODO; every code step shows complete code; every command has an expected result. ✓ |
| 239 | +
|
| 240 | +**Type consistency:** `isConsoleTab(url) → boolean` defined in Task 1, imported/used in Task 2 Step 4, tested in both tasks. `UnsupportedSite({ tabs, isConsole })` exported in Task 2 Step 3, imported in the Task 2 test. Prop name `isConsole` used consistently. ✓ |
0 commit comments