Compare commits

...

20 Commits

Author SHA1 Message Date
GitHub Action
6949044d65 [automated] Apply ESLint and Oxfmt fixes 2026-05-02 21:27:40 +00:00
bymyself
828ba48ba6 fix(test): use dispatchEvent in openUserPopover to bypass backdrop blocking 2026-05-02 21:24:18 +00:00
bymyself
b2a445ed73 fix(test): wait for currentUserButton after reload before yielding to test body 2026-05-02 21:12:03 +00:00
bymyself
19cc62a79c fix(test): replace waitFor+catch with targeted expect to address flakiness concern
Per reviewer feedback, the previous waitFor+try/catch pattern swallows all
errors rather than just timeout errors, hiding genuine test failures.

Replace with expect().toBeVisible().then().catch() that only discards
TimeoutError, re-throwing any other unexpected exception. This preserves the
"dismiss if present" semantics needed for the asynchronous subscription dialog
while making error handling explicit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 20:57:03 +00:00
bymyself
a879e63c77 fix(test): use specific locator to avoid strict-mode violation in cloud tests
getByRole('dialog') matched 2 elements when the GlobalDialog stack had both
a subscription-required dialog and a second dialog open simultaneously.
Use page.locator('[aria-labelledby="subscription-required"]') to precisely
target the subscription dialog by its GlobalDialog key in all three usages
(dismissSubscriptionDialogIfOpen, SubscribeToRun click test, user popover
subscribe click test, and Cleanup prevents stale state test).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 20:56:00 +00:00
bymyself
009ea1c054 fix(test): use dispatchEvent and disable subscription extension
oxlint forbids click({ force: true }). Use dispatchEvent('click')
instead to bypass the PrimeVue Dialog backdrop that briefly intercepts
pointer events between the popover button click and the dialog opening.
The button is already known-visible from openUserPopover, so a
synthetic click is safe here.

Also disable the Comfy.Cloud.Subscription extension via setupSettings
so its requireActiveSubscription watcher does not auto-open the
subscription dialog on app boot. The dialog mask would otherwise
intercept pointer events on every topbar button these tests interact
with. Dialog dismissal is kept as defense-in-depth in case a different
code path surfaces it.
2026-05-01 23:02:24 -07:00
bymyself
5355773e53 fix(test): dismiss auto-opened subscription modal in unsubscribed tests
With `subscription_required` enabled and an unsubscribed status, the
cloud subscription extension auto-opens `SubscriptionRequiredDialogContent`
on app boot. Its PrimeVue overlay mask intercepts pointer events on the
topbar, blocking clicks on the buttons under test.

Add `dismissSubscriptionDialogIfOpen` and call it after the post-mock
`reload()` so subsequent assertions can interact with the topbar
`subscribe-to-run-button` and user popover. Tests that exercise the
button -> dialog flow still verify it explicitly by clicking the button
and asserting the dialog re-appears.
2026-05-01 21:49:50 -07:00
bymyself
c2ddda0eb7 fix(test): mock /api/features so subscription_required survives boot
In the cloud build, `main.ts` awaits `refreshRemoteConfig({ useAuth: false })`
which fetches `/api/features` and assigns the result *wholesale* to
`window.__CONFIG__` (it does not merge). That means the previous
`addInitScript` setting `subscription_required: true` was clobbered
by the boot fetch, leaving the flag undefined when
`ComfyRunButton/index.ts` evaluated and `SubscribeToRun` was
tree-shaken away.

Add a route handler for `**/api/features` returning
`{ subscription_required: true }` so the flag survives the
boot-time refresh. The init-script remains as a defense-in-depth
fallback for any code path that reads `__CONFIG__` before the
fetch resolves.
2026-05-01 21:33:23 -07:00
bymyself
a47a8949ac fix(test): repair cloud playwright fixture for unsubscribed scenario
Two CI failures on the cloud project for the unsubscribed describe block
had a single root cause and a related selector bug:

