mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-06-20 05:22:58 +00:00
Compare commits
9 Commits
feature/dr
...
fix/linear
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7256ae081f | ||
|
|
c8b5589768 | ||
|
|
b4b95980da | ||
|
|
8c0af36c4a | ||
|
|
78a8d6f8fc | ||
|
|
cc41e3e1ac | ||
|
|
f994673dd1 | ||
|
|
444dc3fccd | ||
|
|
ed028a88be |
4
.github/workflows/release-draft-create.yaml
vendored
4
.github/workflows/release-draft-create.yaml
vendored
@@ -92,9 +92,7 @@ jobs:
|
||||
make_latest: >-
|
||||
${{ github.event.pull_request.base.ref == 'main' &&
|
||||
needs.build.outputs.is_prerelease == 'false' }}
|
||||
draft: >-
|
||||
${{ github.event.pull_request.base.ref != 'main' ||
|
||||
needs.build.outputs.is_prerelease == 'true' }}
|
||||
draft: ${{ needs.build.outputs.is_prerelease == 'true' }}
|
||||
prerelease: >-
|
||||
${{ needs.build.outputs.is_prerelease == 'true' }}
|
||||
generate_release_notes: true
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { externalLinks } from '../src/config/routes'
|
||||
import type { Locale } from '../src/i18n/translations'
|
||||
import { t } from '../src/i18n/translations'
|
||||
import { test } from './fixtures/blockExternalMedia'
|
||||
|
||||
const PATH_EN = '/drops'
|
||||
const PATH_ZH = '/zh-CN/drops'
|
||||
const CLOUD_URL = 'https://cloud.comfy.org'
|
||||
|
||||
const LOCALES: ReadonlyArray<readonly [string, Locale]> = [
|
||||
[PATH_EN, 'en'],
|
||||
[PATH_ZH, 'zh-CN']
|
||||
]
|
||||
|
||||
function heroSection(page: Page, locale: Locale) {
|
||||
return page.locator('section').filter({
|
||||
has: page.getByRole('heading', {
|
||||
level: 1,
|
||||
name: t('drops.hero.title', locale)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function ctaSection(page: Page, locale: Locale) {
|
||||
return page.locator('section').filter({
|
||||
has: page.getByRole('heading', {
|
||||
level: 2,
|
||||
name: t('drops.cta.heading', locale)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
test.describe('Drops landing — desktop @smoke', () => {
|
||||
test('renders the configured title at /drops', async ({ page }) => {
|
||||
await page.goto(PATH_EN)
|
||||
await expect(page).toHaveTitle(t('drops.page.title', 'en'))
|
||||
})
|
||||
|
||||
test('renders the localized title at /zh-CN/drops', async ({ page }) => {
|
||||
await page.goto(PATH_ZH)
|
||||
await expect(page).toHaveTitle(t('drops.page.title', 'zh-CN'))
|
||||
})
|
||||
|
||||
test('is indexable at both locales', async ({ page }) => {
|
||||
await page.goto(PATH_EN)
|
||||
await expect(page.locator('meta[name="robots"]')).toHaveCount(0)
|
||||
|
||||
await page.goto(PATH_ZH)
|
||||
await expect(page.locator('meta[name="robots"]')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('hero h1 renders the localized title in both locales', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto(PATH_EN)
|
||||
await expect(
|
||||
page.getByRole('heading', {
|
||||
level: 1,
|
||||
name: t('drops.hero.title', 'en')
|
||||
})
|
||||
).toBeVisible()
|
||||
|
||||
await page.goto(PATH_ZH)
|
||||
await expect(
|
||||
page.getByRole('heading', {
|
||||
level: 1,
|
||||
name: t('drops.hero.title', 'zh-CN')
|
||||
})
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('hero primary CTA links to /download per locale', async ({ page }) => {
|
||||
for (const [path, locale, expectedHref] of [
|
||||
[PATH_EN, 'en', '/download'],
|
||||
[PATH_ZH, 'zh-CN', '/zh-CN/download']
|
||||
] as const) {
|
||||
await page.goto(path)
|
||||
const primary = heroSection(page, locale).getByRole('link', {
|
||||
name: t('drops.hero.primary', locale)
|
||||
})
|
||||
await expect(primary).toBeVisible()
|
||||
await expect(primary).toHaveAttribute('href', expectedHref)
|
||||
}
|
||||
})
|
||||
|
||||
test('hero secondary CTA opens external Cloud in a new tab on both locales', async ({
|
||||
page
|
||||
}) => {
|
||||
for (const [path, locale] of LOCALES) {
|
||||
await page.goto(path)
|
||||
const secondary = heroSection(page, locale).getByRole('link', {
|
||||
name: t('drops.hero.secondary', locale)
|
||||
})
|
||||
await expect(secondary).toBeVisible()
|
||||
await expect(secondary).toHaveAttribute('href', CLOUD_URL)
|
||||
await expect(secondary).toHaveAttribute('target', '_blank')
|
||||
await expect(secondary).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
}
|
||||
})
|
||||
|
||||
test('subscribe banner shows text and a sign-up link in both locales', async ({
|
||||
page
|
||||
}) => {
|
||||
for (const [path, locale] of LOCALES) {
|
||||
await page.goto(path)
|
||||
await expect(page.getByText(t('drops.banner.text', locale))).toBeVisible()
|
||||
|
||||
const signUp = page.getByRole('link', {
|
||||
name: t('drops.banner.cta', locale)
|
||||
})
|
||||
await expect(signUp).toBeVisible()
|
||||
await expect(signUp).toHaveAttribute('href', externalLinks.youtube)
|
||||
await expect(signUp).toHaveAttribute('target', '_blank')
|
||||
await expect(signUp).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
}
|
||||
})
|
||||
|
||||
test('closing CTA shows heading and both action buttons in both locales', async ({
|
||||
page
|
||||
}) => {
|
||||
for (const [path, locale] of LOCALES) {
|
||||
await page.goto(path)
|
||||
const section = ctaSection(page, locale)
|
||||
await expect(
|
||||
section.getByRole('heading', {
|
||||
level: 2,
|
||||
name: t('drops.cta.heading', locale)
|
||||
})
|
||||
).toBeVisible()
|
||||
|
||||
const primary = section.getByRole('link', {
|
||||
name: t('drops.cta.primary', locale)
|
||||
})
|
||||
await expect(primary).toBeVisible()
|
||||
await expect(primary).toHaveAttribute('href', externalLinks.cloud)
|
||||
await expect(primary).toHaveAttribute('target', '_blank')
|
||||
await expect(primary).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
|
||||
const secondary = section.getByRole('link', {
|
||||
name: t('drops.cta.secondary', locale)
|
||||
})
|
||||
await expect(secondary).toBeVisible()
|
||||
await expect(secondary).toHaveAttribute('href', externalLinks.workflows)
|
||||
await expect(secondary).toHaveAttribute('target', '_blank')
|
||||
await expect(secondary).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Drops landing — mobile @mobile', () => {
|
||||
test('closing CTA heading stays within viewport width', async ({ page }) => {
|
||||
await page.goto(PATH_EN)
|
||||
const heading = page.getByRole('heading', {
|
||||
level: 2,
|
||||
name: t('drops.cta.heading', 'en')
|
||||
})
|
||||
await heading.scrollIntoViewIfNeeded()
|
||||
await expect(heading).toBeVisible()
|
||||
|
||||
const box = await heading.boundingBox()
|
||||
expect(box, 'CTA heading bounding box').not.toBeNull()
|
||||
expect(box!.x + box!.width).toBeLessThanOrEqual(
|
||||
page.viewportSize()!.width + 1
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,13 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import type { AnchorHTMLAttributes } from 'vue'
|
||||
|
||||
import Button from '../ui/button/Button.vue'
|
||||
import BrandButton from '../common/BrandButton.vue'
|
||||
|
||||
type Cta = {
|
||||
label: string
|
||||
href: string
|
||||
target?: AnchorHTMLAttributes['target']
|
||||
rel?: AnchorHTMLAttributes['rel']
|
||||
target?: '_blank' | '_self' | '_parent' | '_top'
|
||||
}
|
||||
|
||||
type TermsLink = {
|
||||
@@ -15,18 +12,11 @@ type TermsLink = {
|
||||
href: string
|
||||
}
|
||||
|
||||
const { heading, primaryCta, secondaryCta, termsLink } = defineProps<{
|
||||
defineProps<{
|
||||
heading: string
|
||||
primaryCta: Cta
|
||||
secondaryCta?: Cta
|
||||
termsLink?: TermsLink
|
||||
termsLink: TermsLink
|
||||
}>()
|
||||
|
||||
function resolveRel(cta: Cta): AnchorHTMLAttributes['rel'] {
|
||||
return (
|
||||
cta.rel ?? (cta.target === '_blank' ? 'noopener noreferrer' : undefined)
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -39,32 +29,18 @@ function resolveRel(cta: Cta): AnchorHTMLAttributes['rel'] {
|
||||
{{ heading }}
|
||||
</h2>
|
||||
|
||||
<div class="mt-10 flex flex-col gap-4 sm:flex-row lg:mt-12">
|
||||
<Button
|
||||
as="a"
|
||||
:href="primaryCta.href"
|
||||
:target="primaryCta.target"
|
||||
:rel="resolveRel(primaryCta)"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
>
|
||||
{{ primaryCta.label }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="secondaryCta"
|
||||
as="a"
|
||||
:href="secondaryCta.href"
|
||||
:target="secondaryCta.target"
|
||||
:rel="resolveRel(secondaryCta)"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
>
|
||||
{{ secondaryCta.label }}
|
||||
</Button>
|
||||
</div>
|
||||
<BrandButton
|
||||
:href="primaryCta.href"
|
||||
:target="primaryCta.target"
|
||||
:rel="primaryCta.target === '_blank' ? 'noopener noreferrer' : undefined"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
class="mt-10 px-20 py-4 text-base uppercase lg:mt-12"
|
||||
>
|
||||
{{ primaryCta.label }}
|
||||
</BrandButton>
|
||||
|
||||
<a
|
||||
v-if="termsLink"
|
||||
:href="termsLink.href"
|
||||
class="mt-8 text-sm text-primary-comfy-canvas/70 underline underline-offset-4 transition-colors hover:text-primary-comfy-canvas"
|
||||
>
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { AnchorHTMLAttributes } from 'vue'
|
||||
|
||||
import Button from '../ui/button/Button.vue'
|
||||
|
||||
type Cta = {
|
||||
label: string
|
||||
href: string
|
||||
target?: AnchorHTMLAttributes['target']
|
||||
rel?: AnchorHTMLAttributes['rel']
|
||||
}
|
||||
|
||||
type Visual = {
|
||||
src: string
|
||||
alt: string
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
const { visual, eyebrow, title, subtitle, primaryCta, secondaryCta } =
|
||||
defineProps<{
|
||||
visual?: Visual
|
||||
eyebrow?: string
|
||||
title: string
|
||||
subtitle?: string
|
||||
primaryCta: Cta
|
||||
secondaryCta?: Cta
|
||||
}>()
|
||||
|
||||
function resolveRel(cta: Cta): AnchorHTMLAttributes['rel'] {
|
||||
return (
|
||||
cta.rel ?? (cta.target === '_blank' ? 'noopener noreferrer' : undefined)
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="max-w-9xl mx-auto flex flex-col items-center px-6 py-16 text-center lg:py-24"
|
||||
>
|
||||
<img
|
||||
v-if="visual"
|
||||
:src="visual.src"
|
||||
:alt="visual.alt"
|
||||
:width="visual.width"
|
||||
:height="visual.height"
|
||||
fetchpriority="high"
|
||||
decoding="async"
|
||||
class="mb-10 h-auto w-full max-w-md lg:mb-12 lg:max-w-lg"
|
||||
/>
|
||||
|
||||
<p
|
||||
v-if="eyebrow"
|
||||
class="mb-4 text-sm font-medium tracking-wide text-primary-comfy-canvas/70 uppercase"
|
||||
>
|
||||
{{ eyebrow }}
|
||||
</p>
|
||||
|
||||
<h1
|
||||
class="text-4xl font-light tracking-tight text-primary-comfy-canvas lg:text-6xl"
|
||||
>
|
||||
{{ title }}
|
||||
</h1>
|
||||
|
||||
<p
|
||||
v-if="subtitle"
|
||||
class="mt-6 max-w-2xl text-base text-primary-comfy-canvas/70 lg:text-lg"
|
||||
>
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
|
||||
<div class="mt-10 flex flex-col gap-4 sm:flex-row lg:mt-12">
|
||||
<Button
|
||||
as="a"
|
||||
:href="primaryCta.href"
|
||||
:target="primaryCta.target"
|
||||
:rel="resolveRel(primaryCta)"
|
||||
size="lg"
|
||||
>
|
||||
{{ primaryCta.label }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="secondaryCta"
|
||||
as="a"
|
||||
:href="secondaryCta.href"
|
||||
:target="secondaryCta.target"
|
||||
:rel="resolveRel(secondaryCta)"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
>
|
||||
{{ secondaryCta.label }}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -8,7 +8,6 @@ const baseRoutes = {
|
||||
cloudEnterprise: '/cloud/enterprise',
|
||||
api: '/api',
|
||||
gallery: '/gallery',
|
||||
drops: '/drops',
|
||||
about: '/about',
|
||||
careers: '/careers',
|
||||
customers: '/customers',
|
||||
|
||||
@@ -4928,63 +4928,6 @@ const translations = {
|
||||
'affiliate.cta.termsLabel': {
|
||||
en: 'Read the affiliate program terms',
|
||||
'zh-CN': '阅读联盟计划条款'
|
||||
},
|
||||
|
||||
// Drops page (/drops) — head metadata
|
||||
// zh-CN strings pending native review (see apps/website/.scratch/drops-page/PRD.md)
|
||||
'drops.page.title': {
|
||||
en: 'Drops — Everything new in ComfyUI',
|
||||
'zh-CN': 'Drops — ComfyUI 最新动态'
|
||||
},
|
||||
'drops.page.description': {
|
||||
en: 'Explore everything new in Comfy — releases, features, models, and resources across platform, cloud, community, and developer tools.',
|
||||
'zh-CN':
|
||||
'探索 Comfy 的最新动态 — 涵盖平台、云端、社区和开发者工具的发布、功能、模型和资源。'
|
||||
},
|
||||
|
||||
// Drops page (/drops) — hero section
|
||||
// zh-CN strings pending native review (see apps/website/.scratch/drops-page/PRD.md)
|
||||
'drops.hero.title': {
|
||||
en: 'Everything new in ComfyUI',
|
||||
'zh-CN': 'ComfyUI 全新内容'
|
||||
},
|
||||
'drops.hero.primary': {
|
||||
en: 'Download Desktop',
|
||||
'zh-CN': '下载桌面版'
|
||||
},
|
||||
'drops.hero.secondary': {
|
||||
en: 'Launch Cloud',
|
||||
'zh-CN': '启动云端'
|
||||
},
|
||||
'drops.hero.visualAlt': {
|
||||
en: 'Comfy',
|
||||
'zh-CN': 'Comfy'
|
||||
},
|
||||
|
||||
// Drops page (/drops) — subscribe banner
|
||||
// zh-CN strings pending native review (see apps/website/.scratch/drops-page/PRD.md)
|
||||
'drops.banner.text': {
|
||||
en: 'Subscribe to the live stream and get your questions answered in real time.',
|
||||
'zh-CN': '订阅直播,实时获得您的问题解答。'
|
||||
},
|
||||
'drops.banner.cta': {
|
||||
en: 'Sign up now',
|
||||
'zh-CN': '立即注册'
|
||||
},
|
||||
|
||||
// Drops page (/drops) — closing CTA
|
||||
// zh-CN strings pending native review (see apps/website/.scratch/drops-page/PRD.md)
|
||||
'drops.cta.heading': {
|
||||
en: 'Everything Comfy ships. All in one place.',
|
||||
'zh-CN': 'Comfy 的全部内容,一处尽享。'
|
||||
},
|
||||
'drops.cta.primary': {
|
||||
en: 'Open Comfy Cloud',
|
||||
'zh-CN': '打开 Comfy Cloud'
|
||||
},
|
||||
'drops.cta.secondary': {
|
||||
en: 'Try Workflow',
|
||||
'zh-CN': '试用工作流'
|
||||
}
|
||||
} as const satisfies Record<string, Record<Locale, string>>
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import CtaSection from '../templates/drops/CtaSection.vue'
|
||||
import HeroSection from '../templates/drops/HeroSection.vue'
|
||||
import SubscribeBanner from '../templates/drops/SubscribeBanner.vue'
|
||||
import { t } from '../i18n/translations'
|
||||
|
||||
const locale = 'en' as const
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={t('drops.page.title', locale)}
|
||||
description={t('drops.page.description', locale)}
|
||||
>
|
||||
<SubscribeBanner locale={locale} />
|
||||
<HeroSection locale={locale} />
|
||||
<CtaSection locale={locale} />
|
||||
</BaseLayout>
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import CtaSection from '../../templates/drops/CtaSection.vue'
|
||||
import HeroSection from '../../templates/drops/HeroSection.vue'
|
||||
import SubscribeBanner from '../../templates/drops/SubscribeBanner.vue'
|
||||
import { t } from '../../i18n/translations'
|
||||
|
||||
const locale = 'zh-CN' as const
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={t('drops.page.title', locale)}
|
||||
description={t('drops.page.description', locale)}
|
||||
>
|
||||
<SubscribeBanner locale={locale} />
|
||||
<HeroSection locale={locale} />
|
||||
<CtaSection locale={locale} />
|
||||
</BaseLayout>
|
||||
@@ -1,25 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
|
||||
import CtaCenter01 from '../../components/blocks/CtaCenter01.vue'
|
||||
import { externalLinks } from '../../config/routes'
|
||||
import { t } from '../../i18n/translations'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CtaCenter01
|
||||
:heading="t('drops.cta.heading', locale)"
|
||||
:primary-cta="{
|
||||
label: t('drops.cta.primary', locale),
|
||||
href: externalLinks.cloud,
|
||||
target: '_blank'
|
||||
}"
|
||||
:secondary-cta="{
|
||||
label: t('drops.cta.secondary', locale),
|
||||
href: externalLinks.workflows,
|
||||
target: '_blank'
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,32 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
|
||||
import HeroCenter01 from '../../components/blocks/HeroCenter01.vue'
|
||||
import { externalLinks, getRoutes } from '../../config/routes'
|
||||
import { t } from '../../i18n/translations'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
const routes = getRoutes(locale)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<HeroCenter01
|
||||
:visual="{
|
||||
src: '/affiliates/brand/comfy-amplified-logo.png',
|
||||
alt: t('drops.hero.visualAlt', locale),
|
||||
width: 632,
|
||||
height: 632
|
||||
}"
|
||||
:title="t('drops.hero.title', locale)"
|
||||
:primary-cta="{
|
||||
label: t('drops.hero.primary', locale),
|
||||
href: routes.download
|
||||
}"
|
||||
:secondary-cta="{
|
||||
label: t('drops.hero.secondary', locale),
|
||||
href: externalLinks.cloud,
|
||||
target: '_blank'
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,28 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
|
||||
import { externalLinks } from '../../config/routes'
|
||||
import { t } from '../../i18n/translations'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
// V1 fallback: point at the Comfy YouTube channel until content team supplies
|
||||
// a dedicated event-registration URL (Google Form, Eventbrite, etc.).
|
||||
const signUpHref = externalLinks.youtube
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="bg-primary-comfy-plum text-primary-warm-white flex w-full flex-col items-center justify-center gap-2 px-6 py-3 text-center text-sm sm:flex-row sm:gap-4"
|
||||
>
|
||||
<p>{{ t('drops.banner.text', locale) }}</p>
|
||||
<a
|
||||
:href="signUpHref"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="font-semibold uppercase underline underline-offset-4 hover:no-underline"
|
||||
>
|
||||
{{ t('drops.banner.cta', locale) }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
@@ -69,6 +69,24 @@ export class TemplateHelper {
|
||||
}
|
||||
|
||||
async mockIndex(): Promise<void> {
|
||||
const customTemplatesHandler = async (route: Route) => {
|
||||
const customTemplates: Record<string, string[]> = {}
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify(customTemplates),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store'
|
||||
}
|
||||
})
|
||||
}
|
||||
const customTemplatesPattern = '**/api/workflow_templates'
|
||||
this.routeHandlers.push({
|
||||
pattern: customTemplatesPattern,
|
||||
handler: customTemplatesHandler
|
||||
})
|
||||
await this.page.route(customTemplatesPattern, customTemplatesHandler)
|
||||
|
||||
const indexHandler = async (route: Route) => {
|
||||
const payload = this.index ?? mockTemplateIndex(this.templates)
|
||||
await route.fulfill({
|
||||
|
||||
@@ -2,7 +2,7 @@ import { readFileSync } from 'fs'
|
||||
|
||||
import { test } from '@playwright/test'
|
||||
|
||||
import type { AppMode } from '@/composables/useAppMode'
|
||||
import type { AppMode } from '@/utils/appMode'
|
||||
import type {
|
||||
ComfyApiWorkflow,
|
||||
ComfyWorkflowJSON
|
||||
|
||||
@@ -32,6 +32,10 @@ test.describe('Mask Editor', { tag: '@vue-nodes' }, () => {
|
||||
await expect(dialog.getByText('Save')).toBeVisible()
|
||||
await expect(dialog.getByText('Cancel')).toBeVisible()
|
||||
|
||||
await dialog.getByTestId('pointer-zone').hover()
|
||||
await dialog.getByText('Brush Settings').hover()
|
||||
await expect(dialog.getByTestId('brush-cursor')).toHaveCSS('opacity', '0')
|
||||
|
||||
await comfyPage.expectScreenshot(dialog, 'mask-editor-dialog-open.png')
|
||||
}
|
||||
)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 324 KiB After Width: | Height: | Size: 324 KiB |
@@ -4,7 +4,6 @@ import {
|
||||
} from '@e2e/fixtures/ComfyPage'
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import { fitToViewInstant } from '@e2e/fixtures/utils/fitToView'
|
||||
import type { WorkspaceStore } from '@e2e/types/globals'
|
||||
import type { NodeReference } from '@e2e/fixtures/utils/litegraphUtils'
|
||||
|
||||
// TODO: there might be a better solution for this
|
||||
@@ -35,56 +34,6 @@ async function openSelectionToolboxHelp(comfyPage: ComfyPage) {
|
||||
return comfyPage.page.getByTestId('properties-panel')
|
||||
}
|
||||
|
||||
async function setLocaleAndWaitForWorkflowReload(
|
||||
comfyPage: ComfyPage,
|
||||
locale: string
|
||||
) {
|
||||
await comfyPage.page.evaluate(async (targetLocale) => {
|
||||
const workflow = (window.app!.extensionManager as WorkspaceStore).workflow
|
||||
.activeWorkflow
|
||||
|
||||
if (!workflow) {
|
||||
throw new Error('No active workflow while waiting for locale reload')
|
||||
}
|
||||
|
||||
const changeTracker = workflow.changeTracker.constructor as unknown as {
|
||||
isLoadingGraph: boolean
|
||||
}
|
||||
|
||||
let sawLoading = false
|
||||
const waitForReload = new Promise<void>((resolve, reject) => {
|
||||
const timeoutAt = performance.now() + 5000
|
||||
|
||||
const tick = () => {
|
||||
if (changeTracker.isLoadingGraph) {
|
||||
sawLoading = true
|
||||
}
|
||||
|
||||
if (sawLoading && !changeTracker.isLoadingGraph) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
if (performance.now() > timeoutAt) {
|
||||
reject(
|
||||
new Error(
|
||||
`Timed out waiting for workflow reload after setting locale to ${targetLocale}`
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
requestAnimationFrame(tick)
|
||||
}
|
||||
|
||||
tick()
|
||||
})
|
||||
|
||||
await window.app!.extensionManager.setting.set('Comfy.Locale', targetLocale)
|
||||
await waitForReload
|
||||
}, locale)
|
||||
}
|
||||
|
||||
test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.NodeLibrary.NewDesign', false)
|
||||
@@ -398,34 +347,33 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
|
||||
await expect(helpPage.locator('img[alt="Safe Image"]')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Should handle locale-specific documentation', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Mock different responses for different locales
|
||||
await comfyPage.page.route('**/docs/KSampler/ja.md', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
body: `# KSamplerノード
|
||||
test.describe('Locale-specific documentation', () => {
|
||||
test.use({ initialSettings: { 'Comfy.Locale': 'ja' } })
|
||||
|
||||
test('Should handle locale-specific documentation', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Mock different responses for different locales
|
||||
await comfyPage.page.route('**/docs/KSampler/ja.md', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
body: `# KSamplerノード
|
||||
|
||||
これは日本語のドキュメントです。
|
||||
`
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
await comfyPage.page.route('**/docs/KSampler/en.md', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
body: `# KSampler Node
|
||||
await comfyPage.page.route('**/docs/KSampler/en.md', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
body: `# KSampler Node
|
||||
|
||||
This is English documentation.
|
||||
`
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Set locale to Japanese
|
||||
await setLocaleAndWaitForWorkflowReload(comfyPage, 'ja')
|
||||
|
||||
try {
|
||||
await comfyPage.workflow.loadWorkflow('default')
|
||||
const ksamplerNodes =
|
||||
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
|
||||
@@ -434,9 +382,7 @@ This is English documentation.
|
||||
const helpPage = await openSelectionToolboxHelp(comfyPage)
|
||||
await expect(helpPage).toContainText('KSamplerノード')
|
||||
await expect(helpPage).toContainText('これは日本語のドキュメントです')
|
||||
} finally {
|
||||
await setLocaleAndWaitForWorkflowReload(comfyPage, 'en')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('Should handle network errors gracefully', async ({ comfyPage }) => {
|
||||
|
||||
@@ -10,13 +10,16 @@ import {
|
||||
} from '@e2e/fixtures/utils/painter'
|
||||
import type { TestGraphAccess } from '@e2e/types/globals'
|
||||
|
||||
const HIDDEN_PAINTER_WIDGET_NAMES = ['width', 'height', 'bg_color'] as const
|
||||
const HIDDEN_PAINTER_NUMBER_WIDGET_NAMES = ['width', 'height'] as const
|
||||
|
||||
test.describe('Painter', { tag: ['@widget', '@vue-nodes'] }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.page.evaluate(() => window.app?.graph?.clear())
|
||||
await comfyPage.workflow.loadWorkflow('widgets/painter_widget')
|
||||
})
|
||||
|
||||
test.describe('Widget rendering', { tag: ['@widget'] }, () => {
|
||||
test.describe('Widget rendering', () => {
|
||||
test('Node enforces minimum size', async ({ comfyPage }) => {
|
||||
const size = await comfyPage.page.evaluate(() => {
|
||||
const graph = window.graph as TestGraphAccess | undefined
|
||||
@@ -28,17 +31,15 @@ test.describe('Painter', { tag: ['@widget', '@vue-nodes'] }, () => {
|
||||
expect(size![1]).toBeGreaterThanOrEqual(550)
|
||||
})
|
||||
|
||||
test('Width, height, and bg_color standard widgets are hidden', async ({
|
||||
test('Does not render hidden standard widgets in Vue mode', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const hiddenFlags = await comfyPage.page.evaluate(() => {
|
||||
const graph = window.graph as TestGraphAccess | undefined
|
||||
const node = graph?._nodes_by_id?.['1']
|
||||
return (node?.widgets ?? [])
|
||||
.filter((w) => ['width', 'height', 'bg_color'].includes(w.name))
|
||||
.map((w) => w.options.hidden ?? false)
|
||||
})
|
||||
expect(hiddenFlags).toEqual([true, true, true])
|
||||
const node = comfyPage.vueNodes.getNodeLocator('1')
|
||||
await expect(node).toBeVisible()
|
||||
|
||||
for (const widgetName of HIDDEN_PAINTER_WIDGET_NAMES) {
|
||||
await expect(node.getByLabel(widgetName, { exact: true })).toBeHidden()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -788,6 +789,49 @@ test.describe('Painter', { tag: ['@widget', '@vue-nodes'] }, () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.describe(
|
||||
'Painter legacy LiteGraph rendering',
|
||||
{ tag: ['@widget', '@canvas'] },
|
||||
() => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', false)
|
||||
await comfyPage.page.evaluate(() => window.app?.graph?.clear())
|
||||
await comfyPage.workflow.loadWorkflow('widgets/painter_widget')
|
||||
})
|
||||
|
||||
test('Does not open editors for backend-hidden number widget rows in legacy LiteGraph', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const painterNodes = await comfyPage.nodeOps.getNodeRefsByType('Painter')
|
||||
expect(painterNodes).toHaveLength(1)
|
||||
const painterNode = painterNodes[0]!
|
||||
const maskWidget = await painterNode.getWidgetByName('mask')
|
||||
const maskWidgetClientPosition = await maskWidget.getPosition()
|
||||
const widgetRowClientHeight = await comfyPage.page.evaluate(
|
||||
() =>
|
||||
(window.LiteGraph!.NODE_WIDGET_HEIGHT + 4) *
|
||||
window.app!.canvas.ds.scale
|
||||
)
|
||||
const legacyPrompt = comfyPage.page.locator('.graphdialog')
|
||||
await expect(legacyPrompt).toBeHidden()
|
||||
|
||||
for (const [
|
||||
index,
|
||||
widgetName
|
||||
] of HIDDEN_PAINTER_NUMBER_WIDGET_NAMES.entries()) {
|
||||
await test.step(`Click ${widgetName} row`, async () => {
|
||||
await comfyPage.page.mouse.click(
|
||||
maskWidgetClientPosition.x,
|
||||
maskWidgetClientPosition.y + widgetRowClientHeight * (index + 1)
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
await expect(legacyPrompt).toBeHidden()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test.describe(
|
||||
'Painter — input image connection',
|
||||
{ tag: ['@widget', '@vue-nodes', '@slow'] },
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
|
||||
import { TemplateIncludeOnDistributionEnum } from '@/platform/workflow/templates/types/template'
|
||||
import { comfyPageFixture } from '@e2e/fixtures/ComfyPage'
|
||||
import { createCloudAssetsFixture } from '@e2e/fixtures/assetApiFixture'
|
||||
import { makeTemplate } from '@e2e/fixtures/data/templateFixtures'
|
||||
import { withTemplates } from '@e2e/fixtures/helpers/TemplateHelper'
|
||||
import { TestIds } from '@e2e/fixtures/selectors'
|
||||
import { templateApiFixture } from '@e2e/fixtures/templateApiFixture'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, templateApiFixture)
|
||||
const test = mergeTests(createCloudAssetsFixture([]), templateApiFixture)
|
||||
|
||||
const Cloud = TemplateIncludeOnDistributionEnum.Cloud
|
||||
const Desktop = TemplateIncludeOnDistributionEnum.Desktop
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { watch } from 'vue'
|
||||
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useBillingContext } from '@/composables/billing/useBillingContext'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
|
||||
/**
|
||||
* Cloud-only extension that enforces active subscription requirement
|
||||
*/
|
||||
useExtensionService().registerExtension({
|
||||
name: 'Comfy.Cloud.Subscription',
|
||||
|
||||
setup: async () => {
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
const { requireActiveSubscription } = useBillingContext()
|
||||
|
||||
const checkSubscriptionStatus = () => {
|
||||
if (!isLoggedIn.value) return
|
||||
void requireActiveSubscription()
|
||||
}
|
||||
|
||||
watch(() => isLoggedIn.value, checkSubscriptionStatus, {
|
||||
immediate: true
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -36,10 +36,6 @@ if (isCloud) {
|
||||
await import('./cloudRemoteConfig')
|
||||
await import('./cloudBadges')
|
||||
await import('./cloudSessionCookie')
|
||||
|
||||
if (window.__CONFIG__?.subscription_required) {
|
||||
await import('./cloudSubscription')
|
||||
}
|
||||
}
|
||||
|
||||
// Feedback button for cloud and nightly builds
|
||||
|
||||
@@ -223,8 +223,18 @@ export const MODEL_NODE_MAPPINGS: ReadonlyArray<
|
||||
['film', 'FILM VFI', 'ckpt_name'],
|
||||
|
||||
// ---- Ultralytics YOLO detectors (ComfyUI-Impact-Pack) ----
|
||||
['ultralytics/bbox', 'UltralyticsDetectorProvider', 'model_name'],
|
||||
['ultralytics/segm', 'UltralyticsDetectorProvider', 'model_name'],
|
||||
// Intentionally NOT mapped to the asset-picker. The cloud asset-ingestion
|
||||
// metadata for nested model folders (`ultralytics/bbox`, `ultralytics/segm`)
|
||||
// still has the two known half-bugs described in #12075:
|
||||
// 1. Tag lookup mismatch (cloud stores combined tags, picker queries split).
|
||||
// 2. Submitted value mismatch (picker returns basenames, ingest expects
|
||||
// subdirectory-prefixed `bbox/<file>` / `segm/<file>`).
|
||||
// PR #12151 re-added the bbox/segm entries before either half was fixed,
|
||||
// reintroducing the FaceDetailer breakage. Until BE-689 lands the cloud-side
|
||||
// fixes, leave these disabled so the node falls back to the static combo
|
||||
// populated from `/api/object_info`.
|
||||
// ['ultralytics/bbox', 'UltralyticsDetectorProvider', 'model_name'],
|
||||
// ['ultralytics/segm', 'UltralyticsDetectorProvider', 'model_name'],
|
||||
|
||||
// ---- Mel-Band RoFormer audio separation (ComfyUI-MelBandRoFormer) ----
|
||||
['diffusion_models', 'MelBandRoFormerModelLoader', 'model_name'],
|
||||
|
||||
@@ -302,6 +302,7 @@ export const useLitegraphService = () => {
|
||||
advanced: inputSpec.advanced,
|
||||
hidden: inputSpec.hidden
|
||||
})
|
||||
if (inputSpec.hidden !== undefined) widget.hidden = inputSpec.hidden
|
||||
if (dynamic) widget.tooltip = inputSpec.tooltip
|
||||
}
|
||||
|
||||
|
||||
@@ -88,9 +88,10 @@ const { onResizeEnd } = useStablePrimeVueSplitterSizer(
|
||||
|
||||
const TYPEFORM_WIDGET_ID = 'jmmzmlKw'
|
||||
|
||||
const bottomLeftRef = useTemplateRef('bottomLeftRef')
|
||||
const bottomRightRef = useTemplateRef('bottomRightRef')
|
||||
const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
|
||||
const bottomLeftRef = useTemplateRef<HTMLDivElement>('bottomLeftRef')
|
||||
const bottomRightRef = useTemplateRef<HTMLDivElement>('bottomRightRef')
|
||||
const linearWorkflowRef =
|
||||
useTemplateRef<InstanceType<typeof LinearControls>>('linearWorkflowRef')
|
||||
|
||||
function dragDrop(e: DragEvent) {
|
||||
const { dataTransfer } = e
|
||||
|
||||
Reference in New Issue
Block a user