Skip to content
Closed

1.48.6 #14067

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions browser_tests/fixtures/ComfyPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { config as dotenvConfig } from 'dotenv'
import MCR from 'monocart-coverage-reports'

import { COVERAGE_OUTPUT_DIR } from '@e2e/coverageConfig'
import { TOURS, TOUR_SEEN_SETTING } from '@/platform/onboarding/onboardingTours'
import { NodeBadgeMode } from '@/types/nodeSource'
import { ComfyActionbar } from '@e2e/fixtures/components/Actionbar'
import { ComfyTemplates } from '@e2e/fixtures/components/Templates'
Expand Down Expand Up @@ -542,6 +543,8 @@ export const comfyPageFixture = base.extend<{
'Comfy.userId': userId,
// Set tutorial completed to true to avoid loading the tutorial workflow.
'Comfy.TutorialCompleted': true,
// An auto-opened tour's blocker would break unrelated tests.
[TOUR_SEEN_SETTING]: Object.keys(TOURS),
'Comfy.Queue.MaxHistoryItems': 64,
'Comfy.SnapToGrid.GridSize': testComfySnapToGridGridSize,
// Disable toast warning about version compatibility, as they may or
Expand Down
77 changes: 77 additions & 0 deletions browser_tests/fixtures/components/Tour.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { Locator, Page } from '@playwright/test'

import { TOUR_SEEN_SETTING } from '@/platform/onboarding/onboardingTours'

export type CoachTour = 'appMode'

/** Accessible name of each tour's in-app replay (help) button. */
const TOUR_REPLAY_BUTTONS: Record<CoachTour, string> = {
appMode: 'Take a tour of App Mode'
}

/** Coach-mark overlay (src/platform/onboarding/TourOverlay.vue). */
export class OnboardingCoachmarks {
public readonly landing: Locator
public readonly landingStartButton: Locator
public readonly landingSkipButton: Locator
/** The current spotlight step card (the dialog carrying a "Step N of M" label). */
public readonly card: Locator
public readonly cardNextButton: Locator
public readonly cardDoneButton: Locator

constructor(public readonly page: Page) {
this.landing = page.getByTestId('coach-landing')
this.landingStartButton = this.landing.getByRole('button', {
name: 'Start tutorial'
})
this.landingSkipButton = this.landing.getByRole('button', {
name: 'Skip',
exact: true
})
this.card = page.getByRole('dialog').filter({ hasText: /Step \d+ of \d+/ })
this.cardNextButton = this.card.getByRole('button', { name: 'Next' })
this.cardDoneButton = this.card.getByRole('button', { name: 'Done' })
}

/** The tour's in-app help button, which replays it past the seen-flag. */
replayButton(tour: CoachTour): Locator {
return this.page.getByRole('button', { name: TOUR_REPLAY_BUTTONS[tour] })
}

/** The spotlight card while it is showing the given step number. */
cardForStep(step: number): Locator {
return this.card.filter({ hasText: new RegExp(`Step ${step} of `) })
}

/**
* Clears the pre-seeded seen-flag (so dismissal assertions observe it being
* set again) and clicks the tour's replay button, which must be mounted.
*/
async startTour(tour: CoachTour) {
await this.clearSeen()
await this.replayButton(tour).click()
}

private async clearSeen() {
await this.page.evaluate(
async (key) => window.app!.extensionManager.setting.set(key, []),
TOUR_SEEN_SETTING
)
}

/** An element a tour points at, by its `data-coach-id` anchor. */
coachAnchor(id: string): Locator {
return this.page.locator(`[data-coach-id="${id}"]`)
}

async seen(tour: CoachTour): Promise<boolean> {
const seen = await this.page.evaluate(
async (key) =>
(await window.app!.extensionManager.setting.get(key)) as
| string[]
| undefined,
TOUR_SEEN_SETTING
)
return !!seen?.includes(tour)
}
}
11 changes: 11 additions & 0 deletions browser_tests/fixtures/tourFixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { test as base } from '@playwright/test'

import { OnboardingCoachmarks } from '@e2e/fixtures/components/Tour'

export const onboardingFixture = base.extend<{
onboarding: OnboardingCoachmarks
}>({
onboarding: async ({ page }, use) => {
await use(new OnboardingCoachmarks(page))
}
})
136 changes: 136 additions & 0 deletions browser_tests/tests/tour.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { expect, mergeTests } from '@playwright/test'

import { comfyPageFixture } from '@e2e/fixtures/ComfyPage'
import { onboardingFixture } from '@e2e/fixtures/tourFixture'

import {
COACH_IDS,
TOUR_SEEN_SETTING
} from '@/platform/onboarding/onboardingTours'

const test = mergeTests(comfyPageFixture, onboardingFixture)

// Relies on the test server's default workflow (locally: pnpm dev:test).
test.describe('Onboarding coachmarks', { tag: '@ui' }, () => {
test.describe('app-mode tour', () => {
// With no tour pre-seeded as seen, entering the app auto-opens it.
test.use({
initialSettings: { [TOUR_SEEN_SETTING]: [] }
})

test('auto-opens on the welcome landing, focuses Start, and Skip dismisses it', async ({
comfyPage,
onboarding
}) => {
await comfyPage.appMode.enterAppModeWithInputs([])
const coach = onboarding

await expect(coach.landing).toBeVisible()
await expect(coach.landing.getByRole('heading')).toHaveText(
'Welcome to Apps'
)
await expect(coach.landingStartButton).toBeFocused()

await coach.landingSkipButton.click()
await expect(coach.landing).toBeHidden()
await expect.poll(() => coach.seen('appMode')).toBe(true)
})

test('Escape dismisses the welcome landing and marks it seen', async ({
comfyPage,
onboarding
}) => {
await comfyPage.appMode.enterAppModeWithInputs([])
const coach = onboarding
await expect(coach.landing).toBeVisible()
await expect(coach.landingStartButton).toBeFocused()

await comfyPage.page.keyboard.press('Escape')
await expect(coach.landing).toBeHidden()
await expect.poll(() => coach.seen('appMode')).toBe(true)
})
})

test.describe('coach anchors', () => {
test('every registry id resolves to an element (drift guard)', async ({
comfyPage,
onboarding
}) => {
const coach = onboarding
await comfyPage.appMode.enterAppModeWithInputs([])
// The assets panel only mounts once the assets sidebar tab is open.
for (const id of Object.values(COACH_IDS).filter(
(id) => id !== COACH_IDS.assetsPanel
)) {
await expect(coach.coachAnchor(id)).toBeVisible()
}
await comfyPage.menu.assetsTab.tabButton.click()
await expect(coach.coachAnchor(COACH_IDS.assetsPanel)).toBeVisible()
})
})

test.describe('spotlight focus', () => {
test('focuses the primary action, traps Tab in the card, and re-focuses per step', async ({
comfyPage,
onboarding
}) => {
const coach = onboarding
await comfyPage.page.emulateMedia({ reducedMotion: 'reduce' })
await comfyPage.appMode.enterAppModeWithInputs([])

await coach.startTour('appMode')
await expect(coach.landing).toBeVisible()
await coach.landingStartButton.click()

const step1 = coach.cardForStep(1)
await expect(step1).toBeVisible()
// FocusScope's mount-auto-focus is suppressed so focus lands on the
// primary action rather than the first focusable (Skip).
await expect(coach.cardNextButton).toBeFocused()

// Tab more times than the card has controls; the looped trap must keep
// focus inside the card, never escaping to the app behind the overlay.
for (let i = 0; i < 4; i++) {
await comfyPage.page.keyboard.press('Tab')
await expect(step1.locator(':focus')).toBeVisible()
}
await comfyPage.page.keyboard.press('Shift+Tab')
await expect(step1.locator(':focus')).toBeVisible()

// Advancing to the next step moves focus onto its primary action.
await coach.cardNextButton.click()
await expect(coach.cardForStep(2)).toBeVisible()
await expect(coach.cardNextButton).toBeFocused()
})
})

test.describe('spotlight placement', () => {
test('every spotlight card stays fully within the viewport and Done completes the tour', async ({
comfyPage,
onboarding
}) => {
const coach = onboarding
// Read settled placements, not a transient mid-animation frame.
await comfyPage.page.emulateMedia({ reducedMotion: 'reduce' })
await comfyPage.appMode.enterAppModeWithInputs([])

await coach.startTour('appMode')
await expect(coach.landing).toBeVisible()
await coach.landingStartButton.click()

for (const step of [1, 2, 3]) {
const card = coach.cardForStep(step)
await expect(card).toBeVisible()
await expect(card).toBeInViewport({ ratio: 1 })
await coach.cardNextButton.click()
}

// The final assets step auto-opens the assets panel — no target click.
await expect(coach.cardForStep(4)).toBeInViewport({ ratio: 1 })

await coach.cardDoneButton.click()
await expect(coach.card).toBeHidden()
await expect.poll(() => coach.seen('appMode')).toBe(true)
})
})
})
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.48.5",
"version": "1.48.6",
"private": true,
"description": "Official front-end implementation of ComfyUI",
"homepage": "https://comfy.org",
Expand Down Expand Up @@ -75,6 +75,7 @@
"@comfyorg/tailwind-utils": "workspace:*",
"@customerio/cdp-analytics-browser": "catalog:",
"@datadog/browser-rum": "catalog:",
"@floating-ui/vue": "catalog:",
"@formkit/auto-animate": "catalog:",
"@iconify/json": "catalog:",
"@primeuix/forms": "catalog:",
Expand Down
4 changes: 4 additions & 0 deletions packages/design-system/src/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@
--color-alpha-magenta-700-60: #6a246a99;
--color-alpha-magenta-300-60: #ceaac999;

/* Onboarding coachmark overlay (always renders over a dark scrim) */
--color-coach-scrim: rgb(0 0 0 / 0.6);
--color-coach-ring: #fff;

/* PrimeVue pulled colors */
--color-muted: var(--p-text-muted-color);
--color-highlight: var(--p-primary-color);
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ catalog:
'@customerio/cdp-analytics-browser': ^0.5.3
'@datadog/browser-rum': ^6.33.0
'@eslint/js': ^10.0.1
'@floating-ui/vue': ^1.1.11
'@formkit/auto-animate': ^0.9.0
'@iconify-json/lucide': ^1.1.178
'@iconify/json': ^2.2.380
Expand Down
Binary file added public/assets/images/app-mode-landing.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions scripts/check-unused-i18n-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const IGNORE_PATTERNS = [
/^dataTypes\./, // Data types might be referenced dynamically
/^contextMenu\./, // Context menu items might be dynamic
/^color\./, // Color names might be used dynamically
/^onboardingCoachmarks\.[^.]+\.[^.]+\./, // Step keys derived as onboardingCoachmarks.<tour>.<step>.*
// Auto-generated categories from collect-i18n-general.ts
/^menuLabels\./, // Menu labels generated from command labels
/^settingsCategories\./, // Settings categories generated from setting definitions
Expand Down
6 changes: 5 additions & 1 deletion src/components/dialog/vRekaZIndex.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { ZIndex } from '@primeuix/utils/zindex'
import type { Directive } from 'vue'

/** Shared PrimeVue/Reka modal stacking sequence; later registrations cover earlier ones. */
export const MODAL_Z_KEY = 'modal'
export const MODAL_Z_BASE = 1700

// Both Reka and PrimeVue dialogs can appear at any depth in dialogStack, in
// any order. PrimeVue auto-increments a per-key z-index counter so later
// dialogs always cover earlier ones; Reka uses a static z-1700 class which
Expand All @@ -9,7 +13,7 @@ import type { Directive } from 'vue'
// renderers share one stacking sequence: whichever dialog opens last wins.
export const vRekaZIndex: Directive<HTMLElement> = {
mounted(el) {
ZIndex.set('modal', el, 1700)
ZIndex.set(MODAL_Z_KEY, el, MODAL_Z_BASE)
},
beforeUnmount(el) {
ZIndex.clear(el)
Expand Down
32 changes: 32 additions & 0 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -4640,5 +4640,37 @@
"training": "Training…",
"processingVideo": "Processing video…",
"running": "Running…"
},
"onboardingCoachmarks": {
"stepLabel": "Step {current} of {total}",
"skip": "Skip",
"next": "Next",
"back": "Back",
"done": "Done",
"loadError": "Something went wrong showing this tour",
"appMode": {
"replay": "Take a tour of App Mode",
"landing": {
"title": "Welcome to Apps",
"body": "A quick tour of the essentials, in about a minute. We'll show you where to add inputs, run your app, and find your results.",
"primary": "Start tutorial"
},
"inputs": {
"title": "Add your inputs",
"body": "Add what you want to work with. Your inputs are what the app turns into results."
},
"run": {
"title": "Run your app",
"body": "Happy with your inputs? Hit Run and your result appears in the center the moment it's ready."
},
"outputs": {
"title": "Get your results",
"body": "Your finished results show up here in the center. Download them, or tweak an input and run again."
},
"assets": {
"title": "Find all your assets",
"body": "Every generation and import lives in Media Assets. Open it anytime to browse, download, or reuse past work."
}
}
}
}
Loading
Loading