summaryrefslogtreecommitdiff
path: root/tests/mocks.js
blob: d8b9918ccbe269891e84a5c34c6083899a552df3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
/**
 * Test mocks: fetch (NDJSON stream with controllable chunk boundaries),
 * browser storage, and browser runtime port.
 */
(() => {
  'use strict';

  /**
   * Create a Response whose body streams the given NDJSON lines, split into
   * arbitrary byte chunks (to test JSON-boundary tolerance).
   * @param {string[]} lines NDJSON lines
   * @param {number[]} [chunkSizes] byte sizes per chunk (cycled)
   * @returns {Response}
   */
  function ndjsonResponse(lines, chunkSizes) {
    const text = lines.join('\n') + '\n';
    const bytes = new TextEncoder().encode(text);
    const sizes = chunkSizes && chunkSizes.length ? chunkSizes : [bytes.length];
    let offset = 0;
    let index = 0;
    const stream = new ReadableStream({
      pull(controller) {
        if (offset >= bytes.length) {
          controller.close();
          return;
        }
        const size = sizes[index % sizes.length];
        index++;
        const end = Math.min(offset + size, bytes.length);
        controller.enqueue(bytes.slice(offset, end));
        offset = end;
      }
    });
    return new Response(stream, { status: 200, headers: { 'Content-Type': 'application/x-ndjson' } });
  }

  /**
   * @param {number} status
   * @returns {Response}
   */
  function errorResponse(status) {
    return new Response(JSON.stringify({ error: 'boom' }), { status, headers: { 'Content-Type': 'application/json' } });
  }

  /**
   * Fake browser.storage.local backed by a Map.
   * @returns {object} browser-like API
   */
  function fakeStorage() {
    const data = new Map();
    const listeners = [];
    const local = {
      async get(defaults) {
        const result = {};
        for (const key of Object.keys(defaults)) {
          result[key] = data.has(key) ? data.get(key) : defaults[key];
        }
        return result;
      },
      async set(obj) {
        const changes = {};
        for (const [key, value] of Object.entries(obj)) {
          changes[key] = { oldValue: data.get(key), newValue: value };
          data.set(key, value);
        }
        for (const listener of listeners) listener(changes, 'local');
      }
    };
    return {
      local,
      onChanged: {
        addListener(fn) {
          listeners.push(fn);
        }
      }
    };
  }

  /**
   * Fake runtime.Port with a message queue.
   */
  function fakePort() {
    const listeners = { message: [], disconnect: [] };
    return {
      name: 'translation-stream',
      sender: { id: 'test-extension' },
      postMessage: () => {},
      onMessage: {
        addListener: (fn) => listeners.message.push(fn)
      },
      onDisconnect: {
        addListener: (fn) => listeners.disconnect.push(fn)
      },
      __emit(message) {
        for (const fn of listeners.message) fn(message);
      },
      __emitDisconnect() {
        for (const fn of listeners.disconnect) fn();
      }
    };
  }

  /**
   * Fake browser.runtime sufficient for message-router tests.
   */
  function fakeRuntime() {
    const connectListeners = [];
    const messageListeners = [];
    return {
      onConnect: {
        addListener: (fn) => connectListeners.push(fn),
        __emit(port) {
          for (const fn of connectListeners) fn(port);
        }
      },
      onMessage: {
        addListener: (fn) => messageListeners.push(fn),
        __listener() {
          return messageListeners[messageListeners.length - 1];
        }
      }
    };
  }

  window.TestMocks = { ndjsonResponse, errorResponse, fakeStorage, fakePort, fakeRuntime };
})();