-
-
Notifications
You must be signed in to change notification settings - Fork 22
/
e2e.test.ts
365 lines (315 loc) · 14.2 KB
/
e2e.test.ts
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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
import {
Selector
} from 'testcafe';
import AsyncTestUtil, { wait, randomString, waitUntil } from 'async-test-util';
import {
Metric,
MetricByKey
} from '../src/shared/types';
import * as path from 'path';
import * as fs from 'fs';
const FRONTEND_PORT = 3000;
const ADD_EXAMPLE_DATA_FLAG = 'add-example-data';
const REPLICATION_FLAG = 'replication';
const FRONTEND_URL = 'http://localhost:' + FRONTEND_PORT + '/';
const projectKey: string = process.env.PROJECT_KEY as any;
console.log('Project key: ' + projectKey);
if (!projectKey) {
throw new Error('project key missing');
}
let DO_REPLICATION = false;
switch (projectKey) {
/**
* These projects do not work without replication
*/
case 'aws':
case 'firebase':
DO_REPLICATION = true;
}
const window: any = {};
import { exampleData } from '../example-data';
fixture`Example page`.page(FRONTEND_URL);
async function insertMessageToFirstUser(t: TestController, messageText: string) {
// click on the first user
const firstUserDiv = Selector('.user-with-last-message').nth(0);
await t.click(firstUserDiv);
// click on message input
const messageInput = Selector('.message-input').nth(0);
await t.click(messageInput);
await t
.typeText(messageInput, messageText);
// submit the message
await t.click(Selector('.message-submit-button').nth(0));
}
/**
* Waits until a given metric appears in the logs
*/
async function waitForMetric(t: TestController, metricKey: string, runIdentifier?: string) {
await wait(100);
console.log('# waitForMetric ' + metricKey + ' runIdentifier: ' + runIdentifier);
await waitUntil(async () => {
const innerLogs = await t.getBrowserConsoleMessages();
let matchingLogs = innerLogs.log
.filter(log => log.startsWith('{') && log.endsWith('}'))
.map(log => JSON.parse(log))
.filter((metric: Metric) => metric.key === metricKey);
if (runIdentifier) {
matchingLogs = matchingLogs.filter((metric: Metric) => metric.runIdentifier === runIdentifier);
}
return matchingLogs.length > 0;
}, 1000 * 200, 100);
}
test
.timeouts({
pageLoadTimeout: 2000,
pageRequestTimeout: 60000,
ajaxRequestTimeout: 60000
})
.page(FRONTEND_URL + '?' + REPLICATION_FLAG + '=true&' + ADD_EXAMPLE_DATA_FLAG + '=true')(
'Basic functions', async t => {
console.log('# set own username');
await t.click('#own-name-submit');
// ensure the storageSize handler works
try {
await t.eval(() => window.storageSizeMetric());
} catch (err) {
const consoleLogs = await t.getBrowserConsoleMessages();
console.dir(consoleLogs);
throw err;
}
console.log('# wait until all users are replicated');
await AsyncTestUtil.waitUntil(async () => {
const userDivs = Selector('.user-with-last-message');
const amount = await userDivs.count;
const usersNotOwnAmount = exampleData.users.length - 1;
if (amount > usersNotOwnAmount) {
console.log('got too many users (' + amount + '), something is wrong ' + usersNotOwnAmount);
throw new Error('got too many users, something is wrong');
}
if (amount === usersNotOwnAmount) {
return true;
} else {
console.log('got users: ' + amount + ' of ' + usersNotOwnAmount);
const consoleLogs = await t.getBrowserConsoleMessages();
if (consoleLogs.error.length > 0) {
console.log('got console errors:');
console.log(consoleLogs.error);
console.log('normal console log:');
console.dir(consoleLogs.log);
throw new Error(consoleLogs.error[0]);
}
return false;
}
}, 1000 * 100, 500);
const testMessageText = randomString(10).toLowerCase();
await insertMessageToFirstUser(t, testMessageText);
console.log('# ensure message is there');
await AsyncTestUtil.waitUntil(async () => {
const lastMessageText = Selector('.message .text').nth(-1);
const innerText = await lastMessageText.innerText;
return innerText === testMessageText;
});
console.log('# insert search');
const searchInput = Selector('.search-input').nth(0);
await t.click(searchInput);
await t
.typeText(searchInput, testMessageText);
console.log('# ensure new message is in search results');
await AsyncTestUtil.waitUntil(async () => {
const firstSearchResult = Selector('.search-results .user-with-last-message .last-message').nth(0);
const innerText = await firstSearchResult.innerText;
return innerText === testMessageText;
});
console.log('# wait a bit to ensure replication is done');
await wait(1000 * 4);
});
const metricsRuns = 8;
let metricRunsDone = 0;
const doReplicationString = DO_REPLICATION ? 'true' : 'false';
const metricsTestUrl = FRONTEND_URL + '?' + REPLICATION_FLAG + '=' + doReplicationString + '&' + ADD_EXAMPLE_DATA_FLAG + '=true';
console.log('metricsTestUrl: ' + metricsTestUrl);
test
.page(metricsTestUrl)(
'Measure metrics', async t => {
// set own username
await t.click('#own-name-submit');
/**
* Testcafe will reset the IndexedDB state on a new test run,
* so instead of using new tests, we navigate to the url again each time.
*/
while (metricRunsDone < metricsRuns) {
// wait until all users are shown
await AsyncTestUtil.waitUntil(async () => {
const userDivs = Selector('.user-with-last-message');
const amount = await userDivs.count;
if (amount > exampleData.users.length) {
throw new Error('got too many users, something is wrong');
}
return amount === 9;
});
await wait(1000 * 2); // wait a bit to ensure nothing is running anymore
/**
* Run metric 'INSERT_1_MESSAGE'
*/
await insertMessageToFirstUser(t, 'INSERT_1_MESSAGE');
await wait(1000 * 2); // can take some time
await waitForMetric(t, 'INSERT_1_MESSAGE');
/**
* Run metric 'INSERT_20_MESSAGES_SERIAL'
*/
await insertMessageToFirstUser(t, 'INSERT_20_MESSAGES_SERIAL');
await wait(1000 * 10); // can take some time
await waitForMetric(t, 'INSERT_20_MESSAGES_SERIAL');
/**
* Run metric 'INSERT_20_MESSAGES_PARALLEL'
*/
await insertMessageToFirstUser(t, 'INSERT_20_MESSAGES_PARALLEL');
await wait(1000 * 10); // can take some time
await waitForMetric(t, 'INSERT_20_MESSAGES_PARALLEL');
/**
* Run metric 'MESSAGE_INSERT_TO_MESSAGE_LIST_CHANGE'
*/
await insertMessageToFirstUser(t, 'MESSAGE_INSERT_TO_MESSAGE_LIST_CHANGE');
await wait(1000 * 5); // can take some time
await waitForMetric(t, 'MESSAGE_INSERT_TO_MESSAGE_LIST_CHANGE');
/**
* Run metric 'USER_PAIR_CHANGED_TO_MESSAGE_LIST'
* measured by opening the chat with the user 'millie'
*/
const millieUserDiv = Selector('.user-with-last-message[userid=millie]').nth(0);
await t.click(millieUserDiv);
await wait(1000 * 5); // can take some time
/**
* Run metric 'SEARCH_RESULT_TIME'
*/
const searchInput = Selector('.search-input').nth(0);
await t.click(searchInput);
await t
.typeText(
searchInput,
'message',
// paste so we fire exact one event that triggers the search query
{ paste: true }
);
await wait(1000 * 5); // can take some time
await waitForMetric(t, 'SEARCH_RESULT_TIME');
metricRunsDone = metricRunsDone + 1;
// after the first run, set runIdentifier to non-first
// do not measure metrics on the first run
// because this will include the initial server side sync times
// which would be an unfair comparison with subprojects that do not have a server sync atm.
if ((metricRunsDone < metricsRuns)) {
await t.navigateTo(metricsTestUrl + '&runIdentifier=non-first');
}
}
/**
* Parse metrics
*/
const allMetricsByKey: { [metricKey: string]: Metric[] } = {};
const allMetrics: MetricByKey = {};
const consoleLogs = await t.getBrowserConsoleMessages();
// show all browser logs for easier debug
console.dir(consoleLogs.log);
// parse all console logs and filter for the metrics
consoleLogs.log
.filter(log => log.startsWith('{'))
.filter(log => log.endsWith('}'))
.map(log => JSON.parse(log))
.filter((metric: Metric) => metric.runIdentifier === 'non-first')
.forEach((metric: Metric) => {
if (metric.flag === 'metric') {
if (!allMetricsByKey[metric.key]) {
allMetricsByKey[metric.key] = [];
}
allMetricsByKey[metric.key].push(metric);
}
});
Object.values(allMetricsByKey)
.forEach((metrics) => {
const firstMetric = metrics[0];
let total = 0;
metrics.forEach(m => total = total + m.value);
const avgValue = averageOfTimeValues(
metrics.map(metric => metric.value),
50
);
allMetrics[firstMetric.key] = {
flag: firstMetric.flag,
key: firstMetric.key,
unit: firstMetric.unit,
value: avgValue
};
});
await t.eval(() => console.log('##### base metrics measurement done!'));
await t.navigateTo(metricsTestUrl);
// insert many messages
await insertMessageToFirstUser(t, 'INSERT_MANY_MESSAGES');
console.log('Wait until INSERT_MANY_MESSAGES is done');
await waitForMetric(t, 'INSERT_MANY_MESSAGES');
await wait(1000 * 2); // wait a bit more to be sure
console.log('# load the page again to measure the initial load time');
await t.navigateTo(metricsTestUrl + '&runIdentifier=after-many-messages');
await waitForMetric(t, 'FIRST_FULL_RENDER', 'after-many-messages');
const consoleLogsAfterMany = await t.getBrowserConsoleMessages();
console.dir(consoleLogsAfterMany);
consoleLogsAfterMany.log
.filter(log => log.startsWith('{'))
.filter(log => log.endsWith('}'))
.map(log => JSON.parse(log))
.filter((metric: Metric) => metric.runIdentifier === 'after-many-messages')
.filter((metric: Metric) => metric.key === 'FIRST_FULL_RENDER')
.forEach((metric: Metric) => {
metric.value = Math.ceil(metric.value);
metric.key = 'FIRST_FULL_RENDER_MANY_MESSAGES';
allMetrics[metric.key] = metric;
});
// measure used storage size
await t.eval(() => window.storageSizeMetric());
await wait(500);
await waitForMetric(t, 'STORAGE_SIZE');
const consoleLogsAfterStorageSize = await t.getBrowserConsoleMessages();
const storageSizeMetric: Metric = consoleLogsAfterStorageSize.log
.filter(log => log.startsWith('{'))
.filter(log => log.endsWith('}'))
.map(log => JSON.parse(log))
.find((metric: Metric) => metric.key === 'STORAGE_SIZE');
if (!storageSizeMetric) {
throw new Error('storageSizeMetric missing');
}
storageSizeMetric.value = Math.ceil(storageSizeMetric.value);
allMetrics[storageSizeMetric.key] = storageSizeMetric;
console.log('# allMetricsByKey:');
console.dir(allMetricsByKey);
console.log('# allMetrics:');
console.dir(allMetrics);
const metricsFolder = path.join(
__dirname,
'../metrics'
);
if (!fs.existsSync(metricsFolder)) {
fs.mkdirSync(metricsFolder);
}
fs.writeFileSync(
path.join(
metricsFolder,
projectKey + '.project.json'
),
JSON.stringify(allMetrics, null, 4)
);
});
export function averageOfTimeValues(
times: number[],
/**
* To better account for anomalies
* during time measurements,
* we strip the heighest x percent.
*/
stripHeighestXPercent: number
): number {
times = times.sort((a, b) => a - b);
const stripAmount = Math.floor(times.length * (stripHeighestXPercent * 0.01));
const useNumbers = times.slice(0, times.length - stripAmount);
let total = 0;
useNumbers.forEach(nr => total = total + nr);
return total / useNumbers.length;
}