mirror of
https://github.com/zulip/zulip-desktop.git
synced 2025-10-29 02:53:40 +00:00
Move functional tab pages out of separate webviews.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type {DNDSettings} from "./dnd-util";
|
||||
import type {MenuProps, NavItem, ServerConf} from "./types";
|
||||
import type {MenuProps, ServerConf} from "./types";
|
||||
|
||||
export interface MainMessage {
|
||||
"clear-app-settings": () => void;
|
||||
@@ -66,7 +66,6 @@ export interface RendererMessage {
|
||||
"show-keyboard-shortcuts": () => void;
|
||||
"show-notification-settings": () => void;
|
||||
"switch-server-tab": (index: number) => void;
|
||||
"switch-settings-nav": (navItem: NavItem) => void;
|
||||
"tab-devtools": () => void;
|
||||
"toggle-autohide-menubar": (
|
||||
autoHideMenubar: boolean,
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Zulip - About</title>
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
body > div {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
const {AboutView} = require("./js/pages/about.js");
|
||||
document.body.append(new AboutView().$view);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -304,27 +304,28 @@ body {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
webview {
|
||||
webview,
|
||||
.functional-view {
|
||||
/* transition: opacity 0.3s ease-in; */
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
webview.onload {
|
||||
transition: opacity 1s cubic-bezier(0.95, 0.05, 0.795, 0.035);
|
||||
}
|
||||
|
||||
webview.active {
|
||||
webview.active,
|
||||
.functional-view.active {
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
webview.disabled {
|
||||
webview.disabled,
|
||||
.functional-view.disabled {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,19 @@ import {generateNodeFromHTML} from "./base";
|
||||
import type {TabProps} from "./tab";
|
||||
import Tab from "./tab";
|
||||
|
||||
export interface FunctionalTabProps extends TabProps {
|
||||
$view: Element;
|
||||
}
|
||||
|
||||
export default class FunctionalTab extends Tab {
|
||||
$view: Element;
|
||||
$el: Element;
|
||||
$closeButton?: Element;
|
||||
|
||||
constructor(props: TabProps) {
|
||||
constructor({$view, ...props}: FunctionalTabProps) {
|
||||
super(props);
|
||||
|
||||
this.$view = $view;
|
||||
this.$el = generateNodeFromHTML(this.templateHTML());
|
||||
if (this.props.name !== "Settings") {
|
||||
this.props.$root.append(this.$el);
|
||||
@@ -20,6 +26,23 @@ export default class FunctionalTab extends Tab {
|
||||
}
|
||||
}
|
||||
|
||||
override activate(): void {
|
||||
super.activate();
|
||||
this.$view.classList.add("active");
|
||||
this.$view.classList.remove("disabled");
|
||||
}
|
||||
|
||||
override deactivate(): void {
|
||||
super.deactivate();
|
||||
this.$view.classList.add("disabled");
|
||||
this.$view.classList.remove("active");
|
||||
}
|
||||
|
||||
override destroy(): void {
|
||||
super.destroy();
|
||||
this.$view.remove();
|
||||
}
|
||||
|
||||
templateHTML(): HTML {
|
||||
return html`
|
||||
<div class="tab functional-tab" data-tab-id="${this.props.tabIndex}">
|
||||
|
||||
@@ -5,20 +5,42 @@ import {ipcRenderer} from "../typed-ipc-renderer";
|
||||
import {generateNodeFromHTML} from "./base";
|
||||
import type {TabProps} from "./tab";
|
||||
import Tab from "./tab";
|
||||
import type WebView from "./webview";
|
||||
|
||||
export interface ServerTabProps extends TabProps {
|
||||
webview: WebView;
|
||||
}
|
||||
|
||||
export default class ServerTab extends Tab {
|
||||
webview: WebView;
|
||||
$el: Element;
|
||||
$badge: Element;
|
||||
|
||||
constructor(props: TabProps) {
|
||||
constructor({webview, ...props}: ServerTabProps) {
|
||||
super(props);
|
||||
|
||||
this.webview = webview;
|
||||
this.$el = generateNodeFromHTML(this.templateHTML());
|
||||
this.props.$root.append(this.$el);
|
||||
this.registerListeners();
|
||||
this.$badge = this.$el.querySelector(".server-tab-badge")!;
|
||||
}
|
||||
|
||||
override activate(): void {
|
||||
super.activate();
|
||||
this.webview.load();
|
||||
}
|
||||
|
||||
override deactivate(): void {
|
||||
super.deactivate();
|
||||
this.webview.hide();
|
||||
}
|
||||
|
||||
override destroy(): void {
|
||||
super.destroy();
|
||||
this.webview.$el!.remove();
|
||||
}
|
||||
|
||||
templateHTML(): HTML {
|
||||
return html`
|
||||
<div class="tab" data-tab-id="${this.props.tabIndex}">
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type {TabRole} from "../../../common/types";
|
||||
|
||||
import type WebView from "./webview";
|
||||
|
||||
export interface TabProps {
|
||||
role: TabRole;
|
||||
icon?: string;
|
||||
@@ -12,19 +10,16 @@ export interface TabProps {
|
||||
tabIndex: number;
|
||||
onHover?: () => void;
|
||||
onHoverOut?: () => void;
|
||||
webview: WebView;
|
||||
materialIcon?: string;
|
||||
onDestroy?: () => void;
|
||||
}
|
||||
|
||||
export default abstract class Tab {
|
||||
props: TabProps;
|
||||
webview: WebView;
|
||||
abstract $el: Element;
|
||||
|
||||
constructor(props: TabProps) {
|
||||
this.props = props;
|
||||
this.webview = this.props.webview;
|
||||
}
|
||||
|
||||
registerListeners(): void {
|
||||
@@ -41,16 +36,13 @@ export default abstract class Tab {
|
||||
|
||||
activate(): void {
|
||||
this.$el.classList.add("active");
|
||||
this.webview.load();
|
||||
}
|
||||
|
||||
deactivate(): void {
|
||||
this.$el.classList.remove("active");
|
||||
this.webview.hide();
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.$el.remove();
|
||||
this.webview.$el!.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,12 +107,7 @@ export default class WebView {
|
||||
this.props.onTitleChange();
|
||||
});
|
||||
|
||||
this.$el!.addEventListener("did-navigate-in-page", (event) => {
|
||||
const isSettingPage = event.url.includes("renderer/preference.html");
|
||||
if (isSettingPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$el!.addEventListener("did-navigate-in-page", () => {
|
||||
this.canGoBackButton();
|
||||
});
|
||||
|
||||
@@ -170,10 +165,7 @@ export default class WebView {
|
||||
});
|
||||
|
||||
this.$el!.addEventListener("did-start-loading", () => {
|
||||
const isSettingPage = this.props.url.includes("renderer/preference.html");
|
||||
if (!isSettingPage) {
|
||||
this.props.switchLoading(true, this.props.url);
|
||||
}
|
||||
});
|
||||
|
||||
this.$el!.addEventListener("did-stop-loading", () => {
|
||||
|
||||
@@ -8,12 +8,13 @@ import type {DNDSettings} from "../../common/dnd-util";
|
||||
import * as EnterpriseUtil from "../../common/enterprise-util";
|
||||
import Logger from "../../common/logger-util";
|
||||
import * as Messages from "../../common/messages";
|
||||
import type {RendererMessage} from "../../common/typed-ipc";
|
||||
import type {NavItem, ServerConf, TabData} from "../../common/types";
|
||||
|
||||
import FunctionalTab from "./components/functional-tab";
|
||||
import ServerTab from "./components/server-tab";
|
||||
import WebView from "./components/webview";
|
||||
import {AboutView} from "./pages/about";
|
||||
import {PreferenceView} from "./pages/preference/preference";
|
||||
import {initializeTray} from "./tray";
|
||||
import {ipcRenderer} from "./typed-ipc-renderer";
|
||||
import * as DomainUtil from "./utils/domain-util";
|
||||
@@ -41,7 +42,7 @@ const logger = new Logger({
|
||||
const rendererDirectory = path.resolve(__dirname, "..");
|
||||
type ServerOrFunctionalTab = ServerTab | FunctionalTab;
|
||||
|
||||
class ServerManagerView {
|
||||
export class ServerManagerView {
|
||||
$addServerButton: HTMLButtonElement;
|
||||
$tabsContainer: Element;
|
||||
$reloadButton: HTMLButtonElement;
|
||||
@@ -66,6 +67,7 @@ class ServerManagerView {
|
||||
functionalTabs: Map<string, number>;
|
||||
tabIndex: number;
|
||||
presetOrgs: string[];
|
||||
preferenceView?: PreferenceView;
|
||||
constructor() {
|
||||
this.$addServerButton = document.querySelector("#add-tab")!;
|
||||
this.$tabsContainer = document.querySelector("#tabs-container")!;
|
||||
@@ -111,7 +113,7 @@ class ServerManagerView {
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
initializeTray();
|
||||
initializeTray(this);
|
||||
await this.loadProxy();
|
||||
this.initDefaultSettings();
|
||||
this.initSidebar();
|
||||
@@ -532,16 +534,20 @@ class ServerManagerView {
|
||||
openFunctionalTab(tabProps: {
|
||||
name: string;
|
||||
materialIcon: string;
|
||||
url: string;
|
||||
makeView: () => Element;
|
||||
destroyView: () => void;
|
||||
}): void {
|
||||
if (this.functionalTabs.has(tabProps.name)) {
|
||||
this.activateTab(this.functionalTabs.get(tabProps.name)!);
|
||||
return;
|
||||
}
|
||||
|
||||
this.functionalTabs.set(tabProps.name, this.tabs.length);
|
||||
const index = this.tabs.length;
|
||||
this.functionalTabs.set(tabProps.name, index);
|
||||
|
||||
const tabIndex = this.getTabIndex();
|
||||
const $view = tabProps.makeView();
|
||||
this.$webviewsContainer.append($view);
|
||||
|
||||
this.tabs.push(
|
||||
new FunctionalTab({
|
||||
@@ -549,45 +555,14 @@ class ServerManagerView {
|
||||
materialIcon: tabProps.materialIcon,
|
||||
name: tabProps.name,
|
||||
$root: this.$tabsContainer,
|
||||
index: this.functionalTabs.get(tabProps.name)!,
|
||||
index,
|
||||
tabIndex,
|
||||
onClick: this.activateTab.bind(
|
||||
this,
|
||||
this.functionalTabs.get(tabProps.name)!,
|
||||
),
|
||||
onDestroy: this.destroyTab.bind(
|
||||
this,
|
||||
tabProps.name,
|
||||
this.functionalTabs.get(tabProps.name)!,
|
||||
),
|
||||
webview: new WebView({
|
||||
$root: this.$webviewsContainer,
|
||||
index: this.functionalTabs.get(tabProps.name)!,
|
||||
tabIndex,
|
||||
url: tabProps.url,
|
||||
role: "function",
|
||||
isActive: () =>
|
||||
this.functionalTabs.get(tabProps.name) === this.activeTabIndex,
|
||||
switchLoading: (loading: boolean, url: string) => {
|
||||
if (loading) {
|
||||
this.loading.add(url);
|
||||
} else {
|
||||
this.loading.delete(url);
|
||||
}
|
||||
|
||||
const tab = this.tabs[this.activeTabIndex];
|
||||
this.showLoading(
|
||||
tab instanceof ServerTab &&
|
||||
this.loading.has(tab.webview.props.url),
|
||||
);
|
||||
onClick: this.activateTab.bind(this, index),
|
||||
onDestroy: () => {
|
||||
this.destroyTab(tabProps.name, index);
|
||||
tabProps.destroyView();
|
||||
},
|
||||
onNetworkError: async (index: number) => {
|
||||
await this.openNetworkTroubleshooting(index);
|
||||
},
|
||||
onTitleChange: this.updateBadge.bind(this),
|
||||
nodeIntegration: true,
|
||||
preload: false,
|
||||
}),
|
||||
$view,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -602,20 +577,33 @@ class ServerManagerView {
|
||||
this.openFunctionalTab({
|
||||
name: "Settings",
|
||||
materialIcon: "settings",
|
||||
url: `file://${rendererDirectory}/preference.html#${nav}`,
|
||||
makeView: () => {
|
||||
this.preferenceView = new PreferenceView();
|
||||
this.preferenceView.$view.classList.add("functional-view");
|
||||
return this.preferenceView.$view;
|
||||
},
|
||||
destroyView: () => {
|
||||
this.preferenceView!.destroy();
|
||||
this.preferenceView = undefined;
|
||||
},
|
||||
});
|
||||
this.$settingsButton.classList.add("active");
|
||||
await this.tabs[this.functionalTabs.get("Settings")!].webview.send(
|
||||
"switch-settings-nav",
|
||||
nav,
|
||||
);
|
||||
this.preferenceView!.handleNavigation(nav);
|
||||
}
|
||||
|
||||
openAbout(): void {
|
||||
let aboutView: AboutView;
|
||||
this.openFunctionalTab({
|
||||
name: "About",
|
||||
materialIcon: "sentiment_very_satisfied",
|
||||
url: `file://${rendererDirectory}/about.html`,
|
||||
makeView: () => {
|
||||
aboutView = new AboutView();
|
||||
aboutView.$view.classList.add("functional-view");
|
||||
return aboutView.$view;
|
||||
},
|
||||
destroyView: () => {
|
||||
aboutView.destroy();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -768,16 +756,6 @@ class ServerManagerView {
|
||||
ipcRenderer.send("update-badge", messageCountAll);
|
||||
}
|
||||
|
||||
updateGeneralSettings<Channel extends keyof RendererMessage>(
|
||||
channel: Channel,
|
||||
...args: Parameters<RendererMessage[Channel]>
|
||||
): void {
|
||||
if (this.getActiveWebview()) {
|
||||
const webContentsId = this.getActiveWebview().getWebContentsId();
|
||||
ipcRenderer.sendTo(webContentsId, channel, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
toggleSidebar(show: boolean): void {
|
||||
if (show) {
|
||||
this.$sidebar.classList.remove("sidebar-hide");
|
||||
@@ -802,12 +780,6 @@ class ServerManagerView {
|
||||
return !(url.endsWith("/login/") || tab.webview.loading);
|
||||
}
|
||||
|
||||
getActiveWebview(): Electron.WebviewTag {
|
||||
const selector = "webview:not(.disabled)";
|
||||
const webview: Electron.WebviewTag = document.querySelector(selector)!;
|
||||
return webview;
|
||||
}
|
||||
|
||||
addContextMenu($serverImg: HTMLElement, index: number): void {
|
||||
$serverImg.addEventListener("contextmenu", (event) => {
|
||||
event.preventDefault();
|
||||
@@ -1012,9 +984,6 @@ class ServerManagerView {
|
||||
ipcRenderer.on("toggle-sidebar", (event: Event, show: boolean) => {
|
||||
// Toggle the left sidebar
|
||||
this.toggleSidebar(show);
|
||||
|
||||
// Toggle sidebar switch in the general settings
|
||||
this.updateGeneralSettings("toggle-sidebar", show);
|
||||
});
|
||||
|
||||
ipcRenderer.on("toggle-silent", (event: Event, state: boolean) => {
|
||||
@@ -1040,14 +1009,7 @@ class ServerManagerView {
|
||||
tabs: this.tabsForIpc,
|
||||
activeTabIndex: this.activeTabIndex,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateGeneralSettings(
|
||||
"toggle-autohide-menubar",
|
||||
autoHideMenubar,
|
||||
updateMenu,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1060,8 +1022,6 @@ class ServerManagerView {
|
||||
"toggle-silent",
|
||||
newSettings.silent ?? false,
|
||||
);
|
||||
const webContentsId = this.getActiveWebview().getWebContentsId();
|
||||
ipcRenderer.sendTo(webContentsId, "toggle-dnd", state, newSettings);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ export class PreferenceView {
|
||||
private readonly $shadow: ShadowRoot;
|
||||
private readonly $settingsContainer: Element;
|
||||
private readonly nav: Nav;
|
||||
private navItem: NavItem = "General";
|
||||
|
||||
constructor() {
|
||||
this.$view = document.createElement("div");
|
||||
@@ -49,21 +50,15 @@ export class PreferenceView {
|
||||
onItemSelected: this.handleNavigation,
|
||||
});
|
||||
|
||||
const navItem =
|
||||
this.nav.navItems.find(
|
||||
(navItem) => window.location.hash === `#${navItem}`,
|
||||
) ?? "General";
|
||||
|
||||
this.handleNavigation(navItem);
|
||||
|
||||
ipcRenderer.on("switch-settings-nav", this.handleSwitchSettingsNav);
|
||||
ipcRenderer.on("toggle-sidebar", this.handleToggleSidebar);
|
||||
ipcRenderer.on("toggle-autohide-menubar", this.handleToggleMenubar);
|
||||
ipcRenderer.on("toggle-tray", this.handleToggleTray);
|
||||
ipcRenderer.on("toggle-dnd", this.handleToggleDnd);
|
||||
|
||||
this.handleNavigation(this.navItem);
|
||||
}
|
||||
|
||||
handleNavigation = (navItem: NavItem): void => {
|
||||
this.navItem = navItem;
|
||||
this.nav.select(navItem);
|
||||
switch (navItem) {
|
||||
case "AddServer":
|
||||
@@ -104,11 +99,13 @@ export class PreferenceView {
|
||||
window.location.hash = `#${navItem}`;
|
||||
};
|
||||
|
||||
handleToggleTray(state: boolean) {
|
||||
this.handleToggle("tray-option", state);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
ipcRenderer.off("switch-settings-nav", this.handleSwitchSettingsNav);
|
||||
ipcRenderer.off("toggle-sidebar", this.handleToggleSidebar);
|
||||
ipcRenderer.off("toggle-autohide-menubar", this.handleToggleMenubar);
|
||||
ipcRenderer.off("toggle-tray", this.handleToggleTray);
|
||||
ipcRenderer.off("toggle-dnd", this.handleToggleDnd);
|
||||
}
|
||||
|
||||
@@ -121,13 +118,6 @@ export class PreferenceView {
|
||||
}
|
||||
}
|
||||
|
||||
private readonly handleSwitchSettingsNav = (
|
||||
_event: Event,
|
||||
navItem: NavItem,
|
||||
) => {
|
||||
this.handleNavigation(navItem);
|
||||
};
|
||||
|
||||
private readonly handleToggleSidebar = (_event: Event, state: boolean) => {
|
||||
this.handleToggle("sidebar-option", state);
|
||||
};
|
||||
@@ -136,10 +126,6 @@ export class PreferenceView {
|
||||
this.handleToggle("menubar-option", state);
|
||||
};
|
||||
|
||||
private readonly handleToggleTray = (_event: Event, state: boolean) => {
|
||||
this.handleToggle("tray-option", state);
|
||||
};
|
||||
|
||||
private readonly handleToggleDnd = (
|
||||
_event: Event,
|
||||
_state: boolean,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type {NativeImage, WebviewTag} from "electron";
|
||||
import type {NativeImage} from "electron";
|
||||
import {remote} from "electron";
|
||||
import path from "path";
|
||||
|
||||
import * as ConfigUtil from "../../common/config-util";
|
||||
import type {RendererMessage} from "../../common/typed-ipc";
|
||||
|
||||
import type {ServerManagerView} from "./main";
|
||||
import {ipcRenderer} from "./typed-ipc-renderer";
|
||||
|
||||
const {Tray, Menu, nativeImage, BrowserWindow} = remote;
|
||||
@@ -168,7 +169,7 @@ const createTray = function (): void {
|
||||
}
|
||||
};
|
||||
|
||||
export function initializeTray() {
|
||||
export function initializeTray(serverManagerView: ServerManagerView) {
|
||||
ipcRenderer.on("destroytray", (_event: Event) => {
|
||||
if (!tray) {
|
||||
return;
|
||||
@@ -224,12 +225,7 @@ export function initializeTray() {
|
||||
ConfigUtil.setConfigItem("trayIcon", true);
|
||||
}
|
||||
|
||||
const webview = document.querySelector<WebviewTag>(
|
||||
"webview:not([class*=disabled])",
|
||||
);
|
||||
if (webview !== null) {
|
||||
ipcRenderer.sendTo(webview.getWebContentsId(), "toggle-tray", state);
|
||||
}
|
||||
serverManagerView.preferenceView?.handleToggleTray(state);
|
||||
}
|
||||
|
||||
ipcRenderer.on("toggletray", toggleTray);
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="responsive desktop">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>Zulip - Settings</title>
|
||||
<link rel="stylesheet" href="css/fonts.css" />
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
body > div {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<zd-preference-view></zd-preference-view>
|
||||
<script>
|
||||
const {PreferenceView} = require("./js/pages/preference/preference.js");
|
||||
document.body.append(new PreferenceView().$view);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -16,8 +16,7 @@ test("app runs", async (t) => {
|
||||
const mainWindow = await take(windows);
|
||||
t.equal(await mainWindow.title(), "Zulip");
|
||||
|
||||
const mainWebview = await take(windows);
|
||||
await mainWebview.waitForSelector("#connect");
|
||||
await mainWindow.waitForSelector("#connect");
|
||||
} finally {
|
||||
await setup.endTest(app);
|
||||
}
|
||||
|
||||
@@ -16,9 +16,8 @@ test("add-organization", async (t) => {
|
||||
const mainWindow = await take(windows);
|
||||
t.equal(await mainWindow.title(), "Zulip");
|
||||
|
||||
const mainWebview = await take(windows);
|
||||
await mainWebview.fill(".setting-input-value", "chat.zulip.org");
|
||||
await mainWebview.click("#connect");
|
||||
await mainWindow.fill(".setting-input-value", "chat.zulip.org");
|
||||
await mainWindow.click("#connect");
|
||||
|
||||
const orgWebview = await take(windows);
|
||||
await orgWebview.waitForSelector("#id_username");
|
||||
|
||||
@@ -18,8 +18,7 @@ test("new-org-link", async (t) => {
|
||||
const mainWindow = await take(windows);
|
||||
t.equal(await mainWindow.title(), "Zulip");
|
||||
|
||||
const mainWebview = await take(windows);
|
||||
await mainWebview.click("#open-create-org-link");
|
||||
await mainWindow.click("#open-create-org-link");
|
||||
} finally {
|
||||
await setup.endTest(app);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user