1. `CloudRunButtonWrapper` (which renders `SubscribeToRun`) is gated
   on `isCloud && window.__CONFIG__?.subscription_required` at module
   evaluation time. The previous auto-fixture installed
   `addInitScript` for that flag, but Playwright does not guarantee
   ordering between auto fixtures and `comfyPage` when neither
   declares a dependency. In practice the script was applied after the
   first navigation, so `subscription_required` was undefined when
   `ComfyRunButton/index.ts` evaluated and the queue button was
   imported instead — causing all `subscribe-to-run-button`
   assertions to time out.

   Make `subscriptionHelper` an auto fixture that depends on
   `comfyPage`, install mocks, then `page.reload()` so route mocks
   and the init script are both in place when modules re-evaluate.

2. With `subscription_required` enabled and an unsubscribed status,
   the `Comfy.Cloud.Subscription` extension auto-opens the
   subscription-required modal, blocking pointer events on the topbar
   buttons under test. Disable the extension via
   `Comfy.Extension.Disabled` for these tests; we still test the
   button -> dialog flow by clicking explicitly.

3. Use the new `current-user-button` testid for the topbar trigger
   instead of `current-user-indicator`, which actually belongs to
   `CurrentUserMessage.vue` in the settings dialog.
2026-05-01 21:22:12 -07:00
bymyself
ee88893b1b fix: add data-testid to topbar CurrentUserButton
The subscription E2E tests targeted 'current-user-indicator', which is
the testid on CurrentUserMessage.vue (settings-dialog content), not the
topbar user button. Add a dedicated 'current-user-button' testid for
the topbar trigger so popover-based tests can locate it reliably.
2026-05-01 21:21:14 -07:00
Glary-Bot
747bbf2c2c fix: wait for popover visibility in openUserPopover helper 2026-05-01 21:16:22 -07:00
Glary-Bot
262b74954f refactor: remove redundant inline comments per review feedback 2026-05-01 21:16:22 -07:00
Glary-Bot
618277384e refactor: extract popover interaction helpers to reduce duplication 2026-05-01 21:16:22 -07:00
Glary-Bot
dc634279a1 refactor: replace CSS class locator with data-testid for popover
Add data-testid='current-user-popover' to both popover components
and use TestIds.user.currentUserPopover in tests instead of
locator('.current-user-popover').
2026-05-01 21:16:22 -07:00
Glary-Bot
e1334b1955 fix: remove all manual expect timeouts per review feedback
Default expect timeout (5s) is sufficient. The cloud project's 15s
test timeout handles overall duration.
2026-05-01 21:16:22 -07:00
Glary-Bot
d0e9a5867f fix: remove @mobile tag from responsive test to avoid non-cloud project
The @mobile tag causes the test to also run in the mobile-chrome
Playwright project which lacks cloud auth setup. Without cloud mode,
isActiveSubscription defaults to true and SubscribeToRun never renders.
Use setViewportSize instead to test the responsive label.
2026-05-01 21:16:22 -07:00
Glary-Bot
6c72a9d711 refactor: address CodeRabbit review feedback
- Add subscribeToRunButton to TestIds and use throughout spec
- Import PENDING_SUBSCRIPTION_CHECKOUT_STORAGE_KEY from source
2026-05-01 21:16:22 -07:00
Glary-Bot
f87fc5ec9a fix: address code review — mock lifecycle and pending checkout seeding
- Use auto-fixture factory (createSubscriptionTest) to install
  subscription mocks BEFORE comfyPage's first navigation
- Add seedPendingCheckout() and triggerSubscriptionRefetch() to
  SubscriptionHelper for state transition tests
- Restructure tests by initial subscription state to avoid
  double-setup anti-pattern
2026-05-01 21:15:47 -07:00
Glary-Bot
8360110933 fix: address review feedback for subscription E2E tests
- Scope popover subscribe button selectors to .current-user-popover element
- Target subscription dialog via aria-labelledby instead of generic role=dialog
- Widen checkout route mock to cover tiered checkout paths

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-01 21:15:47 -07:00
Glary-Bot
afed5e98e8 test: add subscription E2E Playwright test fixtures and specs
Add typed fixture factories, stateful route-mocking helper, and 10 @cloud test cases for subscription UI components (SubscribeToRun, TopbarSubscribeButton, user popover subscribe flow).

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-01 21:15:47 -07:00
7 changed files with 605 additions and 1 deletions

