Просмотр исходного кода

ref(abstractExternalIssueForm): drop fetch-mock dep and use api client (#52617)

Jonas 1 год назад
Родитель
Сommit
7e1c97a97d

+ 0 - 1
package.json

@@ -190,7 +190,6 @@
     "jest-canvas-mock": "^2.4.0",
     "jest-environment-jsdom": "^29.4.1",
     "jest-fail-on-console": "^3.0.2",
-    "jest-fetch-mock": "^3.0.3",
     "jest-junit": "15.0.0",
     "jest-sentry-environment": "3.0.0",
     "postcss-jsx": "0.36.4",

+ 9 - 3
static/app/__mocks__/api.tsx

@@ -79,6 +79,15 @@ afterEach(() => {
 });
 
 class Client implements ApiNamespace.Client {
+  activeRequests: Record<string, ApiNamespace.Request> = {};
+  baseUrl = '';
+  // uses the default client json headers. Sadly, we cannot refernce the real client
+  // because it will cause a circular dependency and explode, hence the copy/paste
+  headers = {
+    Accept: 'application/json; charset=utf-8',
+    'Content-Type': 'application/json',
+  };
+
   static mockResponses: MockResponse[] = [];
 
   /**
@@ -160,9 +169,6 @@ class Client implements ApiNamespace.Client {
     });
   }
 
-  activeRequests: Record<string, ApiNamespace.Request> = {};
-  baseUrl = '';
-
   uniqueId() {
     return '123';
   }

+ 13 - 5
static/app/api.tsx

@@ -243,6 +243,11 @@ type ClientOptions = {
    * The base URL path to prepend to API request URIs.
    */
   baseUrl?: string;
+
+  /**
+   * Base set of headers to apply to each request
+   */
+  headers?: HeadersInit;
 };
 
 type HandleRequestErrorOptions = {
@@ -259,9 +264,16 @@ type HandleRequestErrorOptions = {
 export class Client {
   baseUrl: string;
   activeRequests: Record<string, Request>;
+  headers: HeadersInit;
+
+  static JSON_HEADERS = {
+    Accept: 'application/json; charset=utf-8',
+    'Content-Type': 'application/json',
+  };
 
   constructor(options: ClientOptions = {}) {
     this.baseUrl = options.baseUrl ?? '/api/0';
+    this.headers = options.headers ?? Client.JSON_HEADERS;
     this.activeRequests = {};
   }
 
@@ -432,11 +444,7 @@ export class Client {
     // GET requests may not have a body
     const body = method !== 'GET' ? data : undefined;
 
-    const headers = new Headers({
-      Accept: 'application/json; charset=utf-8',
-      'Content-Type': 'application/json',
-      ...options.headers,
-    });
+    const headers = new Headers(this.headers);
 
     // Do not set the X-CSRFToken header when making a request outside of the
     // current domain. Because we use subdomains we loosely compare origins

+ 9 - 2
static/app/components/externalIssues/abstractExternalIssueForm.tsx

@@ -3,6 +3,7 @@ import debounce from 'lodash/debounce';
 import * as qs from 'query-string';
 
 import {ModalRenderProps} from 'sentry/actionCreators/modal';
+import {Client} from 'sentry/api';
 import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
 import FieldFromConfig from 'sentry/components/forms/fieldFromConfig';
 import Form, {FormProps} from 'sentry/components/forms/form';
@@ -40,6 +41,10 @@ type State = {
   integrationDetails: IntegrationIssueConfig | null;
 } & DeprecatedAsyncComponent['state'];
 
+// This exists because /extensions/type/search API is not prefixed with
+// /api/0/, but the default API client on the abstract issue form is...
+const API_CLIENT = new Client({baseUrl: '', headers: {}});
+
 const DEBOUNCE_MS = 200;
 /**
  * @abstract
@@ -171,6 +176,7 @@ export default class AbstractExternalIssueForm<
     const currentOption = this.getDefaultOptions(field).find(
       option => option.value === this.model.getValue(field.name)
     );
+
     if (!currentOption) {
       return result;
     }
@@ -239,9 +245,10 @@ export default class AbstractExternalIssueForm<
       const separator = url.includes('?') ? '&' : '?';
       // We can't use the API client here since the URL is not scoped under the
       // API endpoints (which the client prefixes)
+
       try {
-        const response = await fetch(url + separator + query);
-        cb(null, response.ok ? await response.json() : []);
+        const response = await API_CLIENT.requestPromise(url + separator + query);
+        cb(null, response);
       } catch (err) {
         cb(err);
       }

+ 16 - 32
static/app/components/group/externalIssueForm.spec.tsx

@@ -154,51 +154,35 @@ describe('ExternalIssueForm', () => {
 
     describe('options loaded', () => {
       beforeEach(() => {
-        const mockFetchPromise = () =>
-          new Promise(resolve => {
-            setTimeout(() => {
-              resolve({
-                json: () =>
-                  Promise.resolve([
-                    {
-                      label: '#1337 ref(js): Convert Form to a FC',
-                      value: 1337,
-                    },
-                    {
-                      label: '#2345 perf: Make it faster',
-                      value: 2345,
-                    },
-                  ]),
-                ok: true,
-              });
-            }, 50);
-          });
-
-        window.fetch = jest.fn().mockImplementation(mockFetchPromise);
-
         MockApiClient.addMockResponse({
           url: `/groups/${group.id}/integrations/${integration.id}/?action=link`,
           body: formConfig,
         });
       });
 
-      afterEach(() => {
-        (window.fetch as jest.Mock).mockClear();
-        (window.fetch as jest.Mock | undefined) = undefined;
-      });
-
       it('fast typing is debounced and uses trailing call when fetching data', async () => {
+        const searchResponse = MockApiClient.addMockResponse({
+          url: '/search?field=externalIssue&query=faster&repo=scefali%2Ftest',
+          body: [
+            {
+              label: '#1337 ref(js): Convert Form to a FC',
+              value: 1337,
+            },
+            {
+              label: '#2345 perf: Make it faster',
+              value: 2345,
+            },
+          ],
+        });
+
         await renderComponent('Link');
         jest.useFakeTimers();
         const textbox = screen.getByRole('textbox', {name: 'Issue'});
         await userEvent.click(textbox, {delay: null});
         await userEvent.type(textbox, 'faster', {delay: null});
-        expect(window.fetch).toHaveBeenCalledTimes(0);
+        expect(searchResponse).not.toHaveBeenCalled();
         jest.advanceTimersByTime(300);
-        expect(window.fetch).toHaveBeenCalledTimes(1);
-        expect(window.fetch).toHaveBeenCalledWith(
-          '/search?field=externalIssue&query=faster&repo=scefali%2Ftest'
-        );
+        expect(searchResponse).toHaveBeenCalledTimes(1);
         expect(await screen.findByText('#2345 perf: Make it faster')).toBeInTheDocument();
       });
     });

+ 12 - 25
static/app/views/alerts/rules/issue/ticketRuleModal.spec.tsx

@@ -1,6 +1,5 @@
 import selectEvent from 'react-select-event';
 import styled from '@emotion/styled';
-import fetchMock from 'jest-fetch-mock';
 
 import {initializeOrg} from 'sentry-test/initializeOrg';
 import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
@@ -22,10 +21,6 @@ describe('ProjectAlerts -> TicketRuleModal', function () {
     Footer: p => p.children,
   };
 
-  beforeEach(function () {
-    fetchMock.enableMocks();
-  });
-
   afterEach(function () {
     closeModal.mockReset();
     MockApiClient.clearMockResponses();
@@ -82,23 +77,6 @@ describe('ProjectAlerts -> TicketRuleModal', function () {
     });
   };
 
-  /**
-   * We need to use this alternate mocking scheme because `fetch` isn't available.
-   * @param names String[]
-   */
-  const addMockUsersAPICall = (names: string[] = []) => {
-    (fetch as any).mockResponseOnce(
-      JSON.stringify(
-        names.map(name => {
-          return {
-            label: name,
-            value: name,
-          };
-        })
-      )
-    );
-  };
-
   const renderComponent = (props: Partial<IssueAlertRuleAction> = {}) => {
     const {organization, routerContext} = initializeOrg();
     addMockConfigsAPICall({
@@ -185,12 +163,21 @@ describe('ProjectAlerts -> TicketRuleModal', function () {
 
       await selectEvent.select(screen.getByRole('textbox', {name: 'Issue Type'}), 'Epic');
 
-      addMockUsersAPICall(['Marcos']);
+      // Component makes 1 request per character typed.
+      let txt = '';
+      for (const char of 'Joe') {
+        txt += char;
+        MockApiClient.addMockResponse({
+          url: `http://example.com?field=assignee&issuetype=10001&project=10000&query=${txt}`,
+          method: 'GET',
+          body: [{label: 'Joe', value: 'Joe'}],
+        });
+      }
 
       const menu = screen.getByRole('textbox', {name: 'Assignee'});
       selectEvent.openMenu(menu);
-      await userEvent.type(menu, 'Marc{Escape}');
-      await selectEvent.select(menu, 'Marcos');
+      await userEvent.type(menu, 'Joe{Escape}');
+      await selectEvent.select(menu, 'Joe');
 
       await submitSuccess();
     });

+ 1 - 1
webpack.config.ts

@@ -654,7 +654,7 @@ if (IS_UI_DEV_ONLY) {
     },
     proxy: [
       {
-        context: ['/api/', '/avatar/', '/organization-avatar/'],
+        context: ['/api/', '/avatar/', '/organization-avatar/', '/extensions/'],
         target: 'https://sentry.io',
         secure: false,
         changeOrigin: true,

+ 0 - 45
yarn.lock

@@ -4653,13 +4653,6 @@ cronstrue@^2.26.0:
   resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-2.26.0.tgz#c07c579b51763e26d4fb5c2693f09d0686b7010a"
   integrity sha512-M1VdV3hpBAsd1Zzvqcvf63wgDpcwCuS4WiNEVFpJ0s33MGO2sVDTfswYq0EPypCmESrCzmgL8h68DTzJuSDbVA==
 
-cross-fetch@^3.0.4:
-  version "3.1.5"
-  resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
-  integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
-  dependencies:
-    node-fetch "2.6.7"
-
 cross-spawn@^7.0.2, cross-spawn@^7.0.3:
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@@ -7157,14 +7150,6 @@ jest-fail-on-console@^3.0.2:
   dependencies:
     chalk "^4.1.0"
 
-jest-fetch-mock@^3.0.3:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz#31749c456ae27b8919d69824f1c2bd85fe0a1f3b"
-  integrity sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==
-  dependencies:
-    cross-fetch "^3.0.4"
-    promise-polyfill "^8.1.3"
-
 jest-get-type@^29.2.0:
   version "29.2.0"
   resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.2.0.tgz#726646f927ef61d583a3b3adb1ab13f3a5036408"
@@ -8257,13 +8242,6 @@ node-fetch@2.6.1:
   resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
   integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
 
-node-fetch@2.6.7:
-  version "2.6.7"
-  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
-  integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
-  dependencies:
-    whatwg-url "^5.0.0"
-
 node-fetch@^1.0.1:
   version "1.7.3"
   resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
@@ -9087,11 +9065,6 @@ promise-inflight@^1.0.1:
   resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
   integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==
 
-promise-polyfill@^8.1.3:
-  version "8.2.0"
-  resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.2.0.tgz#367394726da7561457aba2133c9ceefbd6267da0"
-  integrity sha512-k/TC0mIcPVF6yHhUvwAp7cvL6I2fFV7TzF1DuGPI8mBh4QQazf36xCKEHKTZKRysEoTQoQdKyP25J8MPJp7j5g==
-
 promise-retry@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22"
@@ -10581,11 +10554,6 @@ tr46@^3.0.0:
   dependencies:
     punycode "^2.1.1"
 
-tr46@~0.0.3:
-  version "0.0.3"
-  resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
-  integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
-
 trim-newlines@^3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
@@ -10950,11 +10918,6 @@ wbuf@^1.1.0, wbuf@^1.7.3:
   dependencies:
     minimalistic-assert "^1.0.0"
 
-webidl-conversions@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
-  integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
-
 webidl-conversions@^7.0.0:
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a"
@@ -11115,14 +11078,6 @@ whatwg-url@^11.0.0:
     tr46 "^3.0.0"
     webidl-conversions "^7.0.0"
 
-whatwg-url@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
-  integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
-  dependencies:
-    tr46 "~0.0.3"
-    webidl-conversions "^3.0.0"
-
 which-boxed-primitive@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"