diff --git a/e2e-tests/playwright/specs/functional/plugins/demo_plugin/server/slash_commands/demo_dialog.spec.ts b/e2e-tests/playwright/specs/functional/plugins/demo_plugin/server/slash_commands/demo_dialog.spec.ts new file mode 100644 index 00000000000..7a2645a9f97 --- /dev/null +++ b/e2e-tests/playwright/specs/functional/plugins/demo_plugin/server/slash_commands/demo_dialog.spec.ts @@ -0,0 +1,202 @@ +// 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 /dialog and post submit confirmation on submit', async ({pw}) => { + // 1. Setup + const {adminClient, user, team} = await pw.initSetup(); + await setupDemoPlugin(adminClient, pw); + + // 2. Login + const {channelsPage} = await pw.testBrowser.login(user); + await channelsPage.goto(); + await channelsPage.toBeVisible(); + + // 3. Navigate to Demo Plugin channel + await channelsPage.goto(team.name, 'town-square'); + await channelsPage.toBeVisible(); + + // 4. Send /dialog command + await channelsPage.centerView.postCreate.input.fill('/dialog'); + await channelsPage.centerView.postCreate.sendMessage(); + + // 5. Confirm dialog opens with title "Test Title" + const dialog = channelsPage.page.getByRole('dialog'); + await expect(dialog).toBeVisible(); + await expect(dialog.getByRole('heading', {level: 1})).toContainText('Test Title'); + + // 6. Fill required fields + // Display Name already has default "default text" — overwrite + await dialog.getByTestId('realnameinput').fill('Test Input'); + + // Email and Password are required + await dialog.getByTestId('someemailemail').fill('test@example.com'); + await dialog.getByTestId('somepasswordpassword').fill('testpassword123'); + + // Number is required + await dialog.getByTestId('somenumbernumber').fill('42'); + + // Option Selector — required, no default (3rd combobox: User Selector, Channel Selector, Option Selector) + await dialog.getByRole('combobox').nth(2).click(); + await channelsPage.page.getByRole('option', {name: 'Option1'}).click(); + + // Required checkboxes + await dialog.getByRole('checkbox', {name: 'Agree to the terms of service'}).check(); + await dialog.getByRole('checkbox', {name: 'Agree to the annoying terms of service'}).check(); + + // Radio Option Selector — required + await dialog.getByRole('radio', {name: 'Option1'}).click(); + + // 7. Submit the dialog + await dialog.getByRole('button', {name: 'Submit'}).click(); + await expect(dialog).not.toBeVisible(); + + // 8. Verify the submit post appears in the channel + // Note: "Interative" is a typo in the demo plugin — not a test error + await expect( + channelsPage.centerView.container.locator('p').filter({hasText: 'submitted an Interative Dialog'}), + ).toBeVisible(); +}); + +test('should post cancellation notification when /dialog is cancelled', async ({pw}) => { + // 1. Setup + const {adminClient, user, team} = await pw.initSetup(); + await setupDemoPlugin(adminClient, pw); + + // 2. Login + const {channelsPage} = await pw.testBrowser.login(user); + await channelsPage.goto(); + await channelsPage.toBeVisible(); + + // 3. Navigate to Demo Plugin channel + await channelsPage.goto(team.name, 'town-square'); + await channelsPage.toBeVisible(); + + // 4. Send /dialog command + await channelsPage.centerView.postCreate.input.fill('/dialog'); + await channelsPage.centerView.postCreate.sendMessage(); + + // 5. Confirm dialog opens + const dialog = channelsPage.page.getByRole('dialog'); + await expect(dialog).toBeVisible(); + await expect(dialog.getByRole('heading', {level: 1})).toContainText('Test Title'); + await expect(dialog.getByRole('button', {name: 'Cancel'})).toBeVisible(); + await expect(dialog.getByRole('button', {name: 'Submit'})).toBeVisible(); + + // 6. Cancel the dialog + await dialog.getByRole('button', {name: 'Cancel'}).click(); + await expect(dialog).not.toBeVisible(); + + // 7. Verify the cancellation post appears in the channel + // Note: "Interative" is a typo in the demo plugin — not a test error + await expect( + channelsPage.centerView.container.locator('p').filter({hasText: 'canceled an Interative Dialog'}), + ).toBeVisible(); +}); + +test('should show validation errors when required fields are submitted empty', async ({pw}) => { + // 1. Setup + const {adminClient, user, team} = await pw.initSetup(); + await setupDemoPlugin(adminClient, pw); + + // 2. Login + const {channelsPage} = await pw.testBrowser.login(user); + await channelsPage.goto(); + await channelsPage.toBeVisible(); + + // 3. Navigate to Demo Plugin channel + await channelsPage.goto(team.name, 'town-square'); + await channelsPage.toBeVisible(); + + // 4. Send /dialog command + await channelsPage.centerView.postCreate.input.fill('/dialog'); + await channelsPage.centerView.postCreate.sendMessage(); + + // 5. Confirm dialog opens + const dialog = channelsPage.page.getByRole('dialog'); + await expect(dialog).toBeVisible(); + await expect(dialog.getByRole('heading', {level: 1})).toContainText('Test Title'); + + // 6. Clear the Number field and submit + await dialog.getByTestId('somenumbernumber').clear(); + await dialog.getByRole('button', {name: 'Submit'}).click(); + + // 7. Verify dialog stays open with validation errors + await expect(dialog).toBeVisible(); + await expect(dialog.getByText('Please fix all field errors', {exact: true})).toBeVisible(); + await expect(dialog.getByTestId('somenumber').getByText('This field is required.', {exact: true})).toBeVisible(); +}); + +test('should show general error and keep dialog open on /dialog error submit', async ({pw}) => { + // 1. Setup + const {adminClient, user, team} = await pw.initSetup(); + await setupDemoPlugin(adminClient, pw); + + // 2. Login + const {channelsPage} = await pw.testBrowser.login(user); + await channelsPage.goto(); + await channelsPage.toBeVisible(); + + // 3. Navigate to Town Square + await channelsPage.goto(team.name, 'town-square'); + await channelsPage.toBeVisible(); + + // 4. Send /dialog error command + await channelsPage.centerView.postCreate.input.fill('/dialog error'); + await channelsPage.centerView.postCreate.sendMessage(); + + // 5. Confirm dialog opens with title "Simple Dialog Test" + const dialog = channelsPage.page.getByRole('dialog'); + await expect(dialog).toBeVisible(); + await expect(dialog.getByRole('heading', {level: 1})).toContainText('Simple Dialog Test'); + await expect(dialog.getByRole('button', {name: 'Cancel'})).toBeVisible(); + await expect(dialog.getByRole('button', {name: 'Submit Test'})).toBeVisible(); + + // 6. Fill the optional field and submit + await dialog.getByPlaceholder('Enter some text (optional)...').fill('sample test input'); + await dialog.getByRole('button', {name: 'Submit Test'}).click(); + + // 7. Verify general error appears and dialog stays open + await expect(dialog.getByText('some error', {exact: true})).toBeVisible(); + await expect(dialog).toBeVisible(); + await expect(dialog.getByPlaceholder('Enter some text (optional)...')).toHaveValue('sample test input'); +}); + +test('should show general error on /dialog error-no-elements confirm', async ({pw}) => { + // 1. Setup + const {adminClient, user, team} = await pw.initSetup(); + await setupDemoPlugin(adminClient, pw); + + // 2. Login + const {channelsPage} = await pw.testBrowser.login(user); + await channelsPage.goto(); + await channelsPage.toBeVisible(); + + // 3. Navigate to Town Square + await channelsPage.goto(team.name, 'town-square'); + await channelsPage.toBeVisible(); + + // 4. Send /dialog error-no-elements command + await channelsPage.centerView.postCreate.input.fill('/dialog error-no-elements'); + await channelsPage.centerView.postCreate.sendMessage(); + + // 5. Confirm dialog opens with title "Sample Confirmation Dialog" and no form fields + const dialog = channelsPage.page.getByRole('dialog'); + await expect(dialog).toBeVisible(); + await expect(dialog.getByRole('heading', {level: 1})).toContainText('Sample Confirmation Dialog'); + await expect(dialog.getByRole('button', {name: 'Cancel'})).toBeVisible(); + await expect(dialog.getByRole('button', {name: 'Confirm'})).toBeVisible(); + await expect(dialog.getByRole('textbox')).not.toBeVisible(); + + // 6. Click Confirm + await dialog.getByRole('button', {name: 'Confirm'}).click(); + + // 7. Verify general error appears and dialog stays open + await expect(dialog.getByText('some error', {exact: true})).toBeVisible(); + await expect(dialog).toBeVisible(); + await expect(dialog.getByRole('button', {name: 'Cancel'})).toBeVisible(); + await expect(dialog.getByRole('button', {name: 'Confirm'})).toBeVisible(); +}); diff --git a/e2e-tests/playwright/specs/functional/plugins/demo_plugin/server/slash_commands/demo_dialog_date.spec.ts b/e2e-tests/playwright/specs/functional/plugins/demo_plugin/server/slash_commands/demo_dialog_date.spec.ts new file mode 100644 index 00000000000..875a92f1565 --- /dev/null +++ b/e2e-tests/playwright/specs/functional/plugins/demo_plugin/server/slash_commands/demo_dialog_date.spec.ts @@ -0,0 +1,66 @@ +// 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 /dialog date and post submit confirmation after selecting dates', async ({pw}) => { + // 1. Setup + const {adminClient, user, team} = await pw.initSetup(); + await setupDemoPlugin(adminClient, pw); + + // 2. Login + const {channelsPage} = await pw.testBrowser.login(user); + await channelsPage.goto(); + await channelsPage.toBeVisible(); + + // 3. Navigate to Town Square + await channelsPage.goto(team.name, 'town-square'); + await channelsPage.toBeVisible(); + + // 4. Send /dialog date command + await channelsPage.centerView.postCreate.input.fill('/dialog date'); + await channelsPage.centerView.postCreate.sendMessage(); + + // 5. Confirm dialog opens with correct title + const dialog = channelsPage.page.getByRole('dialog'); + await expect(dialog).toBeVisible(); + await expect(dialog.getByRole('heading', {level: 1})).toContainText('Date & DateTime Test Dialog'); + + // 6. Verify field labels and Event Title default value + await expect(dialog.getByText('Meeting Date *', {exact: true})).toBeVisible(); + await expect(dialog.getByText('Meeting Date & Time *', {exact: true})).toBeVisible(); + await expect(dialog.getByText('Event Title *', {exact: true})).toBeVisible(); + await expect(dialog.getByRole('textbox', {name: 'Event Title *'})).toHaveValue('Team Meeting'); + + // 7. Select a date using the Meeting Date picker + await dialog.getByRole('button', {name: /Select a meeting date/i}).click(); + await expect(channelsPage.page.getByRole('grid')).toBeVisible(); + // Click day 20 — reliably available in any month + await channelsPage.page.getByRole('grid').getByText('20', {exact: true}).click(); + + // 8. Select date and time using the Meeting Date & Time picker + await dialog + .getByRole('button', {name: /Date.*Today|Select a date/i}) + .first() + .click(); + await expect(channelsPage.page.getByRole('grid')).toBeVisible(); + await channelsPage.page.getByRole('grid').getByText('22', {exact: true}).click(); + + // Select a time from the time picker + await dialog + .getByRole('button', {name: /Time|Select a time/i}) + .first() + .click(); + await channelsPage.page.getByRole('menuitem', {name: '3:00 PM'}).click(); + + // 9. Submit — button is labelled "Create Event" + await dialog.getByRole('button', {name: 'Create Event'}).click(); + await expect(dialog).not.toBeVisible(); + + // 10. Verify submit post appears in the channel + await expect( + channelsPage.centerView.container.locator('p').filter({hasText: 'submitted a Date Dialog'}), + ).toBeVisible(); +}); diff --git a/e2e-tests/playwright/specs/functional/plugins/demo_plugin/server/slash_commands/demo_dialog_field_refresh.spec.ts b/e2e-tests/playwright/specs/functional/plugins/demo_plugin/server/slash_commands/demo_dialog_field_refresh.spec.ts new file mode 100644 index 00000000000..e126761a498 --- /dev/null +++ b/e2e-tests/playwright/specs/functional/plugins/demo_plugin/server/slash_commands/demo_dialog_field_refresh.spec.ts @@ -0,0 +1,89 @@ +// 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 update form fields dynamically when project type changes via /dialog field-refresh', async ({pw}) => { + // 1. Setup + const {adminClient, user, team} = await pw.initSetup(); + await setupDemoPlugin(adminClient, pw); + + // 2. Login + const {channelsPage} = await pw.testBrowser.login(user); + await channelsPage.goto(); + await channelsPage.toBeVisible(); + + // 3. Navigate to Town Square + await channelsPage.goto(team.name, 'town-square'); + await channelsPage.toBeVisible(); + + // 4. Send /dialog field-refresh command + await channelsPage.centerView.postCreate.input.fill('/dialog field-refresh'); + await channelsPage.centerView.postCreate.sendMessage(); + + // 5. Confirm dialog opens with title "Project Configuration" + const dialog = channelsPage.page.getByRole('dialog'); + await expect(dialog).toBeVisible(); + await expect(dialog.getByRole('heading', {level: 1})).toContainText('Project Configuration'); + + // 6. Verify initial state — only Project Type dropdown visible + await expect(dialog.getByText('Project Type *')).toBeVisible(); + await expect(dialog.getByRole('button', {name: 'Cancel'})).toBeVisible(); + await expect(dialog.getByRole('button', {name: 'Create Project'})).toBeVisible(); + await expect(dialog.getByText('Frontend Framework')).not.toBeVisible(); + await expect(dialog.getByText('Platform')).not.toBeVisible(); + await expect(dialog.getByText('API Type')).not.toBeVisible(); + + // 7. Select "Web Application" — new fields should appear + // Click the react-select control (not the hidden input) to open the dropdown + await dialog.locator('[class*="Select__control"], [class*="react-select__control"]').first().click(); + await channelsPage.page.getByRole('option', {name: 'Web Application'}).click(); + + await expect(dialog.getByText('Frontend Framework *')).toBeVisible(); + await expect(dialog.getByText('Enable PWA')).toBeVisible(); + await expect(dialog.getByText('Project Name *')).toBeVisible(); + await expect(dialog.getByText('Platform')).not.toBeVisible(); + await expect(dialog.getByText('API Type')).not.toBeVisible(); + + // 8. Change to "Mobile Application" — fields update + await dialog.locator('[class*="Select__control"], [class*="react-select__control"]').first().click(); + await channelsPage.page.getByRole('option', {name: 'Mobile Application'}).click(); + + await expect(dialog.getByText('Platform *')).toBeVisible(); + await expect(dialog.getByText('Minimum OS Version *')).toBeVisible(); + await expect(dialog.getByText('Project Name *')).toBeVisible(); + await expect(dialog.getByText('Frontend Framework')).not.toBeVisible(); + await expect(dialog.getByText('Enable PWA')).not.toBeVisible(); + await expect(dialog.getByText('API Type')).not.toBeVisible(); + + // 9. Change to "API Service" — fields update again + await dialog.locator('[class*="Select__control"], [class*="react-select__control"]').first().click(); + await channelsPage.page.getByRole('option', {name: 'API Service'}).click(); + + await expect(dialog.getByText('API Type *')).toBeVisible(); + await expect(dialog.getByRole('radio', {name: 'REST API'})).toBeVisible(); + await expect(dialog.getByRole('radio', {name: 'GraphQL API'})).toBeVisible(); + await expect(dialog.getByRole('radio', {name: 'gRPC Service'})).toBeVisible(); + await expect(dialog.getByText('Database *')).toBeVisible(); + await expect(dialog.getByText('Project Name *')).toBeVisible(); + await expect(dialog.getByText('Platform')).not.toBeVisible(); + await expect(dialog.getByText('Minimum OS Version')).not.toBeVisible(); + + // 10. Fill required fields and submit + await dialog.getByPlaceholder('Enter project name...').fill('Test Project'); + await dialog.getByRole('radio', {name: 'REST API'}).click(); + + // Select PostgreSQL from Database dropdown + await dialog.locator('[class*="Select__control"], [class*="react-select__control"]').last().click(); + await channelsPage.page.getByRole('option', {name: 'PostgreSQL'}).click(); + + await dialog.getByRole('button', {name: 'Create Project'}).click(); + await expect(dialog).not.toBeVisible(); + + // 11. Verify response post in the channel + await expect( + channelsPage.centerView.container.locator('p').filter({hasText: 'api project: Test Project'}), + ).toBeVisible(); +}); diff --git a/e2e-tests/playwright/specs/functional/plugins/demo_plugin/server/slash_commands/demo_ephemeral.spec.ts b/e2e-tests/playwright/specs/functional/plugins/demo_plugin/server/slash_commands/demo_ephemeral.spec.ts new file mode 100644 index 00000000000..2cc2a586afd --- /dev/null +++ b/e2e-tests/playwright/specs/functional/plugins/demo_plugin/server/slash_commands/demo_ephemeral.spec.ts @@ -0,0 +1,72 @@ +// 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 send ephemeral post with Update and Delete actions via /ephemeral command', async ({pw}) => { + // 1. Setup + const {adminClient, user, team} = await pw.initSetup(); + await setupDemoPlugin(adminClient, pw); + + // 2. Login + const {channelsPage} = await pw.testBrowser.login(user); + await channelsPage.goto(); + await channelsPage.toBeVisible(); + + // 3. Navigate to Town Square — avoids noise from demo plugin's own ephemeral messages + await channelsPage.goto(team.name, 'town-square'); + await channelsPage.toBeVisible(); + + // 4. Send /ephemeral command + await channelsPage.centerView.postCreate.input.fill('/ephemeral'); + await channelsPage.centerView.postCreate.sendMessage(); + + // 5. Verify ephemeral post appears with correct content and action buttons + // Scope to the specific post to avoid strict mode violation if multiple ephemeral posts are visible + const ephemeralPost = channelsPage.centerView.container + .getByRole('listitem') + .filter({hasText: 'test ephemeral actions'}) + .last(); + await expect(ephemeralPost.getByText('(Only visible to you)', {exact: true})).toBeVisible(); + await expect(ephemeralPost.getByText('test ephemeral actions', {exact: true})).toBeVisible(); + await expect(ephemeralPost.getByRole('button', {name: 'Update', exact: true})).toBeVisible(); + await expect(ephemeralPost.getByRole('button', {name: 'Delete', exact: true})).toBeVisible(); + + // 6. Click Update and verify post text and button label change + // After clicking Update the text changes — re-find the post by its new content + await ephemeralPost.getByRole('button', {name: 'Update', exact: true}).click(); + const updatedPost = channelsPage.centerView.container + .getByRole('listitem') + .filter({hasText: 'updated ephemeral action'}) + .last(); + await expect(updatedPost.getByText('updated ephemeral action', {exact: true})).toBeVisible(); + await expect(updatedPost.getByRole('button', {name: 'Update 1', exact: true})).toBeVisible(); + await expect(updatedPost.getByRole('button', {name: 'Delete', exact: true})).toBeVisible(); + + // 7. Click Delete and verify post content is removed and buttons are gone + // After delete the text changes again — re-find by the new content + await updatedPost.getByRole('button', {name: 'Delete', exact: true}).click(); + const deletedPost = channelsPage.centerView.container + .getByRole('listitem') + .filter({hasText: '(message deleted)'}) + .last(); + await expect(deletedPost.getByText('(message deleted)', {exact: true})).toBeVisible(); + await expect(deletedPost.getByRole('button', {name: 'Update 1', exact: true})).not.toBeVisible(); + await expect(deletedPost.getByRole('button', {name: 'Delete', exact: true})).not.toBeVisible(); + + // 8. Send /ephemeral_override command (still in Town Square) + await channelsPage.centerView.postCreate.input.fill('/ephemeral_override'); + await channelsPage.centerView.postCreate.sendMessage(); + + // 9. Verify the override ephemeral post appears + const overridePost = channelsPage.centerView.container + .getByRole('listitem') + .filter({hasText: 'This is a demo of overriding an ephemeral post.'}) + .last(); + await expect(overridePost.getByText('(Only visible to you)', {exact: true})).toBeVisible(); + await expect( + overridePost.getByText('This is a demo of overriding an ephemeral post.', {exact: true}), + ).toBeVisible(); +}); diff --git a/e2e-tests/playwright/specs/functional/plugins/demo_plugin/server/slash_commands/demo_interactive.spec.ts b/e2e-tests/playwright/specs/functional/plugins/demo_plugin/server/slash_commands/demo_interactive.spec.ts new file mode 100644 index 00000000000..d1bd4e96fe4 --- /dev/null +++ b/e2e-tests/playwright/specs/functional/plugins/demo_plugin/server/slash_commands/demo_interactive.spec.ts @@ -0,0 +1,55 @@ +// 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 post interactive button and respond with click attribution via /interactive command', async ({pw}) => { + // 1. Setup + const {adminClient, user, team} = await pw.initSetup(); + await setupDemoPlugin(adminClient, pw); + + // 2. Login + const {channelsPage} = await pw.testBrowser.login(user); + await channelsPage.goto(); + await channelsPage.toBeVisible(); + + // 3. Navigate to Town Square + await channelsPage.goto(team.name, 'town-square'); + await channelsPage.toBeVisible(); + + // 4. Send /interactive command + await channelsPage.centerView.postCreate.input.fill('/interactive'); + await channelsPage.centerView.postCreate.sendMessage(); + + // 5. Confirm post appears with 'Test interactive button' and an 'Interactive Button' button + const interactivePost = channelsPage.centerView.container + .getByRole('listitem') + .filter({hasText: 'Test interactive button'}) + .last(); + await expect(interactivePost).toBeVisible(); + await expect(interactivePost.getByRole('button', {name: 'Interactive Button'})).toBeVisible(); + + // 6. Click the Interactive Button + await interactivePost.getByRole('button', {name: 'Interactive Button'}).click(); + + // 7. Wait for thread reply indicator and open the thread + await expect(interactivePost.getByRole('button', {name: /1 reply/})).toBeVisible(); + await interactivePost.getByRole('button', {name: /1 reply/}).click(); + + // 8. Confirm bot response in the thread panel + const threadPanel = channelsPage.page.getByRole('region', {name: /Thread/}); + await expect(threadPanel).toBeVisible(); + + // Verify response credits the user who clicked + await expect( + threadPanel.locator('p').filter({hasText: `${user.username} clicked an interactive button.`}), + ).toBeVisible(); + + // Verify JSON payload contains expected static fields + await expect( + threadPanel.locator('code').filter({hasText: new RegExp(`"user_name"\\s*:\\s*"${user.username}"`)}), + ).toBeVisible(); + await expect(threadPanel.locator('code').filter({hasText: '"type": "button"'})).toBeVisible(); +}); diff --git a/e2e-tests/playwright/specs/functional/plugins/demo_plugin/server/slash_commands/demo_list_files.spec.ts b/e2e-tests/playwright/specs/functional/plugins/demo_plugin/server/slash_commands/demo_list_files.spec.ts new file mode 100644 index 00000000000..d4e41d756e2 --- /dev/null +++ b/e2e-tests/playwright/specs/functional/plugins/demo_plugin/server/slash_commands/demo_list_files.spec.ts @@ -0,0 +1,93 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Client4} from '@mattermost/client'; + +import {expect, getFileFromAsset, test} from '@mattermost/playwright-lib'; + +import {setupDemoPlugin} from '../../helpers'; + +/** + * Uploads a batch of files to the channel via API and posts them as a single message. + * Using the API avoids the demo plugin's custom file upload menu which intercepts + * the attachment button in the UI. + */ +async function uploadAndPostFiles(client: Client4, channelId: string, filenames: string[]): Promise { + const fileIds: string[] = []; + + for (const filename of filenames) { + const file = getFileFromAsset(filename); + const formData = new FormData(); + formData.set('files', file, filename); + formData.set('channel_id', channelId); + const result = await client.uploadFile(formData); + fileIds.push(result.file_infos[0].id); + } + + await client.createPost({ + channel_id: channelId, + message: '', + file_ids: fileIds, + }); +} + +test('should list uploaded files with running total via /list_files command', async ({pw}) => { + // 1. Setup + const {adminClient, user, team} = await pw.initSetup(); + await setupDemoPlugin(adminClient, pw); + + // 2. Create a dedicated channel for file isolation + const channel = pw.random.channel({ + teamId: team.id, + name: 'list-files-test', + displayName: 'List Files Test', + }); + const createdChannel = await adminClient.createChannel(channel); + await adminClient.addToChannel(user.id, createdChannel.id); + + // 3. Login and navigate to the channel + const {channelsPage} = await pw.testBrowser.login(user); + await channelsPage.goto(team.name, 'list-files-test'); + await channelsPage.toBeVisible(); + + // 4. Send /list_files with no files — expect 0 count + await channelsPage.centerView.postCreate.input.fill('/list_files'); + await channelsPage.centerView.postCreate.sendMessage(); + + await expect( + channelsPage.centerView.container.getByText('Last 0 Files uploaded to this channel', {exact: true}), + ).toBeVisible(); + + // 5. Upload first batch of 2 files via API + // (avoids demo plugin's custom attachment menu intercepting the UI) + await uploadAndPostFiles(adminClient, createdChannel.id, ['sample_text_file.txt', 'mattermost-icon_128x128.png']); + + // 6. Send /list_files — expect count of 2 and both file names + await channelsPage.centerView.postCreate.input.fill('/list_files'); + await channelsPage.centerView.postCreate.sendMessage(); + + const response2 = channelsPage.centerView.container + .getByRole('listitem') + .filter({hasText: 'Last 2 Files uploaded to this channel'}) + .last(); + await expect(response2).toBeVisible(); + await expect(response2.getByRole('heading', {name: 'mattermost-icon_128x128.png'})).toBeVisible(); + await expect(response2.getByRole('heading', {name: 'sample_text_file.txt'})).toBeVisible(); + + // 7. Upload second batch of 2 more files via API + await uploadAndPostFiles(adminClient, createdChannel.id, ['mattermost.png', 'archive.zip']); + + // 8. Send /list_files — expect count of 4 and all file names + await channelsPage.centerView.postCreate.input.fill('/list_files'); + await channelsPage.centerView.postCreate.sendMessage(); + + const response4 = channelsPage.centerView.container + .getByRole('listitem') + .filter({hasText: 'Last 4 Files uploaded to this channel'}) + .last(); + await expect(response4).toBeVisible(); + await expect(response4.getByRole('heading', {name: 'mattermost.png'})).toBeVisible(); + await expect(response4.getByRole('heading', {name: 'archive.zip'})).toBeVisible(); + await expect(response4.getByRole('heading', {name: 'mattermost-icon_128x128.png'})).toBeVisible(); + await expect(response4.getByRole('heading', {name: 'sample_text_file.txt'})).toBeVisible(); +}); diff --git a/e2e-tests/playwright/specs/functional/plugins/demo_plugin/server/slash_commands/demo_show_mentions.spec.ts b/e2e-tests/playwright/specs/functional/plugins/demo_plugin/server/slash_commands/demo_show_mentions.spec.ts new file mode 100644 index 00000000000..6e5cbe5e050 --- /dev/null +++ b/e2e-tests/playwright/specs/functional/plugins/demo_plugin/server/slash_commands/demo_show_mentions.spec.ts @@ -0,0 +1,49 @@ +// 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 parse user and channel mentions from /show_mentions command text', async ({pw}) => { + // 1. Setup + const {adminClient, user, team} = await pw.initSetup(); + await setupDemoPlugin(adminClient, pw); + + // 2. Login + const {channelsPage} = await pw.testBrowser.login(user); + await channelsPage.goto(); + await channelsPage.toBeVisible(); + + // 3. Navigate to Town Square + await channelsPage.goto(team.name, 'town-square'); + await channelsPage.toBeVisible(); + + // 4. Send /show_mentions with a user mention and a channel mention + // sysadmin is a stable known user in every PW environment + await channelsPage.centerView.postCreate.input.fill('/show_mentions @sysadmin ~town-square'); + await channelsPage.centerView.postCreate.sendMessage(); + + // 5. Wait for bot response + const responsePost = channelsPage.centerView.container + .getByRole('listitem') + .filter({hasText: 'contains the following different mentions'}) + .last(); + await expect(responsePost).toBeVisible(); + + // 6. Assert user mentions section + await expect(responsePost.getByRole('heading', {name: 'Mentions to users in the team'})).toBeVisible(); + await expect(responsePost.getByRole('columnheader', {name: 'User name'})).toBeVisible(); + await expect(responsePost.getByRole('cell', {name: '@sysadmin'})).toBeVisible(); + + // 7. Assert channel mentions section + await expect(responsePost.getByRole('heading', {name: 'Mentions to public channels'})).toBeVisible(); + await expect(responsePost.getByRole('columnheader', {name: 'Channel name'})).toBeVisible(); + await expect(responsePost.getByRole('cell', {name: '~Town Square'})).toBeVisible(); + + // 8. Assert ~Town Square is a link pointing to the town-square channel + await expect(responsePost.getByRole('link', {name: '~Town Square'})).toHaveAttribute( + 'href', + /\/channels\/town-square$/, + ); +});