View File

@@ -0,0 +1,90 @@
import type { operations } from '@comfyorg/registry-types'
export type SubscriptionStatusResponse =
operations['GetCloudSubscriptionStatus']['responses']['200']['content']['application/json']
export type BalanceResponse =
operations['GetCustomerBalance']['responses']['200']['content']['application/json']
export function createSubscriptionStatus(
overrides: Partial<SubscriptionStatusResponse> = {}
): SubscriptionStatusResponse {
return {
is_active: false,
subscription_id: null,
subscription_tier: 'FREE',
subscription_duration: null,
has_fund: false,
renewal_date: null,
end_date: null,
...overrides
}
}
export function createBalance(
overrides: Partial<BalanceResponse> = {}
): BalanceResponse {
return {
amount_micros: 0,
prepaid_balance_micros: 0,
cloud_credit_balance_micros: 0,
pending_charges_micros: 0,
effective_balance_micros: 0,
currency: 'USD',
...overrides
}
}
export const UNSUBSCRIBED: SubscriptionStatusResponse =
createSubscriptionStatus({
is_active: false,
subscription_id: null,
subscription_tier: 'FREE',
end_date: null
})
export const FREE_TIER_ACTIVE: SubscriptionStatusResponse =
createSubscriptionStatus({
is_active: true,
subscription_id: 'sub_free_001',
subscription_tier: 'FREE',
renewal_date: '2099-12-31T00:00:00.000Z'
})
export const CREATOR_ACTIVE: SubscriptionStatusResponse =
createSubscriptionStatus({
is_active: true,
subscription_id: 'sub_creator_001',
subscription_tier: 'CREATOR',
subscription_duration: 'MONTHLY',
renewal_date: '2099-12-31T00:00:00.000Z'
})
export const PRO_ACTIVE: SubscriptionStatusResponse = createSubscriptionStatus({
is_active: true,
subscription_id: 'sub_pro_001',
subscription_tier: 'PRO',
subscription_duration: 'MONTHLY',
renewal_date: '2099-12-31T00:00:00.000Z'
})
export const CANCELLED_SUBSCRIPTION: SubscriptionStatusResponse =
createSubscriptionStatus({
is_active: true,
subscription_id: 'sub_cancelled_001',
subscription_tier: 'CREATOR',
subscription_duration: 'MONTHLY',
end_date: '2099-12-31T00:00:00.000Z'
})
export const ZERO_BALANCE: BalanceResponse = createBalance({
amount_micros: 0,
effective_balance_micros: 0
})
export const FUNDED_BALANCE: BalanceResponse = createBalance({
amount_micros: 2_500_000,
prepaid_balance_micros: 1_000_000,
cloud_credit_balance_micros: 1_500_000,
effective_balance_micros: 2_500_000
})

View File

