diff options
| author | tanaka takashi <isazaka@gmail.com> | 2026-09-02 03:14:05 +0900 |
|---|---|---|
| committer | tanaka takashi <isazaka@gmail.com> | 2026-09-02 03:14:05 +0900 |
| commit | 82bb55f66071b452e3bc7adeb09eafb5003de11f (patch) | |
| tree | 01d48a68273594fc2ed480d837724562dd6fb05d /tests/ui-smoke-tests.js | |
| parent | f5439335e1d18ee577e2b7c1e67f7844f9f235ac (diff) | |
feat: PDF Viewer translation via context menu and two-mode Action Popupv0.1.0
Add Firefox built-in PDF Viewer support using only public WebExtensions
APIs: a selection context menu ("選択したテキストを翻訳") starts a
translation and the toolbar Action Popup shows the streaming result.
No content-script injection into the privileged PDF Viewer, no PDF.js
internals, no separate windows, no custom viewer (Specifications.md §4.6).
Specification alignment (Phase 1)
- Specifications.md: new requirements PDF-01..04 (PDF selection
translation), CTX-01..03 (context menu translation), POP-01..06
(popup two modes); SEL-06 updated to route PDF through the menus API;
section 13.4 adds a 14-item manual PDF verification checklist; section
15 removes "Firefox内蔵PDF Viewerへの対応" from out-of-scope
- Design.md: architecture diagram with the menus path, menu-translation
component, menus permission, popup translation-result state machine
(idle/starting/streaming/completed/error/cancelled), shared translation
state (§9.2), context-menu integration phase, test plan and API audit
checklist items (menus permission scope, no PDF Viewer internals)
- Security.md: menus permission restricted to selection-context
translation; PDF Viewer treated as privileged browser UI (no injection,
no wrappedJSObject/cloneInto/exportFunction, no internal DOM access);
menus.OnClickData.selectionText treated as untrusted input passing the
same validation pipeline; popup result rendering stays textContent-only
Background (Phase 2)
- translation-service.js (new): the single translation pipeline shared by
every entry point - selection validation (type/trim/empty/control
chars/max length), prompt building, Ollama request, session/abort, and
the shared translation state (requestId, sourceText, translatedText,
status, model, error, startedAt, source). "source" is metadata only; a
PDF-specific translation engine does not exist
- menu-translation.js (new): validates menus.OnClickData.selectionText
and starts a sink-less translation observed via GET_POPUP_STATE
- message-router.js: routes port translations through the shared service;
new runtime messages GET_POPUP_STATE and REGENERATE_LAST; popup-state
port handler broadcasts live state (initial push + every chunk/status
change) so an open popup shows streaming progress in real time
- index.js: registers the selection context menu (contexts: ["selection"]
only, no menus.onShown) and calls browser.action.openPopup()
synchronously inside the click user gesture, catching failures so a
popup-open problem never breaks the translation itself
- manifest.json: permissions now ["storage", "menus"]
- settings-service.js: __resetCacheForTests hook for test isolation
Popup two-mode UI (Phase 3)
- popup.html/js/css: mode nav (設定 / 翻訳). Translation mode shows
status, model, streaming text (textContent only), user-safe error, and
copy (Clipboard API only, no execCommand fallback), regenerate (new
requestId reusing the stored source text) and a settings shortcut.
Closing the popup never cancels the request; reopening shows the
latest text and continues receiving chunks live. Initial mode: active
(starting/streaming) translation wins, otherwise the last tab the user
picked is restored (storage.local.popupMode, default settings)
Fresh-install model auto-selection
- translation-service.js ensureModelConfigured(): when storage.model is
empty, pick the first model from the running Ollama and persist it
before translating, mirroring the popup/options auto-select. A user who
never opened any settings UI can translate immediately; when Ollama is
unreachable a clear OLLAMA_UNAVAILABLE message is shown
User-safe error details
- detailForUser(): strips URLs (no internal host names echoed to the UI),
caps length at 120 chars, drops full JSON response bodies, and hides
meaningless low-level network errors so the popover/popup shows the
base message plus at most a short, safe detail
Tests: 85 passed, 0 failed
- selectionText validation (untrusted input pipeline)
- menu selection -> translation -> popup state streaming/completed
- empty selection rejected without starting a request
- streaming status transitions observed through GET_POPUP_STATE
- Ollama unreachable surfaces a user-safe error into popup state
- newer request replaces older state (abort semantics)
- regenerateLast reuses source text with a fresh requestId
- popup-state port receives live broadcasts; wrong port names ignored
- fresh-install auto-select persists the first Ollama model
- error detail sanitization (URL stripping, length cap, JSON drop)
- popup/options smoke tests updated for the two-mode markup
- setupRouter resets the shared settings cache for test isolation
Docs: README documents the two-mode popup and PDF flow; Testing-Results
records automated results and the manual PDF checklist (to be executed).
No git-side surprises: this commit was created explicitly on request
(requirement 18 of the PDF task had deferred commits until now).
Diffstat (limited to 'tests/ui-smoke-tests.js')
| -rw-r--r-- | tests/ui-smoke-tests.js | 36 |
1 files changed, 29 insertions, 7 deletions
diff --git a/tests/ui-smoke-tests.js b/tests/ui-smoke-tests.js index 84fe4c4..e88a811 100644 --- a/tests/ui-smoke-tests.js +++ b/tests/ui-smoke-tests.js @@ -93,22 +93,44 @@ async function runPageScript(scriptPath, markup, probe) { describe('popup page script smoke', () => { it('initializes language options and model select from markup', async () => { const r = await runPageScript('../src/popup/popup.js', ` - <input type="checkbox" id="enabled-toggle"> - <select id="model-select"></select> - <button id="reload-models"></button> - <p id="model-status" hidden></p> - <select id="language-select"></select> - <button id="open-options"></button> + <header class="popup-header"> + <h1>Ollama Translations</h1> + <nav class="mode-nav"> + <button id="nav-settings" type="button" class="nav-btn active">設定</button> + <button id="nav-translation" type="button" class="nav-btn">翻訳</button> + </nav> + </header> + <section id="translation-mode" class="mode" hidden> + <p id="translation-status" class="status"></p> + <p id="translation-meta" class="meta" hidden></p> + <div id="translation-text" class="translation-text"></div> + <p id="translation-error" class="status error" hidden></p> + <div class="actions"> + <button id="copy-translation" type="button" disabled>コピー</button> + <button id="regenerate-translation" type="button" disabled>再生成</button> + <button id="open-options-from-translation" type="button">設定を開く</button> + </div> + </section> + <section id="settings-mode" class="mode"> + <input type="checkbox" id="enabled-toggle"> + <select id="model-select"></select> + <button id="reload-models"></button> + <p id="model-status" hidden></p> + <select id="language-select"></select> + <button id="open-options"></button> + </section> `, () => ({ langOptions: document.getElementById('language-select').options.length, langValue: document.getElementById('language-select').value, modelOptions: document.getElementById('model-select').options.length, - firstModel: document.getElementById('model-select').options[0]?.textContent ?? null + firstModel: document.getElementById('model-select').options[0]?.textContent ?? null, + translationHidden: document.getElementById('translation-mode').hidden })); assertEqual(r.langOptions, LANGUAGES.length); assertEqual(r.langValue, 'ja'); // default target language assertTrue(r.modelOptions > 0, 'model select should be populated'); assertEqual(r.firstModel, 'fake-model'); + assertEqual(r.translationHidden, true); // no translation yet: settings mode }); }); |
