mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-11 15:45:00 +03:00
89e200ead4
Zod 4: use the `error` param instead of the deprecated `message`. lint:deprecated missed these because tsgolint's no-deprecated does not resolve object-literal properties on a `string | Params` union. Geodata: key geo entry rows by page position. antd deprecates rowKey's index argument, and kind:value repeats within a page because the reader drops domain attributes (22 pairs in geosite_IR.dat, 108 in geosite_RU). Nord/PIA: the "All cities/regions" option used a null value, which antd warns on. Map it through a sentinel at the Select boundary so form state stays null, with tests that fail when the sentinel is not mapped back. Tests: - Run the oxlint guard through node; .bin/oxlint is a sh shim Windows cannot spawn, and the swallowed error left both guard cases vacuous. - Start unit workers with --no-experimental-webstorage; msw's localStorage probe made Node 25+ warn once per forked worker. - Set IS_REACT_ACT_ENVIRONMENT, which RTL never sets with globals: false, and settle the async updates it exposed inside act(). The row-cells memo test now fails when memo is removed. - Disable antd's click wave in Storybook; it re-rendered inside the next story's act() and tripped "not configured to support act". - Assert InboundFormModal's validation log instead of leaking it, and give the rule-form test a well-formed clients/list response.
133 lines
4.0 KiB
TypeScript
133 lines
4.0 KiB
TypeScript
import { useState } from 'react';
|
|
import { act, render, screen } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import { ClientInboundChips, ClientRowActions } from '@/pages/clients/RowCells';
|
|
import type { InboundOption } from '@/hooks/useClients';
|
|
|
|
const PROTOCOL_COLORS = { vless: 'blue', trojan: 'volcano' };
|
|
|
|
// Counts how often the cell reads the inbound map, which happens once per chip
|
|
// per render. A traffic push re-renders the row, so if the cell is not memoised
|
|
// this climbs every five seconds for every visible row.
|
|
function countingInboundMap(source: Record<number, InboundOption>) {
|
|
const reads = { count: 0 };
|
|
const proxy = new Proxy(source, {
|
|
get(target, key) {
|
|
if (typeof key === 'string' && /^\d+$/.test(key)) reads.count += 1;
|
|
return target[key as unknown as number];
|
|
},
|
|
});
|
|
return { proxy, reads };
|
|
}
|
|
|
|
const INBOUNDS: Record<number, InboundOption> = {
|
|
1: { id: 1, tag: 'in-vless', remark: 'DE', protocol: 'vless' },
|
|
2: { id: 2, tag: 'in-trojan', remark: 'NL', protocol: 'trojan' },
|
|
};
|
|
|
|
function Harness({ children }: { children: (bump: () => void) => React.ReactNode }) {
|
|
const [, setTick] = useState(0);
|
|
return <>{children(() => setTick((n) => n + 1))}</>;
|
|
}
|
|
|
|
describe('clients table row cells', () => {
|
|
it('does not re-render the inbound chips when the row re-renders with the same attachments', async () => {
|
|
const { proxy, reads } = countingInboundMap(INBOUNDS);
|
|
const ids = [1, 2];
|
|
let bump: () => void = () => {};
|
|
|
|
render(
|
|
<Harness>
|
|
{(doBump) => {
|
|
bump = doBump;
|
|
return (
|
|
<ClientInboundChips
|
|
ids={ids}
|
|
inboundsById={proxy}
|
|
protocolColors={PROTOCOL_COLORS}
|
|
chipLimit={1}
|
|
/>
|
|
);
|
|
}}
|
|
</Harness>,
|
|
);
|
|
|
|
const afterFirstRender = reads.count;
|
|
expect(afterFirstRender).toBeGreaterThan(0);
|
|
|
|
// Three simulated traffic pushes: the parent re-renders, the props do not change.
|
|
act(() => {
|
|
for (let i = 0; i < 3; i++) bump();
|
|
});
|
|
|
|
expect(reads.count).toBe(afterFirstRender);
|
|
});
|
|
|
|
it('re-renders the chips when the attachments actually change', async () => {
|
|
const { proxy, reads } = countingInboundMap(INBOUNDS);
|
|
|
|
function Swapper() {
|
|
const [ids, setIds] = useState<number[]>([1]);
|
|
return (
|
|
<>
|
|
<button type="button" onClick={() => setIds([1, 2])}>
|
|
swap
|
|
</button>
|
|
<ClientInboundChips
|
|
ids={ids}
|
|
inboundsById={proxy}
|
|
protocolColors={PROTOCOL_COLORS}
|
|
chipLimit={1}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
render(<Swapper />);
|
|
const before = reads.count;
|
|
await userEvent.click(screen.getByRole('button', { name: 'swap' }));
|
|
expect(reads.count).toBeGreaterThan(before);
|
|
});
|
|
|
|
it('keeps the row actions wired to the right client across re-renders', async () => {
|
|
const onShowQr = vi.fn();
|
|
const onEdit = vi.fn();
|
|
const noop = vi.fn();
|
|
let bump: () => void = () => {};
|
|
|
|
render(
|
|
<Harness>
|
|
{(doBump) => {
|
|
bump = doBump;
|
|
return (
|
|
<ClientRowActions
|
|
email="alice@x"
|
|
onShowQr={onShowQr}
|
|
onShowInfo={noop}
|
|
onResetTraffic={noop}
|
|
onEdit={onEdit}
|
|
onDelete={noop}
|
|
/>
|
|
);
|
|
}}
|
|
</Harness>,
|
|
);
|
|
|
|
act(() => {
|
|
for (let i = 0; i < 3; i++) bump();
|
|
});
|
|
|
|
// Queried by position rather than label: the suite loads the real en-US
|
|
// bundle, so the aria-labels are translated strings, not keys. Order is
|
|
// QR, info, reset traffic, edit, delete.
|
|
const buttons = screen.getAllByRole('button');
|
|
expect(buttons).toHaveLength(5);
|
|
await userEvent.click(buttons[0]);
|
|
await userEvent.click(buttons[3]);
|
|
|
|
expect(onShowQr).toHaveBeenCalledExactlyOnceWith('alice@x');
|
|
expect(onEdit).toHaveBeenCalledExactlyOnceWith('alice@x');
|
|
});
|
|
});
|