clients-row-cells-memo.test.tsx 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. import { useState } from 'react';
  2. import { render, screen } from '@testing-library/react';
  3. import userEvent from '@testing-library/user-event';
  4. import { describe, expect, it, vi } from 'vitest';
  5. import { ClientInboundChips, ClientRowActions } from '@/pages/clients/RowCells';
  6. import type { InboundOption } from '@/hooks/useClients';
  7. const PROTOCOL_COLORS = { vless: 'blue', trojan: 'volcano' };
  8. // Counts how often the cell reads the inbound map, which happens once per chip
  9. // per render. A traffic push re-renders the row, so if the cell is not memoised
  10. // this climbs every five seconds for every visible row.
  11. function countingInboundMap(source: Record<number, InboundOption>) {
  12. const reads = { count: 0 };
  13. const proxy = new Proxy(source, {
  14. get(target, key) {
  15. if (typeof key === 'string' && /^\d+$/.test(key)) reads.count += 1;
  16. return target[key as unknown as number];
  17. },
  18. });
  19. return { proxy, reads };
  20. }
  21. const INBOUNDS: Record<number, InboundOption> = {
  22. 1: { id: 1, tag: 'in-vless', remark: 'DE', protocol: 'vless' },
  23. 2: { id: 2, tag: 'in-trojan', remark: 'NL', protocol: 'trojan' },
  24. };
  25. function Harness({ children }: { children: (bump: () => void) => React.ReactNode }) {
  26. const [, setTick] = useState(0);
  27. return <>{children(() => setTick((n) => n + 1))}</>;
  28. }
  29. describe('clients table row cells', () => {
  30. it('does not re-render the inbound chips when the row re-renders with the same attachments', async () => {
  31. const { proxy, reads } = countingInboundMap(INBOUNDS);
  32. const ids = [1, 2];
  33. let bump: () => void = () => {};
  34. render(
  35. <Harness>
  36. {(doBump) => {
  37. bump = doBump;
  38. return (
  39. <ClientInboundChips ids={ids} inboundsById={proxy} protocolColors={PROTOCOL_COLORS} chipLimit={1} />
  40. );
  41. }}
  42. </Harness>,
  43. );
  44. const afterFirstRender = reads.count;
  45. expect(afterFirstRender).toBeGreaterThan(0);
  46. // Three simulated traffic pushes: the parent re-renders, the props do not change.
  47. for (let i = 0; i < 3; i++) bump();
  48. await Promise.resolve();
  49. expect(reads.count).toBe(afterFirstRender);
  50. });
  51. it('re-renders the chips when the attachments actually change', async () => {
  52. const { proxy, reads } = countingInboundMap(INBOUNDS);
  53. function Swapper() {
  54. const [ids, setIds] = useState<number[]>([1]);
  55. return (
  56. <>
  57. <button type="button" onClick={() => setIds([1, 2])}>swap</button>
  58. <ClientInboundChips ids={ids} inboundsById={proxy} protocolColors={PROTOCOL_COLORS} chipLimit={1} />
  59. </>
  60. );
  61. }
  62. render(<Swapper />);
  63. const before = reads.count;
  64. await userEvent.click(screen.getByRole('button', { name: 'swap' }));
  65. expect(reads.count).toBeGreaterThan(before);
  66. });
  67. it('keeps the row actions wired to the right client across re-renders', async () => {
  68. const onShowQr = vi.fn();
  69. const onEdit = vi.fn();
  70. const noop = vi.fn();
  71. let bump: () => void = () => {};
  72. render(
  73. <Harness>
  74. {(doBump) => {
  75. bump = doBump;
  76. return (
  77. <ClientRowActions
  78. email="alice@x"
  79. onShowQr={onShowQr}
  80. onShowInfo={noop}
  81. onResetTraffic={noop}
  82. onEdit={onEdit}
  83. onDelete={noop}
  84. />
  85. );
  86. }}
  87. </Harness>,
  88. );
  89. for (let i = 0; i < 3; i++) bump();
  90. // Queried by position rather than label: the suite loads the real en-US
  91. // bundle, so the aria-labels are translated strings, not keys. Order is
  92. // QR, info, reset traffic, edit, delete.
  93. const buttons = screen.getAllByRole('button');
  94. expect(buttons).toHaveLength(5);
  95. await userEvent.click(buttons[0]);
  96. await userEvent.click(buttons[3]);
  97. expect(onShowQr).toHaveBeenCalledExactlyOnceWith('alice@x');
  98. expect(onEdit).toHaveBeenCalledExactlyOnceWith('alice@x');
  99. });
  100. });