fix(billing): keep education checkout actions accessible on mobile

This commit is contained in:
yyh committed 2026-11-10 19:45:37 +08:00
1 parent 43e83dbd22
commit 8b6240ea8d
3 files changed
+68 -13

No files matched your search

@@ -10,6 +10,7 @@ import {
seedFeatures,
} from '@/test/console/query-data'
import { render } from '@/test/console/render'
import { createDeferred } from '@/test/deferred'
import { PricingContent } from '../content'
let billingPopup: {
@@ -219,11 +220,8 @@ it('offers education pricing before Team checkout when the discount has not been
const { queryClient, show } = setup()
seedFeatures(queryClient, { education: { enabled: true, activated: false } })
seedEducationStatus(queryClient, { is_student: true })
const checkout = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response(JSON.stringify({ url: 'https://billing.example.com/team-checkout' }), {
headers: { 'Content-Type': 'application/json' },
}),
)
const pending = createDeferred<Response>()
const checkout = vi.spyOn(globalThis, 'fetch').mockReturnValue(pending.promise)
show()
await user.click(screen.getByRole('button', { name: 'billing.plansCommon.upgradeToTeam' }))
@@ -234,6 +232,19 @@ it('offers education pricing before Team checkout when the discount has not been
expect(checkout).not.toHaveBeenCalled()
const navigate = vi.spyOn(window.location, 'href', 'set').mockImplementation(() => {})
await user.click(screen.getByRole('button', { name: 'education.educationPricingConfirm.cancel' }))
const switchPlan = screen.getByRole('button', {
name: 'education.educationPricingConfirm.continue',
})
expect(switchPlan).toBeDisabled()
await user.click(switchPlan)
expect(checkout).toHaveBeenCalledTimes(1)
await act(async () =>
pending.resolve(
new Response(JSON.stringify({ url: 'https://billing.example.com/team-checkout' }), {
headers: { 'Content-Type': 'application/json' },
}),
),
)
await waitFor(() =>
expect(navigate).toHaveBeenCalledWith('https://billing.example.com/team-checkout'),
)
@@ -0,0 +1,42 @@
import { page } from 'vite-plus/test/browser'
import { render } from 'vitest-browser-react'
import { createConsoleQueryWrapper } from '@/test/console/query-data'
import { CloudPlanItem } from '../plans/cloud-plan-item'
vi.mock('@/context/workspace-state', async () => {
const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture')
return createWorkspaceStateModuleMock(() => ({ isCurrentWorkspaceManager: true }))
})
it('keeps both education checkout actions inside the dialog on a narrow phone', async () => {
await page.viewport(320, 700)
const { wrapper: Provider } = createConsoleQueryWrapper()
await render(
<Provider>
<CloudPlanItem
plan="team"
billingInterval="month"
billing={{
currentPlan: 'sandbox',
currentBillingInterval: 'month',
isEducationDiscountActivated: false,
isEducationDiscountEligible: true,
}}
/>
</Provider>,
)
await page.getByRole('button', { name: 'billing.plansCommon.upgradeToTeam' }).click()
const dialog = page.getByRole('dialog')
await expect.element(dialog).toBeVisible()
const bounds = dialog.element().getBoundingClientRect()
for (const name of [
'education.educationPricingConfirm.cancel',
'education.educationPricingConfirm.continue',
]) {
const action = dialog.getByRole('button', { name })
await expect.element(action).toBeVisible()
const rect = action.element().getBoundingClientRect()
expect(rect.left).toBeGreaterThanOrEqual(bounds.left)
expect(rect.right).toBeLessThanOrEqual(bounds.right)
}
})
@@ -157,9 +157,6 @@ export function CloudPlanItem({ plan, billingInterval, billing }: CloudPlanItemP
await runPlanAction()
}
const handleUseEducationDiscount = async () => {
await handleEducationDiscount()
}
const handleContinueWithoutEducationDiscount = async () => {
await runPlanAction()
setShowEducationPricingConfirm(false)
@@ -223,6 +220,10 @@ export function CloudPlanItem({ plan, billingInterval, billing }: CloudPlanItemP
variant="tertiary"
size={null}
disabled={isPlanDisabled}
loading={
!showEducationPricingConfirm &&
(isPlanActionPending || isEducationDiscountLoading)
}
className={cn(
'h-auto min-h-15 w-full rounded-lg border p-3 text-2xl leading-[30px] font-medium whitespace-normal shadow-none disabled:cursor-default',
isPrimaryAction
@@ -236,7 +237,7 @@ export function CloudPlanItem({ plan, billingInterval, billing }: CloudPlanItemP
<span className="min-w-0">{buttonLabel}</span>
</Button>
{educationDiscountWarningText && (
<div className="absolute inset-x-0 top-full mt-1.5 text-start system-2xs-medium text-text-tertiary">
<div className="mt-1.5 text-start system-2xs-medium text-text-tertiary">
{educationDiscountWarningText}
</div>
)}
@@ -258,22 +259,23 @@ export function CloudPlanItem({ plan, billingInterval, billing }: CloudPlanItemP
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
/>
</DialogHeader>
<DialogFooter>
<DialogFooter className="max-sm:flex-col max-sm:items-stretch">
<Button
size="large"
onClick={handleContinueWithoutEducationDiscount}
disabled={isEducationDiscountLoading}
loading={isPlanActionPending}
className="min-w-38"
className="h-auto min-h-9 w-full whitespace-normal sm:w-auto sm:min-w-38"
>
{t(($) => $['educationPricingConfirm.cancel'], { ns: 'education' })}
</Button>
<Button
variant="primary"
size="large"
onClick={handleUseEducationDiscount}
onClick={handleEducationDiscount}
disabled={isPlanActionPending}
loading={isEducationDiscountLoading}
className="min-w-61"
className="h-auto min-h-9 w-full whitespace-normal sm:w-auto sm:min-w-61"
>
{t(($) => $['educationPricingConfirm.continue'], { ns: 'education' })}
</Button>