Browse Source

test(ts): Cleanup typing on the mock api (#29860)

Evan Purkhiser 3 years ago
parent
commit
345e1ece07
1 changed files with 64 additions and 35 deletions
  1. 64 35
      static/app/__mocks__/api.tsx

+ 64 - 35
static/app/__mocks__/api.tsx

@@ -1,26 +1,31 @@
-import * as ImportedClient from 'app/api';
+import * as ApiNamespace from 'app/api';
 
-const RealClient: typeof ImportedClient = jest.requireActual('app/api');
+const RealApi: typeof ApiNamespace = jest.requireActual('app/api');
 
 export class Request {}
 
-export const initApiClientErrorHandling = RealClient.initApiClientErrorHandling;
+export const initApiClientErrorHandling = RealApi.initApiClientErrorHandling;
 
-const respond = (isAsync: boolean, fn, ...args): void => {
-  if (fn) {
-    if (isAsync) {
-      setTimeout(() => fn(...args), 1);
-    } else {
-      fn(...args);
-    }
+const respond = (isAsync: boolean, fn?: Function, ...args: any[]): void => {
+  if (!fn) {
+    return;
   }
+
+  if (isAsync) {
+    setTimeout(() => fn(...args), 1);
+    return;
+  }
+
+  fn(...args);
 };
 
 const DEFAULT_MOCK_RESPONSE_OPTIONS = {
   predicate: () => true,
 };
 
-type ResponseType = ImportedClient.ResponseMeta & {
+type FunctionCallback<Args extends any[] = any[]> = (...args: Args) => void;
+
+type ResponseType = ApiNamespace.ResponseMeta & {
   url: string;
   statusCode: number;
   method: string;
@@ -29,30 +34,44 @@ type ResponseType = ImportedClient.ResponseMeta & {
   headers: {[key: string]: string};
 };
 
-class Client {
-  static mockResponses: Array<
-    [
-      ResponseType,
-      jest.Mock,
-      (url: string, options: Readonly<ImportedClient.RequestOptions>) => boolean
-    ]
-  > = [];
+type MockPredicate = (url: string, opts: ApiNamespace.RequestOptions) => boolean;
+
+type MockResponseOptions = {
+  predicate: MockPredicate;
+};
+
+type MockResponse = [resp: ResponseType, mock: jest.Mock, predicate: MockPredicate];
+
+class Client implements ApiNamespace.Client {
+  static mockResponses: MockResponse[] = [];
+
+  static mockAsync = false;
 
   static clearMockResponses() {
     Client.mockResponses = [];
   }
 
   // Returns a jest mock that represents Client.request calls
-  static addMockResponse(response, options = DEFAULT_MOCK_RESPONSE_OPTIONS) {
+  static addMockResponse(
+    response: Partial<ResponseType>,
+    options: MockResponseOptions = DEFAULT_MOCK_RESPONSE_OPTIONS
+  ) {
     const mock = jest.fn();
+
     Client.mockResponses.unshift([
       {
+        url: '',
+        status: 200,
         statusCode: 200,
+        statusText: 'OK',
+        responseText: '',
+        responseJSON: '',
         body: '',
         method: 'GET',
         callCount: 0,
         ...response,
-        headers: response.headers || {},
+        headers: response.headers ?? {},
+        getResponseHeader: (key: string) => response.headers?.[key] ?? null,
       },
       mock,
       options.predicate,
@@ -61,7 +80,7 @@ class Client {
     return mock;
   }
 
-  static findMockResponse(url: string, options: Readonly<ImportedClient.RequestOptions>) {
+  static findMockResponse(url: string, options: Readonly<ApiNamespace.RequestOptions>) {
     return Client.mockResponses.find(([response, _mock, predicate]) => {
       const matchesURL = url === response.url;
       const matchesMethod = (options.method || 'GET') === response.method;
@@ -71,32 +90,40 @@ class Client {
     });
   }
 
+  activeRequests: Record<string, ApiNamespace.Request> = {};
+  baseUrl = '';
+
   uniqueId() {
     return '123';
   }
 
-  // In the real client, this clears in-flight responses. It's NOT clearMockResponses. You probably don't want to call this from a test.
+  /**
+   * In the real client, this clears in-flight responses. It's NOT
+   * clearMockResponses. You probably don't want to call this from a test.
+   */
   clear() {}
 
-  static mockAsync = false;
-
-  wrapCallback(_id, error) {
-    return (...args) => {
+  wrapCallback<T extends any[]>(
+    _id: string,
+    func: FunctionCallback<T> | undefined,
+    _cleanup: boolean = false
+  ) {
+    return (...args: T) => {
       // @ts-expect-error
-      if (RealClient.hasProjectBeenRenamed(...args)) {
+      if (RealApi.hasProjectBeenRenamed(...args)) {
         return;
       }
-      respond(Client.mockAsync, error, ...args);
+      respond(Client.mockAsync, func, ...args);
     };
   }
 
   requestPromise(
-    path,
+    path: string,
     {
       includeAllArgs,
       ...options
-    }: {includeAllArgs?: boolean} & Readonly<ImportedClient.RequestOptions> = {}
-  ) {
+    }: {includeAllArgs?: boolean} & Readonly<ApiNamespace.RequestOptions> = {}
+  ): any {
     return new Promise((resolve, reject) => {
       this.request(path, {
         ...options,
@@ -110,7 +137,9 @@ class Client {
     });
   }
 
-  request(url, options: Readonly<ImportedClient.RequestOptions> = {}) {
+  // XXX(ts): We type the return type for requestPromise and request as `any`. Typically these woul
+
+  request(url: string, options: Readonly<ApiNamespace.RequestOptions> = {}): any {
     const [response, mock] = Client.findMockResponse(url, options) || [
       undefined,
       undefined,
@@ -180,7 +209,7 @@ class Client {
           body,
           {},
           {
-            getResponseHeader: key => response.headers[key],
+            getResponseHeader: (key: string) => response.headers[key],
           }
         );
       }
@@ -189,7 +218,7 @@ class Client {
     respond(Client.mockAsync, options.complete);
   }
 
-  handleRequestError = RealClient.Client.prototype.handleRequestError;
+  handleRequestError = RealApi.Client.prototype.handleRequestError;
 }
 
 export {Client};