mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 16:48:45 +08:00
fix(payment): keep subscription plan titles readable
This commit is contained in:
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user