diff --git a/.github/workflows/ci-tests-e2e.yaml b/.github/workflows/ci-tests-e2e.yaml index edd6b65a8f..f5cfd8046d 100644 --- a/.github/workflows/ci-tests-e2e.yaml +++ b/.github/workflows/ci-tests-e2e.yaml @@ -73,8 +73,8 @@ jobs: strategy: fail-fast: false matrix: - shardIndex: [1, 2, 3, 4, 5, 6, 7, 8] - shardTotal: [8] + shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + shardTotal: [16] steps: - name: Checkout repository uses: actions/checkout@v6 @@ -93,7 +93,7 @@ jobs: # Run sharded tests (browsers pre-installed in container) - name: Run Playwright tests (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}) id: playwright - run: pnpm exec playwright test --project=chromium --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob + run: pnpm exec playwright test --project=chromium --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} env: PLAYWRIGHT_BLOB_OUTPUT_DIR: ./blob-report COLLECT_COVERAGE: 'true' @@ -150,7 +150,7 @@ jobs: # Run tests (browsers pre-installed in container) - name: Run Playwright tests (${{ matrix.browser }}) id: playwright - run: pnpm exec playwright test --project=${{ matrix.browser }} --reporter=blob + run: pnpm exec playwright test --project=${{ matrix.browser }} env: PLAYWRIGHT_BLOB_OUTPUT_DIR: ./blob-report diff --git a/.github/workflows/release-biweekly-comfyui.yaml b/.github/workflows/release-biweekly-comfyui.yaml index 8c3f181fdb..2edad02ac2 100644 --- a/.github/workflows/release-biweekly-comfyui.yaml +++ b/.github/workflows/release-biweekly-comfyui.yaml @@ -23,6 +23,10 @@ on: required: false default: 'Comfy-Org/ComfyUI' type: string + target_branch: + description: 'Optional: force a specific release branch, e.g. core/1.47 or core/2.0. Overrides the pin-derived target — use to skip a dead minor or do an out-of-cadence / major release.' + required: false + type: string jobs: check-release-week: @@ -49,12 +53,15 @@ jobs: fi - name: Summary + env: + TARGET_BRANCH_OVERRIDE: ${{ inputs.target_branch }} run: | echo "## Release Check" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "- Is release week: ${{ steps.check.outputs.is_release_week }}" >> $GITHUB_STEP_SUMMARY echo "- Manual trigger: ${{ github.event_name == 'workflow_dispatch' }}" >> $GITHUB_STEP_SUMMARY echo "- Release type: ${{ inputs.release_type || 'minor (scheduled)' }}" >> $GITHUB_STEP_SUMMARY + echo "- Target branch override: ${TARGET_BRANCH_OVERRIDE:-(none — pin-derived)}" >> $GITHUB_STEP_SUMMARY resolve-version: needs: check-release-week @@ -103,6 +110,7 @@ jobs: working-directory: frontend env: RELEASE_TYPE: ${{ inputs.release_type || 'minor' }} + TARGET_BRANCH: ${{ inputs.target_branch }} run: | set -euo pipefail diff --git a/apps/website/e2e/brand.spec.ts b/apps/website/e2e/brand.spec.ts new file mode 100644 index 0000000000..ca831a9071 --- /dev/null +++ b/apps/website/e2e/brand.spec.ts @@ -0,0 +1,51 @@ +import { expect } from '@playwright/test' + +import { BRAND_ASSETS_ZIP, BRAND_GUIDELINES_PDF } from '../src/data/brandAssets' +import { test } from './fixtures/blockExternalMedia' + +test.describe('Brand portal @smoke', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/brand') + }) + + test('renders each brand guideline section', async ({ page }) => { + await expect( + page.getByRole('heading', { level: 1, name: 'Create with ComfyUI' }) + ).toBeVisible() + await expect( + page.getByRole('heading', { name: 'One mark, many dimensions.' }) + ).toBeVisible() + await expect( + page.getByRole('heading', { name: 'Every color earns its place.' }) + ).toBeVisible() + await expect( + page.getByRole('heading', { name: 'Precise, never cute.' }) + ).toBeVisible() + await expect( + page.getByRole('heading', { name: 'Trademark guidelines.' }) + ).toBeVisible() + }) + + test('shows all four marks', async ({ page }) => { + const logos = page.locator('#logos') + for (const name of [ + 'Core Logo', + 'Logomark', + 'Icon', + 'Amplified Logomark' + ]) { + await expect(logos.getByText(name, { exact: true })).toBeVisible() + } + }) + + test('the hero ctas open the gated guidelines and the logo bundle', async ({ + page + }) => { + await expect( + page.getByRole('link', { name: 'View brand guidelines' }) + ).toHaveAttribute('href', BRAND_GUIDELINES_PDF) + await expect( + page.getByRole('link', { name: 'Download logos' }) + ).toHaveAttribute('href', BRAND_ASSETS_ZIP) + }) +}) diff --git a/apps/website/e2e/careers.spec.ts b/apps/website/e2e/careers.spec.ts index 22263c1bfc..95ac837792 100644 --- a/apps/website/e2e/careers.spec.ts +++ b/apps/website/e2e/careers.spec.ts @@ -8,7 +8,7 @@ test.describe('Careers page @smoke', () => { }) test('has correct title', async ({ page }) => { - await expect(page).toHaveTitle('Careers — Comfy') + await expect(page).toHaveTitle('Careers - Comfy') }) test('Roles section heading is visible', async ({ page }) => { @@ -72,7 +72,7 @@ test.describe('Careers page role links', () => { test.describe('Careers page (zh-CN) @smoke', () => { test('renders localized heading and roles', async ({ page }) => { await page.goto('/zh-CN/careers') - await expect(page).toHaveTitle('招聘 — Comfy') + await expect(page).toHaveTitle('招聘 - Comfy') await expect( page.getByRole('heading', { name: '职位', level: 2 }) ).toBeVisible() diff --git a/apps/website/e2e/cloud-nodes.spec.ts b/apps/website/e2e/cloud-nodes.spec.ts index 9f99bf453b..7ecf9224fb 100644 --- a/apps/website/e2e/cloud-nodes.spec.ts +++ b/apps/website/e2e/cloud-nodes.spec.ts @@ -9,7 +9,7 @@ test.describe('Cloud nodes page @smoke', () => { test('has correct title', async ({ page }) => { await expect(page).toHaveTitle( - 'Custom-node packs on Comfy Cloud — supported by default' + 'Custom-node packs on Comfy Cloud - supported by default' ) }) diff --git a/apps/website/e2e/cloud.spec.ts b/apps/website/e2e/cloud.spec.ts index 603c8de974..d704dec30e 100644 --- a/apps/website/e2e/cloud.spec.ts +++ b/apps/website/e2e/cloud.spec.ts @@ -8,7 +8,7 @@ test.describe('Cloud page @smoke', () => { }) test('has correct title', async ({ page }) => { - await expect(page).toHaveTitle('Comfy Cloud — AI in the Cloud') + await expect(page).toHaveTitle('Comfy Cloud - AI in the Cloud') }) test('HeroSection heading and subtitle are visible', async ({ page }) => { diff --git a/apps/website/e2e/download.spec.ts b/apps/website/e2e/download.spec.ts index 3eb896f34b..ccc7515a90 100644 --- a/apps/website/e2e/download.spec.ts +++ b/apps/website/e2e/download.spec.ts @@ -16,7 +16,7 @@ test.describe('Download page @smoke', () => { test('has correct title', async ({ page }) => { await expect(page).toHaveTitle( - 'Download Comfy Desktop — Run AI on Your Hardware' + 'Download Comfy Desktop - Run AI on Your Hardware' ) }) diff --git a/apps/website/e2e/homepage.spec.ts b/apps/website/e2e/homepage.spec.ts index 787027f615..7504a47fe3 100644 --- a/apps/website/e2e/homepage.spec.ts +++ b/apps/website/e2e/homepage.spec.ts @@ -17,7 +17,7 @@ test.describe('Homepage @smoke', () => { }) test('has correct title', async ({ page }) => { - await expect(page).toHaveTitle('Comfy — Professional Control of Visual AI') + await expect(page).toHaveTitle('Comfy - Professional Control of Visual AI') }) test('HeroSection heading is visible', async ({ page }) => { diff --git a/apps/website/e2e/learning.spec.ts b/apps/website/e2e/learning.spec.ts index 7a23ee884e..6165a56bca 100644 --- a/apps/website/e2e/learning.spec.ts +++ b/apps/website/e2e/learning.spec.ts @@ -13,7 +13,7 @@ test.describe('Learning page @smoke', () => { }) test('has correct title', async ({ page }) => { - await expect(page).toHaveTitle('Learning — Comfy') + await expect(page).toHaveTitle('Learning - Comfy') }) test('hero headline references ComfyUI', async ({ page }) => { @@ -137,7 +137,7 @@ test.describe('Learning page (zh-CN) @smoke', () => { test('renders localized title, headings, and tutorials', async ({ page }) => { await page.goto('/zh-CN/learning') - await expect(page).toHaveTitle('学习 — Comfy') + await expect(page).toHaveTitle('学习 - Comfy') await expect(page.getByRole('heading', { level: 1 })).toContainText( /[一-鿿]/ ) diff --git a/apps/website/e2e/mcp.spec.ts b/apps/website/e2e/mcp.spec.ts new file mode 100644 index 0000000000..e1ce31b0cc --- /dev/null +++ b/apps/website/e2e/mcp.spec.ts @@ -0,0 +1,112 @@ +import { expect } from '@playwright/test' + +import { test } from './fixtures/blockExternalMedia' + +const MCP_ENDPOINT = 'https://cloud.comfy.org/mcp' + +test.describe('MCP page @smoke', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/mcp') + }) + + test('hero and how-it-works INSTALL MCP CTAs anchor to setup', async ({ + page + }) => { + const installLinks = page.getByRole('link', { name: 'INSTALL MCP' }) + await expect(installLinks).toHaveCount(2) + for (const link of await installLinks.all()) { + await expect(link).toHaveAttribute('href', '#setup') + } + }) + + test('setup section shows both install options', async ({ page }) => { + const setup = page.locator('#setup') + await setup.scrollIntoViewIfNeeded() + await expect( + setup.getByRole('heading', { + name: 'Ask your agent to install Comfy MCP' + }) + ).toBeVisible() + await expect( + setup.getByRole('heading', { name: 'Install manually' }) + ).toBeVisible() + await expect(setup.getByText(MCP_ENDPOINT, { exact: true })).toBeVisible() + }) + + test('client tabs swap install instructions', async ({ page }) => { + const setup = page.locator('#setup') + await setup.scrollIntoViewIfNeeded() + const activePanel = setup.locator('[role="tabpanel"][data-state="active"]') + + // Claude Code is the default tab and carries the CLI command + await expect( + setup.getByRole('tab', { name: 'Claude Code' }) + ).toHaveAttribute('data-state', 'active') + await expect(activePanel).toContainText( + `claude mcp add --transport http comfy-cloud ${MCP_ENDPOINT}` + ) + + await setup.getByRole('tab', { name: 'Claude Desktop' }).click() + await expect(activePanel).toContainText('Add custom connector') + + await setup.getByRole('tab', { name: 'Cursor' }).click() + await expect(activePanel).toContainText('X-API-Key') + await expect( + activePanel.getByRole('link', { name: 'platform.comfy.org' }) + ).toHaveAttribute('href', 'https://platform.comfy.org/profile/api-keys') + + await setup.getByRole('tab', { name: 'Codex' }).click() + await expect(activePanel).toContainText( + `codex mcp add comfy-cloud --url ${MCP_ENDPOINT}` + ) + }) + + test('skills plugin link lives in the agent option card', async ({ + page + }) => { + const setup = page.locator('#setup') + await setup.scrollIntoViewIfNeeded() + await expect( + setup.getByRole('link', { name: 'View on GitHub' }) + ).toHaveAttribute('href', 'https://github.com/Comfy-Org/comfy-skills') + }) + + test('capabilities section shows all six tool cards', async ({ page }) => { + for (const title of [ + 'Generate anything', + 'Search the ecosystem', + 'Run real workflows', + 'Direct any model', + 'Generate in batches', + 'Ship it as an app' + ]) { + await expect( + page.getByRole('heading', { name: title, exact: true }) + ).toBeVisible() + } + }) + + test('FAQ lists nine questions and autolinks the server URL', async ({ + page + }) => { + const triggers = page.locator('[id^="faq-trigger-"]') + await triggers.first().scrollIntoViewIfNeeded() + await expect(triggers).toHaveCount(9) + + await page.getByRole('button', { name: "What's the server URL?" }).click() + await expect( + page.getByRole('link', { name: MCP_ENDPOINT, exact: true }) + ).toHaveAttribute('href', MCP_ENDPOINT) + }) +}) + +test.describe('MCP page zh-CN @smoke', () => { + test('setup section renders localized options', async ({ page }) => { + await page.goto('/zh-CN/mcp') + const setup = page.locator('#setup') + await setup.scrollIntoViewIfNeeded() + await expect(setup.getByText('方式一')).toBeVisible() + await expect(setup.getByRole('heading', { name: '手动安装' })).toBeVisible() + await expect(setup.getByText(MCP_ENDPOINT, { exact: true })).toBeVisible() + }) +}) diff --git a/apps/website/e2e/payment.spec.ts b/apps/website/e2e/payment.spec.ts index 2a2e8f535c..528b7f839b 100644 --- a/apps/website/e2e/payment.spec.ts +++ b/apps/website/e2e/payment.spec.ts @@ -22,7 +22,7 @@ test.describe('Payment success page @smoke', () => { }) test('has correct title and is noindex', async ({ page }) => { - await expect(page).toHaveTitle('Payment Successful — Comfy') + await expect(page).toHaveTitle('Payment Successful - Comfy') await expectNoIndex(page) }) @@ -54,7 +54,7 @@ test.describe('Payment failed page @smoke', () => { }) test('has correct title and is noindex', async ({ page }) => { - await expect(page).toHaveTitle('Payment Failed — Comfy') + await expect(page).toHaveTitle('Payment Failed - Comfy') await expectNoIndex(page) }) @@ -84,7 +84,7 @@ test.describe('Payment failed page @smoke', () => { test.describe('Payment pages zh-CN @smoke', () => { test('zh-CN success page renders and links correctly', async ({ page }) => { await page.goto('/zh-CN/payment/success') - await expect(page).toHaveTitle('支付成功 — Comfy') + await expect(page).toHaveTitle('支付成功 - Comfy') await expectNoIndex(page) await expect( page.getByRole('heading', { name: '支付成功', level: 1 }) @@ -99,7 +99,7 @@ test.describe('Payment pages zh-CN @smoke', () => { test('zh-CN failed page renders and links correctly', async ({ page }) => { await page.goto('/zh-CN/payment/failed') - await expect(page).toHaveTitle('支付失败 — Comfy') + await expect(page).toHaveTitle('支付失败 - Comfy') await expectNoIndex(page) await expect( page.getByRole('heading', { name: '无法完成支付', level: 1 }) diff --git a/apps/website/public/icons/comfyicon.svg b/apps/website/public/icons/comfyicon.svg new file mode 100644 index 0000000000..a6d3b8dc8c --- /dev/null +++ b/apps/website/public/icons/comfyicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/website/src/components/blocks/FAQSplit01.vue b/apps/website/src/components/blocks/FAQSplit01.vue index 6cc804df82..432dc0d08b 100644 --- a/apps/website/src/components/blocks/FAQSplit01.vue +++ b/apps/website/src/components/blocks/FAQSplit01.vue @@ -1,6 +1,6 @@ - - diff --git a/apps/website/src/components/blocks/FeatureRows01.vue b/apps/website/src/components/blocks/FeatureRows01.vue index c4637cf2f5..246bc08821 100644 --- a/apps/website/src/components/blocks/FeatureRows01.vue +++ b/apps/website/src/components/blocks/FeatureRows01.vue @@ -8,7 +8,7 @@ import VideoPlayer from '../common/VideoPlayer.vue' import type { VideoTrack } from '../common/VideoPlayer.vue' type RowMedia = - | { type: 'image'; src: string; alt?: string } + | { type: 'image'; src: string; alt?: string; fit?: 'cover' | 'contain' } | { type: 'video' src: string @@ -20,6 +20,7 @@ type RowMedia = loop?: boolean minimal?: boolean hideControls?: boolean + fit?: 'cover' | 'contain' } export interface FeatureRow { @@ -58,7 +59,7 @@ const {
+
diff --git a/apps/website/src/components/common/SiteFooter.vue b/apps/website/src/components/common/SiteFooter.vue index 4a6060b4eb..7d04879fbd 100644 --- a/apps/website/src/components/common/SiteFooter.vue +++ b/apps/website/src/components/common/SiteFooter.vue @@ -85,6 +85,7 @@ const companyColumn: { title: string; links: FooterLink[] } = { links: [ { label: t('footer.about', locale), href: routes.about }, { label: t('nav.careers', locale), href: routes.careers }, + { label: t('nav.brand', locale), href: routes.brand }, { label: t('footer.termsOfService', locale), href: routes.termsOfService }, { label: t('footer.enterpriseMsa', locale), href: routes.enterpriseMsa }, { label: t('footer.privacyPolicy', locale), href: routes.privacyPolicy } @@ -175,10 +176,7 @@ const contactColumn: { title: string; links: FooterLink[] } = {
- + diff --git a/apps/website/src/components/common/VideoPlayer.vue b/apps/website/src/components/common/VideoPlayer.vue index a28296068b..b9eccef0ea 100644 --- a/apps/website/src/components/common/VideoPlayer.vue +++ b/apps/website/src/components/common/VideoPlayer.vue @@ -10,6 +10,7 @@ import { whenever } from '@vueuse/core' import { computed, shallowRef, useTemplateRef, watch } from 'vue' +import type { HTMLAttributes } from 'vue' import { t } from '../../i18n/translations' import type { Locale } from '../../i18n/translations' @@ -30,7 +31,9 @@ const { autoplay = false, loop = false, minimal = false, - hideControls = false + hideControls = false, + fit = 'cover', + class: className } = defineProps<{ locale?: Locale src?: string @@ -40,6 +43,8 @@ const { loop?: boolean minimal?: boolean hideControls?: boolean + fit?: 'cover' | 'contain' + class?: HTMLAttributes['class'] }>() const playerEl = useTemplateRef('playerEl') @@ -189,7 +194,12 @@ function toggleFullscreen() {