mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-05 21:54:50 +00:00
Compare commits
20 Commits
glary/rais
...
glary/subs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6949044d65 | ||
|
|
828ba48ba6 | ||
|
|
b2a445ed73 | ||
|
|
19cc62a79c | ||
|
|
a879e63c77 | ||
|
|
009ea1c054 | ||
|
|
5355773e53 | ||
|
|
c2ddda0eb7 | ||
|
|
a47a8949ac | ||
|
|
ee88893b1b | ||
|
|
747bbf2c2c | ||
|
|
262b74954f | ||
|
|
618277384e | ||
|
|
dc634279a1 | ||
|
|
e1334b1955 | ||
|
|
d0e9a5867f | ||
|
|
6c72a9d711 | ||
|
|
f87fc5ec9a | ||
|
|
8360110933 | ||
|
|
afed5e98e8 |
90
browser_tests/fixtures/data/subscriptionFixtures.ts
Normal file
90
browser_tests/fixtures/data/subscriptionFixtures.ts
Normal 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
|
||||
})
|
||||
246
browser_tests/fixtures/helpers/SubscriptionHelper.ts
Normal file
246
browser_tests/fixtures/helpers/SubscriptionHelper.ts
Normal 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)
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
262
browser_tests/tests/subscription.spec.ts
Normal file
262
browser_tests/tests/subscription.spec.ts
Normal 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()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
Reference in New Issue
Block a user