diff --git a/frontend/src/components/payment/SubscriptionPlanCard.vue b/frontend/src/components/payment/SubscriptionPlanCard.vue index 5b34056bfe..0d60f67791 100644 --- a/frontend/src/components/payment/SubscriptionPlanCard.vue +++ b/frontend/src/components/payment/SubscriptionPlanCard.vue @@ -14,12 +14,12 @@
-
-

{{ plan.name }}

- - {{ pLabel }} - -
+

+ {{ plan.name }} +

{{ plan.description }}

@@ -30,7 +30,12 @@ {{ plan.price }} {{ plan.currency }}
- / {{ validitySuffix }} +
+ + {{ pLabel }} + + / {{ validitySuffix }} +
{{ planCurrencySymbol }}{{ plan.original_price }} {{ discountText }} diff --git a/frontend/src/components/payment/__tests__/SubscriptionPlanCard.spec.ts b/frontend/src/components/payment/__tests__/SubscriptionPlanCard.spec.ts index dc8f993785..853af980ba 100644 --- a/frontend/src/components/payment/__tests__/SubscriptionPlanCard.spec.ts +++ b/frontend/src/components/payment/__tests__/SubscriptionPlanCard.spec.ts @@ -86,4 +86,66 @@ describe("SubscriptionPlanCard", () => { expect(mountPlanCard("openai", { currency: "USD" }).text()).toContain("$10USD"); expect(mountPlanCard("openai", { currency: "" }).text()).toContain("$10"); }); + + it.each([ + ["long Chinese", "企业全球加速专业订阅套餐(含高级模型与优先支持)"], + ["long English", "Enterprise Global Acceleration Subscription with Priority Support"], + ["unbroken token", "EnterpriseGlobalAccelerationSubscriptionWithPrioritySupport1234567890"], + ])("keeps the full %s plan title accessible in a bounded two-line area", (_label, name) => { + const wrapper = mountPlanCard("openai", { name }); + const title = wrapper.get("h3"); + + expect(title.text()).toBe(name); + expect(title.attributes("title")).toBe(name); + expect(title.classes()).toEqual(expect.arrayContaining([ + "min-w-0", + "h-12", + "break-words", + "line-clamp-2", + "[overflow-wrap:anywhere]", + ])); + expect(title.classes()).not.toContain("truncate"); + }); + + it("keeps title, badge, price, description, and purchase action in separate bounded regions", () => { + const wrapper = mountPlanCard("openai", { + name: "Enterprise Global Acceleration Subscription with Priority Support", + price: 123.45, + currency: "USD", + description: "Includes advanced models and priority support.", + }); + const title = wrapper.get("h3"); + const badge = wrapper.findAll("span").find((node) => node.text() === "OpenAI"); + const price = wrapper.findAll("span").find((node) => node.text() === "123.45"); + + expect(title.element.parentElement?.classList).toContain("min-w-0"); + expect(title.element.parentElement?.classList).toContain("flex-1"); + expect(badge?.classes()).toContain("shrink-0"); + expect([...(badge?.element.parentElement?.classList ?? [])]).toEqual(expect.arrayContaining([ + "flex", + "items-center", + "justify-end", + ])); + expect(badge?.element.parentElement?.textContent).toContain("/ 30payment.days"); + expect(badge?.element.parentElement?.parentElement?.classList).toContain("shrink-0"); + expect(price?.element.parentElement?.parentElement?.classList).toContain("shrink-0"); + expect(wrapper.get("p").text()).toBe("Includes advanced models and priority support."); + expect(wrapper.get("button").text()).toBe("payment.subscribeNow"); + }); + + it("keeps short plan titles compact and aligned", () => { + const wrapper = mountPlanCard("openai", { name: "Pro", description: "" }); + const title = wrapper.get("h3"); + const badge = wrapper.findAll("span").find((node) => node.text() === "OpenAI"); + + expect(title.text()).toBe("Pro"); + expect(title.attributes("title")).toBe("Pro"); + expect(title.classes()).toEqual(expect.arrayContaining(["text-base", "font-bold", "h-12"])); + expect([...(badge?.element.parentElement?.classList ?? [])]).toEqual(expect.arrayContaining([ + "flex", + "items-center", + "justify-end", + ])); + expect(badge?.element.parentElement?.textContent).toContain("/ 30payment.days"); + }); }); diff --git a/frontend/src/views/user/__tests__/PaymentView.spec.ts b/frontend/src/views/user/__tests__/PaymentView.spec.ts index aa7c401349..a3d688d5a9 100644 --- a/frontend/src/views/user/__tests__/PaymentView.spec.ts +++ b/frontend/src/views/user/__tests__/PaymentView.spec.ts @@ -3,6 +3,7 @@ import { flushPromises, shallowMount } from '@vue/test-utils' import PaymentView from '../PaymentView.vue' import { PAYMENT_RECOVERY_STORAGE_KEY } from '@/components/payment/paymentFlow' import { formatPaymentAmount } from '@/components/payment/currency' +import SubscriptionPlanCard from '@/components/payment/SubscriptionPlanCard.vue' import type { CheckoutInfoResponse, MethodLimit, SubscriptionPlan } from '@/types/payment' const routeState = vi.hoisted(() => ({ @@ -236,6 +237,61 @@ async function mountSubscriptionConfirm(options: Parameters ({ + ...basePlan, + id: index + 1, + name: `Plan ${index + 1}`, + })) + getCheckoutInfo.mockReset().mockResolvedValue(checkoutInfoFixture({ plans })) + bridgeInvoke.mockReset() + window.localStorage.clear() + ;(window as Window & { WeixinJSBridge?: { invoke: typeof bridgeInvoke } }).WeixinJSBridge = undefined + + const wrapper = shallowMount(PaymentView, { + global: { + stubs: { + AppLayout: { + template: '
', + }, + Teleport: true, + Transition: false, + }, + }, + }) + await flushPromises() + await flushPromises() + return wrapper +} + +describe('PaymentView subscription plan grid', () => { + it.each([3, 4, 6])('keeps %i plans on the existing mobile/tablet/desktop grid', async (planCount) => { + const wrapper = await mountSubscriptionPlanList(planCount) + const cards = wrapper.findAllComponents(SubscriptionPlanCard) + + expect(cards).toHaveLength(planCount) + expect([...(cards[0].element.parentElement?.classList ?? [])]).toEqual(expect.arrayContaining([ + 'grid', + 'grid-cols-1', + 'sm:grid-cols-2', + 'lg:grid-cols-3', + ])) + }) +}) + describe('PaymentView subscription confirmation amounts', () => { it('shows converted CNY pay amount using the subscription rate, not the balance multiplier', async () => { const wrapper = await mountSubscriptionConfirm({