Implement MSC3575: Sliding Sync (#8328)

* Add labs flag for sliding sync; add sliding_sync_proxy_url to config.json

* Disable the labs toggle if sliding_sync_proxy_url is not set

* Do validation checks on the sliding sync proxy URL before enabling it in Labs

* Enable sliding sync and add SlidingSyncManager

* Get room subscriptions working

* Hijack renderSublists in sliding sync mode

* Add support for sorting alphabetically/recency and room name filters

* Filter out tombstoned rooms; start adding show more logic

list ranges update but the UI doesn't

* update the UI when the list is updated

* bugfix: make sure the list sorts numerically

* Get invites transitioning correctly

* Force enable sliding sync and labs for now

* Linting

* Disable spotlight search

* Initial cypress plugins for Sliding Sync Proxy

* Use --rm when running Synapse in Docker for Cypress tests

* Update src/MatrixClientPeg.ts

Co-authored-by: Travis Ralston <travisr@matrix.org>

* Update src/components/views/rooms/RoomSublist.tsx

Co-authored-by: Travis Ralston <travisr@matrix.org>

* Update src/settings/controllers/SlidingSyncController.ts

Co-authored-by: Travis Ralston <travisr@matrix.org>

* Update src/components/views/rooms/RoomSublist.tsx

Co-authored-by: Travis Ralston <travisr@matrix.org>

* WIP add room searching to spotlight search

* Only read sliding sync results when there is a result, else use the local cache

* Use feature_sliding_sync not slidingSync

* Some review comments

* More review comments

* Use RoomViewStore to set room subscriptions

* Comment why any

* Update src/components/views/rooms/RoomSublist.tsx

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix cypress docker abstraction

* Iterate sliding sync proxy support

* Stash mostly functional test

* Update sliding sync proxy image

* i18n

* Add support for spaces; use list ID -> index mappings

- Mappings are more reusable and easier to understand than
  racing for index positions.
- Register for all spaces immediately on startup.

* When the active space is updated, update the list registration

* Set spaces filter in the correct place

* Skeleton placeholder whilst loading the space

* Filter out spaces from the room list

* Use the new txn_id promises

* Ensure we actually resolve list registrations

* Fix matrix-org/sliding-sync#30: don't show tombstoned search results

* Remove unused imports

* Add SYNCV3_SECRET to proxy to ensure it starts up; correct aliases for SS test

* Add another basic sliding sync e2e test

* Unbreak netlify

* Add more logging for debugging duplicate rooms

* If sliding sync is enabled, always use the rooms result even if it's empty

* Drop-in copy of RoomListStore for sliding sync

* Remove conditionals from RoomListStore - we have SlidingRoomListStore now

* WIP SlidingRoomListStore

* Add most sliding sync logic to SlidingRoomListStore

Still lots of logic in RoomSublist. Broken things:
 - Join count is wrong completely.
 - No skeleton placeholder when switching spaces.

* Migrate joined count to SS RLS

* Reinstate the skeleton UI when the list is loading

* linting

* Add support for sticky rooms based on the currently active room

* Add a bunch of passing SS E2E tests; some WIP

* Unbreak build from git merge

* Suppress unread indicators in sliding sync mode

* Add regression test for https://github.com/matrix-org/sliding-sync/issues/28

* Add invite test flows; show the invite list

The refactor to SS RLS removed the invite list entirely.

* Remove show more click as it wasn't the bug

* Linting and i18n

* only enable SS by default on netlify

* Jest fixes; merge conflict fixes; remove debug logging; use right sort enum values

* Actually fix jest tests

* Add support for favourites and low priority

* Bump sliding sync version

* Update sliding sync labs to be user configurable

* delint

* To disable SS or change proxy URL the user has to log out

* Review comments

* Linting

* Apply suggestions from code review

Co-authored-by: Travis Ralston <travisr@matrix.org>

* Update src/stores/room-list/SlidingRoomListStore.ts

Co-authored-by: Travis Ralston <travisr@matrix.org>

* Review comments

* Add issue link for TODO markers

* Linting

* Apply suggestions from code review

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

* More review comments

* More review comments

* stricter types

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Travis Ralston <travisr@matrix.org>
This commit is contained in:
kegsay 2022-09-07 16:42:39 +01:00 committed by GitHub
parent 5bdae150fa
commit a215027c6b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 1632 additions and 51 deletions

View file

@ -0,0 +1,322 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/// <reference types="cypress" />
import _ from "lodash";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { SynapseInstance } from "../../plugins/synapsedocker";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import { Layout } from "../../../src/settings/enums/Layout";
import { ProxyInstance } from "../../plugins/sliding-sync";
describe("Sliding Sync", () => {
beforeEach(() => {
cy.startSynapse("default").as("synapse").then(synapse => {
cy.startProxy(synapse).as("proxy");
});
cy.all([
cy.get<SynapseInstance>("@synapse"),
cy.get<ProxyInstance>("@proxy"),
]).then(([synapse, proxy]) => {
cy.enableLabsFeature("feature_sliding_sync");
cy.intercept("/config.json?cachebuster=*", req => {
return req.continue(res => {
res.send(200, {
...res.body,
setting_defaults: {
feature_sliding_sync_proxy_url: `http://localhost:${proxy.port}`,
},
});
});
});
cy.initTestUser(synapse, "Sloth").then(() => {
return cy.window({ log: false }).then(() => {
cy.createRoom({ name: "Test Room" }).as("roomId");
});
});
});
});
afterEach(() => {
cy.get<SynapseInstance>("@synapse").then(cy.stopSynapse);
cy.get<ProxyInstance>("@proxy").then(cy.stopProxy);
});
// assert order
const checkOrder = (wantOrder: string[]) => {
cy.contains(".mx_RoomSublist", "Rooms").find(".mx_RoomTile_title").should((elements) => {
expect(_.map(elements, (e) => {
return e.textContent;
}), "rooms are sorted").to.deep.equal(wantOrder);
});
};
const bumpRoom = (alias: string) => {
// Send a message into the given room, this should bump the room to the top
cy.get<string>(alias).then((roomId) => {
return cy.sendEvent(roomId, null, "m.room.message", {
body: "Hello world",
msgtype: "m.text",
});
});
};
const createAndJoinBob = () => {
// create a Bob user
cy.get<SynapseInstance>("@synapse").then((synapse) => {
return cy.getBot(synapse, {
displayName: "Bob",
}).as("bob");
});
// invite Bob to Test Room and accept then send a message.
cy.all([cy.get<string>("@roomId"), cy.get<MatrixClient>("@bob")]).then(([roomId, bob]) => {
return cy.inviteUser(roomId, bob.getUserId()).then(() => {
return bob.joinRoom(roomId);
});
});
};
// sanity check everything works
it("should correctly render expected messages", () => {
cy.get<string>("@roomId").then(roomId => cy.visit("/#/room/" + roomId));
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
// Wait until configuration is finished
cy.contains(
".mx_RoomView_body .mx_GenericEventListSummary .mx_GenericEventListSummary_summary",
"created and configured the room.",
);
// Click "expand" link button
cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click();
});
it("should render the Rooms list in reverse chronological order by default and allowing sorting A-Z", () => {
// create rooms and check room names are correct
cy.createRoom({ name: "Apple" }).then(() => cy.contains(".mx_RoomSublist", "Apple"));
cy.createRoom({ name: "Pineapple" }).then(() => cy.contains(".mx_RoomSublist", "Pineapple"));
cy.createRoom({ name: "Orange" }).then(() => cy.contains(".mx_RoomSublist", "Orange"));
// check the rooms are in the right order
cy.get(".mx_RoomTile").should('have.length', 4); // due to the Test Room in beforeEach
checkOrder([
"Orange", "Pineapple", "Apple", "Test Room",
]);
cy.contains(".mx_RoomSublist", "Rooms").find(".mx_RoomSublist_menuButton").click({ force: true });
cy.contains("A-Z").click();
cy.get('.mx_StyledRadioButton_checked').should("contain.text", "A-Z");
checkOrder([
"Apple", "Orange", "Pineapple", "Test Room",
]);
});
it("should move rooms around as new events arrive", () => {
// create rooms and check room names are correct
cy.createRoom({ name: "Apple" }).as("roomA").then(() => cy.contains(".mx_RoomSublist", "Apple"));
cy.createRoom({ name: "Pineapple" }).as("roomP").then(() => cy.contains(".mx_RoomSublist", "Pineapple"));
cy.createRoom({ name: "Orange" }).as("roomO").then(() => cy.contains(".mx_RoomSublist", "Orange"));
// Select the Test Room
cy.contains(".mx_RoomTile", "Test Room").click();
checkOrder([
"Orange", "Pineapple", "Apple", "Test Room",
]);
bumpRoom("@roomA");
checkOrder([
"Apple", "Orange", "Pineapple", "Test Room",
]);
bumpRoom("@roomO");
checkOrder([
"Orange", "Apple", "Pineapple", "Test Room",
]);
bumpRoom("@roomO");
checkOrder([
"Orange", "Apple", "Pineapple", "Test Room",
]);
bumpRoom("@roomP");
checkOrder([
"Pineapple", "Orange", "Apple", "Test Room",
]);
});
it("should not move the selected room: it should be sticky", () => {
// create rooms and check room names are correct
cy.createRoom({ name: "Apple" }).as("roomA").then(() => cy.contains(".mx_RoomSublist", "Apple"));
cy.createRoom({ name: "Pineapple" }).as("roomP").then(() => cy.contains(".mx_RoomSublist", "Pineapple"));
cy.createRoom({ name: "Orange" }).as("roomO").then(() => cy.contains(".mx_RoomSublist", "Orange"));
// Given a list of Orange, Pineapple, Apple - if Pineapple is active and a message is sent in Apple, the list should
// turn into Apple, Pineapple, Orange - the index position of Pineapple never changes even though the list should technically
// be Apple, Orange Pineapple - only when you click on a different room do things reshuffle.
// Select the Pineapple room
cy.contains(".mx_RoomTile", "Pineapple").click();
checkOrder([
"Orange", "Pineapple", "Apple", "Test Room",
]);
// Move Apple
bumpRoom("@roomA");
checkOrder([
"Apple", "Pineapple", "Orange", "Test Room",
]);
// Select the Test Room
cy.contains(".mx_RoomTile", "Test Room").click();
// the rooms reshuffle to match reality
checkOrder([
"Apple", "Orange", "Pineapple", "Test Room",
]);
});
it("should show the right unread notifications", () => {
createAndJoinBob();
// send a message in the test room: unread notif count shoould increment
cy.all([cy.get<string>("@roomId"), cy.get<MatrixClient>("@bob")]).then(([roomId, bob]) => {
return bob.sendTextMessage(roomId, "Hello World");
});
// check that there is an unread notification (grey) as 1
cy.contains(".mx_RoomTile", "Test Room").contains(".mx_NotificationBadge_count", "1");
cy.get(".mx_NotificationBadge").should("not.have.class", "mx_NotificationBadge_highlighted");
// send an @mention: highlight count (red) should be 2.
cy.all([cy.get<string>("@roomId"), cy.get<MatrixClient>("@bob")]).then(([roomId, bob]) => {
return bob.sendTextMessage(roomId, "Hello Sloth");
});
cy.contains(".mx_RoomTile", "Test Room").contains(".mx_NotificationBadge_count", "2");
cy.get(".mx_NotificationBadge").should("have.class", "mx_NotificationBadge_highlighted");
// click on the room, the notif counts should disappear
cy.contains(".mx_RoomTile", "Test Room").click();
cy.contains(".mx_RoomTile", "Test Room").should("not.have.class", "mx_NotificationBadge_count");
});
it("should not show unread indicators", () => { // TODO: for now. Later we should.
createAndJoinBob();
// disable notifs in this room (TODO: CS API call?)
cy.contains(".mx_RoomTile", "Test Room").find(".mx_RoomTile_notificationsButton").click({ force: true });
cy.contains("None").click();
// create a new room so we know when the message has been received as it'll re-shuffle the room list
cy.createRoom({
name: "Dummy",
});
checkOrder([
"Dummy", "Test Room",
]);
cy.all([cy.get<string>("@roomId"), cy.get<MatrixClient>("@bob")]).then(([roomId, bob]) => {
return bob.sendTextMessage(roomId, "Do you read me?");
});
// wait for this message to arrive, tell by the room list resorting
checkOrder([
"Test Room", "Dummy",
]);
cy.contains(".mx_RoomTile", "Test Room").get(".mx_NotificationBadge").should("not.exist");
});
it("should update user settings promptly", () => {
cy.get(".mx_UserMenu_userAvatar").click();
cy.contains("All settings").click();
cy.contains("Preferences").click();
cy.contains(".mx_SettingsFlag", "Show timestamps in 12 hour format").should("exist").find(
".mx_ToggleSwitch_on").should("not.exist");
cy.contains(".mx_SettingsFlag", "Show timestamps in 12 hour format").should("exist").find(
".mx_ToggleSwitch_ball").click();
cy.contains(".mx_SettingsFlag", "Show timestamps in 12 hour format", { timeout: 2000 }).should("exist").find(
".mx_ToggleSwitch_on", { timeout: 2000 },
).should("exist");
});
it("should show and be able to accept/reject/rescind invites", () => {
createAndJoinBob();
let clientUserId;
cy.getClient().then((cli) => {
clientUserId = cli.getUserId();
});
// invite Sloth into 3 rooms:
// - roomJoin: will join this room
// - roomReject: will reject the invite
// - roomRescind: will make Bob rescind the invite
let roomJoin; let roomReject; let roomRescind; let bobClient;
cy.get<MatrixClient>("@bob").then((bob) => {
bobClient = bob;
return Promise.all([
bob.createRoom({ name: "Join" }),
bob.createRoom({ name: "Reject" }),
bob.createRoom({ name: "Rescind" }),
]);
}).then(([join, reject, rescind]) => {
roomJoin = join.room_id;
roomReject = reject.room_id;
roomRescind = rescind.room_id;
return Promise.all([
bobClient.invite(roomJoin, clientUserId),
bobClient.invite(roomReject, clientUserId),
bobClient.invite(roomRescind, clientUserId),
]);
});
// wait for them all to be on the UI
cy.get(".mx_RoomTile").should('have.length', 4); // due to the Test Room in beforeEach
cy.contains(".mx_RoomTile", "Join").click();
cy.contains(".mx_AccessibleButton", "Accept").click();
checkOrder([
"Join", "Test Room",
]);
cy.contains(".mx_RoomTile", "Reject").click();
cy.get(".mx_RoomView").contains(".mx_AccessibleButton", "Reject").click();
// wait for the rejected room to disappear
cy.get(".mx_RoomTile").should('have.length', 3);
// check the lists are correct
checkOrder([
"Join", "Test Room",
]);
cy.contains(".mx_RoomSublist", "Invites").find(".mx_RoomTile_title").should((elements) => {
expect(_.map(elements, (e) => {
return e.textContent;
}), "rooms are sorted").to.deep.equal(["Rescind"]);
});
// now rescind the invite
cy.get<MatrixClient>("@bob").then((bob) => {
return bob.kick(roomRescind, clientUserId);
});
// wait for the rescind to take effect and check the joined list once more
cy.get(".mx_RoomTile").should('have.length', 2);
checkOrder([
"Join", "Test Room",
]);
});
});

2
cypress/global.d.ts vendored
View file

@ -28,6 +28,7 @@ import type {
RoomStateEvent,
Visibility,
RoomMemberEvent,
ICreateClientOpts,
} from "matrix-js-sdk/src/matrix";
import type { MatrixDispatcher } from "../src/dispatcher/dispatcher";
import type PerformanceMonitor from "../src/performance";
@ -55,6 +56,7 @@ declare global {
MemoryCryptoStore: typeof MemoryCryptoStore;
Visibility: typeof Visibility;
Preset: typeof Preset;
createClient(opts: ICreateClientOpts | string);
};
}
}

