Skip to content

Commit 1a9ab0f

Browse files
committed
Merge remote-tracking branch 'origin/main' into benjcooley/matrix-detection-proof
2 parents eba097e + 848cd39 commit 1a9ab0f

60 files changed

Lines changed: 1314 additions & 282 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/website/e2e/seedance.spec.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect } from '@playwright/test'
22

3-
import { externalLinks, getRoutes } from '../src/config/routes'
3+
import { getRoutes } from '../src/config/routes'
44
import { creatorReviews } from '../src/data/creatorReviews'
55
import { seedancePage } from '../src/data/seedance'
66
import { t } from '../src/i18n/translations'
@@ -20,7 +20,10 @@ const HERO_PRIMARY_CTA: ModelLaunchCta | undefined =
2020
if (!HERO_PRIMARY_CTA)
2121
throw new Error('seedancePage must configure a hero primary CTA')
2222
const SEEDANCE_RUN: string = HERO_PRIMARY_CTA.href
23-
const CLOUD_WORKFLOWS_HUB = externalLinks.workflows
23+
// The hub's Seedance family page, which lists the shipped 2.5 workflows. The
24+
// CTA used to open the hub root, leaving the reader to find the model they had
25+
// just read about.
26+
const SEEDANCE_HUB_PAGE: string = seedancePage.hero.secondaryCta?.href ?? ''
2427
const PROMPT_CTA = t('seedance.hero.promptCta', 'en')
2528
const COPY_PROMPT = t('modelLaunch.copyPrompt', 'en')
2629
// `faq` is optional on the template (Wan Animate 2 ships without one), but
@@ -106,7 +109,9 @@ test.describe('Seedance 2.5 page — link targets', () => {
106109
await expect(primary).toHaveAttribute('href', /video_wan2_2/)
107110
})
108111

