Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
54 changes: 54 additions & 0 deletions e2e/helpers/deeplink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 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 {buildServerMap} from './serverMap';

export function mattermostDeepLinkUrl(hostAndPath: string): string {
const protocol = process.env.NODE_ENV === 'test' ? 'mattermost-dev' : 'mattermost';
return `${protocol}://${hostAndPath}`;
}

/** Build a channel deep link that preserves the team path from the configured server URL. */
export function channelDeepLinkUrl(serverUrl: string, channelName: string): string {
const normalized = serverUrl.endsWith('/') ? serverUrl : `${serverUrl}/`;
const parsed = new URL(normalized);
const teamPath = parsed.pathname.replace(/\/$/, '');
const channelPath = teamPath ? `${teamPath}/channels/${channelName}` : `/channels/${channelName}`;
return mattermostDeepLinkUrl(`${parsed.host}${channelPath}`);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

export async function openDeepLinkInApp(app: ElectronApplication, url: string): Promise<void> {
await app.evaluate((_, deepLinkUrl) => {
const openDeepLink = (global as any).__e2eOpenDeepLink as ((value: string) => void) | undefined;
if (!openDeepLink) {
throw new Error('__e2eOpenDeepLink not exposed (NODE_ENV must be test)');
}
openDeepLink(deepLinkUrl);
}, url);
}

/** Poll any tab for the server until one navigates to the expected channel. */
export async function waitForServerChannelNavigation(
app: ElectronApplication,
serverName: string,
channelName: string,
options?: {timeout?: number},
): Promise<void> {
await expect.poll(async () => {
const map = await buildServerMap(app);
const entries = map[serverName] ?? [];
for (const entry of entries) {
const url = await entry.win.url();
if (url.includes(channelName)) {
return true;
}
}
return false;
}, {
timeout: options?.timeout ?? 15_000,
message: `Server view should navigate to ${channelName}`,
}).toBe(true);
}
79 changes: 63 additions & 16 deletions e2e/specs/deep_linking/deeplink.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,21 @@
// See LICENSE.txt for license information.

import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';

import type {ElectronApplication} from 'playwright';

import {test, expect} from '../../fixtures/index';
import {waitForAppReady} from '../../helpers/appReadiness';
import {electronBinaryPath, appDir, demoConfig} from '../../helpers/config';
import {waitForLockFileRelease} from '../../helpers/cleanup';
import {closeElectronAppFast, registerElectronMainProcess} from '../../helpers/electronApp';
import {buildServerMap} from '../../helpers/serverMap';

test.describe('application', () => {
let app: ElectronApplication | undefined;
let userDataDir: string;

test.beforeAll(async ({}, testInfo) => {
test.skip(process.platform !== 'win32', 'Windows only deep link test');

userDataDir = path.join(testInfo.outputDir, 'userdata');
fs.mkdirSync(userDataDir, {recursive: true});
fs.writeFileSync(path.join(userDataDir, 'config.json'), JSON.stringify(demoConfig));
Expand All @@ -36,19 +33,12 @@ test.describe('application', () => {
timeout: 60_000,
});

const pid = app.process()?.pid;
if (pid) {
const registry = path.join(os.tmpdir(), 'mattermost-desktop-e2e-main-pids.txt');
try {
fs.appendFileSync(registry, `${pid}\n`, 'utf8');
} catch { /* non-fatal */ }
}
registerElectronMainProcess(app.process()?.pid);
});

test.afterAll(async () => {
await app?.close();
if (userDataDir) {
await waitForLockFileRelease(userDataDir).catch(() => {});
if (app && userDataDir) {
await closeElectronAppFast(app, userDataDir);
}
});

Expand Down Expand Up @@ -97,7 +87,64 @@ test.describe('application', () => {
const freshView = freshMap[serverName]?.[0]?.win;
return freshView?.url() ?? '';
}, {timeout: 30_000, message: 'deep-linked webContents did not navigate to the expected URL'}).toContain('github.com/test/url');
const dropdownButtonText = await mainWindow.innerText('.ServerDropdownButton');
expect(dropdownButtonText).toBe('github');

await expect.poll(
() => mainWindow.innerText('.ServerDropdownButton'),
{timeout: 15_000, message: 'deep link should activate the github server in the UI'},
).toBe('github');
});
});

