diff --git a/.gitignore b/.gitignore index e8441b44..626725a5 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ test-results/ playwright-report/ resources/vc_redist.x64.exe resources/vc_redist.x64.exe.tmp +docs/screenshots/ diff --git a/docs/view-flow-plan.md b/docs/view-flow-plan.md new file mode 100644 index 00000000..fd19eab3 --- /dev/null +++ b/docs/view-flow-plan.md @@ -0,0 +1,80 @@ +# View/Modal Flow Documentation — Plan + +Issue: [#226](https://github.com/Comfy-Org/ComfyUI-Desktop-2.0-Beta/issues/226) + +## Goal + +Generate a living, regenerable diagram of how all screens (views and modals) in Desktop 2.0 connect to each other, with visual previews. Ideally portable into Figma for designers. + +## Current State + +The app has **6 sidebar tab views** + **7 modal views** + **1 global ModalDialog**, all orchestrated via events in `App.vue`: + +- **Tab views:** Dashboard, InstallationList, Running, Models, Media, Settings +- **Modal views:** DetailModal, ConsoleModal, ProgressModal, NewInstallModal, QuickInstallModal, TrackModal, LoadSnapshotModal +- **Global:** ModalDialog (alerts/confirms/prompts/selects) + +There are 2 e2e test files using Playwright but no screenshot-capture infrastructure yet. + +--- + +## Phase 1: Automated Screenshot Capture (Playwright) + +Add a Playwright test file (`e2e/view-flow-screenshots.spec.ts`) that navigates to each view/modal state and captures a screenshot to `docs/screenshots/`. + +- Mock the Electron IPC layer (already partially done in `e2e/support/`) to render each view with representative dummy data. +- Initial output: 10 PNG files covering 6 tab views + 4 modals (empty/welcome states). +- Future: add populated-state screenshots for DetailModal, ConsoleModal, ProgressModal once mock data seeding is implemented (~13–14 total). + +**Effort:** 2–3 days + +## Phase 2: Flow Graph Generator Script + +Create a Node script (`scripts/generate-view-flow.ts`) that: + +1. **Parses `App.vue`** and all `views/*.vue` files to extract `defineEmits` and `@event` bindings (the emit→handler wiring). +2. Builds a directed graph of view→view transitions. +3. Outputs a **Mermaid diagram** (`docs/view-flow.md`) with clickable links to source files. +4. Optionally outputs an **HTML page** (`docs/view-flow.html`) that renders the graph with screenshot thumbnails embedded at each node (using the Phase 1 PNGs). + +This script can be re-run anytime the views change. Could be added to CI. + +**Effort:** 1–2 days + +## Phase 3: Figma Integration + +### Feasibility + +| Approach | Feasibility | Notes | +|----------|-------------|-------| +| **Figma REST API** | ❌ Cannot create/modify file content | REST API is read-only for nodes/images. No endpoint to create frames or upload image fills. | +| **Figma Plugin** | ✅ Best path | Plugin API has full write access: `createFrame()`, `createImage()`, `createConnector()`. | +| **Figma MCP / Make** | 🟡 Emerging | Not yet stable enough for programmatic pipelines. Worth revisiting. | +| **FigJam board** | ✅ Quick alternative | Connectors + image frames are a natural fit. Possibly better UX for designers. | + +### Recommended Approach + +Build a small **Figma/FigJam plugin** that: + +1. Accepts a JSON file (generated by Phase 2) containing: node list (id, label, screenshot URL/base64), edge list (from, to, label). +2. Creates a frame per view with the screenshot as an image fill. +3. Draws connectors between frames labeled with the event name. +4. Designers run the plugin whenever they want the latest flow — it clears the old one and regenerates. + +**Effort:** 2–3 days (depends on Phase 1 + 2 outputs) + +## Phase 4: CI Integration (Optional) + +- Add a GitHub Actions workflow that runs the Playwright screenshot capture + flow generator on every PR that touches `src/renderer/src/views/` or `src/renderer/src/components/`. +- Commit updated screenshots and Mermaid docs automatically, or post them as PR comments. + +**Effort:** 1 day + +## Summary + +| Phase | Effort | Dependency | +|-------|--------|------------| +| Phase 1 (Screenshots) | 2–3 days | Requires expanding e2e IPC mocks | +| Phase 2 (Graph generator) | 1–2 days | None | +| Phase 3 (Figma plugin) | 2–3 days | Phase 1 + 2 outputs | +| Phase 4 (CI) | 1 day | Phase 1 + 2 | diff --git a/docs/view-flow.md b/docs/view-flow.md new file mode 100644 index 00000000..ee896268 --- /dev/null +++ b/docs/view-flow.md @@ -0,0 +1,99 @@ +# View/Modal Flow — Desktop 2.0 + +> Auto-generated by `scripts/generate-view-flow.mjs` — do not edit manually. +> +> Run: `node scripts/generate-view-flow.mjs` + +## Navigation Graph + +```mermaid +flowchart TD + + %% Tab Views + dashboard-view["📊 Dashboard"] + installation-list["📦 Installs"] + running-view["▶️ Running"] + models-view["📁 Models"] + media-view["🖼️ Media"] + settings-view["⚙️ Settings"] + + %% Modals + quick-install-modal("⚡ Quick Install Modal") + detail-modal("🔍 Detail Modal") + console-modal("💻 Console Modal") + progress-modal("⏳ Progress Modal") + new-install-modal("➕ New Install Modal") + track-modal("📂 Track Existing Modal") + load-snapshot-modal("📸 Load Snapshot Modal") + + sidebar{{"🧭 Sidebar"}} + + %% Sidebar navigation + sidebar --> dashboard-view + sidebar --> installation-list + sidebar --> running-view + sidebar --> models-view + sidebar --> media-view + sidebar --> settings-view + + %% View → Modal transitions + dashboard-view -->|show-quick-install| quick-install-modal + dashboard-view -->|show-settings| settings-view + dashboard-view -->|show-detail| detail-modal + dashboard-view -->|show-console| console-modal + dashboard-view -->|show-progress| progress-modal + installation-list -->|show-detail| detail-modal + installation-list -->|show-migrate| detail-modal + installation-list -->|show-console| console-modal + installation-list -->|show-progress| progress-modal + installation-list -->|show-new-install| new-install-modal + installation-list -->|show-track| track-modal + installation-list -->|show-load-snapshot| load-snapshot-modal + running-view -->|show-detail| detail-modal + running-view -->|show-console| console-modal + running-view -->|show-progress| progress-modal + + %% Modal → View/Modal transitions + detail-modal -->|show-progress| progress-modal + detail-modal -->|navigate-list| installation-list + progress-modal -->|show-detail| detail-modal + progress-modal -->|show-console| console-modal + new-install-modal -->|show-progress| progress-modal + new-install-modal -->|navigate-list| installation-list + quick-install-modal -->|show-progress| progress-modal + track-modal -->|navigate-list| installation-list + load-snapshot-modal -->|show-progress| progress-modal + + %% Styles + classDef tabView fill:#1a1a2e,stroke:#00d9ff,color:#e0e0e0,stroke-width:2px + classDef modal fill:#1a1a2e,stroke:#ff6b6b,color:#e0e0e0,stroke-width:2px + classDef nav fill:#1a1a2e,stroke:#ffd93d,color:#e0e0e0,stroke-width:2px + class dashboard-view,installation-list,running-view,models-view,media-view,settings-view tabView + class quick-install-modal,detail-modal,console-modal,progress-modal,new-install-modal,track-modal,load-snapshot-modal modal + class sidebar nav +``` + +## Legend + +- **Blue border** = Tab view (sidebar navigation) +- **Red border** = Modal (overlay) +- **Yellow border** = Sidebar navigator +- Edge labels show the Vue event name that triggers the transition + +## Source Files + +| View/Modal | Source | +|------------|--------| +| 📊 Dashboard | [`src/renderer/src/views/DashboardView.vue`](../src/renderer/src/views/DashboardView.vue) | +| 📦 Installs | [`src/renderer/src/views/InstallationList.vue`](../src/renderer/src/views/InstallationList.vue) | +| ▶️ Running | [`src/renderer/src/views/RunningView.vue`](../src/renderer/src/views/RunningView.vue) | +| 📁 Models | [`src/renderer/src/views/ModelsView.vue`](../src/renderer/src/views/ModelsView.vue) | +| 🖼️ Media | [`src/renderer/src/views/MediaView.vue`](../src/renderer/src/views/MediaView.vue) | +| ⚙️ Settings | [`src/renderer/src/views/SettingsView.vue`](../src/renderer/src/views/SettingsView.vue) | +| ⚡ Quick Install Modal | [`src/renderer/src/views/QuickInstallModal.vue`](../src/renderer/src/views/QuickInstallModal.vue) | +| 🔍 Detail Modal | [`src/renderer/src/views/DetailModal.vue`](../src/renderer/src/views/DetailModal.vue) | +| 💻 Console Modal | [`src/renderer/src/views/ConsoleModal.vue`](../src/renderer/src/views/ConsoleModal.vue) | +| ⏳ Progress Modal | [`src/renderer/src/views/ProgressModal.vue`](../src/renderer/src/views/ProgressModal.vue) | +| ➕ New Install Modal | [`src/renderer/src/views/NewInstallModal.vue`](../src/renderer/src/views/NewInstallModal.vue) | +| 📂 Track Existing Modal | [`src/renderer/src/views/TrackModal.vue`](../src/renderer/src/views/TrackModal.vue) | +| 📸 Load Snapshot Modal | [`src/renderer/src/views/LoadSnapshotModal.vue`](../src/renderer/src/views/LoadSnapshotModal.vue) | diff --git a/e2e/view-flow-screenshots.spec.ts b/e2e/view-flow-screenshots.spec.ts new file mode 100644 index 00000000..86e1fe02 --- /dev/null +++ b/e2e/view-flow-screenshots.spec.ts @@ -0,0 +1,126 @@ +/** + * View Flow Screenshots — Issue #226 + * + * Captures a screenshot of every major view and modal in the launcher UI. + * Screenshots are saved to docs/screenshots/ and can be used by the + * flow-graph generator (Phase 2) and Figma plugin (Phase 3). + * + * Run: pnpm exec playwright test e2e/view-flow-screenshots.spec.ts + */ +import { expect, test, type Page } from '@playwright/test' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { type LauncherAppHandle, launchLauncherApp } from './support/electronHarness' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const SCREENSHOT_DIR = path.resolve(__dirname, '..', 'docs', 'screenshots') + +const UI_TIMEOUT = 12_000 + +let handle: LauncherAppHandle +let page: Page + +async function screenshot(name: string): Promise { + await page.screenshot({ + path: path.join(SCREENSHOT_DIR, `${name}.png`), + type: 'png', + }) +} + +async function clickSidebar(text: string): Promise { + const item = page.locator('.sidebar-item', { hasText: text }) + await item.click() + await expect(item).toHaveClass(/active/, { timeout: UI_TIMEOUT }) +} + +async function openModal(locator: ReturnType, screenshotName: string): Promise { + await expect(locator).toBeVisible({ timeout: UI_TIMEOUT }) + await locator.click() + await expect(page.locator('.modal-overlay, .modal, [class*="modal"]')).toBeVisible({ timeout: UI_TIMEOUT }) + await screenshot(screenshotName) + await page.keyboard.press('Escape') + await expect(page.locator('.modal-overlay, .modal, [class*="modal"]')).toBeHidden({ timeout: UI_TIMEOUT }) +} + +test.describe('View Flow Screenshots (#226)', () => { + test.beforeAll(async () => { + handle = await launchLauncherApp() + page = await handle.application.firstWindow() + + await expect(page.locator('#app')).toBeAttached({ timeout: UI_TIMEOUT }) + await expect(page.locator('.sidebar')).toBeVisible({ timeout: UI_TIMEOUT }) + // Wait for Vue to finish mounting and i18n to load + await expect(page.locator('.sidebar-item')).toHaveCount(7, { timeout: UI_TIMEOUT }) + }) + + test.afterAll(async () => { + await handle.cleanup() + }) + + // ── Tab Views ────────────────────────────────────────────── + + test('01 — Dashboard @macos @windows @linux', async () => { + await screenshot('01-dashboard') + }) + + test('02 — Installation List @macos @windows @linux', async () => { + await clickSidebar('Installs') + await screenshot('02-installation-list') + }) + + test('03 — Running @macos @windows @linux', async () => { + await clickSidebar('Running') + await screenshot('03-running') + }) + + test('04 — Models @macos @windows @linux', async () => { + await clickSidebar('Models') + await screenshot('04-models') + }) + + test('05 — Media @macos @windows @linux', async () => { + await clickSidebar('Media') + await screenshot('05-media') + }) + + test('06 — Settings @macos @windows @linux', async () => { + await clickSidebar('Settings') + await screenshot('06-settings') + }) + + // ── Modals (opened from Installation List) ───────────────── + + test('07 — New Install modal @macos @windows @linux', async () => { + await clickSidebar('Installs') + await openModal( + page.locator('.toolbar button', { hasText: 'New Install' }), + '07-new-install-modal', + ) + }) + + test('08 — Track Existing modal @macos @windows @linux', async () => { + await clickSidebar('Installs') + await openModal( + page.locator('button', { hasText: 'Track Existing' }), + '08-track-modal', + ) + }) + + test('09 — Load Snapshot modal @macos @windows @linux', async () => { + await clickSidebar('Installs') + await openModal( + page.locator('button', { hasText: 'Load Snapshot' }), + '09-load-snapshot-modal', + ) + }) + + // ── Dashboard modal entry points ─────────────────────────── + + test('10 — Quick Install modal @macos @windows @linux', async () => { + await clickSidebar('Dashboard') + await openModal( + page.locator('button', { hasText: 'Install ComfyUI' }), + '10-quick-install-modal', + ) + }) +}) diff --git a/package.json b/package.json index 1aab914d..5818dc6c 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "format:check": "prettier --check \"src/**/*.{ts,vue}\" \"*.{json,ts,yml}\"", "test": "vitest run", "test:e2e": "playwright test", + "generate:view-flow": "node scripts/generate-view-flow.mjs", "test:e2e:macos": "playwright test --project=macos", "test:e2e:windows": "playwright test --project=windows", "test:e2e:linux": "playwright test --project=linux", diff --git a/scripts/generate-view-flow.mjs b/scripts/generate-view-flow.mjs new file mode 100644 index 00000000..0d9df7e7 --- /dev/null +++ b/scripts/generate-view-flow.mjs @@ -0,0 +1,314 @@ +/** + * View Flow Graph Generator — Phase 2 of Issue #226 + * + * Parses App.vue to extract the view→modal→view navigation graph + * and outputs a Mermaid diagram to docs/view-flow.md. + * + * Run: node scripts/generate-view-flow.mjs + */ +import { readFileSync, writeFileSync, mkdirSync } from 'node:fs' +import { resolve, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const ROOT = resolve(__dirname, '..') +const APP_VUE = resolve(ROOT, 'src/renderer/src/App.vue') +const VIEWS_DIR = resolve(ROOT, 'src/renderer/src/views') +const OUTPUT = resolve(ROOT, 'docs/view-flow.md') + +const VIEW_COMPONENTS = [ + 'DashboardView', 'InstallationList', 'RunningView', + 'SettingsView', 'ModelsView', 'MediaView', + 'DetailModal', 'ConsoleModal', 'ProgressModal', + 'NewInstallModal', 'QuickInstallModal', 'TrackModal', + 'LoadSnapshotModal', +] + +const TAB_VIEWS = ['DashboardView', 'InstallationList', 'RunningView', 'ModelsView', 'MediaView', 'SettingsView'] + +const VIEW_TO_FILE = { + DashboardView: 'DashboardView.vue', + InstallationList: 'InstallationList.vue', + RunningView: 'RunningView.vue', + ModelsView: 'ModelsView.vue', + MediaView: 'MediaView.vue', + SettingsView: 'SettingsView.vue', + DetailModal: 'DetailModal.vue', + ConsoleModal: 'ConsoleModal.vue', + ProgressModal: 'ProgressModal.vue', + NewInstallModal: 'NewInstallModal.vue', + QuickInstallModal: 'QuickInstallModal.vue', + TrackModal: 'TrackModal.vue', + LoadSnapshotModal: 'LoadSnapshotModal.vue', +} + +const NODE_LABELS = { + Sidebar: '🧭 Sidebar', + DashboardView: '📊 Dashboard', + InstallationList: '📦 Installs', + RunningView: '▶️ Running', + ModelsView: '📁 Models', + MediaView: '🖼️ Media', + SettingsView: '⚙️ Settings', + DetailModal: '🔍 Detail Modal', + ConsoleModal: '💻 Console Modal', + ProgressModal: '⏳ Progress Modal', + NewInstallModal: '➕ New Install Modal', + QuickInstallModal: '⚡ Quick Install Modal', + TrackModal: '📂 Track Existing Modal', + LoadSnapshotModal: '📸 Load Snapshot Modal', +} + +const SWITCH_VIEW_MAP = { + dashboard: 'DashboardView', + list: 'InstallationList', + running: 'RunningView', + models: 'ModelsView', + media: 'MediaView', + settings: 'SettingsView', +} + +// ── Helpers ────────────────────────────────────────────────── + +function extractTemplate(vue) { + const match = vue.match(/