happ-settings-presets.test.tsx 7.0 KB

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