Playwright: Convert sliding-sync test to playwright (#11989)

* Add method to send text message

* Add dockerUrl to HomeServerConfig

* Implement sliding sync proxy

* Convert tests

* Reload page after applying labs feature

* Remove converted files

* Remove timeout

* Remove sliding-sync

* Remove proxy import

* Remove reference to proxy

* wait for load

* Update date

* Convert enableLabsFeature to separate fixture

* Enable feature in fixture

* Skip over config and just write to local-storage

* Rename fixture

* Fix room header test

* Use type inference

* Override config instead of setting localstorage

* Set default language

* Always add labs feature

* Put this one test into a separate describe block

* Move labs lag within describe block
This commit is contained in:
R Midhun Suresh 2023-12-16 15:31:26 +05:30 committed by GitHub
parent de5931d5a8
commit 7b3d5b5f21
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 547 additions and 770 deletions

View file

@ -1,502 +0,0 @@
/*
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 { Interception } from "cypress/types/net-stubbing";
import { HomeserverInstance } from "../../plugins/utils/homeserver";
import { ProxyInstance } from "../../plugins/sliding-sync";
describe("Sliding Sync", () => {
beforeEach(() => {
cy.startHomeserver("default")
.as("homeserver")
.then((homeserver) => {
cy.startProxy(homeserver).as("proxy");
});
cy.all([cy.get<HomeserverInstance>("@homeserver"), cy.get<ProxyInstance>("@proxy")]).then(
([homeserver, 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(homeserver, "Sloth").then(() => {
return cy.window({ log: false }).then(() => {
cy.createRoom({ name: "Test Room" }).as("roomId");
});
});
},
);
});
afterEach(() => {
cy.get<HomeserverInstance>("@homeserver").then(cy.stopHomeserver);
cy.get<ProxyInstance>("@proxy").then(cy.stopProxy);
});
// assert order
const checkOrder = (wantOrder: string[]) => {
cy.findByRole("group", { name: "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<HomeserverInstance>("@homeserver").then((homeserver) => {
return cy
.getBot(homeserver, {
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);
});
});
};
it.skip("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.findByRole("treeitem", { name: "Apple" }));
cy.createRoom({ name: "Pineapple" }).then(() => cy.findByRole("treeitem", { name: "Pineapple" }));
cy.createRoom({ name: "Orange" }).then(() => cy.findByRole("treeitem", { name: "Orange" }));
cy.get(".mx_RoomSublist_tiles").within(() => {
cy.findAllByRole("treeitem").should("have.length", 4); // due to the Test Room in beforeEach
});
checkOrder(["Orange", "Pineapple", "Apple", "Test Room"]);
cy.findByRole("group", { name: "Rooms" }).within(() => {
cy.get(".mx_RoomSublist_headerContainer")
.realHover()
.findByRole("button", { name: "List options" })
.click();
});
// force click as the radio button's size is zero
cy.findByRole("menuitemradio", { name: "A-Z" }).click({ force: true });
// Assert that the radio button is checked
cy.get(".mx_StyledRadioButton_checked").within(() => {
cy.findByText("A-Z").should("exist");
});
checkOrder(["Apple", "Orange", "Pineapple", "Test Room"]);
});
it.skip("should move rooms around as new events arrive", () => {
// create rooms and check room names are correct
cy.createRoom({ name: "Apple" })
.as("roomA")
.then(() => cy.findByRole("treeitem", { name: "Apple" }));
cy.createRoom({ name: "Pineapple" })
.as("roomP")
.then(() => cy.findByRole("treeitem", { name: "Pineapple" }));
cy.createRoom({ name: "Orange" })
.as("roomO")
.then(() => cy.findByRole("treeitem", { name: "Orange" }));
// Select the Test Room
cy.findByRole("treeitem", { name: "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.skip("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.findByRole("treeitem", { name: "Apple" }));
cy.createRoom({ name: "Pineapple" })
.as("roomP")
.then(() => cy.findByRole("treeitem", { name: "Pineapple" }));
cy.createRoom({ name: "Orange" })
.as("roomO")
.then(() => cy.findByRole("treeitem", { name: "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.findByRole("treeitem", { name: "Pineapple" }).click();
checkOrder(["Orange", "Pineapple", "Apple", "Test Room"]);
// Move Apple
bumpRoom("@roomA");
checkOrder(["Apple", "Pineapple", "Orange", "Test Room"]);
// Select the Test Room
cy.findByRole("treeitem", { name: "Test Room" }).click();
// the rooms reshuffle to match reality
checkOrder(["Apple", "Orange", "Pineapple", "Test Room"]);
});
it.skip("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.findByRole("treeitem", { name: "Test Room 1 unread message." }).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.findByRole("treeitem", { name: "Test Room 2 unread messages including mentions." }).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.findByRole("treeitem", { name: "Test Room 2 unread messages including mentions." }).click();
cy.findByRole("treeitem", { name: "Test Room" }).should("not.have.class", "mx_NotificationBadge_count");
});
it.skip("should not show unread indicators", () => {
// TODO: for now. Later we should.
createAndJoinBob();
// disable notifs in this room (TODO: CS API call?)
cy.findByRole("treeitem", { name: "Test Room" })
.realHover()
.findByRole("button", { name: "Notification options" })
.click();
cy.findByRole("menuitemradio", { name: "Mute room" }).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.findByRole("treeitem", { name: "Test Room" }).get(".mx_NotificationBadge").should("not.exist");
});
it("should update user settings promptly", () => {
cy.openUserSettings("Preferences");
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.skip("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: "Room to Join" }),
bob.createRoom({ name: "Room to Reject" }),
bob.createRoom({ name: "Room to 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),
]);
});
cy.findByRole("group", { name: "Invites" }).within(() => {
// Exclude headerText
cy.get(".mx_RoomSublist_tiles").within(() => {
// Wait for them all to be on the UI
cy.findAllByRole("treeitem").should("have.length", 3);
});
});
// Select the room to join
cy.findByRole("treeitem", { name: "Room to Join" }).click();
cy.get(".mx_RoomView").within(() => {
// Accept the invite
cy.findByRole("button", { name: "Accept" }).click();
});
checkOrder(["Room to Join", "Test Room"]);
// Select the room to reject
cy.findByRole("treeitem", { name: "Room to Reject" }).click();
cy.get(".mx_RoomView").within(() => {
// Reject the invite
cy.findByRole("button", { name: "Reject" }).click();
});
cy.findByRole("group", { name: "Invites" }).within(() => {
// Exclude headerText
cy.get(".mx_RoomSublist_tiles").within(() => {
// Wait for the rejected room to disappear
cy.findAllByRole("treeitem").should("have.length", 2);
});
});
// check the lists are correct
checkOrder(["Room to Join", "Test Room"]);
cy.findByRole("group", { name: "Invites" })
.find(".mx_RoomTile_title")
.should((elements) => {
expect(
_.map(elements, (e) => {
return e.textContent;
}),
"rooms are sorted",
).to.deep.equal(["Room to Rescind"]);
});
// now rescind the invite
cy.get<MatrixClient>("@bob").then((bob) => {
return bob.kick(roomRescind, clientUserId);
});
cy.findByRole("group", { name: "Rooms" }).within(() => {
// Exclude headerText
cy.get(".mx_RoomSublist_tiles").within(() => {
// Wait for the rescind to take effect and check the joined list once more
cy.findAllByRole("treeitem").should("have.length", 2);
});
});
checkOrder(["Room to Join", "Test Room"]);
});
it("should show a favourite DM only in the favourite sublist", () => {
cy.createRoom({
name: "Favourite DM",
is_direct: true,
})
.as("room")
.then((roomId) => {
cy.getClient().then((cli) => cli.setRoomTag(roomId, "m.favourite", { order: 0.5 }));
});
cy.findByRole("group", { name: "Favourites" }).findByText("Favourite DM").should("exist");
cy.findByRole("group", { name: "People" }).findByText("Favourite DM").should("not.exist");
});
// Regression test for a bug in SS mode, but would be useful to have in non-SS mode too.
// This ensures we are setting RoomViewStore state correctly.
it.skip("should clear the reply to field when swapping rooms", () => {
cy.createRoom({ name: "Other Room" })
.as("roomA")
.then(() => cy.findByRole("treeitem", { name: "Other Room" }));
cy.get<string>("@roomId").then((roomId) => {
return cy.sendEvent(roomId, null, "m.room.message", {
body: "Hello world",
msgtype: "m.text",
});
});
// select the room
cy.findByRole("treeitem", { name: "Test Room" }).click();
cy.get(".mx_ReplyPreview").should("not.exist");
// click reply-to on the Hello World message
cy.get(".mx_EventTile_last")
.within(() => {
cy.findByText("Hello world", { timeout: 1000 });
})
.realHover()
.findByRole("button", { name: "Reply" })
.click();
// check it's visible
cy.get(".mx_ReplyPreview").should("exist");
// now click Other Room
cy.findByRole("treeitem", { name: "Other Room" }).click();
// ensure the reply-to disappears
cy.get(".mx_ReplyPreview").should("not.exist");
// click back
cy.findByRole("treeitem", { name: "Test Room" }).click();
// ensure the reply-to reappears
cy.get(".mx_ReplyPreview").should("exist");
});
// Regression test for https://github.com/vector-im/element-web/issues/21462
it.skip("should not cancel replies when permalinks are clicked", () => {
cy.get<string>("@roomId").then((roomId) => {
// we require a first message as you cannot click the permalink text with the avatar in the way
return cy
.sendEvent(roomId, null, "m.room.message", {
body: "First message",
msgtype: "m.text",
})
.then(() => {
return cy.sendEvent(roomId, null, "m.room.message", {
body: "Permalink me",
msgtype: "m.text",
});
})
.then(() => {
cy.sendEvent(roomId, null, "m.room.message", {
body: "Reply to me",
msgtype: "m.text",
});
});
});
// select the room
cy.findByRole("treeitem", { name: "Test Room" }).click();
cy.get(".mx_ReplyPreview").should("not.exist");
// click reply-to on the Reply to me message
cy.get(".mx_EventTile")
.last()
.within(() => {
cy.findByText("Reply to me");
})
.realHover()
.findByRole("button", { name: "Reply" })
.click();
// check it's visible
cy.get(".mx_ReplyPreview").should("exist");
// now click on the permalink for Permalink me
cy.contains(".mx_EventTile", "Permalink me").find("a").click({ force: true });
// make sure it is now selected with the little green |
cy.contains(".mx_EventTile_selected", "Permalink me").should("exist");
// ensure the reply-to does not disappear
cy.get(".mx_ReplyPreview").should("exist");
});
it.skip("should send unsubscribe_rooms for every room switch", () => {
let roomAId: string;
let roomPId: string;
// create rooms and check room names are correct
cy.createRoom({ name: "Apple" })
.as("roomA")
.then((roomId) => (roomAId = roomId))
.then(() => cy.findByRole("treeitem", { name: "Apple" }));
cy.createRoom({ name: "Pineapple" })
.as("roomP")
.then((roomId) => (roomPId = roomId))
.then(() => cy.findByRole("treeitem", { name: "Pineapple" }));
cy.createRoom({ name: "Orange" })
.as("roomO")
.then(() => cy.findByRole("treeitem", { name: "Orange" }));
// Intercept all calls to /sync
cy.intercept({ method: "POST", url: "**/sync*" }).as("syncRequest");
const assertUnsubExists = (interception: Interception, subRoomId: string, unsubRoomId: string) => {
const body = interception.request.body;
// There may be a request without a txn_id, ignore it, as there won't be any subscription changes
if (body.txn_id === undefined) {
return;
}
expect(body.unsubscribe_rooms).eql([unsubRoomId]);
expect(body.room_subscriptions).to.not.have.property(unsubRoomId);
expect(body.room_subscriptions).to.have.property(subRoomId);
};
// Select the Test Room
cy.findByRole("treeitem", { name: "Apple" }).click();
// and wait for cypress to get the result as alias
cy.wait("@syncRequest").then((interception) => {
// This is the first switch, so no unsubscriptions yet.
assert.isObject(interception.request.body.room_subscriptions, "room_subscriptions is object");
});
// Switch to another room
cy.findByRole("treeitem", { name: "Pineapple" }).click();
cy.wait("@syncRequest").then((interception) => assertUnsubExists(interception, roomPId, roomAId));
// And switch to even another room
cy.findByRole("treeitem", { name: "Apple" }).click();
cy.wait("@syncRequest").then((interception) => assertUnsubExists(interception, roomPId, roomAId));
// TODO: Add tests for encrypted rooms
});
});

