Extract functions for service worker usage, and add initial MSC3916 playwright test (when supported) (#12414)

* Send user credentials to service worker for MSC3916 authentication

* appease linter

* Add initial test

The test fails, seemingly because the service worker isn't being installed or because the network mock can't reach that far.

* Remove unsafe access token code

* Split out base IDB operations to avoid importing `document` in serviceworkers

* Use safe crypto access for service workers

* Fix tests/unsafe access

* Remove backwards compatibility layer & appease linter

* Add docs

* Fix tests

* Appease the linter

* Iterate tests

* Factor out pickle key handling for service workers

* Enable everything we can about service workers

* Appease the linter

* Add docs

* Rename win32 image to linux in hopes of it just working

* Use actual image

* Apply suggestions from code review

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Improve documentation

* Document `??` not working

* Try to appease the tests

* Add some notes

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
Travis Ralston 2024-05-02 16:19:55 -06:00 committed by GitHub
parent 374cee9080
commit d25d529e86
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 435 additions and 176 deletions

View file

@ -70,6 +70,22 @@ const sendEvent = async (client: Client, roomId: string, html = false): Promise<
return client.sendEvent(roomId, null, "m.room.message" as EventType, content);
};
const sendImage = async (
client: Client,
roomId: string,
pngBytes: Buffer,
additionalContent?: any,
): Promise<ISendEventResponse> => {
const upload = await client.uploadContent(pngBytes, { name: "image.png", type: "image/png" });
return client.sendEvent(roomId, null, "m.room.message" as EventType, {
...(additionalContent ?? {}),
msgtype: "m.image" as MsgType,
body: "image.png",
url: upload.content_uri,
});
};
test.describe("Timeline", () => {
test.use({
displayName: OLD_NAME,
@ -1136,5 +1152,91 @@ test.describe("Timeline", () => {
screenshotOptions,
);
});
async function testImageRendering(page: Page, app: ElementAppPage, room: { roomId: string }) {
await app.viewRoomById(room.roomId);
// Reinstall the service workers to clear their implicit caches (global-level stuff)
await page.evaluate(async () => {
const registrations = await window.navigator.serviceWorker.getRegistrations();
registrations.forEach((r) => r.update());
});
await sendImage(app.client, room.roomId, NEW_AVATAR);
await expect(page.locator(".mx_MImageBody").first()).toBeVisible();
// Exclude timestamp and read marker from snapshot
const screenshotOptions = {
mask: [page.locator(".mx_MessageTimestamp")],
css: `
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
display: none !important;
}
`,
};
await expect(page.locator(".mx_ScrollPanel")).toMatchScreenshot(
"image-in-timeline-default-layout.png",
screenshotOptions,
);
}
test("should render images in the timeline", async ({ page, app, room, context }) => {
await testImageRendering(page, app, room);
});
// XXX: This test doesn't actually work because the service worker relies on IndexedDB, which Playwright forces
// to be a localstorage implementation, which service workers cannot access.
// See https://github.com/microsoft/playwright/issues/11164
// See https://github.com/microsoft/playwright/issues/15684#issuecomment-2070862042
//
// In practice, this means this test will *always* succeed because it ends up relying on fallback behaviour tested
// above (unless of course the above tests are also broken).
test.describe("MSC3916 - Authenticated Media", () => {
test("should render authenticated images in the timeline", async ({ page, app, room, context }) => {
// Note: we have to use `context` instead of `page` for routing, otherwise we'll miss Service Worker events.
// See https://playwright.dev/docs/service-workers-experimental#network-events-and-routing
// Install our mocks and preventative measures
await context.route("**/_matrix/client/versions", async (route) => {
// Force enable MSC3916, which may require the service worker's internal cache to be cleared later.
const json = await (await route.fetch()).json();
if (!json["unstable_features"]) json["unstable_features"] = {};
json["unstable_features"]["org.matrix.msc3916"] = true;
await route.fulfill({ json });
});
await context.route("**/_matrix/media/*/download/**", async (route) => {
// should not be called. We don't use `abort` so that it's clearer in the logs what happened.
await route.fulfill({
status: 500,
json: { errcode: "M_UNKNOWN", error: "Unexpected route called." },
});
});
await context.route("**/_matrix/media/*/thumbnail/**", async (route) => {
// should not be called. We don't use `abort` so that it's clearer in the logs what happened.
await route.fulfill({
status: 500,
json: { errcode: "M_UNKNOWN", error: "Unexpected route called." },
});
});
await context.route("**/_matrix/client/unstable/org.matrix.msc3916/download/**", async (route) => {
expect(route.request().headers()["Authorization"]).toBeDefined();
// we can't use route.continue() because no configured homeserver supports MSC3916 yet
await route.fulfill({
body: NEW_AVATAR,
});
});
await context.route("**/_matrix/client/unstable/org.matrix.msc3916/thumbnail/**", async (route) => {
expect(route.request().headers()["Authorization"]).toBeDefined();
// we can't use route.continue() because no configured homeserver supports MSC3916 yet
await route.fulfill({
body: NEW_AVATAR,
});
});
// We check the same screenshot because there should be no user-visible impact to using authentication.
await testImageRendering(page, app, room);
});
});
});
});