mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-27 21:47:14 +00:00
feat(frontend): make Storybook a validated, fully covered component workbench
Storybook existed only as an undocumented local tool: 9 of 24 reusable components had stories, autodocs pages were bare prop tables, nothing built or tested the stories, and no contributor doc mentioned the workbench existed. Every reusable component under src/components/ now has a co-located story with enriched autodocs (component descriptions plus per-prop argTypes, kept as string metadata since the repo bans line comments). Stories double as headless Chromium tests through the Storybook vitest addon, with axe accessibility checks enforced as errors and play-function interaction tests covering the modals, the RHF field bridge, the config block, and the select-all buttons. The preview now mirrors the panel's real theme DOM (body class, shared AntD theme config, seeded theme storage) so what stories render matches production. CI and make verify gain a static Storybook build as a compile gate, and the frontend test job installs Chromium so story tests run on every PR. Contributor docs (frontend README, CONTRIBUTING, agent guides) document the workbench, the story conventions, and the Controls setup. Node engines move to 24 LTS and gen:api drops the type-stripping flags that Node 24 makes default.
This commit is contained in:
@@ -6,6 +6,17 @@ const meta = {
|
||||
title: 'Clients/ClientSpeedTag',
|
||||
component: ClientSpeedTag,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'Blue tag showing a live upload/download speed for one client, formatted for readability. Shown next to online clients that have active traffic.',
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
speed: { description: 'Live upload/download rate in bytes per second (`{ up, down }`).' },
|
||||
},
|
||||
} satisfies Meta<typeof ClientSpeedTag>;
|
||||
|
||||
export default meta;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import { ThemeProvider } from '@/hooks/useTheme';
|
||||
|
||||
import ClientTrafficCell from './ClientTrafficCell';
|
||||
|
||||
const GiB = 1024 ** 3;
|
||||
|
||||
const meta = {
|
||||
title: 'Clients/ClientTrafficCell',
|
||||
component: ClientTrafficCell,
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<ThemeProvider>
|
||||
<Story />
|
||||
</ThemeProvider>
|
||||
),
|
||||
],
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'Traffic usage cell for the clients table: used bytes, a color-coded progress bar, and the quota (or an infinity icon for unlimited clients), with an upload/download/remaining breakdown in a hover popover.',
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
up: { description: 'Uploaded bytes counted against the client.' },
|
||||
down: { description: 'Downloaded bytes counted against the client.' },
|
||||
total: { description: 'Traffic quota in bytes; 0 or less renders as unlimited.' },
|
||||
enabled: { description: 'Grays the bar out when the client is disabled.' },
|
||||
trafficDiff: { description: 'Headroom in bytes below the quota at which the bar shifts from green to orange.' },
|
||||
compact: { description: 'Smaller bar and tighter layout for dense table rows.' },
|
||||
},
|
||||
} satisfies Meta<typeof ClientTrafficCell>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
up: 6 * GiB,
|
||||
down: 35 * GiB,
|
||||
total: 100 * GiB,
|
||||
trafficDiff: 5 * GiB,
|
||||
},
|
||||
};
|
||||
|
||||
export const Unlimited: Story = {
|
||||
args: {
|
||||
up: 87 * GiB,
|
||||
down: 940 * GiB,
|
||||
total: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const Depleted: Story = {
|
||||
args: {
|
||||
up: 9 * GiB,
|
||||
down: 42 * GiB,
|
||||
total: 50 * GiB,
|
||||
trafficDiff: 5 * GiB,
|
||||
},
|
||||
};
|
||||
|
||||
export const DisabledCompact: Story = {
|
||||
args: {
|
||||
up: 2 * GiB,
|
||||
down: 11 * GiB,
|
||||
total: 40 * GiB,
|
||||
enabled: false,
|
||||
compact: true,
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { expect, waitFor } from 'storybook/test';
|
||||
|
||||
import ConfigBlock from './ConfigBlock';
|
||||
|
||||
@@ -6,7 +7,24 @@ const meta = {
|
||||
title: 'Clients/ConfigBlock',
|
||||
component: ConfigBlock,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'padded' },
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'Collapsible panel that displays a client config or share link with copy, download, and QR-code actions. Used on the clients and inbounds pages to present generated links.',
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
label: { description: 'Protocol/type badge shown on the panel header (e.g. `vless`, `trojan`).' },
|
||||
text: { description: 'The config or share-link text to display, copy, download, and encode as a QR code.' },
|
||||
fileName: { description: 'File name used when downloading the text.' },
|
||||
qrRemark: { description: 'Optional remark embedded in the QR panel; falls back to `label`.' },
|
||||
showQr: { description: 'Whether to show the QR-code action button.' },
|
||||
tagColor: { description: 'Ant Design tag color for the header badge.' },
|
||||
defaultOpen: { description: 'Whether the panel starts expanded.' },
|
||||
},
|
||||
} satisfies Meta<typeof ConfigBlock>;
|
||||
|
||||
export default meta;
|
||||
@@ -18,6 +36,15 @@ const sampleLink = 'vless://11112222-3333-4444-5555-666677778888@panel.example.c
|
||||
|
||||
export const Collapsed: Story = {
|
||||
args: { label: 'vless', text: sampleLink, fileName: 'client-config.txt' },
|
||||
play: async ({ canvas, userEvent }) => {
|
||||
await expect(canvas.queryByText(/vless:\/\/11112222/)).not.toBeInTheDocument();
|
||||
await userEvent.click(canvas.getByText('vless'));
|
||||
const configText = await canvas.findByText(/vless:\/\/11112222/);
|
||||
await waitFor(() => expect(configText).toBeVisible());
|
||||
await expect(canvas.getByRole('button', { name: 'Copy' })).toBeVisible();
|
||||
await expect(canvas.getByRole('button', { name: 'Download' })).toBeVisible();
|
||||
await expect(canvas.getByRole('button', { name: 'QR Code' })).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const Expanded: Story = {
|
||||
|
||||
Reference in New Issue
Block a user