mirror of
https://github.com/mattermost/mattermost.git
synced 2026-08-26 21:27:40 -05:00
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:
co-authored by
Claude Sonnet 4.6
Mattermost Build
parent
68535c13bc
commit
53373e3752
@@ -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
|
||||
|
||||
+56
@@ -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();
|
||||
});
|
||||
+158
@@ -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();
|
||||
});
|
||||
+101
@@ -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.
|
||||
});
|
||||
+64
@@ -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);
|
||||
});
|
||||
+76
@@ -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);
|
||||
});
|
||||
+84
@@ -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();
|
||||
});
|
||||
+60
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user