mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-23 06:10:32 +00:00
test: add E2E coverage for credit balance and top-up conversions
Adds a credits.spec.ts that builds on the subscription/credits fixture infrastructure to exercise the credit-related helpers in src/base/credits/comfyCredits.ts via the actual UI surfaces: - Popover credit balance rendering (formatCreditsFromCents -> centsToCredits -> formatNumber) at $5.00 and $0.50 balances - Top-up dialog default state, $10/$100 preset clicks, and bidirectional stepper increments (usdToCredits / creditsToUsd) Also adds Vitest cases for the previously uncovered formatNumber guard (line 25, clamping minimumFractionDigits) and clampUsd, which had zero consumers in app code. A handful of data-testid attributes were added to the affected components to provide stable, role-agnostic selectors for the new E2E spec.
This commit is contained in:
@@ -211,6 +211,13 @@ export const TestIds = {
|
||||
currentUserIndicator: 'current-user-indicator',
|
||||
currentUserPopover: 'current-user-popover'
|
||||
},
|
||||
credits: {
|
||||
popoverCreditBalance: 'popover-credit-balance',
|
||||
addCreditsButton: 'add-credits-button',
|
||||
upgradeToAddCreditsButton: 'upgrade-to-add-credits-button',
|
||||
topUpPayAmount: 'top-up-pay-amount',
|
||||
topUpGetAmount: 'top-up-get-amount'
|
||||
},
|
||||
queue: {
|
||||
overlayToggle: 'queue-overlay-toggle',
|
||||
clearHistoryAction: 'clear-history-action',
|
||||
|
||||
237
browser_tests/tests/credits.spec.ts
Normal file
237
browser_tests/tests/credits.spec.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture } from '@e2e/fixtures/ComfyPage'
|
||||
import {
|
||||
createSubscriptionHelper,
|
||||
withActiveSubscription,
|
||||
withCredits
|
||||
} from '@e2e/fixtures/helpers/SubscriptionHelper'
|
||||
import type { SubscriptionHelper } from '@e2e/fixtures/helpers/SubscriptionHelper'
|
||||
import { TestIds } from '@e2e/fixtures/selectors'
|
||||
|
||||
/**
|
||||
* The `*_balance_micros` fields on the mocked balance response are consumed by
|
||||
* the UI through `formatCreditsFromCents`, which treats the value as cents
|
||||
* (despite the field name). Keep this helper colocated with the assertions so
|
||||
* the unit translation is explicit.
|
||||
*/
|
||||
const usdToBalanceUnits = (usd: number): number => Math.round(usd * 100)
|
||||
|
||||
async function openUserPopover(page: Page): Promise<Locator> {
|
||||
// Use dispatchEvent to bypass the subscription-required dialog backdrop that
|
||||
// can transiently overlay the topbar during page boot, mirroring the
|
||||
// canonical pattern used in subscription.spec.ts.
|
||||
await page.getByTestId(TestIds.user.currentUserButton).dispatchEvent('click')
|
||||
const popover = page.getByTestId(TestIds.user.currentUserPopover)
|
||||
await expect(popover).toBeVisible()
|
||||
return popover
|
||||
}
|
||||
|
||||
async function dismissSubscriptionDialogIfOpen(page: Page): Promise<void> {
|
||||
const dialog = page.locator('[aria-labelledby="subscription-required"]')
|
||||
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()
|
||||
}
|
||||
|
||||
function createCreditsTest(
|
||||
...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 watcher does not
|
||||
// auto-open the subscription dialog on app boot — the PrimeVue
|
||||
// Dialog mask would otherwise intercept pointer events on the topbar.
|
||||
await comfyPage.setupSettings({
|
||||
'Comfy.Extension.Disabled': ['Comfy.Cloud.Subscription']
|
||||
})
|
||||
await comfyPage.page.reload()
|
||||
// Wait for Firebase auth so the topbar user button is rendered before
|
||||
// the test body runs.
|
||||
await expect(
|
||||
comfyPage.page.getByTestId(TestIds.user.currentUserButton)
|
||||
).toBeVisible()
|
||||
await dismissSubscriptionDialogIfOpen(comfyPage.page)
|
||||
await use(helper)
|
||||
await helper.clearMocks()
|
||||
},
|
||||
{ auto: true }
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const FIVE_USD_OPS = [
|
||||
withActiveSubscription('CREATOR'),
|
||||
withCredits(usdToBalanceUnits(5))
|
||||
]
|
||||
|
||||
const FIFTY_CENT_OPS = [
|
||||
withActiveSubscription('CREATOR'),
|
||||
withCredits(usdToBalanceUnits(0.5))
|
||||
]
|
||||
|
||||
const fiveDollarTest = createCreditsTest(...FIVE_USD_OPS)
|
||||
const fiftyCentTest = createCreditsTest(...FIFTY_CENT_OPS)
|
||||
|
||||
fiveDollarTest.describe(
|
||||
'Credits — popover balance ($5)',
|
||||
{ tag: '@cloud' },
|
||||
() => {
|
||||
fiveDollarTest(
|
||||
'shows formatted credits derived from the balance response',
|
||||
async ({ comfyPage }) => {
|
||||
const popover = await openUserPopover(comfyPage.page)
|
||||
const balance = popover.getByTestId(
|
||||
TestIds.credits.popoverCreditBalance
|
||||
)
|
||||
await expect(balance).toHaveText('1,055')
|
||||
}
|
||||
)
|
||||
|
||||
fiveDollarTest(
|
||||
'add credits button is visible for active paid subscribers',
|
||||
async ({ comfyPage }) => {
|
||||
const popover = await openUserPopover(comfyPage.page)
|
||||
await expect(
|
||||
popover.getByTestId(TestIds.credits.addCreditsButton)
|
||||
).toBeVisible()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
fiftyCentTest.describe(
|
||||
'Credits — popover balance ($0.50)',
|
||||
{ tag: '@cloud' },
|
||||
() => {
|
||||
fiftyCentTest(
|
||||
'renders sub-dollar balances as whole credits',
|
||||
async ({ comfyPage }) => {
|
||||
const popover = await openUserPopover(comfyPage.page)
|
||||
const balance = popover.getByTestId(
|
||||
TestIds.credits.popoverCreditBalance
|
||||
)
|
||||
await expect(balance).toHaveText('106')
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
fiveDollarTest.describe(
|
||||
'Credits — top-up dialog conversions',
|
||||
{ tag: '@cloud' },
|
||||
() => {
|
||||
async function openTopUpDialog(page: Page): Promise<void> {
|
||||
const popover = await openUserPopover(page)
|
||||
await popover.getByTestId(TestIds.credits.addCreditsButton).click()
|
||||
await expect(
|
||||
page.getByTestId(TestIds.credits.topUpPayAmount)
|
||||
).toBeVisible()
|
||||
}
|
||||
|
||||
fiveDollarTest(
|
||||
'default $50 selection renders 10,550 credits in You Get',
|
||||
async ({ comfyPage }) => {
|
||||
await openTopUpDialog(comfyPage.page)
|
||||
const getInput = comfyPage.page
|
||||
.getByTestId(TestIds.credits.topUpGetAmount)
|
||||
.getByRole('textbox')
|
||||
await expect(getInput).toHaveValue('10,550')
|
||||
}
|
||||
)
|
||||
|
||||
fiveDollarTest(
|
||||
'$10 preset updates You Get to usdToCredits(10) = 2,110',
|
||||
async ({ comfyPage }) => {
|
||||
await openTopUpDialog(comfyPage.page)
|
||||
await comfyPage.page
|
||||
.getByRole('button', { name: '$10', exact: true })
|
||||
.click()
|
||||
|
||||
const getInput = comfyPage.page
|
||||
.getByTestId(TestIds.credits.topUpGetAmount)
|
||||
.getByRole('textbox')
|
||||
const payInput = comfyPage.page
|
||||
.getByTestId(TestIds.credits.topUpPayAmount)
|
||||
.getByRole('textbox')
|
||||
|
||||
await expect(getInput).toHaveValue('2,110')
|
||||
await expect(payInput).toHaveValue('10')
|
||||
}
|
||||
)
|
||||
|
||||
fiveDollarTest(
|
||||
'$100 preset updates You Get to usdToCredits(100) = 21,100',
|
||||
async ({ comfyPage }) => {
|
||||
await openTopUpDialog(comfyPage.page)
|
||||
await comfyPage.page
|
||||
.getByRole('button', { name: '$100', exact: true })
|
||||
.click()
|
||||
|
||||
const getInput = comfyPage.page
|
||||
.getByTestId(TestIds.credits.topUpGetAmount)
|
||||
.getByRole('textbox')
|
||||
const payInput = comfyPage.page
|
||||
.getByTestId(TestIds.credits.topUpPayAmount)
|
||||
.getByRole('textbox')
|
||||
|
||||
await expect(getInput).toHaveValue('21,100')
|
||||
await expect(payInput).toHaveValue('100')
|
||||
}
|
||||
)
|
||||
|
||||
fiveDollarTest(
|
||||
'incrementing You Pay updates You Get via usdToCredits',
|
||||
async ({ comfyPage }) => {
|
||||
await openTopUpDialog(comfyPage.page)
|
||||
const payContainer = comfyPage.page.getByTestId(
|
||||
TestIds.credits.topUpPayAmount
|
||||
)
|
||||
await payContainer.getByRole('button', { name: /increment/i }).click()
|
||||
|
||||
const getInput = comfyPage.page
|
||||
.getByTestId(TestIds.credits.topUpGetAmount)
|
||||
.getByRole('textbox')
|
||||
const payInput = payContainer.getByRole('textbox')
|
||||
await expect(payInput).toHaveValue('55')
|
||||
await expect(getInput).toHaveValue('11,605')
|
||||
}
|
||||
)
|
||||
|
||||
fiveDollarTest(
|
||||
'incrementing You Get updates You Pay via creditsToUsd',
|
||||
async ({ comfyPage }) => {
|
||||
await openTopUpDialog(comfyPage.page)
|
||||
const getContainer = comfyPage.page.getByTestId(
|
||||
TestIds.credits.topUpGetAmount
|
||||
)
|
||||
await getContainer.getByRole('button', { name: /increment/i }).click()
|
||||
|
||||
const payInput = comfyPage.page
|
||||
.getByTestId(TestIds.credits.topUpPayAmount)
|
||||
.getByRole('textbox')
|
||||
const getInput = getContainer.getByRole('textbox')
|
||||
await expect(getInput).toHaveValue('11,605')
|
||||
await expect(payInput).toHaveValue('55')
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
CREDITS_PER_USD,
|
||||
COMFY_CREDIT_RATE_CENTS,
|
||||
centsToCredits,
|
||||
clampUsd,
|
||||
creditsToCents,
|
||||
creditsToUsd,
|
||||
formatCredits,
|
||||
@@ -43,4 +44,36 @@ describe('comfyCredits helpers', () => {
|
||||
expect(formatCreditsFromUsd({ usd: 1, locale })).toBe('211.00')
|
||||
expect(formatUsd({ value: 4.2, locale })).toBe('4.20')
|
||||
})
|
||||
|
||||
test('clamps minimumFractionDigits when caller lowers maximumFractionDigits below the default minimum', () => {
|
||||
expect(
|
||||
formatCredits({
|
||||
value: 1.5,
|
||||
locale: 'en-US',
|
||||
numberOptions: { maximumFractionDigits: 0 }
|
||||
})
|
||||
).toBe('2')
|
||||
})
|
||||
|
||||
describe('clampUsd', () => {
|
||||
test('returns 0 for NaN', () => {
|
||||
expect(clampUsd(Number.NaN)).toBe(0)
|
||||
})
|
||||
|
||||
test('clamps values below the minimum up to 1', () => {
|
||||
expect(clampUsd(0)).toBe(1)
|
||||
expect(clampUsd(-50)).toBe(1)
|
||||
})
|
||||
|
||||
test('clamps values above the maximum down to 1000', () => {
|
||||
expect(clampUsd(1000.01)).toBe(1000)
|
||||
expect(clampUsd(99999)).toBe(1000)
|
||||
})
|
||||
|
||||
test('passes values within the allowed range through unchanged', () => {
|
||||
expect(clampUsd(1)).toBe(1)
|
||||
expect(clampUsd(50)).toBe(50)
|
||||
expect(clampUsd(1000)).toBe(1000)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
</div>
|
||||
|
||||
<!-- You Get -->
|
||||
<div class="flex flex-1 flex-col gap-3">
|
||||
<div class="flex flex-1 flex-col gap-3" data-testid="top-up-get-amount">
|
||||
<div class="text-sm text-muted-foreground">
|
||||
{{ $t('credits.topUp.youGet') }}
|
||||
</div>
|
||||
|
||||
@@ -39,9 +39,12 @@
|
||||
height="1.25rem"
|
||||
class="w-full"
|
||||
/>
|
||||
<span v-else class="text-base font-semibold text-base-foreground">{{
|
||||
formattedBalance
|
||||
}}</span>
|
||||
<span
|
||||
v-else
|
||||
data-testid="popover-credit-balance"
|
||||
class="text-base font-semibold text-base-foreground"
|
||||
>{{ formattedBalance }}</span
|
||||
>
|
||||
<i
|
||||
v-tooltip="{ value: $t('credits.unified.tooltip'), showDelay: 300 }"
|
||||
class="mr-auto icon-[lucide--circle-help] cursor-help text-base text-muted-foreground"
|
||||
|
||||
@@ -66,9 +66,12 @@
|
||||
height="1.25rem"
|
||||
class="w-full"
|
||||
/>
|
||||
<span v-else class="text-base font-semibold text-base-foreground">{{
|
||||
displayedCredits
|
||||
}}</span>
|
||||
<span
|
||||
v-else
|
||||
data-testid="popover-credit-balance"
|
||||
class="text-base font-semibold text-base-foreground"
|
||||
>{{ displayedCredits }}</span
|
||||
>
|
||||
<i
|
||||
v-tooltip="{ value: $t('credits.unified.tooltip'), showDelay: 300 }"
|
||||
class="mr-auto icon-[lucide--circle-help] cursor-help text-base text-muted-foreground"
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
</div>
|
||||
|
||||
<!-- You Get -->
|
||||
<div class="flex flex-1 flex-col gap-3">
|
||||
<div class="flex flex-1 flex-col gap-3" data-testid="top-up-get-amount">
|
||||
<div class="text-sm text-muted-foreground">
|
||||
{{ $t('credits.topUp.youGet') }}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user