View file

@ -22,7 +22,6 @@ import PluginEvents = Cypress.PluginEvents;
import PluginConfigOptions = Cypress.PluginConfigOptions;
import { synapseDocker } from "./synapsedocker";
import { dendriteDocker } from "./dendritedocker";
import { slidingSyncProxyDocker } from "./sliding-sync";
import { webserver } from "./webserver";
import { docker } from "./docker";
import { log } from "./log";
@ -31,7 +30,7 @@ import { log } from "./log";
* @type {Cypress.PluginConfig}
*/
export default function (on: PluginEvents, config: PluginConfigOptions) {
initPlugins(on, [docker, synapseDocker, dendriteDocker, slidingSyncProxyDocker, webserver, log], config);
initPlugins(on, [docker, synapseDocker, dendriteDocker, webserver, log], config);
installLogsPrinter(on, {
printLogsToConsole: "never",

View file

@ -1,129 +0,0 @@
/*
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 { HomeserverInstance } from "../utils/homeserver";
// A cypress plugin to add command to start & stop https://github.com/matrix-org/sliding-sync
// SLIDING_SYNC_PROXY_TAG env used as the docker tag to use for `ghcr.io/matrix-org/sliding-sync` image.
export interface ProxyInstance {
containerId: string;
postgresId: string;
port: number;
}
const instances = new Map<string, ProxyInstance>();
const PG_PASSWORD = "p4S5w0rD";
async function proxyStart(dockerTag: string, homeserver: HomeserverInstance): 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 homeserverIp = await dockerIp({ containerId: homeserver.serverId });
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...", dockerTag);
const containerId = await dockerRun({
image: "ghcr.io/matrix-org/sliding-sync:" + dockerTag,
containerName: "react-sdk-cypress-sliding-sync-proxy",
params: [
"--rm",
"-p",
`${port}:8008/tcp`,
"-e",
"SYNCV3_SECRET=bwahahaha",
"-e",
`SYNCV3_SERVER=http://${homeserverIp}: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) {
const dockerTag = config.env["SLIDING_SYNC_PROXY_TAG"];
on("task", {
proxyStart: proxyStart.bind(null, dockerTag),
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

@ -38,7 +38,6 @@ import "./iframes";
import "./timeline";
import "./network";
import "./composer";
import "./proxy";
import "./axe";
import "./promise";

View file

@ -1,58 +0,0 @@
/*
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 { HomeserverInstance } from "../plugins/utils/homeserver";
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
/**
* Start a sliding sync proxy instance.
* @param homeserver the homeserver instance returned by startHomeserver
*/
startProxy(homeserver: HomeserverInstance): 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(homeserver: HomeserverInstance): Chainable<ProxyInstance> {
return cy.task<ProxyInstance>("proxyStart", homeserver);
}
function stopProxy(proxy?: ProxyInstance): Chainable<AUTWindow> {
if (!proxy) return;
// Navigate away from app to stop the background network requests which will race with Homeserver 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);