test: deepen billing dialog E2E tests per review feedback

- Cancel dialog: mock /customers/billing route, click button, assert API
  call via expect.poll; add error handling test
- TopUp dialog: assert pay input updates on preset click, use aria-label
  for close button, verify pricing link href and target
- Add aria-label to TopUpCredits close buttons for semantic locators
- Extract shared helpers to reduce duplication
This commit is contained in:
dante01yoon
2026-04-10 10:53:02 +09:00
parent 6ffa0cc573
commit 9ca7708b6d
4 changed files with 113 additions and 60 deletions

View File

@@ -1,17 +1,24 @@
import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
import type { WorkspaceStore } from '@e2e/types/globals'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
async function openCancelSubscriptionDialog(page: Page, cancelAt?: string) {
await page.evaluate((date) => {
void (
window.app!.extensionManager as WorkspaceStore
).dialog.showCancelSubscriptionDialog(date)
}, cancelAt)
}
test.describe('CancelSubscription dialog', { tag: '@ui' }, () => {
test('displays dialog with title and description', async ({ comfyPage }) => {
test('displays dialog with title and formatted date', async ({
comfyPage
}) => {
const { page } = comfyPage
await page.evaluate(() => {
void (
window.app!.extensionManager as WorkspaceStore
).dialog.showCancelSubscriptionDialog('2025-12-31T12:00:00Z')
})
await openCancelSubscriptionDialog(page, '2025-12-31T12:00:00Z')
const dialog = page.getByRole('dialog')
await expect(dialog).toBeVisible()
@@ -25,11 +32,7 @@ test.describe('CancelSubscription dialog', { tag: '@ui' }, () => {
test('"Keep subscription" button closes dialog', async ({ comfyPage }) => {
const { page } = comfyPage
await page.evaluate(() => {
void (
window.app!.extensionManager as WorkspaceStore
).dialog.showCancelSubscriptionDialog()
})
await openCancelSubscriptionDialog(page)
const dialog = page.getByRole('dialog')
await expect(dialog).toBeVisible()
@@ -41,11 +44,7 @@ test.describe('CancelSubscription dialog', { tag: '@ui' }, () => {
test('Escape key closes dialog', async ({ comfyPage }) => {
const { page } = comfyPage
await page.evaluate(() => {
void (
window.app!.extensionManager as WorkspaceStore
).dialog.showCancelSubscriptionDialog()
})
await openCancelSubscriptionDialog(page)
const dialog = page.getByRole('dialog')
await expect(dialog).toBeVisible()
@@ -54,22 +53,77 @@ test.describe('CancelSubscription dialog', { tag: '@ui' }, () => {
await expect(dialog).toBeHidden()
})
test('"Cancel subscription" button is clickable', async ({ comfyPage }) => {
test('"Cancel subscription" button triggers cancel API request', async ({
comfyPage
}) => {
const { page } = comfyPage
await page.evaluate(() => {
void (
window.app!.extensionManager as WorkspaceStore
).dialog.showCancelSubscriptionDialog()
let cancelCalled = false
// Mock the legacy billing portal endpoint (POST /customers/billing)
await page.route('**/customers/billing', (route) => {
cancelCalled = true
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
billing_portal_url: 'https://billing.example.com/portal'
})
})
})
// Mock the subscription status endpoint
await page.route('**/customers/cloud-subscription-status', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
plan_name: 'pro',
is_active: true,
cancel_at_period_end: true
})
})
})
// Prevent window.open from actually opening a new tab
await page.evaluate(() => {
window.open = () => null
})
await openCancelSubscriptionDialog(page)
const dialog = page.getByRole('dialog')
await expect(dialog).toBeVisible()
const cancelBtn = dialog.getByRole('button', {
name: 'Cancel subscription'
})
await expect(cancelBtn).toBeVisible()
await expect(cancelBtn).toBeEnabled()
await cancelBtn.click()
await expect.poll(() => cancelCalled).toBe(true)
await expect(dialog).toBeHidden()
})
test('shows error toast when cancel API fails', async ({ comfyPage }) => {
const { page } = comfyPage
// Mock the legacy billing portal endpoint to fail
await page.route('**/customers/billing', (route) => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ message: 'Internal server error' })
})
})
await openCancelSubscriptionDialog(page)
const dialog = page.getByRole('dialog')
await expect(dialog).toBeVisible()
await dialog.getByRole('button', { name: 'Cancel subscription' }).click()
// Dialog should remain visible on error
await expect(dialog).toBeVisible()
})
})

View File

@@ -1,19 +1,27 @@
import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
import type { WorkspaceStore } from '@e2e/types/globals'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
async function openTopUpCreditsDialog(
page: Page,
options?: { isInsufficientCredits?: boolean }
) {
await page.evaluate((opts) => {
void (
window.app!.extensionManager as WorkspaceStore
).dialog.showTopUpCreditsDialog(opts)
}, options)
}
test.describe('TopUpCredits dialog', { tag: '@ui' }, () => {
test('displays dialog with heading and preset amounts', async ({
comfyPage
}) => {
const { page } = comfyPage
await page.evaluate(() => {
void (
window.app!.extensionManager as WorkspaceStore
).dialog.showTopUpCreditsDialog()
})
await openTopUpCreditsDialog(page)
const dialog = page.getByRole('dialog')
await expect(dialog).toBeVisible()
@@ -41,11 +49,7 @@ test.describe('TopUpCredits dialog', { tag: '@ui' }, () => {
}) => {
const { page } = comfyPage
await page.evaluate(() => {
void (
window.app!.extensionManager as WorkspaceStore
).dialog.showTopUpCreditsDialog({ isInsufficientCredits: true })
})
await openTopUpCreditsDialog(page, { isInsufficientCredits: true })
const dialog = page.getByRole('dialog')
await expect(dialog).toBeVisible()
@@ -58,59 +62,52 @@ test.describe('TopUpCredits dialog', { tag: '@ui' }, () => {
)
})
test('selecting a preset amount updates the selection', async ({
test('selecting a preset amount updates the pay amount', async ({
comfyPage
}) => {
const { page } = comfyPage
await page.evaluate(() => {
void (
window.app!.extensionManager as WorkspaceStore
).dialog.showTopUpCreditsDialog()
})
await openTopUpCreditsDialog(page)
const dialog = page.getByRole('dialog')
await expect(dialog).toBeVisible()
await dialog.getByRole('button', { name: '$10' }).first().click()
// Default preset is $50, click $10 instead
const tenDollarBtn = dialog.getByRole('button', { name: '$10' }).first()
await tenDollarBtn.click()
await expect(dialog).toContainText('Credits')
// The "You Pay" input should reflect $10
const payInput = dialog.locator('input').first()
await expect(payInput).toHaveValue('10')
})
test('close button dismisses dialog', async ({ comfyPage }) => {
const { page } = comfyPage
await page.evaluate(() => {
void (
window.app!.extensionManager as WorkspaceStore
).dialog.showTopUpCreditsDialog()
})
await openTopUpCreditsDialog(page)
const dialog = page.getByRole('dialog')
await expect(dialog).toBeVisible()
const closeButton = dialog.getByRole('button').filter({
has: page.locator('i.icon-\\[lucide--x\\]')
})
await closeButton.click()
await dialog.getByRole('button', { name: 'Close' }).click()
await expect(dialog).toBeHidden()
})
test('shows pricing details link', async ({ comfyPage }) => {
test('pricing details link points to docs pricing page', async ({
comfyPage
}) => {
const { page } = comfyPage
await page.evaluate(() => {
void (
window.app!.extensionManager as WorkspaceStore
).dialog.showTopUpCreditsDialog()
})
await openTopUpCreditsDialog(page)
const dialog = page.getByRole('dialog')
await expect(dialog).toBeVisible()
await expect(
dialog.getByRole('link', { name: 'View pricing details' })
).toBeVisible()
const pricingLink = dialog.getByRole('link', {
name: 'View pricing details'
})
await expect(pricingLink).toBeVisible()
await expect(pricingLink).toHaveAttribute('href', /partner-nodes\/pricing/)
await expect(pricingLink).toHaveAttribute('target', '_blank')
})
})

View File

@@ -13,6 +13,7 @@
</h2>
<button
class="focus-visible:ring-secondary-foreground cursor-pointer rounded-sm border-none bg-transparent p-0 text-muted-foreground transition-colors hover:text-base-foreground focus-visible:ring-1 focus-visible:outline-none"
:aria-label="$t('g.close')"
@click="() => handleClose()"
>
<i class="icon-[lucide--x] size-6" />

View File

@@ -13,6 +13,7 @@
</h2>
<button
class="focus-visible:ring-secondary-foreground cursor-pointer rounded-sm border-none bg-transparent p-0 text-muted-foreground transition-colors hover:text-base-foreground focus-visible:ring-1 focus-visible:outline-none"
:aria-label="$t('g.close')"
@click="() => handleClose()"
>
<i class="icon-[lucide--x] size-6" />