xref: /aosp_15_r20/development/tools/winscope/src/trace_collection/proxy_connection_test.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
1/*
2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {
18  HttpRequest,
19  HttpRequestHeaderType,
20  HttpRequestStatus,
21  HttpResponse,
22} from 'common/http_request';
23import {UserNotification} from 'messaging/user_notification';
24import {
25  ProxyTracingErrors,
26  ProxyTracingWarnings,
27} from 'messaging/user_warnings';
28import {UserNotifierChecker} from 'test/unit/user_notifier_checker';
29import {waitToBeCalled} from 'test/utils';
30import {AdbDevice} from 'trace_collection/adb_device';
31import {ConnectionState} from 'trace_collection/connection_state';
32import {ProxyConnection} from 'trace_collection/proxy_connection';
33import {ProxyEndpoint} from './proxy_endpoint';
34import {TraceRequest} from './trace_request';
35
36type HttpRequestGetType = (
37  path: string,
38  headers: HttpRequestHeaderType,
39  type?: XMLHttpRequest['responseType'],
40) => Promise<HttpResponse>;
41
42type HttpRequestPostType = (
43  path: string,
44  headers: HttpRequestHeaderType,
45  jsonRequest?: object,
46) => Promise<HttpResponse>;
47
48describe('ProxyConnection', () => {
49  const detectStateChangesInUi = jasmine.createSpy();
50  const configOptionsChangeCallback = jasmine.createSpy();
51  const availableTracesChangeCallback = jasmine.createSpy();
52  const mockDevice: AdbDevice = {
53    id: '35562',
54    model: 'Pixel 6',
55    authorized: true,
56    displays: ['"Test Display" 12345 Extra Info'],
57    multiDisplayScreenRecordingAvailable: false,
58  };
59  const mockTraceRequest: TraceRequest = {
60    name: 'layers_trace',
61    config: [],
62  };
63  const getVersionHeader = () => ProxyConnection.VERSION;
64  const successfulEndTraceResponse: HttpResponse = {
65    status: HttpRequestStatus.SUCCESS,
66    type: '',
67    text: '[]',
68    body: '[]',
69    getHeader: getVersionHeader,
70  };
71
72  let connection: ProxyConnection;
73  let getSpy: jasmine.Spy<HttpRequestGetType>;
74  let postSpy: jasmine.Spy<HttpRequestPostType>;
75
76  describe('server not found', () => {
77    const unsentResponse: HttpResponse = {
78      status: HttpRequestStatus.UNSENT,
79      type: '',
80      text: '',
81      body: undefined,
82      getHeader: getVersionHeader,
83    };
84
85    beforeEach(async () => {
86      await setUpTestEnvironment(unsentResponse);
87    });
88
89    afterEach(() => {
90      localStorage.clear();
91    });
92
93    it('requests devices on initialization', () => {
94      checkGetDevicesRequest();
95      expect(connection.getState()).toEqual(ConnectionState.NOT_FOUND);
96    });
97
98    it('requests devices on restarting connection', async () => {
99      resetSpies();
100      await connection.restartConnection();
101      checkGetDevicesRequest();
102      expect(connection.getState()).toEqual(ConnectionState.NOT_FOUND);
103    });
104
105    it('posts start trace request to proxy and updates state if proxy not found', async () => {
106      const requestObj = [mockTraceRequest];
107      await connection.startTrace(mockDevice, requestObj);
108      expect(postSpy).toHaveBeenCalledOnceWith(
109        ProxyConnection.WINSCOPE_PROXY_URL +
110          ProxyEndpoint.START_TRACE +
111          `${mockDevice.id}/`,
112        [['Winscope-Token', '']],
113        requestObj,
114      );
115      expect(connection.getState()).toEqual(ConnectionState.NOT_FOUND);
116    });
117  });
118
119  describe('unsuccessful request', () => {
120    afterEach(() => {
121      localStorage.clear();
122    });
123
124    it('unauthorized server', async () => {
125      const unauthResponse: HttpResponse = {
126        status: HttpRequestStatus.UNAUTH,
127        type: '',
128        text: '',
129        body: undefined,
130        getHeader: getVersionHeader,
131      };
132      await setUpTestEnvironment(unauthResponse);
133      expect(connection.getState()).toEqual(ConnectionState.UNAUTH);
134    });
135
136    it('invalid version - header undefined', async () => {
137      await checkInvalidVersion(() => undefined);
138    });
139
140    it('invalid version - old major', async () => {
141      await checkInvalidVersion(() => '0.0.0');
142    });
143
144    it('invalid version - old minor', async () => {
145      const [major, minor, patch] = ProxyConnection.VERSION.split('.');
146      await checkInvalidVersion(() =>
147        [major, Number(minor) - 1, patch].join('.'),
148      );
149    });
150
151    it('invalid version - old patch', async () => {
152      const [major, minor, patch] = ProxyConnection.VERSION.split('.');
153      await checkInvalidVersion(() =>
154        [major, minor, Number(patch) - 1].join('.'),
155      );
156    });
157
158    it('error state with response type text', async () => {
159      const errorResponse: HttpResponse = {
160        status: HttpRequestStatus.ERROR,
161        type: 'text',
162        text: 'test error message',
163        body: undefined,
164        getHeader: getVersionHeader,
165      };
166      await setUpTestEnvironment(errorResponse);
167      expect(connection.getState()).toEqual(ConnectionState.ERROR);
168      expect(connection.getErrorText()).toEqual(errorResponse.text);
169    });
170
171    it('error state with response type empty', async () => {
172      const errorResponse: HttpResponse = {
173        status: HttpRequestStatus.ERROR,
174        type: '',
175        text: 'test error message',
176        body: undefined,
177        getHeader: getVersionHeader,
178      };
179      await setUpTestEnvironment(errorResponse);
180      expect(connection.getState()).toEqual(ConnectionState.ERROR);
181      expect(connection.getErrorText()).toEqual(errorResponse.text);
182    });
183
184    it('error state with response type array buffer', async () => {
185      const errorResponse: HttpResponse = {
186        status: HttpRequestStatus.ERROR,
187        type: 'arraybuffer',
188        text: '',
189        body: [],
190        getHeader: getVersionHeader,
191      };
192      await setUpTestEnvironment(errorResponse);
193      expect(connection.getState()).toEqual(ConnectionState.ERROR);
194    });
195
196    async function checkInvalidVersion(getHeader: () => string | undefined) {
197      const invalidResponse: HttpResponse = {
198        status: HttpRequestStatus.SUCCESS,
199        type: '',
200        text: '',
201        body: undefined,
202        getHeader,
203      };
204      await setUpTestEnvironment(invalidResponse);
205      expect(connection.getState()).toEqual(ConnectionState.INVALID_VERSION);
206    }
207  });
208
209  describe('successful responses to tracing process', () => {
210    let userNotifierChecker: UserNotifierChecker;
211
212    beforeAll(() => {
213      userNotifierChecker = new UserNotifierChecker();
214    });
215
216    beforeEach(async () => {
217      userNotifierChecker.reset();
218      const successfulResponse: HttpResponse = {
219        status: HttpRequestStatus.SUCCESS,
220        type: '',
221        text: 'True',
222        body: undefined,
223        getHeader: getVersionHeader,
224      };
225      await setUpTestEnvironment(successfulResponse);
226    });
227
228    afterEach(() => {
229      localStorage.clear();
230    });
231
232    it('uses stored token on initialization', async () => {
233      resetSpies();
234      connection.setSecurityToken('test_initial_token');
235      connection = await createProxyConnection();
236      checkGetDevicesRequest('test_initial_token');
237    });
238
239    it('sets security token and sends as header', async () => {
240      resetSpies();
241      connection.setSecurityToken('test_token');
242      await connection.restartConnection();
243      checkGetDevicesRequest('test_token');
244    });
245
246    it('does not set empty token', async () => {
247      connection.setSecurityToken('test_initial_token');
248      connection = await createProxyConnection();
249      resetSpies();
250      connection.setSecurityToken('');
251      await connection.restartConnection();
252      checkGetDevicesRequest('test_initial_token');
253    });
254
255    it('throws error on startTrace if no traces requested', async () => {
256      await expectAsync(
257        connection.startTrace(mockDevice, []),
258      ).toBeRejectedWithError('No traces requested');
259    });
260
261    it('throws error on endTrace if no traces requested', async () => {
262      await expectAsync(connection.endTrace()).toBeRejectedWithError(
263        'Trace not started before stopping',
264      );
265    });
266
267    it('throws error on dumpState if no dumps requested', async () => {
268      await expectAsync(
269        connection.dumpState(mockDevice, []),
270      ).toBeRejectedWithError('No dumps requested');
271    });
272
273    it('posts start trace request to proxy and updates state to tracing', async () => {
274      await checkSuccessfulTraceRequest([mockTraceRequest]);
275    });
276
277    it('posts start trace requests without updating screen recording config', async () => {
278      const requestEmptyStrDisplays: TraceRequest[] = [
279        {
280          name: 'screen_recording',
281          config: [{key: 'displays', value: ''}],
282        },
283      ];
284      await checkSuccessfulTraceRequest(requestEmptyStrDisplays);
285
286      const requestEmptyArrDisplays: TraceRequest[] = [
287        {
288          name: 'screen_recording',
289          config: [{key: 'displays', value: []}],
290        },
291      ];
292      await checkSuccessfulTraceRequest(requestEmptyArrDisplays);
293
294      const requestDisplayStrWithoutName: TraceRequest[] = [
295        {
296          name: 'screen_recording',
297          config: [{key: 'displays', value: '12345 Other Info'}],
298        },
299      ];
300      await checkSuccessfulTraceRequest(requestDisplayStrWithoutName);
301
302      const requestDisplayArrWithoutName: TraceRequest[] = [
303        {
304          name: 'screen_recording',
305          config: [{key: 'displays', value: ['12345 Other Info']}],
306        },
307      ];
308      await checkSuccessfulTraceRequest(requestDisplayArrWithoutName);
309    });
310
311    it('posts start trace requests with updated screen recording config', async () => {
312      const requestDisplayStrWithName: TraceRequest[] = [
313        {
314          name: 'screen_recording',
315          config: [{key: 'displays', value: '"Test Display" 12345 Other Info'}],
316        },
317      ];
318      await checkSuccessfulTraceRequest(requestDisplayStrWithName, [
319        {
320          name: 'screen_recording',
321          config: [{key: 'displays', value: '12345 Other Info'}],
322        },
323      ]);
324
325      const requestDisplayArrWithName: TraceRequest[] = [
326        {
327          name: 'screen_recording',
328          config: [
329            {key: 'displays', value: ['"Test Display" 12345 Other Info']},
330          ],
331        },
332      ];
333      await checkSuccessfulTraceRequest(requestDisplayArrWithName, [
334        {
335          name: 'screen_recording',
336          config: [{key: 'displays', value: ['12345 Other Info']}],
337        },
338      ]);
339    });
340
341    it('posts start trace request to proxy and handles response with warnings', async () => {
342      postSpy.and.returnValue(
343        Promise.resolve({
344          status: HttpRequestStatus.SUCCESS,
345          type: '',
346          text: '',
347          body: '["test warning"]',
348          getHeader: getVersionHeader,
349        }),
350      );
351      await checkSuccessfulTraceRequest(
352        [mockTraceRequest],
353        [mockTraceRequest],
354        [new ProxyTracingWarnings(['test warning'])],
355      );
356    });
357
358    it('handles trace timeout', async () => {
359      const requestObj = [mockTraceRequest];
360      getSpy.and.returnValue(
361        Promise.resolve({
362          status: HttpRequestStatus.SUCCESS,
363          type: '',
364          text: 'False',
365          body: undefined,
366          getHeader: getVersionHeader,
367        }),
368      );
369      postSpy.and.returnValue(
370        Promise.resolve({
371          status: HttpRequestStatus.SUCCESS,
372          type: '',
373          text: 'True',
374          body: '[]',
375          getHeader: getVersionHeader,
376        }),
377      );
378      await connection.startTrace(mockDevice, requestObj);
379
380      expect(postSpy).toHaveBeenCalledWith(
381        ProxyConnection.WINSCOPE_PROXY_URL +
382          ProxyEndpoint.START_TRACE +
383          `${mockDevice.id}/`,
384        [['Winscope-Token', '']],
385        requestObj,
386      );
387      expect(postSpy).toHaveBeenCalledWith(
388        ProxyConnection.WINSCOPE_PROXY_URL +
389          ProxyEndpoint.END_TRACE +
390          `${mockDevice.id}/`,
391        [['Winscope-Token', '']],
392        undefined,
393      );
394      expect(connection.getState()).toEqual(ConnectionState.TRACE_TIMEOUT);
395    });
396
397    it('posts end trace request to proxy and handles response without errors', async () => {
398      await startAndEndTrace(successfulEndTraceResponse);
399      checkTraceEndedSuccessfully();
400      userNotifierChecker.expectNone();
401    });
402
403    it('posts end trace request to proxy and handles response with errors', async () => {
404      await startAndEndTrace({
405        status: HttpRequestStatus.SUCCESS,
406        type: '',
407        text: '["please check your display state", "b\'unknown error\'"]',
408        body: '["please check your display state", "b\'unknown error\'"]',
409        getHeader: getVersionHeader,
410      });
411      checkTraceEndedSuccessfully();
412      userNotifierChecker.expectAdded([
413        new ProxyTracingErrors([
414          'please check your display state (must be on at start of trace)',
415          "'unknown error'",
416        ]),
417      ]);
418    });
419
420    it('posts end trace request to proxy and handles non-serializable errors', async () => {
421      await startAndEndTrace({
422        status: HttpRequestStatus.SUCCESS,
423        type: '',
424        text: '["please check your display state", "b\'unknown error\'"]',
425        body: undefined,
426        getHeader: getVersionHeader,
427      });
428      expect(postSpy).toHaveBeenCalledOnceWith(
429        ProxyConnection.WINSCOPE_PROXY_URL +
430          ProxyEndpoint.END_TRACE +
431          `${mockDevice.id}/`,
432        [['Winscope-Token', '']],
433        undefined,
434      );
435      expect(connection.getState()).toEqual(ConnectionState.ERROR);
436      expect(connection.getErrorText()).toContain(
437        'Error handling request response',
438      );
439    });
440
441    it('posts dump state request to proxy', async () => {
442      await checkSuccessfulDumpRequest([mockTraceRequest]);
443    });
444
445    it('posts dump requests without updating screenshot config', async () => {
446      const requestEmptyArrDisplays: TraceRequest[] = [
447        {
448          name: 'screenshot',
449          config: [{key: 'displays', value: []}],
450        },
451      ];
452      await checkSuccessfulDumpRequest(requestEmptyArrDisplays);
453
454      const requestDisplayArrWithoutName: TraceRequest[] = [
455        {
456          name: 'screenshot',
457          config: [{key: 'displays', value: ['12345 Other Info']}],
458        },
459      ];
460      await checkSuccessfulDumpRequest(requestDisplayArrWithoutName);
461    });
462
463    it('posts dump requests with updated screenshot config', async () => {
464      const requestDisplayArrWithName: TraceRequest[] = [
465        {
466          name: 'screenshot',
467          config: [
468            {key: 'displays', value: ['"Test Display" 12345 Other Info']},
469          ],
470        },
471      ];
472      await checkSuccessfulDumpRequest(requestDisplayArrWithName, [
473        {
474          name: 'screenshot',
475          config: [{key: 'displays', value: ['12345 Other Info']}],
476        },
477      ]);
478    });
479
480    it('posts dump request and handles response with warnings', async () => {
481      postSpy.and.returnValue(
482        Promise.resolve({
483          status: HttpRequestStatus.SUCCESS,
484          type: '',
485          text: '',
486          body: '["test warning"]',
487          getHeader: getVersionHeader,
488        }),
489      );
490      await checkSuccessfulDumpRequest(
491        [mockTraceRequest],
492        [mockTraceRequest],
493        [new ProxyTracingWarnings(['test warning'])],
494      );
495    });
496
497    function checkTraceEndedSuccessfully() {
498      expect(postSpy).toHaveBeenCalledOnceWith(
499        ProxyConnection.WINSCOPE_PROXY_URL +
500          ProxyEndpoint.END_TRACE +
501          `${mockDevice.id}/`,
502        [['Winscope-Token', '']],
503        undefined,
504      );
505      expect(connection.getState()).toEqual(ConnectionState.ENDING_TRACE);
506    }
507
508    async function checkSuccessfulTraceRequest(
509      requests: TraceRequest[],
510      updatedRequests = requests,
511      warnings?: UserNotification[],
512    ) {
513      postSpy.calls.reset();
514      await connection.startTrace(mockDevice, requests);
515
516      expect(postSpy).toHaveBeenCalledOnceWith(
517        ProxyConnection.WINSCOPE_PROXY_URL +
518          ProxyEndpoint.START_TRACE +
519          `${mockDevice.id}/`,
520        [['Winscope-Token', '']],
521        updatedRequests,
522      );
523      expect(connection.getState()).toEqual(ConnectionState.TRACING);
524      if (warnings !== undefined) {
525        userNotifierChecker.expectNotified(warnings);
526      } else {
527        userNotifierChecker.expectNone();
528      }
529    }
530
531    async function checkSuccessfulDumpRequest(
532      requests: TraceRequest[],
533      updatedRequests = requests,
534      warnings?: UserNotification[],
535    ) {
536      postSpy.calls.reset();
537      await connection.dumpState(mockDevice, requests);
538
539      expect(postSpy).toHaveBeenCalledOnceWith(
540        ProxyConnection.WINSCOPE_PROXY_URL +
541          ProxyEndpoint.DUMP +
542          `${mockDevice.id}/`,
543        [['Winscope-Token', '']],
544        updatedRequests,
545      );
546      expect(connection.getState()).toEqual(ConnectionState.DUMPING_STATE);
547      if (warnings !== undefined) {
548        userNotifierChecker.expectNotified(warnings);
549      } else {
550        userNotifierChecker.expectNone();
551      }
552    }
553  });
554
555  describe('wayland trace availability', () => {
556    beforeEach(() => {
557      availableTracesChangeCallback.calls.reset();
558    });
559
560    afterEach(() => {
561      localStorage.clear();
562    });
563
564    it('updates availability of wayland trace if available', async () => {
565      const successfulResponse: HttpResponse = {
566        status: HttpRequestStatus.SUCCESS,
567        type: '',
568        text: 'true',
569        body: undefined,
570        getHeader: getVersionHeader,
571      };
572      await setUpTestEnvironment(successfulResponse);
573      expect(availableTracesChangeCallback).toHaveBeenCalledOnceWith([
574        'wayland_trace',
575      ]);
576    });
577
578    it('does not update availability of traces if call fails', async () => {
579      const unsuccessfulResponse: HttpResponse = {
580        status: HttpRequestStatus.SUCCESS,
581        type: '',
582        text: 'false',
583        body: undefined,
584        getHeader: getVersionHeader,
585      };
586      await setUpTestEnvironment(unsuccessfulResponse);
587      expect(availableTracesChangeCallback).not.toHaveBeenCalled();
588    });
589  });
590
591  describe('finding devices', () => {
592    const successfulResponse: HttpResponse = {
593      status: HttpRequestStatus.SUCCESS,
594      type: 'text',
595      text: JSON.stringify({
596        '35562': {
597          authorized: mockDevice.authorized,
598          model: mockDevice.model,
599          displays: ['Display 12345 Extra Info displayName="Test Display"'],
600        },
601      }),
602      body: undefined,
603      getHeader: getVersionHeader,
604    };
605
606    afterEach(() => {
607      localStorage.clear();
608    });
609
610    it('sets error state if onSuccess callback fails', async () => {
611      const noDevicesResponse: HttpResponse = {
612        status: HttpRequestStatus.SUCCESS,
613        type: 'arraybuffer',
614        text: '[0,]',
615        body: undefined,
616        getHeader: getVersionHeader,
617      };
618      await setUpTestEnvironment(noDevicesResponse);
619      checkGetDevicesRequest();
620      expect(connection.getState()).toEqual(ConnectionState.ERROR);
621      expect(connection.getErrorText()).toEqual(
622        'Could not find devices. Received:\n[0,]',
623      );
624    });
625
626    it('fetches devices', async () => {
627      await setUpTestEnvironment(successfulResponse);
628      checkGetDevicesRequest();
629      expect(connection.getState()).toEqual(ConnectionState.IDLE);
630      expect(connection.getDevices()).toEqual([mockDevice]);
631    });
632
633    it('sets up worker to fetch devices', async () => {
634      await setUpTestEnvironment(successfulResponse);
635      checkGetDevicesRequest();
636      expect(connection.getState()).toEqual(ConnectionState.IDLE);
637      expect(connection.getDevices()).toEqual([mockDevice]);
638      expect(getSpy).toHaveBeenCalledWith(
639        ProxyConnection.WINSCOPE_PROXY_URL + ProxyEndpoint.CHECK_WAYLAND,
640        [['Winscope-Token', '']],
641        undefined,
642      );
643
644      getSpy.calls.reset();
645      detectStateChangesInUi.calls.reset();
646
647      await waitToBeCalled(detectStateChangesInUi, 1);
648      await waitToBeCalled(getSpy, 1);
649      checkGetDevicesRequest();
650      expect(connection.getState()).toEqual(ConnectionState.IDLE);
651      expect(connection.getDevices()).toEqual([mockDevice]);
652      expect(getSpy).not.toHaveBeenCalledWith(
653        ProxyConnection.WINSCOPE_PROXY_URL + ProxyEndpoint.CHECK_WAYLAND,
654        [['Winscope-Token', '']],
655        undefined,
656      );
657    });
658
659    it('handles missing displayName', async () => {
660      const response: HttpResponse = {
661        status: HttpRequestStatus.SUCCESS,
662        type: 'text',
663        text: JSON.stringify({
664          '35562': {
665            authorized: mockDevice.authorized,
666            model: mockDevice.model,
667            displays: ['Display 12345 Extra Info'],
668          },
669        }),
670        body: undefined,
671        getHeader: getVersionHeader,
672      };
673
674      await setUpTestEnvironment(response);
675      checkGetDevicesRequest();
676      expect(connection.getState()).toEqual(ConnectionState.IDLE);
677      expect(connection.getDevices()).toEqual([
678        {
679          id: '35562',
680          model: 'Pixel 6',
681          authorized: true,
682          displays: ['12345 Extra Info'],
683          multiDisplayScreenRecordingAvailable: false,
684        },
685      ]);
686    });
687
688    it('updates multi display screen recording availability for incompatible version', async () => {
689      const oldVersionResponse: HttpResponse = {
690        status: HttpRequestStatus.SUCCESS,
691        type: 'text',
692        text: JSON.stringify({
693          '35562': {
694            authorized: mockDevice.authorized,
695            model: mockDevice.model,
696            displays: ['Display 12345 Extra Info displayName="Test Display"'],
697            screenrecord_version: '1.3',
698          },
699        }),
700        body: undefined,
701        getHeader: getVersionHeader,
702      };
703      await setUpTestEnvironment(oldVersionResponse);
704      checkGetDevicesRequest();
705      expect(connection.getState()).toEqual(ConnectionState.IDLE);
706      expect(connection.getDevices()).toEqual([mockDevice]);
707    });
708
709    it('updates multi display screen recording availability for compatible version', async () => {
710      const compatibleVersionResponse: HttpResponse = {
711        status: HttpRequestStatus.SUCCESS,
712        type: 'text',
713        text: JSON.stringify({
714          '35562': {
715            authorized: mockDevice.authorized,
716            model: mockDevice.model,
717            displays: ['Display 12345 Extra Info displayName="Test Display"'],
718            screenrecord_version: '1.4',
719          },
720        }),
721        body: undefined,
722        getHeader: getVersionHeader,
723      };
724      const mockDeviceWithMultiDisplayScreenRecording: AdbDevice = {
725        id: '35562',
726        model: 'Pixel 6',
727        authorized: true,
728        displays: ['"Test Display" 12345 Extra Info'],
729        multiDisplayScreenRecordingAvailable: true,
730      };
731
732      await setUpTestEnvironment(compatibleVersionResponse);
733      checkGetDevicesRequest();
734      expect(connection.getState()).toEqual(ConnectionState.IDLE);
735      expect(connection.getDevices()).toEqual([
736        mockDeviceWithMultiDisplayScreenRecording,
737      ]);
738    });
739  });
740
741  describe('files', () => {
742    const testFileArray = [window.btoa('[20]')];
743    const testFile = new File(testFileArray, 'test_file');
744
745    const successfulResponse: HttpResponse = {
746      status: HttpRequestStatus.SUCCESS,
747      type: 'arraybuffer',
748      text: 'True',
749      body: new TextEncoder().encode(
750        JSON.stringify({'test_file': testFileArray}),
751      ),
752      getHeader: getVersionHeader,
753    };
754    afterEach(() => {
755      localStorage.clear();
756    });
757
758    it('sets error state if fetching files fails', async () => {
759      const successfulResponse: HttpResponse = {
760        status: HttpRequestStatus.SUCCESS,
761        type: 'arraybuffer',
762        text: 'False',
763        body: new TextEncoder().encode('[0,]'),
764        getHeader: getVersionHeader,
765      };
766      await setUpTestEnvironment(successfulResponse);
767      resetSpies();
768      await connection.fetchLastTracingSessionData(mockDevice);
769
770      expect(getSpy).toHaveBeenCalledOnceWith(
771        ProxyConnection.WINSCOPE_PROXY_URL +
772          ProxyEndpoint.FETCH +
773          `${mockDevice.id}/`,
774        [['Winscope-Token', '']],
775        'arraybuffer',
776      );
777      expect(connection.getState()).toEqual(ConnectionState.ERROR);
778      expect(connection.getErrorText()).toEqual(
779        'Could not fetch files. Received:\nFalse',
780      );
781    });
782
783    it('fetches last tracing session data without ongoing tracing', async () => {
784      await setUpTestEnvironment(successfulResponse);
785      resetSpies();
786      const files = await connection.fetchLastTracingSessionData(mockDevice);
787
788      expect(getSpy).toHaveBeenCalledOnceWith(
789        ProxyConnection.WINSCOPE_PROXY_URL +
790          ProxyEndpoint.FETCH +
791          `${mockDevice.id}/`,
792        [['Winscope-Token', '']],
793        'arraybuffer',
794      );
795      expect(connection.getState()).toEqual(ConnectionState.LOADING_DATA);
796      expect(files).toEqual([testFile]);
797    });
798
799    it('fetches last tracing session data from ongoing tracing', async () => {
800      await setUpTestEnvironment(successfulResponse);
801      await startAndEndTrace(successfulEndTraceResponse);
802      resetSpies();
803      const files = await connection.fetchLastTracingSessionData(mockDevice);
804
805      expect(getSpy).toHaveBeenCalledOnceWith(
806        ProxyConnection.WINSCOPE_PROXY_URL +
807          ProxyEndpoint.FETCH +
808          `${mockDevice.id}/`,
809        [['Winscope-Token', '']],
810        'arraybuffer',
811      );
812      expect(connection.getState()).toEqual(ConnectionState.LOADING_DATA);
813      expect(files).toEqual([testFile]);
814    });
815  });
816
817  async function setUpTestEnvironment(response: HttpResponse) {
818    getSpy = spyOn(HttpRequest, 'get').and.returnValue(
819      Promise.resolve(response),
820    );
821    postSpy = spyOn(HttpRequest, 'post').and.returnValue(
822      Promise.resolve(response),
823    );
824    connection = await createProxyConnection();
825  }
826
827  function checkGetDevicesRequest(header = '') {
828    expect(getSpy).toHaveBeenCalledWith(
829      ProxyConnection.WINSCOPE_PROXY_URL + ProxyEndpoint.DEVICES,
830      [['Winscope-Token', header]],
831      undefined,
832    );
833  }
834
835  async function startAndEndTrace(endingTraceResponse: HttpResponse) {
836    await connection.startTrace(mockDevice, [mockTraceRequest]);
837    resetSpies();
838    postSpy.and.returnValue(Promise.resolve(endingTraceResponse));
839    await connection.endTrace();
840  }
841
842  function resetSpies() {
843    getSpy.calls.reset();
844    postSpy.calls.reset();
845  }
846
847  async function createProxyConnection() {
848    const connection = new ProxyConnection();
849    await connection.initialize(
850      detectStateChangesInUi,
851      availableTracesChangeCallback,
852      configOptionsChangeCallback,
853    );
854    return connection;
855  }
856});
857