-
-
Notifications
You must be signed in to change notification settings - Fork 1
Add first-run onboarding flow for new installs #453
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
4b9be99
Add: First-run onboarding flow for new installs
ujiro99 b62b6c6
Update: Refine onboarding flow with return hint, pattern captions, an…
ujiro99 6438e16
Update: Redesign onboarding chrome for clarity and mental-model reinf…
ujiro99 5cd3492
Update: Adjust styling.
ujiro99 76bdfb2
Refactor: Extract popupContext into hooks/usePopupContext
ujiro99 596e365
Update: Refine onboarding flow copy, callout motion, and styling
ujiro99 ee3de98
Update: Keep the onboarding rail's beat animation continuous through …
ujiro99 ca814c4
refactoring.
ujiro99 a45a8b9
Update: Localize onboarding copy tweaks to all supported languages
ujiro99 01635b4
Update: Refine onboarding step transitions, menu placement, and copy
ujiro99 1199c91
Update: Add return-phase handling to Steps 2-3 and refresh onboarding…
ujiro99 17ec602
Update: Storybooの導入ができなかったときの調査記録
ujiro99 e9b7351
Update: Refine Step4 screenshots, Step5 animation sequencing, and con…
ujiro99 d8c3e7c
Update: Add onboarding value sub-messages and localize step4/step3 copy
ujiro99 276bd7e
Fix: Prevent to send duplicated events.
ujiro99 00e29f2
Refactor: Manage onboarding step animation delays per StepPhase
ujiro99 f4f33e6
Fix: Rename onboarding analytics events and dedupe value/complete tra…
ujiro99 ec4902f
Update: Revise onboarding step3/complete copy and localize to all lan…
ujiro99 69582bb
Fix: Skip link command analytics event on the onboarding page
ujiro99 5d73436
Fix: Collect event timing of ONBOARDING_TEXT_SELECTION at AiPrompt.
ujiro99 c10d898
Fix: Correct onboarding analytics bugs found in code review
ujiro99 0d919e2
Update: Reorder default commands at korean.
ujiro99 3d9e00e
Refactor: Centralize onboarding AiPrompt command definitions and rena…
ujiro99 e69994a
Fix: Load all content_script CSS chunks in production builds
ujiro99 c14e27b
Refactor: Drop now-unneeded predictable CSS filename hacks
ujiro99 c27c51c
Fix onboarding tab and popup handling
Copilot 7ac069e
Fix: Popup position during onboarding.
ujiro99 6f29e1d
Refactor: Resolve onboarding AiPrompt icon from serviceId
ujiro99 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| # Storybook導入 調査記録 | ||
|
|
||
| ## 目的 | ||
|
|
||
| `packages/extension` へ Storybook を導入し、`OnboardingPage.tsx` の各ステップ | ||
| (`OnboardingStep` × `StepPhase` の組み合わせ)を個別に確認できるようにする | ||
| 試みを行った。作業時間の制約により完了に至らなかったため、原因と得られた | ||
| 知見を記録する。 | ||
|
|
||
| ## 採用した方針(概要) | ||
|
|
||
| - `OnboardingPage` は自身の状態を `useOnboardingState` フックで管理しており、 | ||
| Props を受け取らない。そのため、各ステップ用の Story を個別に自作する | ||
| のではなく、`useOnboardingState.ts` に既に存在する e2e 用の URL 上書き | ||
| 機構(`?step=SEARCH&phase=value_shown`、`e2e/onboarding-shots.spec.ts` が | ||
| スクリーンショット撮影に使用しているもの)を Storybook からも流用する | ||
| 方針を取った。 | ||
| - `chrome.*` API は Storybook のプレビュー用に独自モック(`chrome.storage` | ||
| のメモリ実装、`chrome.i18n.getMessage` の実メッセージ引き当て等)を用意 | ||
| し、`Storage.setCommands(getDefaultCommands(locale))` を Story 読み込み時 | ||
| に実行して、本番の初回インストール時と同じ状態を再現する設計とした。 | ||
|
|
||
| ## 遭遇した問題と原因 | ||
|
|
||
| ### 1. Storybook の Vite ビルダーが、拡張機能用の `vite.config.ts` を自動で読み込んでしまう | ||
|
|
||
| - `@storybook/react-vite` は既定で `.storybook` の親ディレクトリにある | ||
| `vite.config.ts` を自動検出し、Storybook 用の設定にマージする仕様になっ | ||
| ている(`@storybook/builder-vite` 内部の `getBuilderOptions()` / | ||
| `loadConfigFromFile()`)。 | ||
| - 本プロジェクトの `vite.config.ts` は Chrome 拡張機能のビルド専用で、 | ||
| `@crxjs/vite-plugin`(manifest.json の解釈やコンテンツスクリプトの特別 | ||
| なバンドリング)や、Shadow DOM 環境向けにカスタムした | ||
| `vite-plugin-css-injected-by-js` の `injectCodeFunction` | ||
| (`document.getElementById("selection-command")?.shadowRoot ?? document.head` | ||
| への注入)を含んでいる。 | ||
| - これが Storybook のプレビュー(通常の DOM ページ)に対しても適用されて | ||
| しまい、 | ||
| - `@crxjs/vite-plugin` の HMR ハンドラがエラーを送出してオーバーレイ | ||
| 表示になる | ||
| - CSS が期待通り `<style>` タグとして注入されない(Tailwind の生成物 | ||
| 自体は正しいのに、実際のDOMには反映されない) | ||
| という2つの不具合を引き起こしていた。 | ||
| - **対策**: `.storybook/main.ts` の `core.builder.options.viteConfigPath` | ||
| に、拡張機能とは無関係の空の `vite.config.ts` を指定し、本来の | ||
| `vite.config.ts` が読み込まれないようにすることで、HMR エラーと | ||
| Shadow DOM 向け CSS 注入コードの混入は解消した。 | ||
|
|
||
| ### 2. `chrome` グローバルのモック順序・カバレッジ不足によるハング | ||
|
|
||
| - `commandStorage.ts` など一部のモジュールは、**import された時点**(トッ | ||
| プレベル)で `chrome.storage.onChanged.addListener(...)` を呼び出す。 | ||
| そのため、Storybook のプレビュー側で `chrome` のモックを設定するコード | ||
| は、アプリ本体のどのモジュールよりも先に評価される必要があった | ||
| (ESM の import 文はファイル内の他の文より先に評価されるため、モック | ||
| 用モジュールは import 文の先頭に置く、かつ本体コードを import しない | ||
| 独立したファイルにする、という工夫が必要だった)。 | ||
| - `services/storage/index.ts` の `debouncedSyncSet()` は、 | ||
| `chrome.storage.sync.set(items, callback)` という**コールバック形式** | ||
| で呼び出している。Storybook 用のモックを Promise ベースのみで実装して | ||
| いたためコールバックが一切呼ばれず、`Storage.setCommands()` の | ||
| Promise が永久に解決しない(= Story の初期化処理がハングする)という、 | ||
| 原因の特定が難しい不具合が発生した。 | ||
| - 症状としては「Storybook のプレビューがローディングスピナーのまま | ||
| 止まる」だけで、コンソールにもエラーが出ないため、`chrome.storage` | ||
| まわりの実装差異に気づくまでに時間を要した。 | ||
| - **対策**: モックの `set`/`remove`/`clear` を、Promise を返しつつ | ||
| コールバック引数が渡された場合はそれも呼び出す形に修正し、解消した。 | ||
|
|
||
| ### 3. 自動化ブラウザに実機の拡張機能がインストールされていたことによるノイズ | ||
|
|
||
| - 動作確認に使用したブラウザ(claude-in-chrome)に、この拡張機能自体の | ||
| 開発ビルドと思われる拡張機能が既にインストールされており、あらゆる | ||
| ページ(Storybook のプレビュー含む)に content script や Vite の | ||
| HMR クライアントを注入していた。 | ||
| - これにより、コンソールログやネットワークログに無関係な | ||
| `chrome-extension://...` 由来のログが大量に混入し、問題の切り分け | ||
| (どこまでが自分たちの Storybook 由来で、どこからが無関係な拡張機能 | ||
| 由来か)が難しくなった。 | ||
|
|
||
| ## 未解決だった点 | ||
|
|
||
| - 上記 1, 2 を解消した直後の時点で、Tailwind のユーティリティクラス自体 | ||
| は正しく生成されるようになっていたが、実際にブラウザへ注入された | ||
| `<style>` タグには反映されておらず、`@/components/App.css` の Vite | ||
| モジュールを直接 `import()` しても `<style data-vite-dev-id="...">` | ||
| が生成されない事象が残っていた。原因(Vite のモジュールキャッシュ、 | ||
| もしくは項番3のノイズの影響)を切り分けている途中で作業を打ち切った。 | ||
|
|
||
| ## 次回への申し送り | ||
|
|
||
| - `.storybook/main.ts` で `viteConfigPath` を明示的に空の設定に差し替え | ||
| る対策は有効だったので、再挑戦する場合はこの対策を先に適用した状態 | ||
| から始めるとよい。 | ||
| - `chrome.storage.*` のモックは、コールバック形式・Promise形式の両方を | ||
| サポートする実装にしておく(`src/test/setup.ts` の Vitest 用モックも | ||
| 同様の考慮がされているため、参考にできる)。 | ||
| - 動作確認は、他の拡張機能がインストールされていないクリーンなブラウザ | ||
| プロファイルで行うことを推奨する。 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| import { test, chromium } from "@playwright/test" | ||
| import fs from "fs" | ||
| import path from "path" | ||
| import { fileURLToPath } from "url" | ||
| import { OnboardingStep, StepPhase } from "@/types/onboarding" | ||
|
|
||
| // Not part of the regular e2e suite - a design-review tool. Skipped unless | ||
| // explicitly requested (`CAPTURE=1 yarn test:e2e onboarding-shots`) so it | ||
| // never slows down or flakes the normal CI run. | ||
| // | ||
| // Uses `?step=..&phase=..` (see useOnboardingState.ts's readE2eOverride(), | ||
| // e2e-build only) to land directly on each screen instead of driving the | ||
| // real selection/command flow, and launches its own persistent context per | ||
| // locale (rather than the shared `fixtures.ts` one) so this file alone | ||
| // controls `--lang`. | ||
|
|
||
| const __dirname = path.dirname(fileURLToPath(import.meta.url)) | ||
| const pathToExtension = path.join(__dirname, "../dist") | ||
|
|
||
| type Shot = { | ||
| name: string | ||
| step: keyof typeof OnboardingStep | ||
| phase?: StepPhase | ||
| } | ||
|
|
||
| // One shot per visually distinct screen - not one per StepPhase transition; | ||
| // e.g. Step2's WAIT_EXECUTE looks like Step1's, so only Step1 gets it. | ||
| const SHOTS: Shot[] = [ | ||
| { name: "00-intro", step: "INTRO" }, | ||
| { name: "01-search-explain", step: "SEARCH", phase: StepPhase.EXPLAIN }, | ||
| { | ||
| name: "02-search-wait-execute", | ||
| step: "SEARCH", | ||
| phase: StepPhase.WAIT_EXECUTE, | ||
| }, | ||
| { | ||
| name: "03-search-wait-return", | ||
| step: "SEARCH", | ||
| phase: StepPhase.WAIT_RETURN, | ||
| }, | ||
| { name: "04-search-value", step: "SEARCH", phase: StepPhase.VALUE_SHOWN }, | ||
| { name: "05-ai-explain", step: "AI_PROMPT", phase: StepPhase.EXPLAIN }, | ||
| { name: "06-ai-value", step: "AI_PROMPT", phase: StepPhase.VALUE_SHOWN }, | ||
| { | ||
| name: "07-preview-explain", | ||
| step: "LINK_PREVIEW", | ||
| phase: StepPhase.EXPLAIN, | ||
| }, | ||
| { | ||
| name: "08-preview-value", | ||
| step: "LINK_PREVIEW", | ||
| phase: StepPhase.VALUE_SHOWN, | ||
| }, | ||
| { name: "09-customize", step: "CUSTOMIZE" }, | ||
| { name: "10-complete", step: "COMPLETE" }, | ||
| ] | ||
|
|
||
| const LOCALES = ["ja", "en-US", "de"] | ||
|
|
||
| test.describe("onboarding screenshots (design review)", () => { | ||
| test.skip( | ||
| !process.env.CAPTURE, | ||
| "set CAPTURE=1 to run onboarding screenshot capture", | ||
| ) | ||
|
|
||
| for (const locale of LOCALES) { | ||
| test(`capture (${locale})`, async () => { | ||
| test.setTimeout(120_000) | ||
|
|
||
| const context = await chromium.launchPersistentContext("", { | ||
| headless: false, | ||
| args: [ | ||
| `--lang=${locale}`, | ||
| `--disable-extensions-except=${pathToExtension}`, | ||
| `--load-extension=${pathToExtension}`, | ||
| ], | ||
| }) | ||
|
|
||
| try { | ||
| let [sw] = context.serviceWorkers() | ||
| if (!sw) sw = await context.waitForEvent("serviceworker") | ||
| const extensionId = sw.url().split("/")[2] | ||
|
|
||
| const page = await context.newPage() | ||
| const outDir = path.join( | ||
| __dirname, | ||
| "../test-results/onboarding", | ||
| locale, | ||
| ) | ||
| fs.mkdirSync(outDir, { recursive: true }) | ||
|
|
||
| for (const shot of SHOTS) { | ||
| const params = new URLSearchParams({ step: shot.step }) | ||
| if (shot.phase) params.set("phase", shot.phase) | ||
| await page.goto( | ||
| `chrome-extension://${extensionId}/src/onboarding_page.html?${params}`, | ||
| ) | ||
| // Let entrance animations and the selection-demo cursor settle | ||
| // into a representative frame before capturing. | ||
| await page.waitForTimeout(700) | ||
| await page.screenshot({ path: path.join(outDir, `${shot.name}.png`) }) | ||
| } | ||
| } finally { | ||
| await context.close() | ||
| } | ||
| }) | ||
| } | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.