happ-settings-presets.test.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import { useState } from 'react';
  2. import { describe, expect, it, vi } from 'vitest';
  3. import { fireEvent, screen } from '@testing-library/react';
  4. import { message } from 'antd';
  5. import { AllSetting } from '@/models/setting';
  6. import HappSettingsContent from '@/pages/settings/HappSettingsContent';
  7. import { renderWithProviders } from './test-utils';
  8. vi.spyOn(message, 'success').mockImplementation(() => undefined as never);
  9. const chinaProfile = {
  10. Name: 'Bypass-CN',
  11. GlobalProxy: 'true',
  12. RouteOrder: 'block-proxy-direct',
  13. RemoteDNSType: 'DoH',
  14. RemoteDNSDomain: 'https://cloudflare-dns.com/dns-query',
  15. RemoteDNSIP: '1.1.1.1',
  16. DomesticDNSType: 'DoH',
  17. DomesticDNSDomain: 'https://dns.alidns.com/dns-query',
  18. DomesticDNSIP: '223.5.5.5',
  19. DnsHosts: {
  20. 'cloudflare-dns.com': '1.1.1.1',
  21. 'dns.alidns.com': '223.5.5.5',
  22. },
  23. DirectSites: ['geosite:private', 'geosite:cn', 'geosite:geolocation-cn'],
  24. DirectIp: [
  25. 'geoip:cn',
  26. 'geoip:private',
  27. '127.0.0.0/8',
  28. '10.0.0.0/8',
  29. '172.16.0.0/12',
  30. '192.168.0.0/16',
  31. '169.254.0.0/16',
  32. '224.0.0.0/4',
  33. '255.255.255.255',
  34. ],
  35. ProxySites: [],
  36. ProxyIp: [],
  37. BlockSites: [],
  38. BlockIp: [],
  39. DomainStrategy: 'IPIfNonMatch',
  40. FakeDNS: 'false',
  41. UseChunkFiles: 'true',
  42. };
  43. function renderSettings(isMobile = false) {
  44. const updateSetting = vi.fn<(patch: Partial<AllSetting>) => void>();
  45. function SettingsHarness() {
  46. const [allSetting, setAllSetting] = useState(() => {
  47. const settings = new AllSetting();
  48. settings.subRoutingRules = 'https://example.com/existing-routing';
  49. return settings;
  50. });
  51. return (
  52. <HappSettingsContent
  53. allSetting={allSetting}
  54. updateSetting={(patch) => {
  55. updateSetting(patch);
  56. setAllSetting((previous) => Object.assign(new AllSetting(), previous, patch));
  57. }}
  58. isMobile={isMobile}
  59. remoteSourceBadge={() => null}
  60. />
  61. );
  62. }
  63. renderWithProviders(<SettingsHarness />);
  64. return updateSetting;
  65. }
  66. function openPresets() {
  67. const select = screen.getByRole('combobox').closest('.ant-select');
  68. if (!select) throw new Error('Routing preset select not found');
  69. fireEvent.mouseDown(select.querySelector('.ant-select-selector') ?? select);
  70. return Array.from(
  71. document.querySelectorAll(
  72. '.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option',
  73. ),
  74. );
  75. }
  76. function choosePreset(label: string) {
  77. const option = openPresets().find((item) => item.textContent === label);
  78. if (!option) throw new Error(`Routing preset '${label}' not found`);
  79. fireEvent.click(option);
  80. }
  81. function routingRules() {
  82. return (screen.getByRole('textbox', { name: 'Routing rules' }) as HTMLTextAreaElement).value;
  83. }
  84. function routingProfile(): Record<string, unknown> {
  85. const prefix = 'happ://routing/onadd/';
  86. const link = routingRules();
  87. expect(link.startsWith(prefix)).toBe(true);
  88. return JSON.parse(atob(link.slice(prefix.length))) as Record<string, unknown>;
  89. }
  90. describe('Happ routing preset controls', () => {
  91. it('keeps selecting and toggling local until Apply updates the routing rules', () => {
  92. const updateSetting = renderSettings();
  93. choosePreset('China Direct (Bypass-CN)');
  94. const includeAdblock = screen.getByRole('switch', { name: 'Include AdBlock' });
  95. expect(includeAdblock.getAttribute('aria-checked')).toBe('false');
  96. fireEvent.click(includeAdblock);
  97. expect(includeAdblock.getAttribute('aria-checked')).toBe('true');
  98. expect(updateSetting).not.toHaveBeenCalled();
  99. expect(routingRules()).toBe('https://example.com/existing-routing');
  100. fireEvent.click(screen.getByRole('button', { name: 'Apply preset' }));
  101. expect(updateSetting).toHaveBeenCalledExactlyOnceWith({ subRoutingRules: routingRules() });
  102. expect(routingProfile().BlockSites).toEqual(['geosite:category-ads-all']);
  103. });
  104. it.each([
  105. ['Iran Bypass', 'Iran Bypass'],
  106. ['China Direct (Bypass-CN)', 'Bypass-CN'],
  107. ['Full Proxy', 'Global Proxy'],
  108. ['Global Bypass Local Network', 'Global Bypass Local Network'],
  109. ])('applies %s without AdBlock by default', (label, profileName) => {
  110. renderSettings();
  111. choosePreset(label);
  112. expect(
  113. screen.getByRole('switch', { name: 'Include AdBlock' }).getAttribute('aria-checked'),
  114. ).toBe('false');
  115. fireEvent.click(screen.getByRole('button', { name: 'Apply preset' }));
  116. expect(routingProfile()).toMatchObject({ Name: profileName, BlockSites: [] });
  117. });
  118. it('keeps LAN bypass separate from Full Proxy when switching presets', () => {
  119. renderSettings();
  120. choosePreset('Global Bypass Local Network');
  121. fireEvent.click(screen.getByRole('switch', { name: 'Include AdBlock' }));
  122. fireEvent.click(screen.getByRole('button', { name: 'Apply preset' }));
  123. expect(routingProfile()).toMatchObject({
  124. Name: 'Global Bypass Local Network',
  125. GlobalProxy: 'true',
  126. DirectSites: ['geosite:private'],
  127. DirectIp: ['geoip:private'],
  128. BlockSites: ['geosite:category-ads-all'],
  129. });
  130. choosePreset('Full Proxy');
  131. fireEvent.click(screen.getByRole('button', { name: 'Apply preset' }));
  132. expect(routingProfile()).toMatchObject({
  133. Name: 'Global Proxy',
  134. GlobalProxy: 'true',
  135. DirectSites: [],
  136. DirectIp: [],
  137. BlockSites: ['geosite:category-ads-all'],
  138. });
  139. });
  140. it('preserves China routing when opting in and clears AdBlock when reapplied after opting out', () => {
  141. const updateSetting = renderSettings(true);
  142. choosePreset('China Direct (Bypass-CN)');
  143. const includeAdblock = screen.getByRole('switch', { name: 'Include AdBlock' });
  144. fireEvent.click(includeAdblock);
  145. fireEvent.click(screen.getByRole('button', { name: 'Apply preset' }));
  146. expect(routingProfile()).toEqual({
  147. ...chinaProfile,
  148. BlockSites: ['geosite:category-ads-all'],
  149. });
  150. const withAdblock = routingRules();
  151. fireEvent.click(includeAdblock);
  152. expect(routingRules()).toBe(withAdblock);
  153. expect(updateSetting).toHaveBeenCalledTimes(1);
  154. fireEvent.click(screen.getByRole('button', { name: 'Apply preset' }));
  155. expect(updateSetting).toHaveBeenCalledTimes(2);
  156. expect(routingProfile()).toEqual(chinaProfile);
  157. });
  158. it('offers only base presets so AdBlock cannot replace a country preset', () => {
  159. renderSettings();
  160. expect(openPresets().map((option) => option.textContent)).toEqual([
  161. 'Iran Bypass',
  162. 'China Direct (Bypass-CN)',
  163. 'Full Proxy',
  164. 'Global Bypass Local Network',
  165. 'Disable Routing (happ://routing/off)',
  166. ]);
  167. });
  168. it('disables AdBlock for Off and applies the off link even after opting in', () => {
  169. const updateSetting = renderSettings();
  170. const includeAdblock = screen.getByRole('switch', { name: 'Include AdBlock' });
  171. fireEvent.click(includeAdblock);
  172. choosePreset('Disable Routing (happ://routing/off)');
  173. expect((includeAdblock as HTMLButtonElement).disabled).toBe(true);
  174. expect(updateSetting).not.toHaveBeenCalled();
  175. fireEvent.click(screen.getByRole('button', { name: 'Apply preset' }));
  176. expect(updateSetting).toHaveBeenCalledExactlyOnceWith({
  177. subRoutingRules: 'happ://routing/off',
  178. });
  179. expect(routingRules()).toBe('happ://routing/off');
  180. });
  181. });