Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
db20b56
E2E: Playwright harness, fixtures, and CI wiring (1/10).
yasserfaraazkhan Jun 19, 2026
f46ebbc
E2E: Main-process test hooks and shared launch helpers (2/10).
yasserfaraazkhan Jun 19, 2026
8f76529
E2E: Server management, bad servers, certificate trust (6/10).
yasserfaraazkhan Jun 19, 2026
b41357d
Merge origin/master into e2e/06-server-management
yasserfaraazkhan Jun 30, 2026
4b68d19
Address CodeRabbit review on server management E2E specs
yasserfaraazkhan Jun 30, 2026
984cd72
E2E: Popout, drag-drop, startup, and Linux specs (7/10). (#3861)
yasserfaraazkhan Jul 1, 2026
dfda040
E2E: Tray, settings, and deep linking (#3864)
yasserfaraazkhan Jul 1, 2026
10c57c0
Address CodeRabbit review on deep linking, settings, and tray E2E specs.
yasserfaraazkhan Jul 1, 2026
9630cc9
fix(e2e): add missing mattermostShell helper for drag_and_drop tests
yasserfaraazkhan Jul 1, 2026
6bee0fe
fix(e2e): harden mattermostShell textbox helpers
yasserfaraazkhan Jul 1, 2026
ba7b5db
fix(e2e): await ServerView.url() and harden copy link test
yasserfaraazkhan Jul 1, 2026
c9e1022
fix code gaps
yasserfaraazkhan Jul 1, 2026
44a0ae0
fix(e2e): fix CI failures in tray_icon_hide and bad_servers specs
yasserfaraazkhan Jul 1, 2026
fb818e2
fix(e2e): fix mkdir bug in tray_icon_hide relaunch, retry dropdown click
yasserfaraazkhan Jul 1, 2026
84e0523
fix(e2e): address outstanding CodeRabbit review comments
yasserfaraazkhan Jul 1, 2026
a0ed0a5
fix(e2e): revert popout predicate removal, add bringToFront for dropdown
yasserfaraazkhan Jul 1, 2026
e66524c
fix(e2e): stop re-clicking the server dropdown toggle button on timeout
yasserfaraazkhan Jul 1, 2026
ff9b9ab
diag(e2e): instrument the server dropdown open failure instead of gue…
yasserfaraazkhan Jul 1, 2026
37a72d1
fix(e2e): fix the real root cause of the server dropdown open failure
yasserfaraazkhan Jul 1, 2026
4e906c1
fix(e2e): remove overly-broad beforeEach that destroyed server dropdo…
yasserfaraazkhan Jul 1, 2026
acba854
fix(e2e): guard config.json reads against a real non-atomic write race
yasserfaraazkhan Jul 1, 2026
da5dab5
Merge remote-tracking branch 'origin/master' into e2e/06-server-manag…
yasserfaraazkhan Jul 2, 2026
49fb2d6
fix linux failure
yasserfaraazkhan Jul 2, 2026
6d4f076
remove main code refs
yasserfaraazkhan Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions e2e/helpers/errorView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import {expect} from '@playwright/test';
import type {ElectronApplication} from 'playwright';

import {clearCertificateErrorCallbacks} from './dialog';
import {evaluateInMainProcessWithArg} from './testRefs';

type WaitForErrorViewOptions = {
serverName?: string;
timeout?: number;
};

/**
* Wait for the renderer to mount, then reload server views so load failures
* that fired before IPC listeners were registered are surfaced in ErrorView.
*/
export async function waitForRendererThenReload(
app: ElectronApplication,
serverName?: string,
): Promise<void> {
const mainWindow = app.windows().find((window) => window.url().includes('index'));
if (!mainWindow) {
return;
}

await mainWindow.waitForSelector('.ServerDropdownButton', {timeout: 15_000}).catch(() => {});

if (serverName) {
await expect.poll(() => {
return !app.windows().some((window) => {
try {
return window.url().includes('newServer');
} catch {
return false;
}
});
}, {timeout: 10_000, message: 'Add server modal should close after confirm'}).toBe(true);

await expect.poll(async () => {
return mainWindow.innerText('.ServerDropdownButton');
}, {timeout: 10_000, message: `Active server should switch to ${serverName}`}).toContain(serverName);
}

// Wait until ServerManager knows about the target server. We intentionally do NOT
// require a WebContentsManager entry to exist here: on platforms where the initial
// load fails very fast (e.g. expired cert at startup), the view may never be added
// to WebContentsManager before this poll's deadline. If no view exists, the reload
// step below becomes a no-op and the existing load failure already surfaces in
// `.ErrorView`, which is what the caller is polling for.
//
// NOTE: Playwright's ElectronApplication.evaluate(fn, arg) always passes the
// `electron` module as the FIRST argument to `fn`. The user-supplied `arg` is the
// SECOND argument. Hence the `(_electron, targetServerName)` signature below.
await expect.poll(() => {
return evaluateInMainProcessWithArg(app, (_electron, targetServerName) => {
const refs = (global as any).__e2eTestRefs;
if (!refs) {
return false;
}
const servers: Array<{id: string; name: string}> = refs.ServerManager?.getAllServers?.() ?? [];
const serversToCheck = targetServerName ?
servers.filter((server) => server.name === targetServerName) :
servers;
return serversToCheck.length > 0;
}, serverName);
}, {timeout: 15_000, message: 'Target server should be registered before reload'}).toBe(true);

await clearCertificateErrorCallbacks(app).catch(() => {});

await evaluateInMainProcessWithArg(app, (_electron, targetServerName) => {
const refs = (global as any).__e2eTestRefs;
if (!refs) {
return;
}
const servers: Array<{id: string; name: string}> = refs.ServerManager?.getAllServers?.() ?? [];
const serversToReload = targetServerName ?
servers.filter((server) => server.name === targetServerName) :
servers;
for (const server of serversToReload) {
const views: Array<{id: string}> = refs.ViewManager?.getViewsByServerId?.(server.id) ?? [];
for (const view of views) {
const wcEntry = refs.WebContentsManager?.getView?.(view.id);
wcEntry?.reload?.();
}
}
}, serverName);

await expect.poll(async () => {
return evaluateInMainProcessWithArg(app, (_electron, targetServerName) => {
const refs = (global as any).__e2eTestRefs;
if (!refs) {
return false;
}
const servers: Array<{id: string; name: string}> = refs.ServerManager?.getAllServers?.() ?? [];
const serversToCheck = targetServerName ?
servers.filter((server) => server.name === targetServerName) :
servers;
for (const server of serversToCheck) {
const views: Array<{id: string}> = refs.ViewManager?.getViewsByServerId?.(server.id) ?? [];
for (const view of views) {
const wcEntry = refs.WebContentsManager?.getView?.(view.id);
if (wcEntry?.webContents?.isLoading?.()) {
return false;
}
}
}
return true;
}, serverName);
}, {timeout: 15_000, message: 'Server views should finish reloading after renderer is ready'}).toBe(true);
}

export async function waitForErrorView(
app: ElectronApplication,
options: WaitForErrorViewOptions = {},
): Promise<void> {
const timeout = options.timeout ?? (process.env.CI ? 60_000 : 45_000);
const deadline = Date.now() + timeout;
let lastError: unknown;
while (Date.now() < deadline) {
try {
const mainWindow = app.windows().find((window) => window.url().includes('index'));
if (!mainWindow) {
throw new Error('Main index window is not available yet');
}
await waitForRendererThenReload(app, options.serverName);
await mainWindow.waitForSelector('.ErrorView', {
timeout: Math.min(10_000, deadline - Date.now()),
});
return;
} catch (error) {
lastError = error;
await clearCertificateErrorCallbacks(app).catch(() => {});
await new Promise((resolve) => setTimeout(resolve, 250));
}
}

throw lastError instanceof Error ? lastError : new Error('ErrorView did not appear before timeout');
}
29 changes: 10 additions & 19 deletions e2e/specs/server_management/add_server_modal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as path from 'path';
import {test, expect} from '../../fixtures/index';
import {waitForAppReady} from '../../helpers/appReadiness';
import {electronBinaryPath, appDir, demoConfig, writeConfigFile} from '../../helpers/config';
import {waitForLockFileRelease} from '../../helpers/cleanup';
import {closeElectronAppFast} from '../../helpers/electronApp';

async function waitForWindow(app: Awaited<ReturnType<typeof import('playwright')['_electron']['launch']>>, pattern: string, timeout = 30_000) {
const timeoutAt = Date.now() + timeout;
Expand Down Expand Up @@ -66,8 +66,7 @@ test.describe('Add Server Modal', () => {
const isFocused = await newServerView.$eval('#serverUrlInput', (el) => el.isSameNode(document.activeElement));
expect(isFocused).toBe(true);
} finally {
await app.close();
await waitForLockFileRelease(userDataDir);
await closeElectronAppFast(app, userDataDir);
}
});

Expand All @@ -79,8 +78,7 @@ test.describe('Add Server Modal', () => {
const existing = Boolean(app.windows().find((w) => w.url().includes('newServer')));
expect(existing).toBe(false);
} finally {
await app.close();
await waitForLockFileRelease(userDataDir);
await closeElectronAppFast(app, userDataDir);
}
});

Expand All @@ -91,8 +89,7 @@ test.describe('Add Server Modal', () => {
const disabled = await newServerView.getAttribute('#newServerModal_confirm', 'disabled');
expect(disabled === '').toBe(true);
} finally {
await app.close();
await waitForLockFileRelease(userDataDir);
await closeElectronAppFast(app, userDataDir);
}
});

