Add Playwright E2E tests for demo plugin webapp components (#36560)

* Add Playwright E2E tests for demo plugin webapp components

  Adds 7 new spec files covering the demo plugin's webapp components:
  Root Modal (user actions, menus, post dropdown), sidebar components,
  channel header button/RHS, file upload/preview components, and user
  settings. Updates helpers.ts with shared assertRootModal and
  closeRootModal helpers. Adds sample-file.demo test asset.

  Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* Address CodeRabbit review feedback

  - Scope 'more actions' button to post container in demo_file_components
  - Replace regex with string for profile popover accessible name
  - Replace evaluate click with .click() and clean up dialog handler in demo_user_settings
  - Remove Cancel bug test from demo_root_modal_menus with explanatory comment

  Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* Implement feedback as per review

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* Implement CodeRabbit suggestion: wait for upload response in uploadFileViaYourComputer

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* Implement CodeRabbit suggestion: scope hover to post container in demo_file_components

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: use import type for Client4 to satisfy consistent-type-imports rule

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* E2E/Test: Skip broken Sample Confirmation Dialog test; fix default_config after master merge

- Skip demo_root_modal_menus "Sample Confirmation Dialog": unable to resolve this test failure, it is a bug with the demo plugin. v0.10.3 does not set a URL on the openInteractiveDialog call, causing the webapp to log "Interactive dialog missing URL" and render nothing. Test will be re-enabled once the plugin is fixed and the build bumped.

- Remove EnableAccessControlAuditLogging and AIRecapSettings from default_config.ts — both fields were removed when I synced up from master.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* E2E/Test: Restore EnableAccessControlAuditLogging and AIRecapSettings to default_config

Both fields are still required in @mattermost/types as of master. They were
incorrectly removed in the previous commit because the local dist was stale
after the master merge. Rebuilt webapp platform packages to confirm correct
type state before restoring.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dylan Haussermann
2026-08-08 04:17:04 +02:00
committed by GitHub
co-authored by Claude Sonnet 4.6 Mattermost Build
parent 68535c13bc
commit 53373e3752
10 changed files with 661 additions and 1 deletions
@@ -0,0 +1 @@
this is a sample .demo file
@@ -8,11 +8,13 @@ export default class ChannelsAppBar {
readonly container: Locator;
readonly playbooksIcon;
readonly demoPluginButton;
constructor(container: Locator) {
this.container = container;
this.playbooksIcon = container.locator('#app-bar-icon-playbooks').getByRole('img');
this.demoPluginButton = container.locator('#app-bar-icon-com\\.mattermost\\.demo-plugin');
}
async toBeVisible() {
@@ -1,11 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import path from 'node:path';
import type {Page} from '@playwright/test';
import type {Client4} from '@mattermost/client';
import {ClientError} from '@mattermost/client';
import {mergeWithOnPremServerConfig} from '@mattermost/playwright-lib';
import {expect, mergeWithOnPremServerConfig} from '@mattermost/playwright-lib';
const assetPath = path.resolve(__dirname, '../../../../asset');
const DEMO_PLUGIN_ID = 'com.mattermost.demo-plugin';
const DEMO_PLUGIN_URL =
@@ -13,10 +17,64 @@ const DEMO_PLUGIN_URL =
export {DEMO_PLUGIN_ID, DEMO_PLUGIN_URL};
// Repeated in all Root Modal tests — avoids duplicating the long trigger string
const ROOT_MODAL_TRIGGER_TEXT = 'You have triggered the root component of the demo plugin.';
/**
* Asserts the Root Modal is visible with its 3 base lines.
* Pass elementClicked to also assert the "Element clicked in the menu: X" line.
* Note: "Element clicked in the menu: " and the item name render in separate <span> elements,
* so they are asserted individually.
*/
export async function assertRootModal(page: Page, elementClicked?: string): Promise<void> {
await expect(page.getByText(ROOT_MODAL_TRIGGER_TEXT, {exact: true})).toBeVisible();
await expect(page.getByText('Click anywhere to close.', {exact: true})).toBeVisible();
await expect(page.getByText('This is the English String', {exact: true})).toBeVisible();
if (elementClicked) {
await expect(page.getByText(/Element clicked in the menu:/)).toBeVisible();
await expect(page.getByText(elementClicked, {exact: true})).toBeVisible();
}
}
/**
* Closes the Root Modal by clicking its trigger text and verifies it is gone.
*/
export async function closeRootModal(page: Page): Promise<void> {
await page.getByText(ROOT_MODAL_TRIGGER_TEXT).click();
await expect(page.getByText(ROOT_MODAL_TRIGGER_TEXT)).not.toBeVisible();
}
/**
* Run `send` (typically fill slash command + click Send) while waiting for
* POST /api/v4/commands/execute so the server finishes the slash handler before assertions.
*/
/**
* Upload a file via the UI attachment menu when the demo plugin is active.
* The demo plugin intercepts the attachment button and shows a submenu — this
* helper clicks "Your computer" from that submenu to reach the native file chooser.
*/
export async function uploadFileViaYourComputer(
page: Page,
attachmentButton: {click: () => Promise<void>},
filename: string,
): Promise<void> {
const filePath = path.join(assetPath, filename);
const uploadResponsePromise = page.waitForResponse(
(r) =>
r.url().includes('/api/v4/files') &&
r.request().method() === 'POST' &&
r.status() >= 200 &&
r.status() < 300,
{timeout: 60_000},
);
const fileChooserPromise = page.waitForEvent('filechooser');
await attachmentButton.click();
await page.getByText('Your computer').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(filePath);
await uploadResponsePromise;
}
export async function sendDemoSlashCommand(page: Page, send: () => Promise<void>) {
// Accept any response status (including 5xx) so the 45 s timeout does not fire when the
// plugin is transiently inactive and the server returns HTTP 500. The caller is responsible
@@ -0,0 +1,56 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
import {setupDemoPlugin} from '../helpers';
test('should open right-hand sidebar when demo plugin App Bar button is clicked', async ({pw}) => {
// 1. Setup
const {adminClient, user, team} = await pw.initSetup();
await setupDemoPlugin(adminClient, pw);
// 2. Login and navigate to Town Square
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// 3. Locate and click the demo plugin button in the right App Bar
await expect(channelsPage.appBar.demoPluginButton).toBeVisible();
// 4. Click the App Bar button
await channelsPage.appBar.demoPluginButton.click();
// 5. Verify the RHS opens with expected content
const rhsPanel = channelsPage.page.getByRole('region', {name: 'Demo Plugin'});
await expect(rhsPanel).toBeVisible();
await expect(
rhsPanel.getByText('You have triggered the right-hand sidebar component of the demo plugin.', {exact: true}),
).toBeVisible();
await expect(rhsPanel.getByText('This is the English String', {exact: true})).toBeVisible();
// Custom route links — rendered as plain <a> tags with no href, text content is the path
await expect(rhsPanel.getByText('/plug/com.mattermost.demo-plugin/roottest')).toBeVisible();
await expect(rhsPanel.getByText(/com\.mattermost\.demo-plugin\/teamtest/)).toBeVisible();
// Pop Out section
await expect(rhsPanel.getByText('Pop Out RHS Demo', {exact: true})).toBeVisible();
await expect(rhsPanel.getByRole('button', {name: 'Pop Out RHS'})).toBeVisible();
await expect(rhsPanel.getByRole('button', {name: 'Pop Out via useEffect'})).toBeVisible();
// 6. Verify pop-out buttons are present and enabled (but do NOT click — pop-out crashes in test env)
await expect(rhsPanel.getByRole('button', {name: 'Pop Out RHS'})).toBeEnabled();
await expect(rhsPanel.getByRole('button', {name: 'Pop Out via useEffect'})).toBeEnabled();
// 7. Navigate to the /roottest custom route link and verify the page content
await rhsPanel.getByText('/plug/com.mattermost.demo-plugin/roottest').click();
await expect(channelsPage.page).toHaveURL(/\/plug\/com\.mattermost\.demo-plugin\/roottest$/);
await expect(channelsPage.page.getByText('Demo plugin route.')).toBeVisible();
await channelsPage.page.goBack();
// 8. Close the RHS and verify it dismisses
const rhsPanelAfterNav = channelsPage.page.getByRole('region', {name: 'Demo Plugin'});
await rhsPanelAfterNav.getByRole('button', {name: 'Close'}).click();
await expect(rhsPanelAfterNav).not.toBeVisible();
});
@@ -0,0 +1,158 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {Client4} from '@mattermost/client';
import {expect, getFileFromAsset, test} from '@mattermost/playwright-lib';
import {assertRootModal, closeRootModal, setupDemoPlugin, uploadFileViaYourComputer} from '../helpers';
async function uploadDemoFile(client: Client4, channelId: string): Promise<void> {
const file = getFileFromAsset('sample-file.demo');
const formData = new FormData();
formData.set('files', file, 'sample-file.demo');
formData.set('channel_id', channelId);
const result = await client.uploadFile(formData);
await client.createPost({
channel_id: channelId,
message: '',
file_ids: [result.file_infos[0].id],
});
}
test('should show "Upload using Demo Plugin" entry in attachment menu and open Root Modal', async ({pw}) => {
// 1. Setup
const {adminClient, user, team} = await pw.initSetup();
await setupDemoPlugin(adminClient, pw);
// 2. Login and navigate to Town Square
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// 3. Click the attachment button to open the upload type menu
await channelsPage.centerView.postCreate.attachmentButton.click();
// 4. Verify both upload entries are visible
await expect(channelsPage.page.getByText('Your computer')).toBeVisible();
await expect(channelsPage.page.getByText('Upload using Demo Plugin')).toBeVisible();
// 5. Click "Upload using Demo Plugin" — opens the Root Modal
await channelsPage.page.getByText('Upload using Demo Plugin').click();
// 6. Assert Root Modal (no element context for this entry point)
await assertRootModal(channelsPage.page);
await closeRootModal(channelsPage.page);
});
test('should show Demo Plugin entry in file attachment dropdown for .demo files and open Root Modal', async ({pw}) => {
// 1. Setup
const {adminClient, user, team} = await pw.initSetup();
await setupDemoPlugin(adminClient, pw);
// 2. Create a channel, add user, upload a .demo file via API
// (API upload avoids the demo plugin's attachment submenu interception)
const channel = pw.random.channel({
teamId: team.id,
name: 'demo-file-test',
displayName: 'Demo File Test',
});
const createdChannel = await adminClient.createChannel(channel);
await adminClient.addToChannel(user.id, createdChannel.id);
await uploadDemoFile(adminClient, createdChannel.id);
// 3. Login and navigate to the channel
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'demo-file-test');
await channelsPage.toBeVisible();
// 4. Hover the file attachment to reveal the kebab menu, scoped to the post
const post = channelsPage.centerView.container.getByRole('listitem').filter({hasText: 'sample-file.demo'}).last();
await post.getByText('sample-file.demo').hover();
await post.getByRole('button', {name: 'more actions'}).click();
// 5. Verify the file attachment dropdown contains the Demo Plugin entry
await expect(channelsPage.page.getByRole('menuitem', {name: 'Get a public link'})).toBeVisible();
await expect(channelsPage.page.getByRole('menuitem', {name: 'Demo Plugin'})).toBeVisible();
// 6. Click "Demo Plugin" — opens Root Modal
await channelsPage.page.getByRole('menuitem', {name: 'Demo Plugin'}).click();
// 7. Assert Root Modal
await assertRootModal(channelsPage.page);
await closeRootModal(channelsPage.page);
});
test('should render custom preview for .demo files', async ({pw}) => {
// 1. Setup
const {adminClient, user, team} = await pw.initSetup();
await setupDemoPlugin(adminClient, pw);
// 2. Upload a .demo file via API to a dedicated channel
const channel = pw.random.channel({
teamId: team.id,
name: 'demo-preview-test',
displayName: 'Demo Preview Test',
});
const createdChannel = await adminClient.createChannel(channel);
await adminClient.addToChannel(user.id, createdChannel.id);
await uploadDemoFile(adminClient, createdChannel.id);
// 3. Login and navigate to the channel
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'demo-preview-test');
await channelsPage.toBeVisible();
// 4. Click the file thumbnail to open the preview
await channelsPage.page.getByRole('link', {name: /file thumbnail sample-file\.demo/}).click();
// 5. Verify the preview dialog opens with the custom demo plugin content
const previewDialog = channelsPage.page.getByRole('dialog');
await expect(previewDialog).toBeVisible();
// The custom demo plugin preview renders the filename as an h3 heading
await expect(previewDialog.getByRole('heading', {name: 'sample-file.demo', level: 3})).toBeVisible();
// The plugin also renders a Close button inside the preview content area
// (distinct from the standard modal Close/X in the header)
const pluginCloseButton = previewDialog.getByRole('button', {name: 'Close'}).last();
await expect(pluginCloseButton).toBeVisible();
// Standard file preview chrome is also present
await expect(previewDialog.getByText(/Shared in ~/)).toBeVisible();
await expect(previewDialog.getByRole('link', {name: 'Download'})).toBeVisible();
// 6. Close via the plugin's Close button
await pluginCloseButton.click();
await expect(previewDialog).not.toBeVisible();
});
test('should upload a file via "Your computer" from the demo plugin attachment submenu', async ({pw}) => {
// 1. Setup
const {adminClient, user, team} = await pw.initSetup();
await setupDemoPlugin(adminClient, pw);
// 2. Login and navigate to Town Square
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// 3. Upload a file via the UI — the demo plugin intercepts the attachment button
// and shows a submenu. uploadFileViaYourComputer clicks "Your computer" to reach
// the native file chooser.
await uploadFileViaYourComputer(
channelsPage.page,
channelsPage.centerView.postCreate.attachmentButton,
'sample_text_file.txt',
);
// 4. Verify the file preview appears in the compose area before sending
await channelsPage.centerView.postCreate.waitUntilFilePreviewContains(['sample_text_file.txt']);
// 5. Send the message with file
await channelsPage.centerView.postCreate.postMessage('file upload test');
// 6. Verify the file attachment appears in the channel post
const lastPost = await channelsPage.centerView.getLastPost();
await expect(lastPost.container.getByText('sample_text_file.txt')).toBeVisible();
});
@@ -0,0 +1,101 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
import {assertRootModal, closeRootModal, setupDemoPlugin} from '../helpers';
test('should open Root Modal from team dropdown main menu', async ({pw}) => {
// 1. Setup
const {adminClient, user, team} = await pw.initSetup();
await setupDemoPlugin(adminClient, pw);
// 2. Login and navigate to Town Square
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// 3. Open the team name dropdown in the left sidebar
await channelsPage.sidebarLeft.teamMenuButton.click();
// 4. Confirm Demo Plugin entries are visible and click "Demo Plugin"
await expect(channelsPage.page.getByRole('menuitem', {name: 'Demo Plugin'})).toBeVisible();
await channelsPage.page.getByRole('menuitem', {name: 'Demo Plugin'}).click();
// 5. Assert Root Modal (no "Element clicked" line for main menu)
await assertRootModal(channelsPage.page);
// 6. Close
await closeRootModal(channelsPage.page);
});
test('should open Root Modal from channel header dropdown More actions', async ({pw}) => {
// 1. Setup
const {adminClient, user, team} = await pw.initSetup();
await setupDemoPlugin(adminClient, pw);
// 2. Login and navigate to Town Square
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// 3. Open channel header dropdown
await channelsPage.centerView.header.openChannelMenu();
// 4. Hover "More actions" to reveal the submenu, then click "Demo Plugin"
const moreActionsItem = channelsPage.page.getByRole('menuitem', {name: 'More actions'});
await expect(moreActionsItem).toBeVisible();
await moreActionsItem.hover();
const submenu = channelsPage.page.getByRole('menu', {name: 'More actions'});
await expect(submenu).toBeVisible();
// Move mouse directly to the Demo Plugin item to avoid submenu collapsing
const demoPluginItem = submenu.getByRole('menuitem', {name: 'Demo Plugin'});
await demoPluginItem.hover();
await demoPluginItem.click();
// 5. Assert Root Modal base text
await assertRootModal(channelsPage.page);
// Channel header entry also shows "Element clicked in the menu: <channel_id>" (dynamic)
await expect(channelsPage.page.getByText(/Element clicked in the menu:/)).toBeVisible();
// 6. Close
await closeRootModal(channelsPage.page);
});
// Skipped: demo plugin v0.10.3 does not set a URL on the openInteractiveDialog call.
// The webapp logs "Interactive dialog missing URL - this is a configuration error" and no dialog renders.
// Re-enable once the demo plugin is fixed and the build URL in helpers.ts is updated.
test.skip('should open Sample Confirmation Dialog from team dropdown and respond to Confirm and Cancel', async ({
pw,
}) => {
// 1. Setup
const {adminClient, user, team} = await pw.initSetup();
await setupDemoPlugin(adminClient, pw);
// 2. Login and navigate to Town Square
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// 3. Open team dropdown and click "Sample Confirmation Dialog"
await channelsPage.sidebarLeft.teamMenuButton.click();
await expect(channelsPage.page.getByRole('menuitem', {name: 'Sample Confirmation Dialog'})).toBeVisible();
await channelsPage.page.getByRole('menuitem', {name: 'Sample Confirmation Dialog'}).click();
// 4. Confirm dialog opens with title and action buttons but no form fields
const dialog = channelsPage.page.getByRole('dialog', {name: 'Sample Confirmation Dialog'});
await expect(dialog).toBeVisible();
await expect(dialog.getByRole('heading', {name: 'Sample Confirmation Dialog', level: 1})).toBeVisible();
await expect(dialog.getByRole('button', {name: 'Cancel'})).toBeVisible();
await expect(dialog.getByRole('button', {name: 'Confirm'})).toBeVisible();
await expect(dialog.getByRole('textbox')).not.toBeVisible();
// 5. Click Confirm — dialog closes and a post appears
await dialog.getByRole('button', {name: 'Confirm'}).click();
await expect(dialog).not.toBeVisible();
await expect(
channelsPage.centerView.container.locator('p').filter({hasText: 'confirmed an Interactive Dialog'}),
).toBeVisible();
// Cancel test omitted due to unexpected behavior. Will re-add once issue is resolved.
});
@@ -0,0 +1,64 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
import {assertRootModal, closeRootModal, setupDemoPlugin} from '../helpers';
test('should open Root Modal from post actions menu and all submenu items', async ({pw}) => {
// 1. Setup
const {adminClient, user, team} = await pw.initSetup();
await setupDemoPlugin(adminClient, pw);
// 2. Login and navigate to Town Square
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// 3. Post a message to use as the target post
await channelsPage.centerView.postCreate.input.fill('Test post for Root Modal validation');
await channelsPage.centerView.postCreate.sendMessage();
const post = await channelsPage.centerView.getLastPost();
// Local helper: hover the post and open the actions (bolt icon) menu via the library
// NOTE: Plugin actions are in the "actions" (⚡) button, NOT the "..." (more) button
async function openActionsMenu() {
await post.hover();
await post.postMenu.actionsButton.click();
}
// ── Top-level "Demo Plugin" action ──────────────────────────────────────
await openActionsMenu();
await channelsPage.page.getByRole('button', {name: 'Demo Plugin'}).click();
// Top-level action does NOT show "Element clicked in the menu"
await assertRootModal(channelsPage.page);
await expect(channelsPage.page.getByText(/Element clicked in the menu:/)).not.toBeVisible();
await closeRootModal(channelsPage.page);
// ── Submenu Example → First Item ────────────────────────────────────────
await openActionsMenu();
await channelsPage.page.getByRole('button', {name: /Submenu Example/}).hover();
// Submenu items have role="button" but a broken aria-label — filter by text content
await channelsPage.page.getByRole('button').filter({hasText: 'First Item'}).last().click();
await assertRootModal(channelsPage.page, 'First Item');
await closeRootModal(channelsPage.page);
// ── Submenu Example → Second Item ───────────────────────────────────────
await openActionsMenu();
await channelsPage.page.getByRole('button', {name: /Submenu Example/}).hover();
await channelsPage.page.getByRole('button').filter({hasText: 'Second Item'}).last().click();
await assertRootModal(channelsPage.page, 'Second Item');
await closeRootModal(channelsPage.page);
// ── Submenu Example → Third Item ────────────────────────────────────────
await openActionsMenu();
await channelsPage.page.getByRole('button', {name: /Submenu Example/}).hover();
await channelsPage.page.getByRole('button').filter({hasText: 'Third Item'}).last().click();
await assertRootModal(channelsPage.page, 'Third Item');
await closeRootModal(channelsPage.page);
});
@@ -0,0 +1,76 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
import {assertRootModal, closeRootModal, setupDemoPlugin} from '../helpers';
test('should show Demo Plugin User Attributes link in profile popover and close popover on click', async ({pw}) => {
// 1. Setup
const {adminClient, user, team} = await pw.initSetup();
await setupDemoPlugin(adminClient, pw);
// 2. Login and navigate to Town Square
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// 3. Post a message so we have a post with the user's avatar to click
await channelsPage.centerView.postCreate.input.fill('Test post for user attributes');
await channelsPage.centerView.postCreate.sendMessage();
// 4. Click the user's avatar to open the profile popover
const post = await channelsPage.centerView.getLastPost();
const profileImage = await post.getProfileImage(user.username);
await profileImage.click();
const popover = channelsPage.page.getByRole('dialog', {
name: `${user.username}'s profile popover`,
});
await expect(popover).toBeVisible();
// 5. Confirm "Demo Plugin: User Attributes" link is present
await expect(popover.getByText('Demo Plugin: User Attributes', {exact: true})).toBeVisible();
// 6. Click the link — it should close the popover
await popover.getByText('Demo Plugin: User Attributes', {exact: true}).click();
await expect(popover).not.toBeVisible();
});
test('should open Root Modal from user profile popover Action button', async ({pw}) => {
// 1. Setup
const {adminClient, user, team} = await pw.initSetup();
await setupDemoPlugin(adminClient, pw);
// 2. Login and navigate to Town Square
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// 3. Post a message so we have a post with the user's avatar to click
await channelsPage.centerView.postCreate.input.fill('Test post for profile popover');
await channelsPage.centerView.postCreate.sendMessage();
// 4. Click the user's avatar on the post to open the profile popover
const post = await channelsPage.centerView.getLastPost();
const profileImage = await post.getProfileImage(user.username);
await profileImage.click();
// 5. Confirm profile popover is visible with Demo Plugin Action button
const popover = channelsPage.page.getByRole('dialog', {
name: `${user.username}'s profile popover`,
});
await expect(popover).toBeVisible();
await expect(popover.getByText('Demo Plugin: User Attributes')).toBeVisible();
await expect(popover.getByRole('button', {name: 'Action'})).toBeVisible();
// 6. Click Action button → Root Modal should appear
await popover.getByRole('button', {name: 'Action'}).click();
await expect(popover).not.toBeVisible();
// 7. Assert Root Modal
await assertRootModal(channelsPage.page);
// 8. Close modal by clicking its text
await closeRootModal(channelsPage.page);
});
@@ -0,0 +1,84 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
import {setupDemoPlugin} from '../helpers';
test('should show Demo Plugin enabled/disabled status in left sidebar header', async ({pw}) => {
// 1. Setup
const {adminClient, user, team} = await pw.initSetup();
await setupDemoPlugin(adminClient, pw);
// 2. Login and navigate to Town Square — slash commands work from any channel
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// The sidebar indicator: a span containing "Demo Plugin:" with a sibling span for the status
const hookStatus = channelsPage.page
.locator('span')
.filter({hasText: 'Demo Plugin:'})
.locator('..')
.locator('span')
.last();
// 3. Verify initial state — hooks enabled by setupDemoPlugin
await expect(hookStatus).toHaveText('Enabled');
// 4. Disable hooks and verify indicator updates
await channelsPage.centerView.postCreate.input.fill('/demo_plugin false');
await channelsPage.centerView.postCreate.sendMessage();
await expect(hookStatus).toHaveText('Disabled');
// 5. Re-enable hooks and verify indicator restores
await channelsPage.centerView.postCreate.input.fill('/demo_plugin true');
await channelsPage.centerView.postCreate.sendMessage();
await expect(hookStatus).toHaveText('Enabled');
});
test('should show demo plugin plug icon at the bottom of the team sidebar', async ({pw}) => {
// 1. Setup
const {adminClient, user, team} = await pw.initSetup();
await setupDemoPlugin(adminClient, pw);
// 2. Login and navigate to Town Square
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// 3. Verify the plug icon is visible in the team sidebar
// The icon has no accessible name, role, or testid — it is a purely visual,
// non-interactive element rendered with the fa-plug CSS class.
// CSS selector is the only viable locator here.
await expect(channelsPage.page.locator('.fa.fa-plug').first()).toBeVisible();
});
test('should show Demo Plugin Item in Browse or create channels menu and trigger alert with team ID', async ({pw}) => {
// 1. Setup
const {adminClient, user, team} = await pw.initSetup();
await setupDemoPlugin(adminClient, pw);
// 2. Login and navigate to Town Square
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// 3. Click the "Browse or create channels" button in the channel sidebar header
await channelsPage.sidebarLeft.browseOrCreateChannelButton.click();
// 4. Verify the menu is open and contains the Demo Plugin Item entry
const menu = channelsPage.page.getByRole('menu', {name: 'Browse or create channels'});
await expect(menu).toBeVisible();
await expect(menu.getByRole('menuitem', {name: 'Demo Plugin Item'})).toBeVisible();
// 5. Click "Demo Plugin Item" — triggers a browser alert with the team ID
const dialogPromise = channelsPage.page.waitForEvent('dialog');
await menu.getByRole('menuitem', {name: 'Demo Plugin Item'}).click();
const dialog = await dialogPromise;
// 6. Assert alert message contains expected text and dynamic team ID
expect(dialog.type()).toBe('alert');
expect(dialog.message()).toMatch(/^Demo Plugin: Browse menu item clicked! Team ID: [a-z0-9]{26}$/);
await dialog.accept();
});
@@ -0,0 +1,60 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
import {setupDemoPlugin} from '../helpers';
test('should show demo plugin settings sections and save changes with alert confirmation', async ({pw}) => {
// 1. Setup
const {adminClient, user, team} = await pw.initSetup();
await setupDemoPlugin(adminClient, pw);
// 2. Login and navigate to Town Square
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, 'town-square');
await channelsPage.toBeVisible();
// 3. Open Settings via the library method
const settingsModal = await channelsPage.openSettings();
// 4. Navigate to the Demo Plugin settings tab
await expect(settingsModal.container.getByText('PLUGIN PREFERENCES')).toBeVisible();
await settingsModal.container.getByRole('tab', {name: /Demo Plugin/i}).click();
// 5. Verify Demo Plugin Settings panel and all four section titles
await expect(settingsModal.container.getByRole('heading', {name: 'Demo Plugin Settings', level: 3})).toBeVisible();
await expect(settingsModal.container.getByRole('heading', {name: 'Example action', level: 4})).toBeVisible();
await expect(settingsModal.container.getByRole('heading', {name: 'Test section number 1', level: 4})).toBeVisible();
await expect(settingsModal.container.getByRole('heading', {name: 'Test section number 2', level: 4})).toBeVisible();
await expect(settingsModal.container.getByRole('heading', {name: 'Test section disabled', level: 4})).toBeVisible();
// 6. Verify Example action section has its button
await expect(settingsModal.container.getByRole('button', {name: 'Here is the button text'})).toBeVisible();
// 7. Verify Edit buttons visible for active sections (disabled section has none)
const editButtons = settingsModal.container.locator('.section-min__edit');
await expect(editButtons).toHaveCount(2);
// 8. Expand Section 1, select Option 2, save
// page.on captures the synchronous alert() that fires during Save click
const alerts: string[] = [];
const dialogHandler = async (dialog: {message: () => string; accept: () => Promise<void>}) => {
alerts.push(dialog.message());
await dialog.accept();
};
channelsPage.page.on('dialog', dialogHandler);
await editButtons.first().click();
await settingsModal.container.getByRole('radio', {name: 'Option 2'}).first().click();
await channelsPage.page.getByTestId('saveSetting').click();
expect(alerts[0]).toBe('saving {setting1}: 2');
// 9. Expand Section 2, select Option 1, save
await editButtons.nth(1).click();
await settingsModal.container.getByRole('radio', {name: 'Option 1'}).first().click();
await channelsPage.page.getByTestId('saveSetting').click();
expect(alerts[1]).toBe('saving {setting3}: 1');
channelsPage.page.off('dialog', dialogHandler);
});