SponsorCard.stories.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. import type { Meta, StoryObj } from '@storybook/react-vite';
  2. import { expect, within } from 'storybook/test';
  3. import type { Sponsor } from '@/generated/types';
  4. import SponsorCard from './SponsorCard';
  5. const sponsor: Sponsor = {
  6. id: 'acme-2026-10',
  7. name: 'Acme VPS',
  8. slots: ['dashboard', 'page', 'login'],
  9. until: '2099-01-01T00:00:00Z',
  10. title: { en: 'Acme VPS — fast NVMe servers', fa: 'سرورهای سریع Acme' },
  11. text: { en: 'Deploy 3X-UI in 60 seconds. 20% off for panel users.' },
  12. link: 'https://acme.example/?utm_source=3x-ui',
  13. };
  14. // Copy as long as the real sponsors.json entries; each story renders it at its slot's width.
  15. const longSponsor: Sponsor = {
  16. ...sponsor,
  17. title: { en: 'Advertise here — reach 3X-UI server admins' },
  18. text: {
  19. en: 'Put your VPS, hosting or domain brand in front of the admins who use 3X-UI every day. Message us for slots and pricing.',
  20. },
  21. };
  22. function part(root: HTMLElement, selector: string): HTMLElement {
  23. const el = root.querySelector<HTMLElement>(selector);
  24. if (!el) throw new Error(`missing ${selector}`);
  25. return el;
  26. }
  27. async function expectInsideCard(root: HTMLElement, selector: string) {
  28. const card = part(root, '.sponsor-card').getBoundingClientRect();
  29. const box = part(root, selector).getBoundingClientRect();
  30. await expect(box.left).toBeGreaterThanOrEqual(card.left - 0.5);
  31. await expect(box.right).toBeLessThanOrEqual(card.right + 0.5);
  32. }
  33. async function expectNotClipped(root: HTMLElement, selector: string) {
  34. const el = part(root, selector);
  35. await expect(el.scrollWidth).toBeLessThanOrEqual(el.clientWidth + 1);
  36. await expect(el.scrollHeight).toBeLessThanOrEqual(el.clientHeight + 1);
  37. }
  38. const meta = {
  39. title: 'Sponsor/SponsorCard',
  40. component: SponsorCard,
  41. tags: ['autodocs'],
  42. parameters: {
  43. docs: {
  44. description: {
  45. component:
  46. 'Paid sponsor placement fed by the project sponsors.json. Always labelled as a sponsor; links open in a new tab with rel="sponsored".',
  47. },
  48. },
  49. },
  50. argTypes: {
  51. sponsor: { description: 'Sponsor entry from GET /sponsors.' },
  52. variant: {
  53. description: 'banner (dashboard), compact (login) or card (Sponsors page).',
  54. },
  55. onClose: { description: 'When set, shows a close button (temporary dismiss).' },
  56. },
  57. args: { sponsor },
  58. } satisfies Meta<typeof SponsorCard>;
  59. export default meta;
  60. type Story = StoryObj<typeof meta>;
  61. export const Banner: Story = {
  62. args: { variant: 'banner', onClose: () => {} },
  63. play: async ({ canvasElement }) => {
  64. const canvas = within(canvasElement);
  65. await expect(canvas.getByText('Sponsor')).toBeInTheDocument();
  66. await expect(canvas.getByRole('link')).toHaveAttribute('rel', 'noopener noreferrer sponsored');
  67. },
  68. };
  69. export const Compact: Story = {
  70. args: { variant: 'compact', onClose: () => {} },
  71. render: (args) => (
  72. <div style={{ width: 334 }}>
  73. <SponsorCard {...args} />
  74. </div>
  75. ),
  76. };
  77. export const Card: Story = {
  78. args: { variant: 'card' },
  79. render: (args) => (
  80. <div style={{ width: 320 }}>
  81. <SponsorCard {...args} />
  82. </div>
  83. ),
  84. };
  85. export const BannerLongCopy: Story = {
  86. args: { variant: 'banner', sponsor: longSponsor, onClose: () => {} },
  87. render: (args) => (
  88. <div style={{ width: 360 }}>
  89. <SponsorCard {...args} />
  90. </div>
  91. ),
  92. play: async ({ canvasElement }) => {
  93. await expectNotClipped(canvasElement, '.sponsor-title');
  94. },
  95. };
  96. export const LoginLongCopy: Story = {
  97. args: { variant: 'compact', sponsor: longSponsor, onClose: () => {} },
  98. render: (args) => (
  99. <div style={{ width: 334 }}>
  100. <SponsorCard {...args} />
  101. </div>
  102. ),
  103. play: async ({ canvasElement }) => {
  104. await expectNotClipped(canvasElement, '.sponsor-title');
  105. await expectNotClipped(canvasElement, '.sponsor-text');
  106. await expect(part(canvasElement, '.sponsor-main')).toHaveAttribute(
  107. 'title',
  108. `${longSponsor.title.en}\n${longSponsor.text.en}`,
  109. );
  110. },
  111. };
  112. export const PageLongCopy: Story = {
  113. args: { variant: 'card', sponsor: longSponsor },
  114. render: (args) => (
  115. <div style={{ width: 320 }}>
  116. <SponsorCard {...args} />
  117. </div>
  118. ),
  119. play: async ({ canvasElement }) => {
  120. await expectInsideCard(canvasElement, '.sponsor-title');
  121. await expectInsideCard(canvasElement, '.sponsor-text');
  122. await expectNotClipped(canvasElement, '.sponsor-title');
  123. },
  124. };