@@ -0,0 +1,246 @@
import type { Page, Route } from '@playwright/test'
import { PENDING_SUBSCRIPTION_CHECKOUT_STORAGE_KEY } from '@/platform/cloud/subscription/utils/subscriptionCheckoutTracker'
import {
createBalance,
createSubscriptionStatus,
UNSUBSCRIBED,
ZERO_BALANCE
} from '@e2e/fixtures/data/subscriptionFixtures'
import type {
BalanceResponse,
SubscriptionStatusResponse
} from '@e2e/fixtures/data/subscriptionFixtures'
export interface SubscriptionConfig {
status: SubscriptionStatusResponse
balance: BalanceResponse
}
function emptyConfig(): SubscriptionConfig {
return {
status: createSubscriptionStatus(UNSUBSCRIBED),
balance: createBalance(ZERO_BALANCE)
}
}
export type SubscriptionOperator = (
config: SubscriptionConfig
) => SubscriptionConfig
export function withSubscriptionStatus(
overrides: Partial<SubscriptionStatusResponse>
): SubscriptionOperator {
return (config) => ({
...config,
status: { ...config.status, ...overrides }
})
}
export function withBalance(
overrides: Partial<BalanceResponse>
): SubscriptionOperator {
return (config) => ({
...config,
balance: { ...config.balance, ...overrides }
})
}
export function withActiveSubscription(
tier: NonNullable<SubscriptionStatusResponse['subscription_tier']> = 'CREATOR'
): SubscriptionOperator {
return withSubscriptionStatus({
is_active: true,
subscription_tier: tier,
renewal_date: '2099-12-31T00:00:00.000Z',
end_date: null
})
}
export function withFreeTier(): SubscriptionOperator {
return withSubscriptionStatus({
is_active: true,
subscription_tier: 'FREE',
end_date: null
})
}
export function withCancelledSubscription(): SubscriptionOperator {
return withSubscriptionStatus({
is_active: true,
subscription_tier: 'CREATOR',
end_date: '2099-12-31T00:00:00.000Z'
})
}
export function withUnsubscribed(): SubscriptionOperator {
return withSubscriptionStatus({
is_active: false,
subscription_tier: 'FREE',
end_date: null,
renewal_date: null
})
}
export function withCredits(amountMicros: number): SubscriptionOperator {
return withBalance({
amount_micros: amountMicros,
effective_balance_micros: amountMicros
})
}
export class SubscriptionHelper {
private statusResponse: SubscriptionStatusResponse
private balanceResponse: BalanceResponse
private routeHandlers: Array<{
pattern: string
handler: (route: Route) => Promise<void>
}> = []
constructor(
private readonly page: Page,
config: SubscriptionConfig = emptyConfig()
) {
this.statusResponse = createSubscriptionStatus(config.status)
this.balanceResponse = createBalance(config.balance)
}
async mock(): Promise<void> {
await this.page.addInitScript(() => {
window.__CONFIG__ = {
...window.__CONFIG__,
subscription_required: true
}
})
// The cloud build calls `/api/features` at boot via `refreshRemoteConfig`,
// which overwrites `window.__CONFIG__` wholesale. Mock it to preserve
// `subscription_required: true` after that fetch resolves.
const featuresPattern = '**/api/features'
const featuresHandler = async (route: Route) => {
await route.fulfill({ json: { subscription_required: true } })
}
this.routeHandlers.push({
pattern: featuresPattern,
handler: featuresHandler
})
await this.page.route(featuresPattern, featuresHandler)
const statusPattern = '**/customers/cloud-subscription-status'
const statusHandler = async (route: Route) => {
await route.fulfill({ json: this.statusResponse })
}
this.routeHandlers.push({ pattern: statusPattern, handler: statusHandler })
await this.page.route(statusPattern, statusHandler)
const balancePattern = '**/customers/balance'
const balanceHandler = async (route: Route) => {
await route.fulfill({ json: this.balanceResponse })
}
this.routeHandlers.push({
pattern: balancePattern,
handler: balanceHandler
})
await this.page.route(balancePattern, balanceHandler)
const checkoutPattern = '**/customers/cloud-subscription-checkout**'
const checkoutHandler = async (route: Route) => {
await route.fulfill({
json: { checkout_url: 'https://checkout.stripe.com/mock' }
})
}
this.routeHandlers.push({
pattern: checkoutPattern,
handler: checkoutHandler
})
await this.page.route(checkoutPattern, checkoutHandler)
}
configure(...operators: SubscriptionOperator[]): void {
const base: SubscriptionConfig = {
status: createSubscriptionStatus(this.statusResponse),
balance: createBalance(this.balanceResponse)
}
const config = operators.reduce<SubscriptionConfig>(
(cfg, op) => op(cfg),
base
)
this.statusResponse = createSubscriptionStatus(config.status)
this.balanceResponse = createBalance(config.balance)
}
setStatus(overrides: Partial<SubscriptionStatusResponse>): void {
this.statusResponse = {
...this.statusResponse,
...overrides
}
}
setBalance(overrides: Partial<BalanceResponse>): void {
this.balanceResponse = {
...this.balanceResponse,
...overrides
}
}
/**
* Seed localStorage with a pending checkout attempt.
* Required for `visibilitychange` to trigger a subscription re-fetch,
* because `recoverPendingSubscriptionCheckout` checks
* `hasPendingSubscriptionCheckoutAttempt()` before fetching.
*
* Call AFTER page navigation (localStorage needs a page context).
*/
async seedPendingCheckout(
tier: string = 'standard',
cycle: string = 'monthly'
): Promise<void> {
const storageKey = PENDING_SUBSCRIPTION_CHECKOUT_STORAGE_KEY
await this.page.evaluate(
([key, t, c]) => {
localStorage.setItem(
key,
JSON.stringify({
attempt_id: `test-${Date.now()}`,
started_at_ms: Date.now(),
tier: t,
cycle: c,
checkout_type: 'new'
})
)
},
[storageKey, tier, cycle] as const
)
}
/**
* Dispatch `visibilitychange` to trigger pending-checkout recovery.
* The app listens for this event and re-fetches subscription status
* when a pending checkout attempt exists in localStorage.
*/
async triggerSubscriptionRefetch(): Promise<void> {
await this.page.evaluate(() => {
document.dispatchEvent(new Event('visibilitychange'))
})
}
async clearMocks(): Promise<void> {
for (const { pattern, handler } of this.routeHandlers) {
await this.page.unroute(pattern, handler)
}
this.routeHandlers = []
this.statusResponse = createSubscriptionStatus(UNSUBSCRIBED)
this.balanceResponse = createBalance(ZERO_BALANCE)
}
}
export function createSubscriptionHelper(
page: Page,
...operators: SubscriptionOperator[]
): SubscriptionHelper {
const config = operators.reduce<SubscriptionConfig>(
(cfg, op) => op(cfg),
emptyConfig()
)
return new SubscriptionHelper(page, config)
}