test.describe('macOS open-url deep link', () => {
let app: ElectronApplication | undefined;
let userDataDir: string;

test.beforeAll(async ({}, testInfo) => {
userDataDir = path.join(testInfo.outputDir, 'open-url-userdata');
fs.mkdirSync(userDataDir, {recursive: true});
fs.writeFileSync(path.join(userDataDir, 'config.json'), JSON.stringify(demoConfig));

const {_electron: electron} = await import('playwright');

app = await electron.launch({
executablePath: electronBinaryPath,
args: [appDir, `--user-data-dir=${userDataDir}`, '--no-sandbox', '--disable-gpu'],
env: {...process.env, NODE_ENV: 'test'},
timeout: 60_000,
});

registerElectronMainProcess(app.process()?.pid);
});

test.afterAll(async () => {
if (app && userDataDir) {
await closeElectronAppFast(app, userDataDir);
}
});

test(
'DL-02 macOS cold start via open-url event navigates to deep link',
{tag: ['@P1', '@darwin']},
async () => {
await waitForAppReady(app!);

await app!.evaluate(({app: electronApp}) => {
electronApp.emit('open-url', {preventDefault: () => undefined}, 'mattermost-dev://github.com/test/url');
});

const serverName = demoConfig.servers[1].name;
await expect.poll(async () => {
const freshMap = await buildServerMap(app!);
const freshView = freshMap[serverName]?.[0]?.win;
return freshView?.url() ?? '';
}, {timeout: 30_000, message: 'open-url deep link should navigate the target server view'}).toContain('github.com/test/url');

const mainWindow = app!.windows().find((window) => window.url().includes('index'));
expect(mainWindow).toBeDefined();
await expect.poll(
() => mainWindow!.innerText('.ServerDropdownButton'),
{timeout: 15_000},
).toBe('github');
},
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
76 changes: 50 additions & 26 deletions e2e/specs/deep_linking/deeplink_running.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import {execSync} from 'child_process';

import {test, expect} from '../../fixtures/index';
import {mattermostURL, demoMattermostConfig} from '../../helpers/config';
import {test} from '../../fixtures/index';
import {mattermostURL, demoMattermostConfig, type AppConfig} from '../../helpers/config';
import {channelDeepLinkUrl, openDeepLinkInApp, waitForServerChannelNavigation} from '../../helpers/deeplink';
import {loginToMattermost} from '../../helpers/login';

// Use a real Mattermost server config so serverMap.example points to localhost:8065
Expand All @@ -13,12 +12,7 @@ test.use({appConfig: demoMattermostConfig});
test(
'deep link navigates to correct server while app is running',
{tag: ['@P1', '@darwin', '@win32']},
async ({serverMap}) => {
if (process.platform === 'linux') {
test.skip(true, 'Deep link not supported on Linux');
return;
}

async ({electronApp, serverMap}) => {
if (!process.env.MM_TEST_SERVER_URL) {
test.skip(true, 'MM_TEST_SERVER_URL required');
return;
Expand All @@ -33,23 +27,53 @@ test(
await loginToMattermost(serverWin);
await serverWin.waitForSelector('#sidebarItem_town-square', {timeout: 30_000});

// Trigger deep link from the OS
const channelName = 'town-square';
const deepLink = `mattermost://${new URL(mattermostURL).host}/channels/${channelName}`;
const deepLink = channelDeepLinkUrl(mattermostURL, channelName);

if (process.platform === 'darwin') {
execSync(`open "${deepLink}"`);
} else if (process.platform === 'win32') {
execSync(`start "" "${deepLink}"`);
}

// Wait for navigation to the linked channel
await expect.poll(
() => serverWin!.url(),
{
timeout: 15_000,
message: `Server view should navigate to ${channelName}`,
},
).toContain(channelName);
await openDeepLinkInApp(electronApp, deepLink);
await waitForServerChannelNavigation(electronApp, 'example', channelName);
},
);

test.describe('deep link server URL without trailing slash', () => {
const serverUrlWithoutSlash = mattermostURL.replace(/\/$/, '');
const configWithoutTrailingSlash: AppConfig = {
...demoMattermostConfig,
servers: demoMattermostConfig.servers.map((server, index) => (
index === 0 ? {...server, url: serverUrlWithoutSlash} : server
)),
};

test.use({appConfig: configWithoutTrailingSlash});

test(
'DL-01 deep link navigates when configured server URL has no trailing slash',
{tag: ['@P1', '@darwin', '@win32']},
async ({electronApp, serverMap}) => {
if (!process.env.MM_TEST_SERVER_URL) {
test.skip(true, 'MM_TEST_SERVER_URL required');
return;
}

const serverWin = serverMap.example?.[0]?.win;
if (!serverWin) {
test.skip(true, 'No server view available');
return;
}

await loginToMattermost(serverWin);
await serverWin.waitForSelector('#sidebarItem_town-square', {timeout: 30_000});

const channelName = 'off-topic';
const deepLink = channelDeepLinkUrl(serverUrlWithoutSlash, channelName);

await openDeepLinkInApp(electronApp, deepLink);
await waitForServerChannelNavigation(
electronApp,
'example',
channelName,
{timeout: 15_000},
);
},
);
});
38 changes: 38 additions & 0 deletions e2e/specs/deep_linking/oauth_callback.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import {test, expect} from '../../fixtures/index';
import {waitForAppReady} from '../../helpers/appReadiness';
import {demoConfig} from '../../helpers/config';
import {mattermostDeepLinkUrl, openDeepLinkInApp} from '../../helpers/deeplink';
import {buildServerMap} from '../../helpers/serverMap';

test(
'DL-03 OAuth callback deep link navigates the active server view',
{tag: ['@P1', '@all']},
async ({electronApp}) => {
await waitForAppReady(electronApp);

const serverName = demoConfig.servers[0].name;
const oauthPath = '/oauth/authorize?client_id=desktop&response_type=code&state=e2e-test';
const deepLink = mattermostDeepLinkUrl(`example.com${oauthPath}`);

await openDeepLinkInApp(electronApp, deepLink);

await expect.poll(async () => {
const serverMap = await buildServerMap(electronApp);
const view = serverMap[serverName]?.[0]?.win;
return view?.url() ?? '';
}, {
timeout: 30_000,
message: 'OAuth callback deep link should navigate the example server view',
}).toContain('example.com/oauth/authorize');

const mainWindow = electronApp.windows().find((window) => window.url().includes('index'));
expect(mainWindow).toBeDefined();
await expect.poll(
() => mainWindow!.innerText('.ServerDropdownButton'),
{timeout: 15_000},
).toBe(serverName);
},
);
8 changes: 5 additions & 3 deletions e2e/specs/policy/policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import {_electron as electron} from 'playwright';

import {test, expect} from '../../fixtures/index';
import {waitForAppReady} from '../../helpers/appReadiness';
import {waitForLockFileRelease} from '../../helpers/cleanup';
import {appDir, demoConfig, electronBinaryPath, exampleURL, mattermostURL, writeConfigFile} from '../../helpers/config';
import {closeElectronAppFast} from '../../helpers/electronApp';
import {buildServerMap} from '../../helpers/serverMap';

const isSupported = (process.platform === 'win32' || process.platform === 'darwin') && process.env.RUN_POLICY_E2E === 'true';
Expand Down Expand Up @@ -187,8 +187,10 @@ async function launchPolicyApp(testInfo: {outputDir: string}, options: LaunchOpt
}

async function closePolicyApp(app: Awaited<ReturnType<typeof electron.launch>> | undefined, userDataDir: string) {
await app?.close().catch(() => {});
await waitForLockFileRelease(userDataDir).catch(() => {});
if (!app) {
return;
}
await closeElectronAppFast(app, userDataDir);
}

async function getMainWindow(app: Awaited<ReturnType<typeof electron.launch>>) {
Expand Down
4 changes: 2 additions & 2 deletions e2e/specs/popup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {test, expect} from '../fixtures/index';
import {cmdOrCtrl, demoMattermostConfig} from '../helpers/config';
import {waitForAppReady} from '../helpers/appReadiness';
import {appDir, electronBinaryPath, writeConfigFile} from '../helpers/config';
import {waitForWindow, closeElectronApp} from '../helpers/electronApp';
import {waitForWindow, closeElectronAppFast} from '../helpers/electronApp';
import {loginToMattermost} from '../helpers/login';
import {buildServerMap} from '../helpers/serverMap';
import type {ServerView} from '../helpers/serverView';
Expand Down Expand Up @@ -128,7 +128,7 @@ test.describe('popup', () => {
});

test.afterAll(async () => {
await closeElectronApp(electronApp, userDataDir);
await closeElectronAppFast(electronApp, userDataDir);
});

test('MM-T2827_1 should be able to select all in popup windows', {tag: ['@P2', '@all']}, async () => {
Expand Down
18 changes: 3 additions & 15 deletions e2e/specs/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,7 @@ test.describe('Settings', () => {
});

test.describe('Save tray icon setting on mac', () => {
test("MM-T4393_2 should be saved when it's selected", {tag: ['@P2', '@all']}, async ({electronApp}, testInfo) => {
if (!['darwin', 'linux'].includes(process.platform)) {
test.skip(true, 'darwin/linux only');
return;
}
test("MM-T4393_2 should be saved when it's selected", {tag: ['@P2', '@darwin', '@linux']}, async ({electronApp}, testInfo) => {
const settingsWindow = await openSettingsWindow(electronApp);
await settingsWindow.waitForSelector('#settingCategoryButton-general');
await settingsWindow.click('#settingCategoryButton-general');
Expand All @@ -100,11 +96,7 @@ test.describe('Settings', () => {
});

test.describe('Save tray icon theme on linux', () => {
test("MM-T4393_3 should be saved when it's selected", {tag: ['@P2', '@all']}, async ({electronApp}, testInfo) => {
if (process.platform !== 'linux') {
test.skip(true, 'Linux only');
return;
}
test("MM-T4393_3 should be saved when it's selected", {tag: ['@P2', '@linux']}, async ({electronApp}, testInfo) => {
const settingsWindow = await openSettingsWindow(electronApp);
await settingsWindow.waitForSelector('#settingCategoryButton-general');
await settingsWindow.click('#settingCategoryButton-general');
Expand Down Expand Up @@ -208,11 +200,7 @@ test.describe('Settings', () => {
});

test.describe('Enable automatic check for updates', () => {
test('MM-T4549 should save selected option', {tag: ['@P2', '@all']}, async ({electronApp}, testInfo) => {
if (process.platform === 'darwin') {
test.skip(true, 'Not applicable on macOS');
return;
}
test('MM-T4549 should save selected option', {tag: ['@P2', '@win32', '@linux']}, async ({electronApp}, testInfo) => {
const ID_INPUT_ENABLE_AUTO_UPDATES = '#CheckSetting_autoCheckForUpdates button';
const settingsWindow = await openSettingsWindow(electronApp);
await settingsWindow.waitForSelector('#settingCategoryButton-general');
Expand Down
Loading
Loading