View file

@ -17,6 +17,7 @@ limitations under the License.
/// <reference types="cypress" />
import * as os from "os";
import * as crypto from "crypto";
import * as childProcess from "child_process";
import * as fse from "fs-extra";
@ -25,28 +26,32 @@ import PluginConfigOptions = Cypress.PluginConfigOptions;
// A cypress plugin to run docker commands
export function dockerRun(args: {
export function dockerRun(opts: {
image: string;
containerName: string;
params?: string[];
cmd?: string;
}): Promise<string> {
const userInfo = os.userInfo();
const params = args.params ?? [];
const params = opts.params ?? [];
if (userInfo.uid >= 0) {
if (params?.includes("-v") && userInfo.uid >= 0) {
// On *nix we run the docker container as our uid:gid otherwise cleaning it up its media_store can be difficult
params.push("-u", `${userInfo.uid}:${userInfo.gid}`);
}
const args = [
"run",
"--name", `${opts.containerName}-${crypto.randomBytes(4).toString("hex")}`,
"-d",
...params,
opts.image,
];
if (opts.cmd) args.push(opts.cmd);
return new Promise<string>((resolve, reject) => {
childProcess.execFile('docker', [
"run",
"--name", args.containerName,
"-d",
...params,
args.image,
"run",
], (err, stdout) => {
childProcess.execFile("docker", args, (err, stdout) => {
if (err) reject(err);
resolve(stdout.trim());
});
@ -122,6 +127,21 @@ export function dockerRm(args: {
});
}
export function dockerIp(args: {
containerId: string;
}): Promise<string> {
return new Promise<string>((resolve, reject) => {
childProcess.execFile('docker', [
"inspect",
"-f", "{{ .NetworkSettings.IPAddress }}",
args.containerId,
], (err, stdout) => {
if (err) reject(err);
else resolve(stdout.trim());
});
});
}
/**
* @type {Cypress.PluginConfig}
*/
@ -132,5 +152,6 @@ export function docker(on: PluginEvents, config: PluginConfigOptions) {
dockerLogs,
dockerStop,
dockerRm,
dockerIp,
});
}

View file

@ -20,6 +20,7 @@ import PluginEvents = Cypress.PluginEvents;
import PluginConfigOptions = Cypress.PluginConfigOptions;
import { performance } from "./performance";
import { synapseDocker } from "./synapsedocker";
import { slidingSyncProxyDocker } from "./sliding-sync";
import { webserver } from "./webserver";
import { docker } from "./docker";
import { log } from "./log";
@ -31,6 +32,7 @@ export default function(on: PluginEvents, config: PluginConfigOptions) {
docker(on, config);
performance(on, config);
synapseDocker(on, config);
slidingSyncProxyDocker(on, config);
webserver(on, config);
log(on, config);
}

View file

@ -0,0 +1,128 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/// <reference types="cypress" />
import PluginEvents = Cypress.PluginEvents;
import PluginConfigOptions = Cypress.PluginConfigOptions;
import { dockerExec, dockerIp, dockerRun, dockerStop } from "../docker";
import { getFreePort } from "../utils/port";
import { SynapseInstance } from "../synapsedocker";
// A cypress plugins to add command to start & stop https://github.com/matrix-org/sliding-sync
export interface ProxyInstance {
containerId: string;
postgresId: string;
port: number;
}
const instances = new Map<string, ProxyInstance>();
const PG_PASSWORD = "p4S5w0rD";
async function proxyStart(synapse: SynapseInstance): Promise<ProxyInstance> {
console.log(new Date(), "Starting sliding sync proxy...");
const postgresId = await dockerRun({
image: "postgres",
containerName: "react-sdk-cypress-sliding-sync-postgres",
params: [
"--rm",
"-e", `POSTGRES_PASSWORD=${PG_PASSWORD}`,
],
});
const postgresIp = await dockerIp({ containerId: postgresId });
const synapseIp = await dockerIp({ containerId: synapse.synapseId });
console.log(new Date(), "postgres container up");
const waitTimeMillis = 30000;
const startTime = new Date().getTime();
let lastErr: Error;
while ((new Date().getTime() - startTime) < waitTimeMillis) {
try {
await dockerExec({
containerId: postgresId,
params: [
"pg_isready",
"-U", "postgres",
],
});
lastErr = null;
break;
} catch (err) {
console.log("pg_isready: failed");
lastErr = err;
}
}
if (lastErr) {
console.log("rethrowing");
throw lastErr;
}
const port = await getFreePort();
console.log(new Date(), "starting proxy container...");
const containerId = await dockerRun({
image: "ghcr.io/matrix-org/sliding-sync-proxy:v0.4.0",
containerName: "react-sdk-cypress-sliding-sync-proxy",
params: [
"--rm",
"-p", `${port}:8008/tcp`,
"-e", "SYNCV3_SECRET=bwahahaha",
"-e", `SYNCV3_SERVER=http://${synapseIp}:8008`,
"-e", `SYNCV3_DB=user=postgres dbname=postgres password=${PG_PASSWORD} host=${postgresIp} sslmode=disable`,
],
});
console.log(new Date(), "started!");
const instance: ProxyInstance = { containerId, postgresId, port };
instances.set(containerId, instance);
return instance;
}
async function proxyStop(instance: ProxyInstance): Promise<void> {
await dockerStop({
containerId: instance.containerId,
});
await dockerStop({
containerId: instance.postgresId,
});
instances.delete(instance.containerId);
console.log(new Date(), "Stopped sliding sync proxy.");
// cypress deliberately fails if you return 'undefined', so
// return null to signal all is well, and we've handled the task.
return null;
}
/**
* @type {Cypress.PluginConfig}
*/
export function slidingSyncProxyDocker(on: PluginEvents, config: PluginConfigOptions) {
on("task", {
proxyStart,
proxyStop,
});
on("after:spec", async (spec) => {
for (const instance of instances.values()) {
console.warn(`Cleaning up proxy on port ${instance.port} after ${spec.name}`);
await proxyStop(instance);
}
});
}

View file

@ -101,12 +101,13 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
const synapseId = await dockerRun({
image: "matrixdotorg/synapse:develop",
containerName: `react-sdk-cypress-synapse-${crypto.randomBytes(4).toString("hex")}`,
containerName: `react-sdk-cypress-synapse`,
params: [
"--rm",
"-v", `${synCfg.configDir}:/data`,
"-p", `${synCfg.port}:8008/tcp`,
],
cmd: "run",
});
console.log(`Started synapse with id ${synapseId} on port ${synCfg.port}.`);

View file

@ -36,4 +36,5 @@ import "./iframes";
import "./timeline";
import "./network";
import "./composer";
import "./proxy";
import "./axe";

58
cypress/support/proxy.ts Normal file
View file

@ -0,0 +1,58 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/// <reference types="cypress" />
import Chainable = Cypress.Chainable;
import AUTWindow = Cypress.AUTWindow;
import { ProxyInstance } from '../plugins/sliding-sync';
import { SynapseInstance } from "../plugins/synapsedocker";
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
/**
* Start a sliding sync proxy instance.
* @param synapse the synapse instance returned by startSynapse
*/
startProxy(synapse: SynapseInstance): Chainable<ProxyInstance>;
/**
* Custom command wrapping task:proxyStop whilst preventing uncaught exceptions
* for if Docker stopping races with the app's background sync loop.
* @param proxy the proxy instance returned by startProxy
*/
stopProxy(proxy: ProxyInstance): Chainable<AUTWindow>;
}
}
}
function startProxy(synapse: SynapseInstance): Chainable<ProxyInstance> {
return cy.task<ProxyInstance>("proxyStart", synapse);
}
function stopProxy(proxy?: ProxyInstance): Chainable<AUTWindow> {
if (!proxy) return;
// Navigate away from app to stop the background network requests which will race with Synapse shutting down
return cy.window({ log: false }).then((win) => {
win.location.href = 'about:blank';
cy.task("proxyStop", proxy);
});
}
Cypress.Commands.add("startProxy", startProxy);
Cypress.Commands.add("stopProxy", stopProxy);