View File

@@ -87,6 +87,7 @@ export const TestIds = {
queueModeMenuTrigger: 'queue-mode-menu-trigger',
saveButton: 'save-workflow-button',
subscribeButton: 'topbar-subscribe-button',
subscribeToRunButton: 'subscribe-to-run-button',
loginButton: 'login-button',
loginButtonPopover: 'login-button-popover',
loginButtonPopoverLearnMore: 'login-button-popover-learn-more',
@@ -206,7 +207,9 @@ export const TestIds = {
workflowCard: (id: string) => `template-workflow-${id}`
},
user: {
currentUserIndicator: 'current-user-indicator'
currentUserButton: 'current-user-button',
currentUserIndicator: 'current-user-indicator',
currentUserPopover: 'current-user-popover'
},
queue: {
overlayToggle: 'queue-overlay-toggle',

View File

@@ -0,0 +1,262 @@
import { expect } from '@playwright/test'
import { comfyPageFixture } from '@e2e/fixtures/ComfyPage'
import { TestIds } from '@e2e/fixtures/selectors'
import {
createSubscriptionHelper,
withActiveSubscription,
withFreeTier,
withUnsubscribed
} from '@e2e/fixtures/helpers/SubscriptionHelper'
import type { Locator, Page } from '@playwright/test'
import type { SubscriptionHelper } from '@e2e/fixtures/helpers/SubscriptionHelper'
async function openUserPopover(page: Page): Promise<Locator> {
// Use dispatchEvent instead of click() to bypass Playwright's actionability
// check — in the cloud environment a subscription dialog backdrop can be
// present during initial page load and would block a standard click.
await page.getByTestId(TestIds.user.currentUserButton).dispatchEvent('click')
const popover = page.getByTestId(TestIds.user.currentUserPopover)
await expect(popover).toBeVisible()
return popover
}
async function clickPopoverSubscribe(page: Page): Promise<void> {
const popover = await openUserPopover(page)
// Use dispatchEvent instead of click() because the click opens the
// subscription dialog whose backdrop appears mid-action; Playwright's
// actionability re-check would otherwise see the mask intercepting and
// retry until timeout. The button is already known-visible from
// openUserPopover, so dispatching a synthetic click is safe here.
await popover
.getByRole('button', { name: /subscribe/i })
.first()
.dispatchEvent('click')
}
// Closes the auto-opened subscription-required dialog if present.
// Polls briefly because the dialog opens asynchronously after the
// `isLoggedIn` watcher fires on app boot.
async function dismissSubscriptionDialogIfOpen(page: Page): Promise<void> {
// Target only the subscription-required dialog by its known aria-labelledby
// key — avoids strict-mode violations when multiple GlobalDialog items are
// on the stack simultaneously.
const dialog = page.locator('[aria-labelledby="subscription-required"]')
// Use expect with a short timeout: this is intentionally a "dismiss if open"
// helper, so absence of the dialog (TimeoutError) is not a failure — we
// discard only the timeout error, not any other unexpected exception.
const appeared = await expect(dialog)
.toBeVisible({ timeout: 2000 })
.then(() => true)
.catch((e: Error) => {
if (e.message.includes('Timeout')) return false
throw e
})
if (!appeared) return
const closeButton = dialog.getByRole('button', { name: /close/i }).first()
if (await closeButton.isVisible()) {
await closeButton.click()
} else {
await page.keyboard.press('Escape')
}
await expect(dialog).toBeHidden()
}
// Installs subscription mocks AFTER comfyPage.setup() and reloads the page
// so `addInitScript` (which sets `window.__CONFIG__.subscription_required`)
// applies before module-level reads in `ComfyRunButton/index.ts` evaluate.
// Depending on `comfyPage` here forces ordering: comfyPage's auth + setup
// runs first, then mocks are installed, then the page reloads with the
// mocked config + intercepted endpoints in place.
function createSubscriptionTest(
...defaultOps: Parameters<typeof createSubscriptionHelper>[1][]
) {
return comfyPageFixture.extend<{
subscriptionHelper: SubscriptionHelper
}>({
subscriptionHelper: [
async ({ comfyPage }, use) => {
const helper = createSubscriptionHelper(comfyPage.page, ...defaultOps)
await helper.mock()
// Disable the cloud-subscription extension so its `requireActive
// Subscription` watcher doesn't auto-open the subscription dialog
// on app boot. The PrimeVue Dialog mask would otherwise intercept
// pointer events on every topbar button these tests interact with.
await comfyPage.setupSettings({
'Comfy.Extension.Disabled': ['Comfy.Cloud.Subscription']
})
await comfyPage.page.reload()
// Wait for Firebase auth to resolve: the user button is v-if="isLoggedIn"
// and only renders after onAuthStateChanged fires with the mock user from
// IndexedDB. waitForAppReady() does not wait for this — Firebase resolves
// asynchronously after app boot. Waiting here ensures the button is
// present before any test body tries to click it.
await expect(
comfyPage.page.getByTestId(TestIds.user.currentUserButton)
).toBeVisible()
// Defense-in-depth: if the dialog still surfaces (e.g. via a
// different code path), dismiss it before the test runs.
await dismissSubscriptionDialogIfOpen(comfyPage.page)
await use(helper)
await helper.clearMocks()
},
{ auto: true }
]
})
}
const unsubscribedTest = createSubscriptionTest(withUnsubscribed())
const subscribedTest = createSubscriptionTest(withActiveSubscription('CREATOR'))
const freeTierTest = createSubscriptionTest(withFreeTier())
unsubscribedTest.describe(
'Subscription buttons — unsubscribed',
{ tag: '@cloud' },
() => {
unsubscribedTest(
'SubscribeToRun visible when unsubscribed',
async ({ comfyPage }) => {
await expect(
comfyPage.page.getByTestId(TestIds.topbar.subscribeToRunButton)
).toBeVisible()
}
)
unsubscribedTest(
'SubscribeToRun click opens subscription dialog',
async ({ comfyPage }) => {
await comfyPage.page
.getByTestId(TestIds.topbar.subscribeToRunButton)
.click()
await expect(
comfyPage.page.locator('[aria-labelledby="subscription-required"]')
).toBeVisible()
}
)
unsubscribedTest(
'SubscribeToRun shows short label at narrow viewport',
async ({ comfyPage }) => {
await comfyPage.page.setViewportSize({ width: 393, height: 851 })
const btn = comfyPage.page.getByTestId(
TestIds.topbar.subscribeToRunButton
)
await expect(btn).toBeVisible()
await expect(btn).not.toContainText(/to run/i)
}
)
unsubscribedTest(
'User popover shows subscribe when unsubscribed',
async ({ comfyPage }) => {
const popover = await openUserPopover(comfyPage.page)
await expect(
popover.getByRole('button', { name: /subscribe/i })
).toBeVisible()
}
)
unsubscribedTest(
'User popover subscribe click opens dialog',
async ({ comfyPage }) => {
await clickPopoverSubscribe(comfyPage.page)
await expect(
comfyPage.page.locator('[aria-labelledby="subscription-required"]')
).toBeVisible()
}
)
unsubscribedTest(
'Subscription state transition updates UI after re-fetch',
async ({ comfyPage, subscriptionHelper }) => {
await expect(
comfyPage.page.getByTestId(TestIds.topbar.subscribeToRunButton)
).toBeVisible()
// Simulate returning from Stripe checkout: seed pending checkout,
// mutate mock to return active subscription, trigger re-fetch.
await subscriptionHelper.seedPendingCheckout('standard', 'monthly')
subscriptionHelper.setStatus({
is_active: true,
subscription_tier: 'STANDARD',
subscription_duration: 'MONTHLY'
})
await subscriptionHelper.triggerSubscriptionRefetch()
await expect(
comfyPage.page.getByTestId(TestIds.topbar.subscribeToRunButton)
).toBeHidden()
}
)
unsubscribedTest(
'Cleanup prevents stale subscription state after dialog close',
async ({ comfyPage, subscriptionHelper }) => {
await clickPopoverSubscribe(comfyPage.page)
// Use the aria-labelledby key to target only the subscription dialog —
// avoids strict-mode violations when a second GlobalDialog is stacked.
const dialog = comfyPage.page.locator(
'[aria-labelledby="subscription-required"]'
)
await expect(dialog).toBeVisible()
await dialog.getByRole('button', { name: /close/i }).first().click()
await expect(dialog).toBeHidden()
await subscriptionHelper.seedPendingCheckout('standard', 'monthly')
subscriptionHelper.setStatus({
is_active: true,
subscription_tier: 'STANDARD',
subscription_duration: 'MONTHLY'
})
await subscriptionHelper.triggerSubscriptionRefetch()
await expect(dialog).toBeHidden()
}
)
}
)
subscribedTest.describe(
'Subscription buttons — subscribed',
{ tag: '@cloud' },
() => {
subscribedTest(
'SubscribeToRun hidden when subscribed',
async ({ comfyPage }) => {
await expect(
comfyPage.page.getByTestId(TestIds.topbar.queueButton)
).toBeVisible()
await expect(
comfyPage.page.getByTestId(TestIds.topbar.subscribeToRunButton)
).toBeHidden()
}
)
subscribedTest(
'Topbar subscribe button hidden for paid tier',
async ({ comfyPage }) => {
await expect(
comfyPage.page.getByTestId(TestIds.topbar.queueButton)
).toBeVisible()
await expect(
comfyPage.page.getByTestId(TestIds.topbar.subscribeButton)
).toBeHidden()
}
)
}
)
freeTierTest.describe(
'Subscription buttons — free tier',
{ tag: '@cloud' },
() => {
freeTierTest(
'Topbar subscribe button visible for free tier',
async ({ comfyPage }) => {
await expect(
comfyPage.page.getByTestId(TestIds.topbar.subscribeButton)
).toBeVisible()
}
)
}
)

View File

@@ -6,6 +6,7 @@
class="p-1 hover:bg-transparent"
variant="muted-textonly"
:aria-label="$t('g.currentUser')"
data-testid="current-user-button"
@click="popover?.toggle($event)"
>
<div

View File

@@ -1,6 +1,7 @@
<!-- A popover that shows current user information and actions -->
<template>
<div
data-testid="current-user-popover"
class="current-user-popover -m-3 w-80 rounded-lg border border-border-default bg-base-background p-2 shadow-[1px_1px_8px_0_rgba(0,0,0,0.4)]"
>
<!-- User Info Section -->

View File

@@ -1,6 +1,7 @@
<!-- A popover that shows current user information and actions -->
<template>
<div
data-testid="current-user-popover"
class="current-user-popover -m-3 w-80 rounded-lg border border-border-default bg-base-background p-2 shadow-[1px_1px_8px_0_rgba(0,0,0,0.4)]"
>
<!-- User Info Section -->