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 };
})();
|