-
Notifications
You must be signed in to change notification settings - Fork 567
Expand file tree
/
Copy pathpresenceTracker.test.ts
More file actions
343 lines (297 loc) · 11.2 KB
/
presenceTracker.test.ts
File metadata and controls
343 lines (297 loc) · 11.2 KB
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
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
import type { Browser, Page } from "puppeteer";
import { launch } from "puppeteer";
import { globals } from "../jest.config.cjs";
const initializeBrowser = async () => {
const browser = await launch({
// https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md#setting-up-chrome-linux-sandbox
args: ["--no-sandbox", "--disable-setuid-sandbox"],
// output browser console to cmd line
dumpio: process.env.FLUID_TEST_VERBOSE !== undefined,
// Use chrome-headless-shell because that's what the CI pipeline installs; see AB#7150.
headless: "shell",
});
return browser;
};
/* Disabled for common window["foo"] access. */
/* eslint-disable @typescript-eslint/dot-notation */
/**
* @param page - The page to load the presence tracker app on.
* @param url - The URL to load the presence tracker app from.
* @returns The session id of the loaded app.
*/
const loadPresenceTrackerApp = async (page: Page, url: string): Promise<string> => {
const loadResponse = await page.goto(url, { waitUntil: "load" });
// A null response indicates a navigation to the same URL with a different hash
// and is not an actual page load (or resetting of state). In this case, we
// need to force reload the page. https://pptr.dev/api/puppeteer.page.goto#remarks
if (loadResponse === null) {
await page.reload({ waitUntil: "load" });
}
// Be extra careful using check for hash expectation
const targetUrl = new URL(url);
const idMatch = targetUrl.hash.slice(1);
const waitFunction = idMatch
? (hash: string) => window["fluidContainerId"] === hash
: () => (window["fluidContainerId"] ?? "") !== "";
await page.waitForFunction(waitFunction, { timeout: 1500 }, idMatch).catch(async () => {
const after = await page.evaluate(() => `${window["fluidContainerId"]}`);
throw new Error(
`failed waiting for app load to id ${idMatch ? idMatch : '!== ""'} (after timeout=${after})`,
);
});
return page.evaluate(() => `${window["fluidSessionId"]}`);
};
/* eslint-enable @typescript-eslint/dot-notation */
// Most tests are passing when tinylicious is running. Those that aren't are individually skipped.
describe("presence-tracker", () => {
let session1id: string;
beforeAll(async () => {
// Wait for the page to load first before running any tests giving a more generous timeout
// so this time isn't attributed to the first test.
await loadPresenceTrackerApp(page, globals.PATH);
}, 45000);
beforeEach(async () => {
session1id = await loadPresenceTrackerApp(page, globals.PATH);
});
afterEach(() => {
session1id = "session1id needs reloaded";
});
describe("Single client", () => {
it("Document is connected", async () => {
// Page's url should be updated to have document id
expect(page.url()).not.toEqual(globals.PATH);
await page.waitForFunction(() => document.isConnected);
});
it("Focus content element exists", async () => {
await page.waitForFunction(() => document.getElementById("focus-content"));
});
it("Focus div exists", async () => {
await page.waitForFunction(() => document.getElementById("focus-div"));
});
it("Mouse position element exists", async () => {
await page.waitForFunction(() => document.getElementById("mouse-position"));
});
it("Current user has focus", async () => {
const elementHandle = await page.waitForFunction(() =>
document.getElementById("focus-div"),
);
const innerHTML = await page.evaluate(
(element) => element?.innerHTML.trim(),
elementHandle,
);
expect(innerHTML).toMatch(/^[^<]+ has focus/);
});
it("First client shows single client connected", async () => {
// eslint-disable-next-line @typescript-eslint/dot-notation, @typescript-eslint/no-unsafe-return
const attendeeCount = await page.evaluate(() => window["fluidSessionAttendeeCount"]);
expect(attendeeCount).toBe(1);
const elementHandle = await page.waitForFunction(() =>
document.getElementById("focus-div"),
);
const clientListHtml = await page.evaluate(
(element) => element?.innerHTML.trim(),
elementHandle,
);
// There should only be a single client connected; verify by asserting there's no <br> tag in the innerHtml, which
// means a single client.
expect(clientListHtml).toMatch(/^[^<]+$/);
// Expect that page's session id is listed.
expect(clientListHtml).toMatch(session1id);
});
});
describe("Multiple clients", () => {
let browser2: Browser;
let page2: Page;
let session2id: string;
beforeAll(async () => {
// Create a second browser instance.
browser2 = await initializeBrowser();
page2 = await browser2.newPage();
// Prime the second browser instance under long timeout.
// Use the "default" path to ensure an instance of app. At this
// point `page.url()` is effectively random, unlike in beforeEach.
await loadPresenceTrackerApp(page2, globals.PATH);
}, 45000);
beforeEach(async () => {
// Page's url should be updated to have document id
expect(page.url()).not.toEqual(globals.PATH);
session2id = await loadPresenceTrackerApp(page2, page.url());
// Both browser instances should be pointing to the same URL now.
expect(page2.url()).toEqual(page.url());
});
afterEach(() => {
session2id = "session2id needs reloaded";
});
afterAll(async () => {
await browser2.close();
});
it("Second user can join", async () => {
// Both browser instances should be pointing to the same URL now.
expect(page2.url()).toEqual(page.url());
await page2.waitForFunction(() => document.isConnected);
});
async function waitForAttendeeState(
page: Page,
expected: Record<string, string>,
timeoutErrorMessage: string,
) {
/* Disabled for common window["foo"] access. */
/* eslint-disable @typescript-eslint/dot-notation */
await page
.waitForFunction(
(expectation) =>
(
window["fluidSessionAttendeeCheck"] as (
expected: Record<string, string>,
) => boolean
)(expectation),
{ timeout: 300 },
expected,
)
.catch(async () => {
const attendeeData = await page.evaluate(() => ({
attendeeCount: `${window["fluidSessionAttendeeCount"]}`,
attendees: window["fluidSessionAttendees"] ?? {},
attendeeConnectedCalled: `${window["fluidattendeeConnectedCalled"]}`,
attendeeDisconnectedCalled: `${window["fluidAttendeeDisconnectedCalled"]}`,
}));
throw new Error(`${timeoutErrorMessage} (${JSON.stringify(attendeeData)})`);
});
/* eslint-enable @typescript-eslint/dot-notation */
}
it("Second client shows two clients connected", async () => {
await waitForAttendeeState(
page2,
{
[session1id]: "Connected",
[session2id]: "Connected",
},
"failed waiting for app to observe two connected attendees",
);
// Get the client list from the second browser instance; it should show two connected.
const elementHandle = await page2.waitForFunction(() =>
document.getElementById("focus-div"),
);
const clientListHtml = await page2.evaluate(
(element) => element?.innerHTML?.trim(),
elementHandle,
);
// Assert that there is a single <br> tag and no other HTML tags in the text, which indicates that two clients are
// connected.
expect(clientListHtml).toMatch(/^[^<]+<br>[^<]+$/);
// Expect that page2's session id is listed.
expect(clientListHtml).toMatch(session2id);
// Expect that first page's session id is listed.
expect(clientListHtml).toMatch(session1id);
});
it("First client shows two clients connected", async () => {
await waitForAttendeeState(
page,
{
[session1id]: "Connected",
[session2id]: "Connected",
},
"failed waiting for app to observe two connected attendees",
);
});
it("First client shows one client connected when second client leaves", async () => {
// Setup
await waitForAttendeeState(
page,
{
[session1id]: "Connected",
[session2id]: "Connected",
},
"failed waiting for app to observe two connected attendees",
);
// Act
// Navigate the second client away.
const response = await page2.goto("about:blank", { waitUntil: "load" });
// Loosely verify that a navigation happened. Puppeteer docs note:
// "Navigation to about:blank or navigation to the same URL with a different hash will succeed and
// return null."
expect(response).toBe(null);
// Verify
await waitForAttendeeState(
page,
{
[session1id]: "Connected",
[session2id]: "Disconnected",
},
"failed waiting for app to observe second attendee as disconnected",
);
});
// TODO: AB#28502: presence-tracker example multi-client test should not be skipped
it.skip("First client shows two clients connected in UI", async () => {
await waitForAttendeeState(
page,
{
[session1id]: "Connected",
[session2id]: "Connected",
},
"failed waiting for app to observe two connected attendees",
);
// Get the client list from the first browser instance; it should show two connected.
const elementHandle = await page.waitForFunction(() =>
document.getElementById("focus-div"),
);
const clientListHtml = await page.evaluate(
(element) => element?.innerHTML?.trim(),
elementHandle,
);
// Assert that there is a single <br> tag and no other HTML tags in the text, which indicates that two clients are
// connected.
expect(clientListHtml).toMatch(/^[^<]+<br>[^<]+$/);
// Expect that first page's session id is listed.
expect(clientListHtml).toMatch(session1id);
// Expect that page2's session id is listed.
expect(clientListHtml).toMatch(session2id);
});
// TODO: AB#28502: presence-tracker example multi-client test should not be skipped
// This test should not be enabled without the prior test being enabled as it
// may have false positives. It has also been demonstrated to fail occasionally.
// Occasional failures are likely due to same issue impact the prior "in UI" test.
it.skip("First client shows one client connected in UI when second client leaves", async () => {
// Setup
await waitForAttendeeState(
page,
{
[session1id]: "Connected",
[session2id]: "Connected",
},
"failed waiting for app to observe two connected attendees",
);
// Act
// Navigate the second client away.
const response = await page2.goto("about:blank", { waitUntil: "load" });
// Loosely verify that a navigation happened. Puppeteer docs note:
// "Navigation to about:blank or navigation to the same URL with a different hash will succeed and
// return null."
expect(response).toBe(null);
// Verify
await waitForAttendeeState(
page,
{
[session1id]: "Connected",
[session2id]: "Disconnected",
},
"failed waiting for app to observe second attendee as disconnected",
);
// Get the client list from the first browser; it should have a single element.
const elementHandle = await page.waitForFunction(() =>
document.getElementById("focus-div"),
);
const clientListHtml = await page.evaluate(
(element) => element?.innerHTML?.trim(),
elementHandle,
);
expect(clientListHtml).toMatch(/^[^<]+$/);
// Expect that first page's session id is listed.
expect(clientListHtml).toMatch(session1id);
});
});
});