109-
test('the hero run CTA opens the same Cloud workflow', async ({ page }) => {
112+
test('the hero CTAs open Cloud to run and the hub to browse', async ({
113+
page
114+
}) => {
110115
const hero = page.locator('section').filter({
111116
has: page.getByRole('heading', { level: 1, name: HERO_TITLE })
112117
})
@@ -116,7 +121,10 @@ test.describe('Seedance 2.5 page — link targets', () => {
116121

117122
await expect(
118123
hero.getByRole('link', { name: t('seedance.hero.secondaryCta', 'en') })
119-
).toHaveAttribute('href', CLOUD_WORKFLOWS_HUB)
124+
).toHaveAttribute('href', SEEDANCE_HUB_PAGE)
125+
await expect(
126+
hero.getByRole('link', { name: t('seedance.hero.secondaryCta', 'en') })
127+
).toHaveAttribute('href', /\/workflows\/model\/seedance$/)
120128
})
121129

122130
test('renders one step card per configured step', async ({ page }) => {

apps/website/src/data/seedance.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,34 @@ function pageCopy(locale: Locale): { label: string; text: string }[] {
3030
]
3131
}
3232

33+
describe('seedance 2.5 workflow links', () => {
34+
it('sends "try workflows" to the Seedance family page, not the hub root', () => {
35+
// The hub root makes the reader search for the model they just read about.
36+
// The family page lists the shipped 2.5 workflows, which is what the launch
37+
// playbook asks the page to link, and matches what /ltx-2.5 already does.
38+
expect(seedancePage.hero.secondaryCta?.href).toBe(
39+
'https://comfy.org/workflows/model/seedance'
40+
)
41+
})
42+
43+
it('keeps the run CTAs on Cloud, which is a separate deliberate choice', () => {
44+
// Reference-to-video on Cloud is the "run it" path chosen for this launch;
45+
// linking the hub instead would undo that, so it is pinned here.
46+
expect(seedancePage.hero.primaryCta?.href).toContain(
47+
'cloud.comfy.org/?template=api_seedance2_5_r2v'
48+
)
49+
})
50+
51+
it('keeps the draft CTA on a template that costs nothing to run', () => {
52+
// The step above this CTA promises "zero credits" in both locales, so it
53+
// has to open a Cloud template that is not an `api_` one; those bill per
54+
// render.
55+
expect(seedancePage.steps?.primaryCta?.href).toMatch(
56+
/cloud\.comfy\.org\/\?template=(?!api_)/
57+
)
58+
})
59+
})
60+
3361
describe('seedance 2.5 landing copy', () => {
3462
// Offenders are collected rather than asserted one by one so a failure names
3563
// every surface that has to change.

apps/website/src/data/seedance.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ import { externalLinks } from '../config/routes'
1010
// section points at Wan 2.2 instead, which is open source and costs nothing.
1111
const seedanceLinks = {
1212
cloudRun: 'https://cloud.comfy.org/?template=api_seedance2_5_r2v',
13-
freeDraft: 'https://cloud.comfy.org/?template=video_wan2_2_14B_t2v'
13+
freeDraft: 'https://cloud.comfy.org/?template=video_wan2_2_14B_t2v',
14+
// The hub's Seedance family page, which lists the shipped 2.5 workflows
15+
// (text to video, reference to video, first-last frame and the rest). The
16+
// "try workflows" CTA pointed at the hub root, which makes the reader find
17+
// them, the same fix /ltx-2.5 already carries.
18+
hubModel: `${externalLinks.workflows}/model/seedance`
1419
} as const
1520

1621
// Seedance 2.5 renders, encoded to the site's web video profile (VP9 webm,
@@ -196,7 +201,7 @@ export const seedancePage: ModelLaunchPage = {
196201
},
197202
secondaryCta: {
198203
labelKey: 'seedance.hero.secondaryCta',
199-
href: externalLinks.workflows,
204+
href: seedanceLinks.hubModel,
200205
target: '_blank'
201206
}
202207
},

browser_tests/fixtures/workspaceSwitcherFixture.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import type { WorkspaceTokenResponse } from '@/platform/workspace/stores/workspaceAuthStore'
22

33
import { comfyPageFixture } from '@e2e/fixtures/ComfyPage'
4+
import {
5+
EMPTY_BILLING_BALANCE,
6+
EMPTY_BILLING_PLANS,
7+
ENDED_STANDARD_BILLING_STATUS
8+
} from '@e2e/fixtures/data/cloudWorkspace'
49
import {
510
WORKSPACE_SWITCHER_REMOTE_CONFIG,
611
WORKSPACE_SWITCHER_WORKSPACES
@@ -51,6 +56,15 @@ export const workspaceSwitcherTest = comfyPageFixture.extend({
5156
await page.route('**/api/auth/session', (route) =>
5257
route.fulfill({ status: 204 })
5358
)
59+
await page.route('**/api/billing/status', (route) =>
60+
route.fulfill(jsonRoute(ENDED_STANDARD_BILLING_STATUS))
61+
)
62+
await page.route('**/api/billing/balance', (route) =>
63+
route.fulfill(jsonRoute(EMPTY_BILLING_BALANCE))
64+
)
65+
await page.route('**/api/billing/plans', (route) =>
66+
route.fulfill(jsonRoute(EMPTY_BILLING_PLANS))
67+
)
5468

5569
await use(page)
5670
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { expect } from '@playwright/test'
2+
3+
import {
4+
PERSONAL_WORKSPACE_NAME,
5+
TEAM_WORKSPACE_NAME
6+
} from '@e2e/fixtures/data/workspaceSwitcher'
7+
import { workspaceSwitcherTest as test } from '@e2e/fixtures/workspaceSwitcherFixture'
8+
9+
test.describe('Local workspace switcher', { tag: '@auth' }, () => {
10+
test.describe.configure({ timeout: 60_000 })
11+
12+
test('switches the active workspace without reloading or losing the workflow', async ({
13+
comfyPage
14+
}) => {
15+
const page = comfyPage.page
16+
const localOrigin = new URL(page.url()).origin
17+
const billingRequestUrls: string[] = []
18+
const localBillingRequestUrls: string[] = []
19+
const billingAuthorizationHeaders: string[] = []
20+
let tokenRequestBody: unknown
21+
22+
page.on('request', (request) => {
23+
if (!request.url().includes('/api/billing/')) return
24+
billingRequestUrls.push(request.url())
25+
billingAuthorizationHeaders.push(request.headers().authorization ?? '')
26+
if (new URL(request.url()).origin === localOrigin) {
27+
localBillingRequestUrls.push(request.url())
28+
}
29+
})
30+
page.on('request', (request) => {
31+
if (
32+
request.method() === 'POST' &&
33+
request.url().endsWith('/api/auth/token')
34+
) {
35+
tokenRequestBody = request.postDataJSON()
36+
}
37+
})
38+
39+
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
40+
await comfyPage.settings.setSetting(
41+
'Comfy.Workflow.WorkflowTabsPosition',
42+
'Topbar'
43+
)
44+
const workflowName = `local-workspace-${Date.now().toString(36)}`
45+
await comfyPage.menu.topbar.saveWorkflow(workflowName)
46+
await page.evaluate(() => {
47+
document.body.dataset.workspaceSwitchDocument = 'original'
48+
})
49+
50+
await page.getByRole('button', { name: 'Current user' }).click()
51+
await expect(page.getByTestId('workspace-switcher-trigger')).toContainText(
52+
PERSONAL_WORKSPACE_NAME
53+
)
54+
await page.getByTestId('workspace-switcher-trigger').click()
55+
56+
const panel = page.getByTestId('workspace-switcher-panel')
57+
await expect(panel.getByText(PERSONAL_WORKSPACE_NAME)).toBeVisible()
58+
await expect(panel.getByText(TEAM_WORKSPACE_NAME)).toBeVisible()
59+
await expect(panel.getByText('Owner')).toHaveCount(2)
60+
await expect(panel.getByText('Member')).toHaveCount(1)
61+
const scopeCaption = panel.getByText(
62+
'Workspaces only affect which credits you use.'
63+
)
64+
await expect(scopeCaption).toBeVisible()
65+
await scopeCaption.locator('..').locator('.pi-info-circle').hover()
66+
await expect(page.getByRole('tooltip')).toHaveText(
67+
'Runs that use partner nodes spend credits from this workspace. Unlike on Cloud, every workspace saves to your usual output folder.'
68+
)
69+
70+
await panel.getByText(TEAM_WORKSPACE_NAME, { exact: true }).click()
71+
72+
await expect(page.getByTestId('workspace-switcher-trigger')).toContainText(
73+
TEAM_WORKSPACE_NAME
74+
)
75+
await expect.poll(() => billingRequestUrls.length).toBeGreaterThan(0)
76+
expect(tokenRequestBody).toEqual({ workspace_id: 'ws-team' })
77+
expect(billingRequestUrls).toEqual(
78+
expect.arrayContaining([
79+
expect.stringMatching(
80+
/^https:\/\/testcloud\.comfy\.org\/api\/billing\//
81+
)
82+
])
83+
)
84+
expect(localBillingRequestUrls).toEqual([])
85+
await expect
86+
.poll(() => billingAuthorizationHeaders)
87+
.toContain('Bearer mock-workspace-token-ws-team')
88+
await expect
89+
.poll(() => comfyPage.menu.topbar.getTabNames())
90+
.toContain(workflowName)
91+
expect(
92+
await page.evaluate(() => document.body.dataset.workspaceSwitchDocument)
93+
).toBe('original')
94+
})
95+
})

browser_tests/tests/workspaceSwitcher.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ test.describe('Workspace switcher', { tag: '@cloud' }, () => {
4242

4343
const panel = page.getByTestId('workspace-switcher-panel')
4444
await expect(panel).toBeVisible()
45+
await expect(
46+
panel.getByText('Workspaces only affect which credits you use.')
47+
).toHaveCount(0)
4548

4649
const profileMenu = page.locator('.current-user-popover')
4750
const panelBox = await panel.boundingBox()

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@comfyorg/comfyui-frontend",
3-
"version": "1.52.2",
3+
"version": "1.52.3",
44
"private": true,
55
"description": "Official front-end implementation of ComfyUI",
66
"homepage": "https://comfy.org",

src/components/topbar/CurrentUserButton.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,4 +217,25 @@ describe('CurrentUserButton', () => {
217217
expect(screen.getByText('WorkspaceProfilePic')).toBeInTheDocument()
218218
expect(screen.queryByText('Avatar')).not.toBeInTheDocument()
219219
})
220+
221+
it('shows WorkspaceProfilePic for an active local team workspace', () => {
222+
mockTeamWorkspaceStore.initState.value = 'ready'
223+
mockTeamWorkspaceStore.isInPersonalWorkspace.value = false
224+
mockTeamWorkspaceStore.workspaceName.value = 'My Team'
225+
226+
renderComponent()
227+
228+
expect(screen.getByText('WorkspaceProfilePic')).toBeInTheDocument()
229+
expect(screen.queryByText('Avatar')).not.toBeInTheDocument()
230+
})
231+
232+
it('shows workspace actions after local workspace initialization', async () => {
233+
mockTeamWorkspaceStore.initState.value = 'ready'
234+
const { user } = renderComponent()
235+
236+
await user.click(screen.getByRole('button', { name: 'Current user' }))
237+
238+
expect(screen.getByText('Workspace Popover Content')).toBeInTheDocument()
239+
expect(screen.queryByText('Popover Content')).not.toBeInTheDocument()
240+
})
220241
})

src/components/topbar/CurrentUserButton.vue

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
@show="onPopoverShow"
5050
>
5151
<CurrentUserPopoverWorkspace
52-
v-if="isCloud"
52+
v-if="showWorkspacePopover"
5353
ref="workspacePopoverContent"
5454
:account-actions-only="initState !== 'ready'"
5555
@close="closePopover"
@@ -101,7 +101,10 @@ const showWorkspaceSkeleton = computed(
101101
() => isCloud && initState.value === 'loading'
102102
)
103103
const showWorkspaceIcon = computed(
104-
() => isCloud && initState.value === 'ready' && !isInPersonalWorkspace.value
104+
() => initState.value === 'ready' && !isInPersonalWorkspace.value
105+
)
106+
const showWorkspacePopover = computed(
107+
() => isCloud || initState.value === 'ready'
105108
)
106109
107110
const workspaceName = computed(() => {

0 commit comments

Comments
 (0)