Expand All @@ -107,8 +104,7 @@ test.describe('Add Server Modal', () => {
const disabled = await newServerView.getAttribute('#newServerModal_confirm', 'disabled');
expect(disabled === '').toBe(false);
} finally {
await app.close();
await waitForLockFileRelease(userDataDir);
await closeElectronAppFast(app, userDataDir);
}
});

Expand All @@ -121,8 +117,7 @@ test.describe('Add Server Modal', () => {
const disabled = await newServerView.getAttribute('#newServerModal_confirm', 'disabled');
expect(disabled === '').toBe(true);
} finally {
await app.close();
await waitForLockFileRelease(userDataDir);
await closeElectronAppFast(app, userDataDir);
}
});
});
Expand All @@ -140,8 +135,7 @@ test.describe('Add Server Modal', () => {
expect(existingUrl).toBe(false);
expect(disabled === '').toBe(true);
} finally {
await app.close();
await waitForLockFileRelease(userDataDir);
await closeElectronAppFast(app, userDataDir);
}
});
});
Expand All @@ -155,8 +149,7 @@ test.describe('Add Server Modal', () => {
const existing = await newServerView.isVisible('#customMessage_url.Input___error');
expect(existing).toBe(true);
} finally {
await app.close();
await waitForLockFileRelease(userDataDir);
await closeElectronAppFast(app, userDataDir);
}
});

Expand All @@ -170,8 +163,7 @@ test.describe('Add Server Modal', () => {
const disabled = await newServerView.getAttribute('#newServerModal_confirm', 'disabled');
expect(disabled === null).toBe(true);
} finally {
await app.close();
await waitForLockFileRelease(userDataDir);
await closeElectronAppFast(app, userDataDir);
}
});

Expand Down Expand Up @@ -202,8 +194,7 @@ test.describe('Add Server Modal', () => {
}),
]));
} finally {
await app.close();
await waitForLockFileRelease(userDataDir);
await closeElectronAppFast(app, userDataDir);
}
});
});
Expand Down
Loading
Loading