fix(payment): keep subscription plan titles readable

This commit is contained in:
BayinForge
2026-07-29 21:23:16 +08:00
parent 5a6143097d
commit 0ee9ea5765
3 changed files with 130 additions and 7 deletions
@@ -14,12 +14,12 @@
<!-- Header: name + badge + price -->
<div class="mb-3 flex items-start justify-between gap-2">
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<h3 class="truncate text-base font-bold text-gray-900 dark:text-white">{{ plan.name }}</h3>
<span :class="['shrink-0 rounded-full px-2 py-0.5 text-[11px] font-medium', badgeLightClass]">
{{ pLabel }}
</span>
</div>
<h3
:title="plan.name"
class="h-12 min-w-0 break-words [overflow-wrap:anywhere] text-base font-bold leading-6 text-gray-900 dark:text-white line-clamp-2"
>
{{ plan.name }}
</h3>
<p v-if="plan.description" class="mt-0.5 text-xs leading-relaxed text-gray-500 dark:text-dark-400 line-clamp-2">
{{ plan.description }}
</p>
@@ -30,7 +30,12 @@
<span :class="['text-2xl font-extrabold tracking-tight', textClass]">{{ plan.price }}</span>
<span v-if="plan.currency" class="text-xs font-medium text-gray-400 dark:text-dark-500">{{ plan.currency }}</span>
</div>
<span class="text-[11px] text-gray-400 dark:text-dark-500">/ {{ validitySuffix }}</span>
<div class="flex items-center justify-end gap-1">
<span :class="['inline-flex shrink-0 rounded-full px-2 py-0.5 text-[11px] font-medium', badgeLightClass]">
{{ pLabel }}
</span>
<span class="text-[11px] text-gray-400 dark:text-dark-500">/ {{ validitySuffix }}</span>
</div>
<div v-if="plan.original_price" class="mt-0.5 flex items-center justify-end gap-1.5">
<span class="text-xs text-gray-400 line-through dark:text-dark-500">{{ planCurrencySymbol }}{{ plan.original_price }}<template v-if="plan.currency"> {{ plan.currency }}</template></span>
<span :class="['rounded px-1 py-0.5 text-[10px] font-semibold', discountClass]">{{ discountText }}</span>
@@ -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");
});
});
@@ -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<typeof checkoutInfoW
return wrapper
}
async function mountSubscriptionPlanList(planCount: number) {
vi.useRealTimers()
routeState.path = '/purchase'
routeState.query = { tab: 'subscription' }
routerReplace.mockReset().mockResolvedValue(undefined)
routerPush.mockReset().mockResolvedValue(undefined)
routerResolve.mockClear()
createOrder.mockReset()
refreshUser.mockReset()
fetchActiveSubscriptions.mockReset().mockResolvedValue(undefined)
showError.mockReset()
showInfo.mockReset()
showWarning.mockReset()
const basePlan = checkoutInfoWithPlansFixture().data.plans[0]
const plans = Array.from({ length: planCount }, (_, index) => ({
...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: '<div><slot /></div>',
},
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({