From fde30c2b4f4d69d0a0028bb79eee704ec3dff724 Mon Sep 17 00:00:00 2001 From: Glary-Bot Date: Tue, 5 May 2026 08:07:11 +0000 Subject: [PATCH] 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. --- browser_tests/fixtures/selectors.ts | 7 + browser_tests/tests/credits.spec.ts | 237 ++++++++++++++++++ src/base/credits/comfyCredits.test.ts | 33 +++ .../TopUpCreditsDialogContentLegacy.vue | 2 +- .../topbar/CurrentUserPopoverLegacy.vue | 9 +- .../CurrentUserPopoverWorkspace.vue | 9 +- .../TopUpCreditsDialogContentWorkspace.vue | 2 +- 7 files changed, 291 insertions(+), 8 deletions(-) create mode 100644 browser_tests/tests/credits.spec.ts diff --git a/browser_tests/fixtures/selectors.ts b/browser_tests/fixtures/selectors.ts index 49e0f35b57..5b044606b6 100644 --- a/browser_tests/fixtures/selectors.ts +++ b/browser_tests/fixtures/selectors.ts @@ -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', diff --git a/browser_tests/tests/credits.spec.ts b/browser_tests/tests/credits.spec.ts new file mode 100644 index 0000000000..cf80b95f8d --- /dev/null +++ b/browser_tests/tests/credits.spec.ts @@ -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 { + // 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 { + 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[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 { + 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') + } + ) + } +) diff --git a/src/base/credits/comfyCredits.test.ts b/src/base/credits/comfyCredits.test.ts index a2ef78a537..8ecb85397d 100644 --- a/src/base/credits/comfyCredits.test.ts +++ b/src/base/credits/comfyCredits.test.ts @@ -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) + }) + }) }) diff --git a/src/components/dialog/content/TopUpCreditsDialogContentLegacy.vue b/src/components/dialog/content/TopUpCreditsDialogContentLegacy.vue index 79abba76d7..c6dccbba26 100644 --- a/src/components/dialog/content/TopUpCreditsDialogContentLegacy.vue +++ b/src/components/dialog/content/TopUpCreditsDialogContentLegacy.vue @@ -74,7 +74,7 @@ -
+
{{ $t('credits.topUp.youGet') }}
diff --git a/src/components/topbar/CurrentUserPopoverLegacy.vue b/src/components/topbar/CurrentUserPopoverLegacy.vue index c25061bf32..afba09a10a 100644 --- a/src/components/topbar/CurrentUserPopoverLegacy.vue +++ b/src/components/topbar/CurrentUserPopoverLegacy.vue @@ -39,9 +39,12 @@ height="1.25rem" class="w-full" /> - {{ - formattedBalance - }} + {{ formattedBalance }} - {{ - displayedCredits - }} + {{ displayedCredits }} -
+
{{ $t('credits.topUp.youGet') }}