diff --git a/web/app/components/billing/pricing/__tests__/content.spec.tsx b/web/app/components/billing/pricing/__tests__/content.spec.tsx index cecb302b3f4..f8123d243d8 100644 --- a/web/app/components/billing/pricing/__tests__/content.spec.tsx +++ b/web/app/components/billing/pricing/__tests__/content.spec.tsx @@ -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() + 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'), ) diff --git a/web/app/components/billing/pricing/__tests__/education-layout.browser.spec.tsx b/web/app/components/billing/pricing/__tests__/education-layout.browser.spec.tsx new file mode 100644 index 00000000000..a9027c4f546 --- /dev/null +++ b/web/app/components/billing/pricing/__tests__/education-layout.browser.spec.tsx @@ -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( + + + , + ) + 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) + } +}) diff --git a/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx b/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx index e0d14cdd8c8..06d6a480f58 100644 --- a/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx +++ b/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx @@ -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 {buttonLabel} {educationDiscountWarningText && ( -
+
{educationDiscountWarningText}
)} @@ -258,22 +259,23 @@ export function CloudPlanItem({ plan, billingInterval, billing }: CloudPlanItemP aria-label={t(($) => $['operation.close'], { ns: 'common' })} /> - +